浏览代码

Add an automated downloader that reads tracks from Spotify playlists

debenoldert 5 年之前
父节点
当前提交
ec61dfc7c1
共有 8 个文件被更改,包括 203 次插入39 次删除
  1. 3 1
      command/__init__.py
  2. 42 23
      command/search.py
  3. 125 0
      command/spotify.py
  4. 11 1
      helper/settings.py
  5. 4 2
      helper/settingsentry.py
  6. 15 9
      helper/sites/download.py
  7. 1 1
      helper/sites/itemsort.py
  8. 2 2
      sites/default.py

+ 3 - 1
command/__init__.py

@@ -5,10 +5,12 @@ from .help import help
 from .last import last
 from .last import last
 from .quality import quality
 from .quality import quality
 from .settings import settings
 from .settings import settings
+from .spotify import spotify
 
 
 commands = [
 commands = [
     ('help', help, 'Display this list'),
     ('help', help, 'Display this list'),
     ('last', last, 'See the last search'),
     ('last', last, 'See the last search'),
     ('search', search, 'Search the selected site'),
     ('search', search, 'Search the selected site'),
-    ('settings', settings, 'View/change settings')
+    ('settings', settings, 'View/change settings'),
+    ('spotify', spotify, 'Find music from your spotify profile')
 ]
 ]

+ 42 - 23
command/search.py

@@ -4,9 +4,13 @@ from main import selected_site
 from helper.sites import itemsort
 from helper.sites import itemsort
 from .last import last
 from .last import last
 
 
-def search(cache=None):
+
+def search(cache=None, keywords=None, automode=False, tags=None):
     if cache is None:
     if cache is None:
-        keywords = console.ask_input('Search for')
+        if keywords is None:
+            keywords = console.ask_input('Search for')
+        keywords = keywords.lower()
+
         _sites = []
         _sites = []
         if isinstance(sites.available[selected_site], sites.DefaultSite):
         if isinstance(sites.available[selected_site], sites.DefaultSite):
             _sites = sites.available[1:]
             _sites = sites.available[1:]
@@ -22,42 +26,57 @@ def search(cache=None):
 
 
         if len(items) == 0:
         if len(items) == 0:
             console.output('No results found')
             console.output('No results found')
+            if automode:
+                return False
             return True
             return True
 
 
         items = itemsort.smart(items, keywords)
         items = itemsort.smart(items, keywords)
 
 
+        if len(items) == 0:
+            console.output('No correct results found - consider searching this song manually')
+            if automode:
+                return False
+            return True
+
         last(items)
         last(items)
     else:
     else:
         items = cache
         items = cache
 
 
     while True:
     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 not automode:
+            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)
+                                                ])
+        else:
+            # Automode assume that the true track is on top position (index=0)
+            picked_item = 0
+
         if picked_item is None:
         if picked_item is None:
             break
             break
         else:
         else:
             item = items[picked_item]
             item = items[picked_item]
             console.output('Downloading {0}'.format(item.title))
             console.output('Downloading {0}'.format(item.title))
 
 
-            item.site.download(item)
+            result = item.site.download(item, automode=automode, tags=tags)
 
 
-            break
+            if automode:
+                return result
 
 
+            break
     return True
     return True

+ 125 - 0
command/spotify.py

@@ -0,0 +1,125 @@
+# PIP Spotipy | https://spotipy.readthedocs.io/en/2.13.0/
+#
+#
+# 1). Login
+# 2). Select playlist
+# 3). Iterate/Download items
+# 4). Use spotify metadata to ID3 tag song
+# 5). Songs that could not be found are listed an possible exported for later use
+#
+# This will be awesome!
+#
+from command import search, settings
+from helper import console, tagging
+
+import colorama as color
+import spotipy
+from spotipy.oauth2 import SpotifyOAuth
+
+from helper.settings import Settings
+from helper.tags.tagitem import TagItem
+
+
+def spotify():
+    if Settings.SpotifyUsername.get() == '' or Settings.SpotifyClientID.get() == '' or Settings.SpotifySecret.get() == '':
+        console.output('You need to request a developers account at spotify. You only have to do this once.')
+        console.output('This is very easy, quick, and free to do and this doesn\'t further affect your account')
+        console.output('Please go to:')
+        console.output('\thttps://developer.spotify.com/dashboard/applications')
+        console.output('And create a new application.')
+        console.output('Fill in a name, description and enter in the Redirect URI\'s this:')
+        console.output('\thttp://spotify-finder')
+        console.output('Save it and make sure you find Client ID and Client Secret on your screen')
+        console.ask_input('Are you ready to fill in your username, client ID, and secret? [y]')
+
+        settings()
+        return True
+
+    console.output('Logging in to Spotify')
+
+    spotify = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=Settings.SpotifyScope.get(),
+                                                        client_id=Settings.SpotifyClientID.get(),
+                                                        client_secret=Settings.SpotifySecret.get(),
+                                                        redirect_uri='http://spotify-finder',
+                                                        username=Settings.SpotifyUsername.get()))
+
+    playlists = spotify.current_user_playlists()
+
+    console.output('Found {0} spotify playlists'.format(len(playlists['items'])), console.DBG_INFO)
+
+    playlist = console.option_picker("Pick a playlist that you want to download from",
+                          playlists['items'],
+                          quit=True,
+                          objects=[
+                              '__id__',
+                              'x[name]',
+                              'x[description]'
+                          ],
+                          table=[
+                              ('ID', 2),
+                              ('Name', 50),
+                              ('Description', 100)
+                          ])
+
+    if playlist is None:
+        return True
+    else:
+        playlist = playlists['items'][playlist]
+        tracks = spotify.playlist_tracks(playlist['id'], fields='items(track(album(name,images(url)),artists(name),name))')
+
+        console.output('Found {0} tracks in playlist {1}'.format(len(tracks['items']), playlist['name']), console.DBG_INFO)
+
+        console.output('Start finding music from playlist \'{0}\''.format(playlist['name']))
+
+        success = []
+        fail = []
+
+        for track in tracks['items']:
+            track = track['track']
+            tag = TagItem()
+            tag.set_title(track['name'])
+            tag.set_artist(', '.join(list(i['name'] for i in track['artists'])))
+            tag.set_cover_url(track['album']['images'][0]['url'])  # 640x640
+            tag.set_album(track['album']['name'])
+            tag.set_genre('')  # unknown
+
+            keywords = '{0} - {1}'.format(track['artists'][0]['name'], track['name'])
+
+            console.output('Looking up {0} - {1}'.format(track['artists'][0]['name'], track['name']))
+
+            result = search(keywords=keywords, automode=True, tags=tag)
+
+            if result:
+                success.append(tag)
+                console.output(color.Fore.GREEN + '[ OK ] {tag.artist} - {tag.title}'.format(tag=tag))
+            else:
+                fail.append(tag)
+                console.output(color.Fore.RED + '[FAIL] {tag.artist} - {tag.title}'.format(tag=tag))
+
+        console.output('Sucessfully downloaded {0} songs. {1} songs failed to download'.format(len(success), len(fail)))
+
+        if len(fail) > 0:
+            seefails = console.ask_input('Do you want to see a list of the failed songs and manually search for it? [y/n]')
+
+            if seefails == 'y':
+                while True:
+                    song = console.option_picker('Pick a song to manually download it',
+                                                 fail,
+                                                 quit=True,
+                                                 objects=[
+                                                     '__id__',
+                                                     'x.artist',
+                                                     'x.title'
+                                                 ],
+                                                 table=[
+                                                     ('ID', 2),
+                                                     ('Artist', 50),
+                                                     ('Title', 100)
+                                                 ])
+                    if song is None:
+                        break
+                    else:
+                        keywords = '{tag.artist} - {tag.title}'.format(tag=tag)
+                        search(keywords=keywords)
+
+    return True

+ 11 - 1
helper/settings.py

@@ -29,6 +29,12 @@ class Settings:
         # Format for downloaded file ID3 comment
         # Format for downloaded file ID3 comment
         Settings.CommentFormat = SettingEntry('commentFormat', 'MD_COMMENT', '', 'music-downloader')
         Settings.CommentFormat = SettingEntry('commentFormat', 'MD_COMMENT', '', 'music-downloader')
 
 
+        # Spotify settings
+        Settings.SpotifyUsername = SettingEntry(namespace='spotify', name='username')
+        Settings.SpotifyClientID = SettingEntry(namespace='spotify', name='clientid')
+        Settings.SpotifySecret = SettingEntry(namespace='spotify', name='secret')
+        Settings.SpotifyScope = SettingEntry(namespace='spotify', name='scope', default='playlist-read-private,playlist-read-collaborative')
+
     def write():
     def write():
         for entry in Settings.__list__:
         for entry in Settings.__list__:
             entry.push_config()
             entry.push_config()
@@ -36,7 +42,7 @@ class Settings:
         Settings.__config__.write(Settings.__file__.open('w'))
         Settings.__config__.write(Settings.__file__.open('w'))
 
 
     def read():
     def read():
-        _config = configparser.ConfigParser()
+        _config = configparser.RawConfigParser()
         if Settings.__file__.exists():
         if Settings.__file__.exists():
             console.output('Reading settings from \'{0}\''.format(Settings.__file__.absolute()), console.DBG_INFO)
             console.output('Reading settings from \'{0}\''.format(Settings.__file__.absolute()), console.DBG_INFO)
             _config.read(Settings.__file__)
             _config.read(Settings.__file__)
@@ -49,3 +55,7 @@ class Settings:
     Debuglvl = None
     Debuglvl = None
     MinQuality = None
     MinQuality = None
     CommentFormat = None
     CommentFormat = None
+    SpotifyUsername = None
+    SpotifyClientID = None
+    SpotifySecret = None
+    SpotifyScope = None

+ 4 - 2
helper/settingsentry.py

@@ -4,7 +4,7 @@ from .settings import Settings
 
 
 
 
 class SettingEntry:
 class SettingEntry:
-    def __init__(self, name, env_name=None, default=None, namespace='DEFAULT', type=str):
+    def __init__(self, name, env_name='', default='', namespace='DEFAULT', type=str):
         self.name = name
         self.name = name
         self.environment = env_name
         self.environment = env_name
         self.default = default
         self.default = default
@@ -13,7 +13,7 @@ class SettingEntry:
         self.read_from_env = False
         self.read_from_env = False
         self.type = type
         self.type = type
 
 
-        self.str_value = None
+        self.str_value = ''
 
 
         self.read_config()
         self.read_config()
 
 
@@ -34,6 +34,8 @@ class SettingEntry:
             self.set(Settings.__config__[self.namespace][self.name])
             self.set(Settings.__config__[self.namespace][self.name])
 
 
     def push_config(self):
     def push_config(self):
+        if not Settings.__config__.has_section(self.namespace):
+            Settings.__config__.add_section(self.namespace)
         Settings.__config__[self.namespace][self.name] = self.str_value
         Settings.__config__[self.namespace][self.name] = self.str_value
 
 
     def set(self, value):
     def set(self, value):

+ 15 - 9
helper/sites/download.py

@@ -11,7 +11,7 @@ import json
 import os
 import os
 
 
 
 
-def download(item, type='GET', parameters=None, headers=None, cookies=None, stream=True):
+def download(item, type='GET', parameters=None, headers=None, cookies=None, stream=True, automode=False, tags=None):
     console.output('Requesting dl: {0}'.format(item.download_url), level=console.DBG_INFO)
     console.output('Requesting dl: {0}'.format(item.download_url), level=console.DBG_INFO)
     try:
     try:
         if type == 'GET':
         if type == 'GET':
@@ -28,7 +28,7 @@ def download(item, type='GET', parameters=None, headers=None, cookies=None, stre
                                    stream=stream)
                                    stream=stream)
     except:
     except:
         notExist(item)
         notExist(item)
-        return
+        return False
 
 
     name = urllib.parse.unquote('{x.artist} - {x.title}'.format(x=item))
     name = urllib.parse.unquote('{x.artist} - {x.title}'.format(x=item))
 
 
@@ -51,15 +51,18 @@ def download(item, type='GET', parameters=None, headers=None, cookies=None, stre
                 console.output('Bitrate: {0}kbps'.format(int(item.bitrate)))
                 console.output('Bitrate: {0}kbps'.format(int(item.bitrate)))
 
 
             if item.duration_seconds is not None and int(item.bitrate) < Settings.MinQuality.get():
             if item.duration_seconds is not None and int(item.bitrate) < Settings.MinQuality.get():
-                ans = console.ask_input('Continue downloading? [y/n]')
-
-                if ans != 'y':
+                if automode:
                     return
                     return
+                else:
+                    ans = console.ask_input('Continue downloading? [y/n]')
+
+                    if ans != 'y':
+                        return False
 
 
-    savefileprogress(name, full_name, file, item)
+    return savefileprogress(name, full_name, file, item, tags=tags)
 
 
 
 
-def savefileprogress(name, full_name, file, item):
+def savefileprogress(name, full_name, file, item, tags=None):
     console.output('Saving to: {0}'.format(full_name), level=console.DBG_INFO)
     console.output('Saving to: {0}'.format(full_name), level=console.DBG_INFO)
     with open(full_name, 'wb') as f:
     with open(full_name, 'wb') as f:
         progress = 0
         progress = 0
@@ -76,7 +79,8 @@ def savefileprogress(name, full_name, file, item):
         else:
         else:
             f.write(file.content)
             f.write(file.content)
 
 
-    tags = tagging.search_tags(item)
+    if tags is None:
+        tags = tagging.search_tags(item)
 
 
     if tags is not None:
     if tags is not None:
         item.link_tag_item(tags)
         item.link_tag_item(tags)
@@ -85,7 +89,7 @@ def savefileprogress(name, full_name, file, item):
 
 
         tagging.write_tags_to_file(full_name, item)
         tagging.write_tags_to_file(full_name, item)
 
 
-        name = urllib.parse.unquote('{x.artist} - {x.title}'.format(x=tags)) + '.mp3'
+        name = urllib.parse.unquote('{x.artist} - {x.title}.mp3'.format(x=tags))
 
 
     full_save_name = os.path.abspath('{0}/{1}'.format(Settings.SaveDir.get(), name))
     full_save_name = os.path.abspath('{0}/{1}'.format(Settings.SaveDir.get(), name))
     os.rename(full_name, full_save_name)
     os.rename(full_name, full_save_name)
@@ -93,6 +97,8 @@ def savefileprogress(name, full_name, file, item):
 
 
     console.output('Download of {0} completed!'.format(name))
     console.output('Download of {0} completed!'.format(name))
 
 
+    return True
+
 
 
 def notExist(item):
 def notExist(item):
     console.output('{x.title} at {x.download_url} does not exist'.format(x=item))
     console.output('{x.title} at {x.download_url} does not exist'.format(x=item))

+ 1 - 1
helper/sites/itemsort.py

@@ -4,7 +4,7 @@ from helper import console
 def smart(items, criteria):
 def smart(items, criteria):
 
 
     if '-' in criteria:
     if '-' in criteria:
-        _criteria = criteria.split('-')
+        _criteria = criteria.split('-', 1)
         artist = _criteria[0].split(' ')
         artist = _criteria[0].split(' ')
         title = _criteria[1].split(' ')
         title = _criteria[1].split(' ')
     else:
     else:

+ 2 - 2
sites/default.py

@@ -45,7 +45,7 @@ class DefaultSite:
 
 
 
 
     @staticmethod
     @staticmethod
-    def download(item):
+    def download(item, automode=False, tags=None):
         item.format_original_url()
         item.format_original_url()
         # item.original_url = item.site.format_url(item.original_url)
         # item.original_url = item.site.format_url(item.original_url)
 
 
@@ -54,4 +54,4 @@ class DefaultSite:
         elif 'krakenfiles' in item.original_url:
         elif 'krakenfiles' in item.original_url:
             download.krakenfiles(item)
             download.krakenfiles(item)
         else:
         else:
-            download.download(item)
+            return download.download(item, automode=automode, tags=tags)