From 42f33162ea1f3ae17de176114c1c1c668bb5040a Mon Sep 17 00:00:00 2001 From: Manveer Basra Date: Sun, 28 Oct 2018 16:14:27 -0400 Subject: [PATCH 1/7] --list flag accepts only text files using mimetypes --- spotdl/handle.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spotdl/handle.py b/spotdl/handle.py index f76f46c..f4168fd 100644 --- a/spotdl/handle.py +++ b/spotdl/handle.py @@ -5,6 +5,10 @@ import logging import yaml import argparse import mimetypes +<<<<<<< HEAD +======= + +>>>>>>> --list flag accepts only text files using mimetypes import os import spotdl From 0492c711cc4f2c76ac48193649862026db241133 Mon Sep 17 00:00:00 2001 From: Manveer Basra Date: Mon, 29 Oct 2018 13:11:05 -0400 Subject: [PATCH 2/7] Refactored for consistency --- spotdl/handle.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spotdl/handle.py b/spotdl/handle.py index f4168fd..f76f46c 100644 --- a/spotdl/handle.py +++ b/spotdl/handle.py @@ -5,10 +5,6 @@ import logging import yaml import argparse import mimetypes -<<<<<<< HEAD -======= - ->>>>>>> --list flag accepts only text files using mimetypes import os import spotdl From d215ce685d9f90911d76a005e4091bd487e6c993 Mon Sep 17 00:00:00 2001 From: Manveer Basra Date: Tue, 30 Oct 2018 14:12:37 -0400 Subject: [PATCH 3/7] Exposed Spotify Client ID/Secret in config.yml --- spotdl/handle.py | 14 ++++++++++++++ spotdl/spotdl.py | 1 + spotdl/spotify_tools.py | 32 ++++++++++++++++++++++---------- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/spotdl/handle.py b/spotdl/handle.py index f76f46c..fdfe719 100644 --- a/spotdl/handle.py +++ b/spotdl/handle.py @@ -34,6 +34,8 @@ default_conf = { "skip": None, "write-successful": None, "log-level": "INFO", + "spotify_client_id": "4fe3fecfe5334023a1472516cc99d805", + "spotify_client_secret": "0f02b7c483c04257984695007a4a8d5c" } } @@ -260,6 +262,18 @@ def get_arguments(raw_args=None, to_group=True, to_merge=True): default=config["write-successful"], help="path to file to write successful tracks to", ) + parser.add_argument( + "-sci", + "--spotify-client-id", + default=config["spotify_client_id"], + help=argparse.SUPPRESS + ) + parser.add_argument( + "-scs", + "--spotify-client-secret", + default=config["spotify_client_secret"], + help=argparse.SUPPRESS + ) parser.add_argument( "-c", "--config", default=None, help="path to custom config.yml file" ) diff --git a/spotdl/spotdl.py b/spotdl/spotdl.py index 8e74360..18c4a1b 100644 --- a/spotdl/spotdl.py +++ b/spotdl/spotdl.py @@ -51,6 +51,7 @@ def main(): internals.filter_path(const.args.folder) youtube_tools.set_api_key() + spotify_tools.set_client_credentials() logzero.setup_default_logger(formatter=const._formatter, level=const.args.log_level) diff --git a/spotdl/spotify_tools.py b/spotdl/spotify_tools.py index 2a60284..0349c0f 100644 --- a/spotdl/spotify_tools.py +++ b/spotdl/spotify_tools.py @@ -12,28 +12,40 @@ import os from spotdl import const from spotdl import internals +spotify = None + def generate_token(): - """ Generate the token. Please respect these credentials :) """ - credentials = oauth2.SpotifyClientCredentials( - client_id="4fe3fecfe5334023a1472516cc99d805", - client_secret="0f02b7c483c04257984695007a4a8d5c", - ) + """ Generate the token. """ + if const.args.spotify_client_id and const.args.spotify_client_secret: + credentials = oauth2.SpotifyClientCredentials( + client_id=const.args.spotify_client_id, + client_secret=const.args.spotify_client_secret, + ) + else: + # Please respect these credentials :) + credentials = oauth2.SpotifyClientCredentials( + client_id="4fe3fecfe5334023a1472516cc99d805", + client_secret="0f02b7c483c04257984695007a4a8d5c", + ) token = credentials.get_access_token() return token def refresh_token(): - """ Refresh expired token""" + """ Refresh expired token. """ global spotify new_token = generate_token() spotify = spotipy.Spotify(auth=new_token) -# token is mandatory when using Spotify's API -# https://developer.spotify.com/news-stories/2017/01/27/removing-unauthenticated-calls-to-the-web-api/ -_token = generate_token() -spotify = spotipy.Spotify(auth=_token) +def set_client_credentials(): + """ Setup and initialize Spotipy object. """ + # token is mandatory when using Spotify's API + # https://developer.spotify.com/news-stories/2017/01/27/removing-unauthenticated-calls-to-the-web-api/ + global spotify + token = generate_token() + spotify = spotipy.Spotify(auth=token) def generate_metadata(raw_song): From cf9b0690fd3bdbad7fb00de4b8859f446a6d08b6 Mon Sep 17 00:00:00 2001 From: Manveer Basra Date: Tue, 30 Oct 2018 14:18:40 -0400 Subject: [PATCH 4/7] Set default client id/secret in handle.py to None --- spotdl/handle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spotdl/handle.py b/spotdl/handle.py index fdfe719..ed4bdb1 100644 --- a/spotdl/handle.py +++ b/spotdl/handle.py @@ -34,8 +34,8 @@ default_conf = { "skip": None, "write-successful": None, "log-level": "INFO", - "spotify_client_id": "4fe3fecfe5334023a1472516cc99d805", - "spotify_client_secret": "0f02b7c483c04257984695007a4a8d5c" + "spotify_client_id": None, + "spotify_client_secret": None } } From c886ccf603ee8e56ff7ca769fd70558ec297b88d Mon Sep 17 00:00:00 2001 From: Manveer Basra Date: Tue, 30 Oct 2018 16:38:15 -0400 Subject: [PATCH 5/7] Refactored to pass tests --- spotdl/spotify_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spotdl/spotify_tools.py b/spotdl/spotify_tools.py index 0349c0f..1e27674 100644 --- a/spotdl/spotify_tools.py +++ b/spotdl/spotify_tools.py @@ -17,7 +17,7 @@ spotify = None def generate_token(): """ Generate the token. """ - if const.args.spotify_client_id and const.args.spotify_client_secret: + if const.args is not None and const.args.spotify_client_id and const.args.spotify_client_secret: credentials = oauth2.SpotifyClientCredentials( client_id=const.args.spotify_client_id, client_secret=const.args.spotify_client_secret, From 4fc23a84dc0b52bc3c3a7b15a4d174bc083c6355 Mon Sep 17 00:00:00 2001 From: Manveer Basra Date: Mon, 12 Nov 2018 10:49:21 -0500 Subject: [PATCH 6/7] Refactored to use spotify_tools.SpotifyAuthorize class --- spotdl/handle.py | 4 +- spotdl/spotdl.py | 2 +- spotdl/spotify_tools.py | 437 ++++++++++++++++++++-------------------- spotdl/youtube_tools.py | 1 + 4 files changed, 217 insertions(+), 227 deletions(-) diff --git a/spotdl/handle.py b/spotdl/handle.py index ed4bdb1..fdfe719 100644 --- a/spotdl/handle.py +++ b/spotdl/handle.py @@ -34,8 +34,8 @@ default_conf = { "skip": None, "write-successful": None, "log-level": "INFO", - "spotify_client_id": None, - "spotify_client_secret": None + "spotify_client_id": "4fe3fecfe5334023a1472516cc99d805", + "spotify_client_secret": "0f02b7c483c04257984695007a4a8d5c" } } diff --git a/spotdl/spotdl.py b/spotdl/spotdl.py index 18c4a1b..eb03011 100644 --- a/spotdl/spotdl.py +++ b/spotdl/spotdl.py @@ -51,7 +51,7 @@ def main(): internals.filter_path(const.args.folder) youtube_tools.set_api_key() - spotify_tools.set_client_credentials() + spotify = spotify_tools.SpotifyAuthorize() logzero.setup_default_logger(formatter=const._formatter, level=const.args.log_level) diff --git a/spotdl/spotify_tools.py b/spotdl/spotify_tools.py index 1e27674..bb62734 100644 --- a/spotdl/spotify_tools.py +++ b/spotdl/spotify_tools.py @@ -15,246 +15,235 @@ from spotdl import internals spotify = None -def generate_token(): - """ Generate the token. """ - if const.args is not None and const.args.spotify_client_id and const.args.spotify_client_secret: +# token = generate_token() +# spotify = spotipy.Spotify(auth=token) + + +class SpotifyAuthorize: + """ Class to handle all interactions with spotipy instance. """ + def __init__(self): + self.client_id = const.args.spotify_client_id + self.client_secret = const.args.spotify_client_secret + token = self.generate_token() + self.spotify = spotipy.Spotify(auth=token) + + def generate_token(self): + """ Generate the token. """ credentials = oauth2.SpotifyClientCredentials( - client_id=const.args.spotify_client_id, - client_secret=const.args.spotify_client_secret, + client_id=self.client_id, + client_secret=self.client_secret, ) - else: - # Please respect these credentials :) - credentials = oauth2.SpotifyClientCredentials( - client_id="4fe3fecfe5334023a1472516cc99d805", - client_secret="0f02b7c483c04257984695007a4a8d5c", - ) - token = credentials.get_access_token() - return token + token = credentials.get_access_token() + return token + def refresh_token(self): + """ Refresh expired token. """ + new_token = self.generate_token() + self.spotify = spotipy.Spotify(auth=new_token) -def refresh_token(): - """ Refresh expired token. """ - global spotify - new_token = generate_token() - spotify = spotipy.Spotify(auth=new_token) - - -def set_client_credentials(): - """ Setup and initialize Spotipy object. """ - # token is mandatory when using Spotify's API - # https://developer.spotify.com/news-stories/2017/01/27/removing-unauthenticated-calls-to-the-web-api/ - global spotify - token = generate_token() - spotify = spotipy.Spotify(auth=token) - - -def generate_metadata(raw_song): - """ Fetch a song's metadata from Spotify. """ - if internals.is_spotify(raw_song): - # fetch track information directly if it is spotify link - log.debug("Fetching metadata for given track URL") - meta_tags = spotify.track(raw_song) - else: - # otherwise search on spotify and fetch information from first result - log.debug('Searching for "{}" on Spotify'.format(raw_song)) - try: - meta_tags = spotify.search(raw_song, limit=1)["tracks"]["items"][0] - except IndexError: - return None - artist = spotify.artist(meta_tags["artists"][0]["id"]) - album = spotify.album(meta_tags["album"]["id"]) - - try: - meta_tags[u"genre"] = titlecase(artist["genres"][0]) - except IndexError: - meta_tags[u"genre"] = None - try: - meta_tags[u"copyright"] = album["copyrights"][0]["text"] - except IndexError: - meta_tags[u"copyright"] = None - try: - meta_tags[u"external_ids"][u"isrc"] - except KeyError: - meta_tags[u"external_ids"][u"isrc"] = None - - meta_tags[u"release_date"] = album["release_date"] - meta_tags[u"publisher"] = album["label"] - meta_tags[u"total_tracks"] = album["tracks"]["total"] - - log.debug("Fetching lyrics") - - try: - meta_tags["lyrics"] = lyricwikia.get_lyrics( - meta_tags["artists"][0]["name"], meta_tags["name"] - ) - except lyricwikia.LyricsNotFound: - meta_tags["lyrics"] = None - - # Some sugar - meta_tags["year"], *_ = meta_tags["release_date"].split("-") - meta_tags["duration"] = meta_tags["duration_ms"] / 1000.0 - meta_tags["spotify_metadata"] = True - # Remove unwanted parameters - del meta_tags["duration_ms"] - del meta_tags["available_markets"] - del meta_tags["album"]["available_markets"] - - log.debug(pprint.pformat(meta_tags)) - return meta_tags - - -def get_playlists(username): - """ Fetch user playlists when using the -u option. """ - playlists = spotify.user_playlists(username) - links = [] - check = 1 - - while True: - for playlist in playlists["items"]: - # in rare cases, playlists may not be found, so playlists['next'] - # is None. Skip these. Also see Issue #91. - if playlist["name"] is not None: - log.info( - u"{0:>5}. {1:<30} ({2} tracks)".format( - check, playlist["name"], playlist["tracks"]["total"] - ) - ) - playlist_url = playlist["external_urls"]["spotify"] - log.debug(playlist_url) - links.append(playlist_url) - check += 1 - if playlists["next"]: - playlists = spotify.next(playlists) + def generate_metadata(self, raw_song): + """ Fetch a song's metadata from Spotify. """ + if internals.is_spotify(raw_song): + # fetch track information directly if it is spotify link + log.debug("Fetching metadata for given track URL") + meta_tags = self.spotify.track(raw_song) else: - break + # otherwise search on spotify and fetch information from first result + log.debug('Searching for "{}" on Spotify'.format(raw_song)) + try: + meta_tags = self.spotify.search(raw_song, limit=1)["tracks"]["items"][0] + except IndexError: + return None + artist = self.spotify.artist(meta_tags["artists"][0]["id"]) + album = self.spotify.album(meta_tags["album"]["id"]) - return links + try: + meta_tags[u"genre"] = titlecase(artist["genres"][0]) + except IndexError: + meta_tags[u"genre"] = None + try: + meta_tags[u"copyright"] = album["copyrights"][0]["text"] + except IndexError: + meta_tags[u"copyright"] = None + try: + meta_tags[u"external_ids"][u"isrc"] + except KeyError: + meta_tags[u"external_ids"][u"isrc"] = None + meta_tags[u"release_date"] = album["release_date"] + meta_tags[u"publisher"] = album["label"] + meta_tags[u"total_tracks"] = album["tracks"]["total"] -def write_user_playlist(username, text_file=None): - links = get_playlists(username=username) - playlist = internals.input_link(links) - return write_playlist(playlist, text_file) + log.debug("Fetching lyrics") + try: + meta_tags["lyrics"] = lyricwikia.get_lyrics( + meta_tags["artists"][0]["name"], meta_tags["name"] + ) + except lyricwikia.LyricsNotFound: + meta_tags["lyrics"] = None -def fetch_playlist(playlist): - try: - playlist_id = internals.extract_spotify_id(playlist) - except IndexError: - # Wrong format, in either case - log.error("The provided playlist URL is not in a recognized format!") - sys.exit(10) - try: - results = spotify.user_playlist( - user=None, playlist_id=playlist_id, fields="tracks,next,name" - ) - except spotipy.client.SpotifyException: - log.error("Unable to find playlist") - log.info("Make sure the playlist is set to publicly visible and then try again") - sys.exit(11) + # Some sugar + meta_tags["year"], *_ = meta_tags["release_date"].split("-") + meta_tags["duration"] = meta_tags["duration_ms"] / 1000.0 + # Remove unwanted parameters + del meta_tags["duration_ms"] + del meta_tags["available_markets"] + del meta_tags["album"]["available_markets"] - return results + log.debug(pprint.pformat(meta_tags)) + return meta_tags + def write_user_playlist(self, username, text_file=None): + """ Write user playlists to text_file """ + links = self.get_playlists(username=username) + playlist = internals.input_link(links) + return self.write_playlist(playlist, text_file) -def write_playlist(playlist_url, text_file=None): - playlist = fetch_playlist(playlist_url) - tracks = playlist["tracks"] - if not text_file: - text_file = u"{0}.txt".format(slugify(playlist["name"], ok="-_()[]{}")) - filepath = os.path.join(const.args.folder if const.args.folder else "", text_file) - return write_tracks(tracks, filepath) + def get_playlists(self, username): + """ Fetch user playlists when using the -u option. """ + playlists = self.spotify.user_playlists(username) + links = [] + check = 1 - -def fetch_album(album): - album_id = internals.extract_spotify_id(album) - album = spotify.album(album_id) - return album - - -def fetch_albums_from_artist(artist_url, album_type=None): - """ - This funcction returns all the albums from a give artist_url using the US - market - :param artist_url - spotify artist url - :param album_type - the type of album to fetch (ex: single) the default is - all albums - :param return - the album from the artist - """ - - # fetching artist's albums limitting the results to the US to avoid duplicate - # albums from multiple markets - artist_id = internals.extract_spotify_id(artist_url) - results = spotify.artist_albums(artist_id, album_type=album_type, country="US") - - albums = results["items"] - - # indexing all pages of results - while results["next"]: - results = spotify.next(results) - albums.extend(results["items"]) - - return albums - - -def write_all_albums_from_artist(artist_url, text_file=None): - """ - This function gets all albums from an artist and writes it to a file in the - current working directory called [ARTIST].txt, where [ARTIST] is the artist - of the album - :param artist_url - spotify artist url - :param text_file - file to write albums to - """ - - album_base_url = "https://open.spotify.com/album/" - - # fetching all default albums - albums = fetch_albums_from_artist(artist_url, album_type=None) - - # if no file if given, the default save file is in the current working - # directory with the name of the artist - if text_file is None: - text_file = albums[0]["artists"][0]["name"] + ".txt" - - for album in albums: - # logging album name - log.info("Fetching album: " + album["name"]) - write_album(album_base_url + album["id"], text_file=text_file) - - -def write_album(album_url, text_file=None): - album = fetch_album(album_url) - tracks = spotify.album_tracks(album["id"]) - if not text_file: - text_file = u"{0}.txt".format(slugify(album["name"], ok="-_()[]{}")) - filepath = os.path.join(const.args.folder if const.args.folder else "", text_file) - return write_tracks(tracks, filepath) - - -def write_tracks(tracks, text_file): - log.info(u"Writing {0} tracks to {1}".format(tracks["total"], text_file)) - track_urls = [] - with open(text_file, "a") as file_out: while True: - for item in tracks["items"]: - if "track" in item: - track = item["track"] - else: - track = item - try: - track_url = track["external_urls"]["spotify"] - log.debug(track_url) - file_out.write(track_url + "\n") - track_urls.append(track_url) - except KeyError: - log.warning( - u"Skipping track {0} by {1} (local only?)".format( - track["name"], track["artists"][0]["name"] + for playlist in playlists["items"]: + # in rare cases, playlists may not be found, so playlists['next'] + # is None. Skip these. Also see Issue #91. + if playlist["name"] is not None: + log.info( + u"{0:>5}. {1:<30} ({2} tracks)".format( + check, playlist["name"], playlist["tracks"]["total"] ) ) - # 1 page = 50 results - # check if there are more pages - if tracks["next"]: - tracks = spotify.next(tracks) + playlist_url = playlist["external_urls"]["spotify"] + log.debug(playlist_url) + links.append(playlist_url) + check += 1 + if playlists["next"]: + playlists = self.spotify.next(playlists) else: break - return track_urls + + return links + + def fetch_playlist(self, playlist): + try: + playlist_id = internals.extract_spotify_id(playlist) + except IndexError: + # Wrong format, in either case + log.error("The provided playlist URL is not in a recognized format!") + sys.exit(10) + try: + results = self.spotify.user_playlist( + user=None, playlist_id=playlist_id, fields="tracks,next,name" + ) + except spotipy.client.SpotifyException: + log.error("Unable to find playlist") + log.info("Make sure the playlist is set to publicly visible and then try again") + sys.exit(11) + + return results + + def write_playlist(self, playlist_url, text_file=None): + playlist = self.fetch_playlist(playlist_url) + tracks = playlist["tracks"] + if not text_file: + text_file = u"{0}.txt".format(slugify(playlist["name"], ok="-_()[]{}")) + return self.write_tracks(tracks, text_file) + + def fetch_album(self, album): + album_id = internals.extract_spotify_id(album) + album = self.spotify.album(album_id) + return album + + def fetch_album_from_artist(self, artist_url, album_type="album"): + """ + This funcction returns all the albums from a give artist_url using the US + market + :param artist_url - spotify artist url + :param album_type - the type of album to fetch (ex: single) the default is + a standard album + :param return - the album from the artist + """ + + # fetching artist's albums limitting the results to the US to avoid duplicate + # albums from multiple markets + artist_id = internals.extract_spotify_id(artist_url) + results = self.spotify.artist_albums(artist_id, album_type=album_type, country="US") + + albums = results["items"] + + # indexing all pages of results + while results["next"]: + results = self.spotify.next(results) + albums.extend(results["items"]) + + return albums + + def write_all_albums_from_artist(self, artist_url, text_file=None): + """ + This function gets all albums from an artist and writes it to a file in the + current working directory called [ARTIST].txt, where [ARTIST] is the artist + of the album + :param artist_url - spotify artist url + :param text_file - file to write albums to + """ + + album_base_url = "https://open.spotify.com/album/" + + # fetching all default albums + albums = self.fetch_album_from_artist(artist_url) + + # if no file if given, the default save file is in the current working + # directory with the name of the artist + if text_file is None: + text_file = albums[0]["artists"][0]["name"] + ".txt" + + for album in albums: + # logging album name + log.info("Fetching album: " + album["name"]) + self.write_album(album_base_url + album["id"], text_file=text_file) + + # fetching all single albums + singles = self.fetch_album_from_artist(artist_url, album_type="single") + + for single in singles: + log.info("Fetching single: " + single["name"]) + self.write_album(album_base_url + single["id"], text_file=text_file) + + def write_album(self, album_url, text_file=None): + album = self.fetch_album(album_url) + tracks = self.spotify.album_tracks(album["id"]) + if not text_file: + text_file = u"{0}.txt".format(slugify(album["name"], ok="-_()[]{}")) + return self.write_tracks(tracks, text_file) + + def write_tracks(self, tracks, text_file): + log.info(u"Writing {0} tracks to {1}".format(tracks["total"], text_file)) + track_urls = [] + with open(text_file, "a") as file_out: + while True: + for item in tracks["items"]: + if "track" in item: + track = item["track"] + else: + track = item + try: + track_url = track["external_urls"]["spotify"] + log.debug(track_url) + file_out.write(track_url + "\n") + track_urls.append(track_url) + except KeyError: + log.warning( + u"Skipping track {0} by {1} (local only?)".format( + track["name"], track["artists"][0]["name"] + ) + ) + # 1 page = 50 results + # check if there are more pages + if tracks["next"]: + tracks = self.spotify.next(tracks) + else: + break + return track_urls diff --git a/spotdl/youtube_tools.py b/spotdl/youtube_tools.py index 7b81d73..2b47581 100644 --- a/spotdl/youtube_tools.py +++ b/spotdl/youtube_tools.py @@ -48,6 +48,7 @@ def go_pafy(raw_song, meta_tags=None): def match_video_and_metadata(track): """ Get and match track data from YouTube and Spotify. """ meta_tags = None + spotipy = spotify_tools.SpotifyAuthorize() def fallback_metadata(meta_tags): From e9f046bea1c57267e731a40ed3894ab9407ed252 Mon Sep 17 00:00:00 2001 From: Ritiek Malhotra Date: Tue, 26 Feb 2019 23:41:37 +0530 Subject: [PATCH 7/7] Rebase fixes --- spotdl/handle.py | 2 +- spotdl/metadata.py | 3 +- spotdl/spotify_tools.py | 18 +++------- spotdl/youtube_tools.py | 11 +++--- test/test_download_with_metadata.py | 2 +- test/test_spotify_tools.py | 53 +++++++++++++++-------------- test/test_youtube_tools.py | 2 +- 7 files changed, 43 insertions(+), 48 deletions(-) diff --git a/spotdl/handle.py b/spotdl/handle.py index fdfe719..9d92137 100644 --- a/spotdl/handle.py +++ b/spotdl/handle.py @@ -149,7 +149,7 @@ def get_arguments(raw_args=None, to_group=True, to_merge=True): "-nf", "--no-fallback-metadata", default=config["no-fallback-metadata"], - help="use YouTube metadata as fallback if track not found on Spotify", + help="do not use YouTube as fallback for metadata if track not found on Spotify", action="store_true", ) parser.add_argument( diff --git a/spotdl/metadata.py b/spotdl/metadata.py index d67fabc..aeecd90 100644 --- a/spotdl/metadata.py +++ b/spotdl/metadata.py @@ -148,7 +148,8 @@ class EmbedMetadata: def _embed_basic_metadata(self, audiofile, preset=TAG_PRESET): meta_tags = self.meta_tags audiofile[preset["artist"]] = meta_tags["artists"][0]["name"] - audiofile[preset["albumartist"]] = meta_tags["album"]["artists"][0]["name"] + if meta_tags["album"]["artists"][0]["name"]: + audiofile[preset["albumartist"]] = meta_tags["album"]["artists"][0]["name"] if meta_tags["album"]["name"]: audiofile[preset["album"]] = meta_tags["album"]["name"] audiofile[preset["title"]] = meta_tags["name"] diff --git a/spotdl/spotify_tools.py b/spotdl/spotify_tools.py index bb62734..1064b88 100644 --- a/spotdl/spotify_tools.py +++ b/spotdl/spotify_tools.py @@ -12,9 +12,6 @@ import os from spotdl import const from spotdl import internals -spotify = None - - # token = generate_token() # spotify = spotipy.Spotify(auth=token) @@ -86,6 +83,7 @@ class SpotifyAuthorize: # Some sugar meta_tags["year"], *_ = meta_tags["release_date"].split("-") meta_tags["duration"] = meta_tags["duration_ms"] / 1000.0 + meta_tags["spotify_metadata"] = True # Remove unwanted parameters del meta_tags["duration_ms"] del meta_tags["available_markets"] @@ -157,13 +155,13 @@ class SpotifyAuthorize: album = self.spotify.album(album_id) return album - def fetch_album_from_artist(self, artist_url, album_type="album"): + def fetch_albums_from_artist(self, artist_url, album_type=None): """ This funcction returns all the albums from a give artist_url using the US market :param artist_url - spotify artist url :param album_type - the type of album to fetch (ex: single) the default is - a standard album + all albums :param return - the album from the artist """ @@ -181,6 +179,7 @@ class SpotifyAuthorize: return albums + def write_all_albums_from_artist(self, artist_url, text_file=None): """ This function gets all albums from an artist and writes it to a file in the @@ -193,7 +192,7 @@ class SpotifyAuthorize: album_base_url = "https://open.spotify.com/album/" # fetching all default albums - albums = self.fetch_album_from_artist(artist_url) + albums = self.fetch_albums_from_artist(artist_url, album_type=None) # if no file if given, the default save file is in the current working # directory with the name of the artist @@ -205,13 +204,6 @@ class SpotifyAuthorize: log.info("Fetching album: " + album["name"]) self.write_album(album_base_url + album["id"], text_file=text_file) - # fetching all single albums - singles = self.fetch_album_from_artist(artist_url, album_type="single") - - for single in singles: - log.info("Fetching single: " + single["name"]) - self.write_album(album_base_url + single["id"], text_file=text_file) - def write_album(self, album_url, text_file=None): album = self.fetch_album(album_url) tracks = self.spotify.album_tracks(album["id"]) diff --git a/spotdl/youtube_tools.py b/spotdl/youtube_tools.py index 2b47581..7e6bffb 100644 --- a/spotdl/youtube_tools.py +++ b/spotdl/youtube_tools.py @@ -48,7 +48,7 @@ def go_pafy(raw_song, meta_tags=None): def match_video_and_metadata(track): """ Get and match track data from YouTube and Spotify. """ meta_tags = None - spotipy = spotify_tools.SpotifyAuthorize() + spotify = spotify_tools.SpotifyAuthorize() def fallback_metadata(meta_tags): @@ -68,13 +68,13 @@ def match_video_and_metadata(track): content = go_pafy(track, meta_tags=None) track = slugify(content.title).replace("-", " ") if not const.args.no_metadata: - meta_tags = spotify_tools.generate_metadata(track) + meta_tags = spotify.generate_metadata(track) meta_tags = fallback_metadata(meta_tags) elif internals.is_spotify(track): log.debug("Input song is a Spotify URL") # Let it generate metadata, YouTube doesn't know Spotify slang - meta_tags = spotify_tools.generate_metadata(track) + meta_tags = spotify.generate_metadata(track) content = go_pafy(track, meta_tags) if const.args.no_metadata: meta_tags = None @@ -84,7 +84,7 @@ def match_video_and_metadata(track): if const.args.no_metadata: content = go_pafy(track, meta_tags=None) else: - meta_tags = spotify_tools.generate_metadata(track) + meta_tags = spotify.generate_metadata(track) content = go_pafy(track, meta_tags=meta_tags) meta_tags = fallback_metadata(meta_tags) @@ -98,7 +98,8 @@ def generate_metadata(content): "artists": [{"name": content.author}], "duration": content.length, "external_urls": {"youtube": content.watchv_url}, - "album": {"images" : [{"url": content.getbestthumb()}], "name": None}, + "album": {"images" : [{"url": content.getbestthumb()}], + "artists": [{"name": None}],"name": None}, "year": content.published.split("-")[0], "release_date": content.published.split(" ")[0], "type": "track", diff --git a/test/test_download_with_metadata.py b/test/test_download_with_metadata.py index 9c376c0..ee85f17 100644 --- a/test/test_download_with_metadata.py +++ b/test/test_download_with_metadata.py @@ -33,7 +33,7 @@ def pytest_namespace(): @pytest.fixture(scope="module") def metadata_fixture(): - meta_tags = spotify_tools.generate_metadata(SPOTIFY_TRACK_URL) + meta_tags = spotify_tools.SpotifyAuthorize().generate_metadata(SPOTIFY_TRACK_URL) return meta_tags diff --git a/test/test_spotify_tools.py b/test/test_spotify_tools.py index 1981c20..db3b051 100644 --- a/test/test_spotify_tools.py +++ b/test/test_spotify_tools.py @@ -6,23 +6,26 @@ import loader loader.load_defaults() +@pytest.fixture(scope="module") +def spotify(): + return spotify_tools.SpotifyAuthorize() -def test_generate_token(): - token = spotify_tools.generate_token() +def test_generate_token(spotify): + token = spotify.generate_token() assert len(token) == 83 -def test_refresh_token(): - old_instance = spotify_tools.spotify - spotify_tools.refresh_token() - new_instance = spotify_tools.spotify +def test_refresh_token(spotify): + old_instance = spotify.spotify + spotify.refresh_token() + new_instance = spotify.spotify assert not old_instance == new_instance class TestGenerateMetadata: @pytest.fixture(scope="module") - def metadata_fixture(self): - metadata = spotify_tools.generate_metadata("ncs - spectre") + def metadata_fixture(self, spotify): + metadata = spotify.generate_metadata("ncs - spectre") return metadata def test_len(self, metadata_fixture): @@ -38,7 +41,7 @@ class TestGenerateMetadata: assert metadata_fixture["duration"] == 230.634 -def test_get_playlists(): +def test_get_playlists(spotify): expect_playlist_ids = [ "34gWCK8gVeYDPKcctB6BQJ", "04wTU2c2WNQG9XE5oSLYfj", @@ -50,15 +53,15 @@ def test_get_playlists(): for playlist_id in expect_playlist_ids ] - playlists = spotify_tools.get_playlists("uqlakumu7wslkoen46s5bulq0") + playlists = spotify.get_playlists("uqlakumu7wslkoen46s5bulq0") assert playlists == expect_playlists -def test_write_user_playlist(tmpdir, monkeypatch): +def test_write_user_playlist(tmpdir, spotify, monkeypatch): expect_tracks = 17 text_file = os.path.join(str(tmpdir), "test_us.txt") monkeypatch.setattr("builtins.input", lambda x: 1) - spotify_tools.write_user_playlist("uqlakumu7wslkoen46s5bulq0", text_file) + spotify.write_user_playlist("uqlakumu7wslkoen46s5bulq0", text_file) with open(text_file, "r") as f: tracks = len(f.readlines()) assert tracks == expect_tracks @@ -66,8 +69,8 @@ def test_write_user_playlist(tmpdir, monkeypatch): class TestFetchPlaylist: @pytest.fixture(scope="module") - def playlist_fixture(self): - playlist = spotify_tools.fetch_playlist( + def playlist_fixture(self, spotify): + playlist = spotify.fetch_playlist( "https://open.spotify.com/playlist/0fWBMhGh38y0wsYWwmM9Kt" ) return playlist @@ -79,10 +82,10 @@ class TestFetchPlaylist: assert playlist_fixture["tracks"]["total"] == 14 -def test_write_playlist(tmpdir): +def test_write_playlist(tmpdir, spotify): expect_tracks = 14 text_file = os.path.join(str(tmpdir), "test_pl.txt") - spotify_tools.write_playlist( + spotify.write_playlist( "https://open.spotify.com/playlist/0fWBMhGh38y0wsYWwmM9Kt", text_file ) with open(text_file, "r") as f: @@ -93,8 +96,8 @@ def test_write_playlist(tmpdir): # XXX: Mock this test off if it fails in future class TestFetchAlbum: @pytest.fixture(scope="module") - def album_fixture(self): - album = spotify_tools.fetch_album( + def album_fixture(self, spotify): + album = spotify.fetch_album( "https://open.spotify.com/album/499J8bIsEnU7DSrosFDJJg" ) return album @@ -109,14 +112,13 @@ class TestFetchAlbum: # XXX: Mock this test off if it fails in future class TestFetchAlbumsFromArtist: @pytest.fixture(scope="module") - def albums_from_artist_fixture(self): - albums = spotify_tools.fetch_albums_from_artist( + def albums_from_artist_fixture(self, spotify): + albums = spotify.fetch_albums_from_artist( "https://open.spotify.com/artist/7oPftvlwr6VrsViSDV7fJY" ) return albums def test_len(self, albums_from_artist_fixture): - # TODO: Mock this test (failed in #493) assert len(albums_from_artist_fixture) == 52 def test_zeroth_album_name(self, albums_from_artist_fixture): @@ -132,11 +134,10 @@ class TestFetchAlbumsFromArtist: assert albums_from_artist_fixture[0]["total_tracks"] == 12 -# TODO: Mock this test (failed in #493) -def test_write_all_albums_from_artist(tmpdir): +def test_write_all_albums_from_artist(tmpdir, spotify): expect_tracks = 282 text_file = os.path.join(str(tmpdir), "test_ab.txt") - spotify_tools.write_all_albums_from_artist( + spotify.write_all_albums_from_artist( "https://open.spotify.com/artist/4dpARuHxo51G3z768sgnrY", text_file ) with open(text_file, "r") as f: @@ -144,10 +145,10 @@ def test_write_all_albums_from_artist(tmpdir): assert tracks == expect_tracks -def test_write_album(tmpdir): +def test_write_album(tmpdir, spotify): expect_tracks = 15 text_file = os.path.join(str(tmpdir), "test_al.txt") - spotify_tools.write_album( + spotify.write_album( "https://open.spotify.com/album/499J8bIsEnU7DSrosFDJJg", text_file ) with open(text_file, "r") as f: diff --git a/test/test_youtube_tools.py b/test/test_youtube_tools.py index 364760e..077008b 100644 --- a/test/test_youtube_tools.py +++ b/test/test_youtube_tools.py @@ -40,7 +40,7 @@ class TestYouTubeAPIKeys: @pytest.fixture(scope="module") def metadata_fixture(): - metadata = spotify_tools.generate_metadata(TRACK_SEARCH) + metadata = spotify_tools.SpotifyAuthorize().generate_metadata(TRACK_SEARCH) return metadata