#41 Use search result scores to show best matching result on top of list

Спојено
DebenOldert споји(ла) 3 комит(е) из DebenOldert/scoring_results у DebenOldert/master пре 5 година
14 измењених фајлова са 334 додато и 172 уклоњено
  1. 14 0
      command/__init__.py
  2. 10 0
      command/directory.py
  3. 9 0
      command/help.py
  4. 14 0
      command/last.py
  5. 9 0
      command/quality.py
  6. 63 0
      command/search.py
  7. 37 0
      command/settings.py
  8. 12 0
      command/site.py
  9. 0 2
      helper/console.py
  10. 47 53
      helper/settings.py
  11. 44 0
      helper/settingsentry.py
  12. 7 0
      helper/sites/item.py
  13. 61 0
      helper/sites/itemsort.py
  14. 7 117
      main.py

+ 14 - 0
command/__init__.py

@@ -0,0 +1,14 @@
+from .search import search
+from .site import choose_site
+from .directory import directory
+from .help import help
+from .last import last
+from .quality import quality
+from .settings import settings
+
+commands = [
+    ('help', help, 'Display this list'),
+    ('last', last, 'See the last search'),
+    ('search', search, 'Search the selected site'),
+    ('settings', settings, 'View/change settings')
+]

+ 10 - 0
command/directory.py

@@ -0,0 +1,10 @@
+from helper import console
+
+
+def directory(dir=None):
+    if dir is None:
+        dir = console.ask_input('Enter new save directory')
+    Settings.SaveDir.set(dir)
+    os.makedirs(Settings.SaveDir.get(), exist_ok=True)
+    console.output('New save directory: {0}'.format(Settings.SaveDir.get()), level=console.DBG_INFO)
+    return True

+ 9 - 0
command/help.py

@@ -0,0 +1,9 @@
+import command
+from helper import console
+
+
+def help():
+    console.output('Possible commands are:')
+    for comm in command.commands:
+        console.output('{0}\t{1}'.format(comm[0], comm[2]))
+    return True

+ 14 - 0
command/last.py

@@ -0,0 +1,14 @@
+from command import search
+from helper import console
+
+
+def last(items=None):
+    global search_cache
+    if items is not None:
+        search_cache = items
+    else:
+        if search_cache is None:
+            console.output('First search has yet te be made', console.DBG_ERROR)
+        else:
+            search(search_cache)
+    return True

+ 9 - 0
command/quality.py

@@ -0,0 +1,9 @@
+from helper import console
+
+
+def quality(quality=None):
+    if quality is None:
+        quality = console.ask_input('Enter minimal quality for auto download')
+    Settings.MinQuality.set(quality)
+    console.output('Minimal quality for auto downloading is set to {0}'.format(Settings.MinQuality.get()), level=console.DBG_INFO)
+    return True

+ 63 - 0
command/search.py

@@ -0,0 +1,63 @@
+import sites
+from helper import console
+from main import selected_site
+from helper.sites import itemsort
+from .last import last
+
+def search(cache=None):
+    if cache is None:
+        keywords = console.ask_input('Search for')
+        _sites = []
+        if isinstance(sites.available[selected_site], sites.DefaultSite):
+            _sites = sites.available[1:]
+        else:
+            _sites = [sites.available[selected_site]]
+
+        items = []
+        for site in _sites:
+            console.output('Searching: {0}'.format(site.url))
+            _items = site.perform_search(keywords)
+            console.output('\tFound {0} results'.format(len(_items)))
+            items.extend(_items)
+
+        if len(items) == 0:
+            console.output('No results found')
+            return True
+
+        items = itemsort.smart(items, keywords)
+
+        last(items)
+    else:
+        items = cache
+
+    while True:
+        picked_item = console.option_picker('Pick a song to download',
+                                            items,
+                                            quit=True,
+                                            objects=[
+                                                '__id__',
+                                                'x.score',
+                                                'x.duration',
+                                                'x.size',
+                                                'x.artist',
+                                                'x.title'
+                                            ],
+                                            table=[
+                                                ('ID', 2),
+                                                ('Score', 5),
+                                                ('Time', 5),
+                                                ('Size', 4),
+                                                ('Artist', 50),
+                                                ('Title', 100)
+                                            ])
+        if picked_item is None:
+            break
+        else:
+            item = items[picked_item]
+            console.output('Downloading {0}'.format(item.title))
+
+            item.site.download(item)
+
+            break
+
+    return True

+ 37 - 0
command/settings.py

@@ -0,0 +1,37 @@
+from helper import console
+from helper.settings import Settings
+
+
+def settings():
+    while True:
+        setting = console.option_picker('Choose which setting to change',
+                              Settings.__list__,
+                              quit=True,
+                              objects=[
+                                  '__id__',
+                                  'x.name',
+                                  'x.str_value'
+                              ],
+                              table=[
+                                  ('ID', 2),
+                                  ('Setting', 20),
+                                  ('Value', 100)
+                              ])
+
+        if setting is None:
+            console.output('Saving settings...')
+            Settings.write()
+            console.output('Settings saved')
+            break
+        else:
+            entry = Settings.__list__[setting]
+
+            value = console.ask_input('Enter new value', entry.type, quit=True)
+
+            if value is None:
+                console.output('Value not changed', console.DBG_INFO)
+                continue
+            else:
+                console.output('Changing value of {x.name} from {x.value} to {new}'.format(x=entry, new=value), console.DBG_INFO)
+                entry.set(value)
+    return True

+ 12 - 0
command/site.py

@@ -0,0 +1,12 @@
+from helper import console
+
+
+def choose_site():
+    global selected_site
+    _index = console.option_picker('Available sites',
+                                   list(map(lambda x: x.url, sites.available)),
+                                   selected_site,
+                                   True)
+    if _index is not None:
+        selected_site = _index
+    return True

+ 0 - 2
helper/console.py

@@ -66,10 +66,8 @@ def option_picker(question, options, objects=None, selected=None, quit=False, ta
         if objects is not None and table is not None and len(objects) == len(table):
             for j in range(0, len(objects)):
                 _row += '[{{{name}:{size}.{size}}}]'.format(name=str(objects[j]), size=str(table[j][1]))
-                # _row += '[{' + str(objects[j]) + ':' + str(table[j][1]) + '.' + str(table[j][1]) + '}] '
 
             output(_row.format(__id__=str(i), x=option))
-            # output(_row.format(__id__=str(i), x=dict(map(lambda s: s if s is not None else '', option.__dict__.items()))).strip())
 
         else:
             if i == selected:

+ 47 - 53
helper/settings.py

@@ -1,57 +1,51 @@
-import os
 import configparser
-
-
-CONFIG = configparser.ConfigParser()
-if os.path.isfile('settings.ini'):
-    CONFIG.read('settings.ini')
-
-
-class SettingEntry:
-    def __init__(self, name, env_name=None, default=None, namespace='DEFAULT', type=str):
-        self.name = name
-        self.environment = env_name
-        self.default = default
-        self.value = default
-        self.namespace = namespace
-        self.read_from_env = False
-        self.type = type
-
-        self.read_config()
-
-        if os.getenv(self.environment) is not None:
-            self.read_from_env = True
-            self.set(os.getenv(self.environment))
-
-    def __str__(self):
-        return '{} => {}'.format(self.name, self.value)
-
-    def __eq__(self, other):
-        return self.name == other
-
-    def read_config(self):
-        if self.namespace in CONFIG and self.name in CONFIG[self.namespace] and not self.read_from_env:
-            self.set(CONFIG[self.namespace][self.name])
-
-    def set(self, value):
-        self.value = self.type(value)
-
-    def get(self):
-        return self.type(self.value)
+import os
+from pathlib import Path
+from helper import console
 
 
 class Settings:
-    # Directory to permanently save the file (ENV: MD_SAVEDIR)
-    SaveDir = SettingEntry('saveDir', 'MD_SAVEDIR', '~/Downloads', 'music-downloader', os.path.expanduser)
-
-    # Directory to temporary download the file (ENV: MD_TMP)
-    tmpDir = SettingEntry('tmpDir', 'MD_TMP', '~/tmp', 'music-downloader', os.path.expanduser)
-
-    # Minimal debug level (ENV: MD_LOGGING)
-    Debuglvl = SettingEntry('debuglvl', 'MD_LOGGING', 0, 'music-downloader', int)
-
-    # Minimal bitrate to auto download the file (ENV: MD_QUALITY)
-    MinQuality = SettingEntry('minQuality', 'MD_QUALITY', 300, 'music-downloader', int)
-
-    #Format for downloaded file ID3 comment
-    CommentFormat = SettingEntry('commentFormat', 'MD_COMMENT', '', 'music-downloader')
+    # Collect all the setting instances
+    __list__ = []
+
+    __config__ = None
+
+    __file__ = Path('settings.ini')
+
+    def initialize():
+        from .settingsentry import SettingEntry
+        # Directory to permanently save the file (ENV: MD_SAVEDIR)
+        Settings.SaveDir = SettingEntry('saveDir', 'MD_SAVEDIR', '~/Downloads', 'music-downloader', os.path.expanduser)
+
+        # Directory to temporary download the file (ENV: MD_TMP)
+        Settings.tmpDir = SettingEntry('tmpDir', 'MD_TMP', '~/tmp', 'music-downloader', os.path.expanduser)
+
+        # Minimal debug level (ENV: MD_LOGGING)
+        Settings.Debuglvl = SettingEntry('debuglvl', 'MD_LOGGING', 0, 'music-downloader', int)
+
+        # Minimal bitrate to auto download the file (ENV: MD_QUALITY)
+        Settings.MinQuality = SettingEntry('minQuality', 'MD_QUALITY', 300, 'music-downloader', int)
+
+        # Format for downloaded file ID3 comment
+        Settings.CommentFormat = SettingEntry('commentFormat', 'MD_COMMENT', '', 'music-downloader')
+
+    def write():
+        for entry in Settings.__list__:
+            entry.push_config()
+        console.output('Writing settings to \'{0}\''.format(Settings.__file__.absolute()), console.DBG_INFO)
+        Settings.__config__.write(Settings.__file__.open('w'))
+
+    def read():
+        _config = configparser.ConfigParser()
+        if Settings.__file__.exists():
+            console.output('Reading settings from \'{0}\''.format(Settings.__file__.absolute()), console.DBG_INFO)
+            _config.read(Settings.__file__)
+            Settings.__config__ = _config
+        else:
+            console.output('Settings not found at \'{0}\''.format(Settings.__file__.absolute()), console.DBG_INFO)
+
+    SaveDir = None
+    tmpDir = None
+    Debuglvl = None
+    MinQuality = None
+    CommentFormat = None

+ 44 - 0
helper/settingsentry.py

@@ -0,0 +1,44 @@
+import os
+
+from .settings import Settings
+
+
+class SettingEntry:
+    def __init__(self, name, env_name=None, default=None, namespace='DEFAULT', type=str):
+        self.name = name
+        self.environment = env_name
+        self.default = default
+        self.value = default
+        self.namespace = namespace
+        self.read_from_env = False
+        self.type = type
+
+        self.str_value = None
+
+        self.read_config()
+
+        if os.getenv(self.environment) is not None:
+            self.read_from_env = True
+            self.set(os.getenv(self.environment))
+
+        Settings.__list__.append(self)
+
+    def __str__(self):
+        return '{0} => {1}'.format(self.name, self.value)
+
+    def __eq__(self, other):
+        return self.name == other
+
+    def read_config(self):
+        if self.namespace in Settings.__config__ and self.name in Settings.__config__[self.namespace] and not self.read_from_env:
+            self.set(Settings.__config__[self.namespace][self.name])
+
+    def push_config(self):
+        Settings.__config__[self.namespace][self.name] = self.str_value
+
+    def set(self, value):
+        self.value = self.type(value)
+        self.str_value = str(self.value)
+
+    def get(self):
+        return self.type(self.value)

+ 7 - 0
helper/sites/item.py

@@ -44,6 +44,8 @@ class Item:
         self.bytes = None
         self.duration_seconds = None
 
+        self.score = 0.0
+
         # self.calculate_duration_seconds()
         # self.calculate_bytes()
         # self.calculate_bitrate()
@@ -104,3 +106,8 @@ class Item:
         if self.duration_seconds is not None and self.bytes is not None:
             self.bitrate = self.bytes / self.duration_seconds / 1024 * 8  # bytes / seconds / 1024 * 8
 
+    def increment_score(self, by=1):
+        self.score += by
+
+    def decrement_score(self, by=1):
+        self.score -= by

+ 61 - 0
helper/sites/itemsort.py

@@ -0,0 +1,61 @@
+from helper import console
+
+
+def smart(items, criteria):
+
+    if '-' in criteria:
+        _criteria = criteria.split('-')
+        artist = _criteria[0].split(' ')
+        title = _criteria[1].split(' ')
+    else:
+        artist = title = criteria.split(' ')
+
+    for item in items:
+
+        # First do negative stuff, bad news comes always first
+
+        if item.duration_seconds is not None and item.duration_seconds < 180:
+            item.decrement_score(0.5)
+
+        if item.duration_seconds is not None and item.duration_seconds > 600:
+            item.decrement_score(1)
+
+        if 'radio' in item.title.lower():
+            item.decrement_score(1)
+
+        if len(set(item.artist.lower().split(' ')).intersection(artist)) == 0:
+            item.decrement_score(2)
+
+        if len(set(item.title.lower().split(' ')).intersection(title)) == 0:
+            item.decrement_score(2)
+
+        # Now do the positive stuff
+
+        # The following 2 rules are not always correct. Need more testing to evaluate
+
+        # if 'original' in item.title.lower():
+        #     item.increment_score(0.5)
+        # if 'extended' in item.title.lower():
+        #     item.increment_score(1)
+
+        if ' '.join(title).strip().lower() == item.title.strip().lower():
+            item.increment_score(1)
+
+        if item.duration_seconds is not None:
+            item.increment_score(0.1)
+
+        if item.size is not None:
+            item.increment_score(0.1)
+
+    sortedlist = []
+
+    for item in items:
+        # We don't show negative scores
+        if item.score > 0:
+            sortedlist.append(item)
+        else:
+            console.output('Removing {x.artist} - {x.title} from list: Score to low ({x.score})'.format(x=item), console.DBG_INFO)
+
+    sortedlist.sort(reverse=True, key=lambda x: (x.score, x.duration_seconds))
+
+    return sortedlist

+ 7 - 117
main.py

@@ -2,13 +2,16 @@ import sites as sites
 from helper.settings import Settings
 from helper import console as console
 import os.path
+import command
 
 
 def main():
+    Settings.read()
+    Settings.initialize()
+
     os.makedirs(Settings.tmpDir.get(), exist_ok=True)
     console.unlock()
 
-
     console.output('Music downloader by Deben Oldert')
     console.output()
 
@@ -28,136 +31,23 @@ def main():
     exit(0)
 
 
-def choose_site():
-    global selected_site
-    _index = console.option_picker('Available sites',
-                                   list(map(lambda x: x.url, sites.available)),
-                                   selected_site,
-                                   True)
-    if _index is not None:
-        selected_site = _index
-    return True
-
-
 def idle():
     _command = console.ask_input('Type command')
 
-    for command in commands:
-        if command[0] == _command.lower():
-            return command[1]()
+    for comm in command.commands:
+        if comm[0] == _command.lower():
+            return comm[1]()
 
     console.output('Command \'{0}\' not found. Type help to see a list of possible commands'.format(_command))
     return True
 
 
-def help():
-    console.output('Possible commands are:')
-    for command in commands:
-        console.output('{0}\t{1}'.format(command[0], command[2]))
-    return True
-
-
-def search(cache=None):
-    if cache is None:
-        keywords = console.ask_input('Search for')
-        _sites = []
-        if isinstance(sites.available[selected_site], sites.DefaultSite):
-            _sites = sites.available[1:]
-        else:
-            _sites = [sites.available[selected_site]]
-
-        items = []
-        for site in _sites:
-            console.output('Searching: {0}'.format(site.url))
-            _items = site.perform_search(keywords)
-            console.output('\tFound {0} results'.format(len(_items)))
-            items.extend(_items)
-
-        if len(items) == 0:
-            console.output('No results found')
-            return True
-
-        last(items)
-    else:
-        items = cache
-
-    while True:
-        picked_item = console.option_picker('Pick a song to download',
-                                            items,
-                                            # list(map(lambda x: '[{0:5}] [{1:4}] {2}'.format(x.duration, x.size, x.title), items)),
-                                            quit=True,
-                                            objects=[
-                                                '__id__',
-                                                'x.duration',
-                                                'x.size',
-                                                'x.artist',
-                                                'x.title'
-                                            ],
-                                            table=[
-                                                ('ID', 2),
-                                                ('Time', 5),
-                                                ('Size', 4),
-                                                ('Artist', 50),
-                                                ('Title', 100)
-                                            ])
-        if picked_item is None:
-            break
-        else:
-            item = items[picked_item]
-            console.output('Downloading {0}'.format(item.title))
-
-            item.site.download(item)
-
-            break
-
-    return True
-
-
-def directory(dir=None):
-    if dir is None:
-        dir = console.ask_input('Enter new save directory')
-    Settings.SaveDir.set(dir)
-    os.makedirs(Settings.SaveDir.get(), exist_ok=True)
-    console.output('New save directory: {0}'.format(Settings.SaveDir.get()), level=console.DBG_INFO)
-    return True
-
-
-def quality(quality=None):
-    if quality is None:
-        quality = console.ask_input('Enter minimal quality for auto download')
-    Settings.MinQuality.set(quality)
-    console.output('Minimal quality for auto downloading is set to {0}'.format(Settings.MinQuality.get()), level=console.DBG_INFO)
-    return True
-
-
-def last(items=None):
-    global search_cache
-    if items is not None:
-        search_cache = items
-    else:
-        if search_cache is None:
-            console.output('First search has yet te be made', console.DBG_ERROR)
-        else:
-            search(search_cache)
-    return True
-
-
 def quit():
     return False
 
 
 selected_site = 0
 search_cache = None
-commands = [
-    ('help', help, 'Display this list'),
-    ('last', last, 'See the last search'),
-    ('save', directory, 'Change the download directory'),
-    ('search', search, 'Search the selected site'),
-    ('sites', choose_site, 'Pick the site to search'),
-    ('quality', quality, 'Set the minimal quality to download without confirmation'),
-    ('quit', quit, 'Quit the program')
-]
-
 
 if __name__ == '__main__':
     main()