#43 Spotify integration

Open
DebenOldert wants to merge 1 commits from DebenOldert/spotify into DebenOldert/master
8 changed files with 203 additions and 39 deletions
  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 .quality import quality
 from .settings import settings
+from .spotify import spotify
 
 commands = [
     ('help', help, 'Display this list'),
     ('last', last, 'See the last search'),
     ('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 .last import last
 
-def search(cache=None):
+
+def search(cache=None, keywords=None, automode=False, tags=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 = []
         if isinstance(sites.available[selected_site], sites.DefaultSite):
             _sites = sites.available[1:]
@@ -22,42 +26,57 @@ def search(cache=None):
 
         if len(items) == 0:
             console.output('No results found')
+            if automode:
+                return False
             return True
 
         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)
     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 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:
             break
         else:
             item = items[picked_item]
             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

+ 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
         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():
         for entry in Settings.__list__:
             entry.push_config()
@@ -36,7 +42,7 @@ class Settings:
         Settings.__config__.write(Settings.__file__.open('w'))
 
     def read():
-        _config = configparser.ConfigParser()
+        _config = configparser.RawConfigParser()
         if Settings.__file__.exists():
             console.output('Reading settings from \'{0}\''.format(Settings.__file__.absolute()), console.DBG_INFO)
             _config.read(Settings.__file__)
@@ -49,3 +55,7 @@ class Settings:
     Debuglvl = None
     MinQuality = 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:
-    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.environment = env_name
         self.default = default
@@ -13,7 +13,7 @@ class SettingEntry:
         self.read_from_env = False
         self.type = type
 
-        self.str_value = None
+        self.str_value = ''
 
         self.read_config()
 
@@ -34,6 +34,8 @@ class SettingEntry:
             self.set(Settings.__config__[self.namespace][self.name])
 
     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
 
     def set(self, value):

+ 15 - 9
helper/sites/download.py

@@ -11,7 +11,7 @@ import json
 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)
     try:
         if type == 'GET':
@@ -28,7 +28,7 @@ def download(item, type='GET', parameters=None, headers=None, cookies=None, stre
                                    stream=stream)
     except:
         notExist(item)
-        return
+        return False
 
     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)))
 
             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
+                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)
     with open(full_name, 'wb') as f:
         progress = 0
@@ -76,7 +79,8 @@ def savefileprogress(name, full_name, file, item):
         else:
             f.write(file.content)
 
-    tags = tagging.search_tags(item)
+    if tags is None:
+        tags = tagging.search_tags(item)
 
     if tags is not None:
         item.link_tag_item(tags)
@@ -85,7 +89,7 @@ def savefileprogress(name, full_name, file, 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))
     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))
 
+    return True
+
 
 def notExist(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):
 
     if '-' in criteria:
-        _criteria = criteria.split('-')
+        _criteria = criteria.split('-', 1)
         artist = _criteria[0].split(' ')
         title = _criteria[1].split(' ')
     else:

+ 2 - 2
sites/default.py

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