From c9bf0bc0203f03eeaf84fe39308ea3b1d197416a Mon Sep 17 00:00:00 2001 From: Ritiek Malhotra Date: Thu, 7 May 2020 19:36:38 +0530 Subject: [PATCH] De-clutter metadata search --- spotdl/command_line/lib.py | 218 ++++++-------- spotdl/helpers/__init__.py | 0 spotdl/lyrics/providers/genius.py | 23 +- spotdl/metadata/embedders/default_embedder.py | 6 +- spotdl/metadata/providers/spotify.py | 14 +- spotdl/metadata/providers/youtube.py | 9 +- spotdl/metadata_search.py | 275 ++++++++++++++++++ spotdl/util.py | 31 +- 8 files changed, 410 insertions(+), 166 deletions(-) create mode 100644 spotdl/helpers/__init__.py create mode 100644 spotdl/metadata_search.py diff --git a/spotdl/command_line/lib.py b/spotdl/command_line/lib.py index 61335ce..e553731 100644 --- a/spotdl/command_line/lib.py +++ b/spotdl/command_line/lib.py @@ -20,6 +20,7 @@ import spotdl.config from spotdl.command_line.exceptions import NoYouTubeVideoFoundError from spotdl.command_line.exceptions import NoYouTubeVideoMatchError +from spotdl.metadata_search import MetadataSearch from spotdl.helpers.spotify import SpotifyHelpers @@ -28,133 +29,7 @@ import os import urllib.request import logging -logger = logging.getLogger(name=__name__) - -def search_lyrics(query): - provider = Genius() - try: - lyrics = provider.from_query(query) - except LyricsNotFoundError: - lyrics = None - return lyrics - - -def search_metadata_on_spotify(query): - provider = ProviderSpotify() - try: - metadata = provider.from_query(query) - except SpotifyMetadataNotFoundError: - metadata = {} - return metadata - - -def prompt_for_youtube_search_result(videos): - max_index_length = len(str(len(videos))) - max_title_length = max(len(v["title"]) for v in videos) - print(" 0. Skip downloading this track", file=sys.stderr) - for index, video in enumerate(videos, 1): - vid_details = "{index:>{max_index}}. {title:<{max_title}} {url} [{duration}]".format( - index=index, - max_index=max_index_length, - title=video["title"], - max_title=max_title_length, - url=video["url"], - duration=video["duration"], - ) - print(vid_details, file=sys.stderr) - print("", file=sys.stderr) - - selection = spotdl.util.prompt_user_for_selection(range(1, len(videos)+1)) - - if selection is None: - return None - return videos[selection-1] - - -def search_metadata(track, search_format="{artist} - {track-name} lyrics", manual=False): - # TODO: Clean this function - youtube = ProviderYouTube() - youtube_searcher = YouTubeSearch() - - if spotdl.util.is_spotify(track): - logger.debug("Input track is a Spotify URL.") - spotify = ProviderSpotify() - spotify_metadata = spotify.from_url(track) - lyric_query = spotdl.metadata.format_string( - "{artist} - {track-name}", - spotify_metadata, - ) - search_query = spotdl.metadata.format_string(search_format, spotify_metadata) - youtube_videos = youtube_searcher.search(search_query) - if not youtube_videos: - raise NoYouTubeVideoFoundError( - 'YouTube returned no videos for the search query "{}".'.format(search_query) - ) - if manual: - youtube_video = prompt_for_youtube_search_result(youtube_videos) - else: - youtube_video = youtube_videos.bestmatch() - - if youtube_video is None: - raise NoYouTubeVideoMatchError( - 'No matching videos found on YouTube for the search query "{}".'.format( - search_query - ) - ) - - youtube_metadata = youtube.from_url(youtube_video["url"]) - metadata = spotdl.util.merge( - youtube_metadata, - spotify_metadata - ) - - elif spotdl.util.is_youtube(track): - logger.debug("Input track is a YouTube URL.") - metadata = youtube.from_url(track) - lyric_query = spotdl.metadata.format_string( - "{artist} - {track-name}", - metadata, - ) - - else: - logger.debug("Input track is a search query.") - search_query = track - lyric_query = search_query - spotify_metadata = spotdl.util.ThreadWithReturnValue( - target=search_metadata_on_spotify, - args=(search_query,) - ) - spotify_metadata.start() - youtube_videos = youtube_searcher.search(search_query) - if not youtube_videos: - raise NoYouTubeVideoFoundError( - 'YouTube returned no videos for the search query "{}".'.format(search_query) - ) - if manual: - youtube_video = prompt_for_youtube_search_result(youtube_videos) - else: - youtube_video = youtube_videos.bestmatch() - - if youtube_video is None: - raise NoYouTubeVideoMatchError( - 'No apparent matching videos found on YouTube for the search query "{}"'.format( - search_query - ) - ) - - youtube_metadata = youtube.from_url(youtube_video["url"]) - metadata = spotdl.util.merge( - youtube_metadata, - spotify_metadata.join() - ) - - metadata["lyrics"] = spotdl.util.ThreadWithReturnValue( - target=search_lyrics, - args=(lyric_query,) - ) - - metadata["lyrics"].start() - return metadata +logger = logging.getLogger(__name__) class Spotdl: @@ -200,13 +75,14 @@ class Spotdl: self.download_track(track) elif self.arguments["list"]: if self.arguments["write_m3u"]: - youtube_tools.generate_m3u( - track_file=self.arguments["list"] + self.write_m3u( + self.arguments["list"], + self.arguments["write_to"] ) else: list_download = { "synchronous": self.download_tracks_from_file, - "threaded" : self.download_tracks_from_file_threaded, + # "threaded" : self.download_tracks_from_file_threaded, }[self.arguments["processor"]] list_download( @@ -226,14 +102,75 @@ class Spotdl: playlist = spotify_tools.fetch_playlist(playlist_url) spotify_tools.write_playlist_tracks(playlist, self.arguments["write_to"]) + def write_m3u(self, track_file, target_file=None): + with open(track_file, "r") as fin: + tracks = fin.read().splitlines() + + logger.info( + "Checking and removing any duplicate tracks in {}.".format(track_file) + ) + # Remove duplicates and empty elements + # Also strip whitespaces from elements (if any) + spotdl.util.remove_duplicates( + tracks, + condition=lambda x: x, + operation=str.strip + ) + + if target_file is None: + target_file = "{}.m3u".format(track_file.split(".")[0]) + + total_tracks = len(tracks) + logger.info("Generating {0} from {1} YouTube URLs".format(target_file, total_tracks)) + with open(target_file, "w") as output_file: + output_file.write("#EXTM3U\n\n") + + youtube_searcher = YouTubeSearch() + videos = [] + for n, track in enumerate(tracks, 1): + try: + search_results = youtube_searcher.search(search_query) + if not search_results: + raise NoYouTubeVideoFoundError( + 'YouTube returned no videos for the search query "{}".'.format(search_query) + ) + video = search_results.bestmatch() + if not video: + raise NoYouTubeVideoMatchError( + 'No matching videos found on YouTube for the search query "{}".'.format(search_query) + ) + except (NoYouTubeVideoFoundError, NoYouTubeVideoMatchError) as e: + logger.error(e.args[0]) + else: + print(video) + exit() + logger.info( + "Matched track {0}/{1} ({2})".format( + str(n).zfill(len(str(total_tracks))), + total_tracks, + content.watchv_url + ) + ) + logger.debug(track) + m3u_key = "#EXTINF:{duration},{title}\n{youtube_url}\n".format( + duration=internals.get_sec(content.duration), + title=content.title, + youtube_url=content.watchv_url, + ) + logger.debug(m3u_key) + with open(target_file, "a") as output_file: + output_file.write(m3u_key) + def download_track(self, track): logger.info('Downloading "{}"'.format(track)) + search_metadata = MetadataSearch( + track, + lyrics=True, + yt_search_format=self.arguments["search_format"], + yt_manual=self.arguments["manual"] + ) try: - metadata = search_metadata( - track, - search_format=self.arguments["search_format"], - manual=self.arguments["manual"], - ) + metadata = search_metadata.on_youtube_and_spotify() except (NoYouTubeVideoFoundError, NoYouTubeVideoMatchError) as e: logger.error(e.args[0]) else: @@ -316,7 +253,6 @@ class Spotdl: "Checking and removing any duplicate tracks in {}.".format(path) ) with open(path, "r") as fin: - # Read tracks into a list and remove any duplicates tracks = fin.read().splitlines() # Remove duplicates and empty elements @@ -334,13 +270,19 @@ class Spotdl: print("", file=sys.stderr) for number, track in enumerate(tracks, 1): + search_metadata = MetadataSearch( + track, + lyrics=True, + yt_search_format=self.arguments["search_format"], + yt_manual=self.arguments["manual"] + ) try: log_track_query = '{position}. Downloading "{track}"'.format( position=number, track=track ) logger.info(log_track_query) - metadata = search_metadata(track, self.arguments["search_format"]) + metadata = search_metadata.on_youtube_and_spotify() self.download_track_from_metadata(metadata) except (urllib.request.URLError, TypeError, IOError) as e: logger.exception(e.args[0]) @@ -360,6 +302,7 @@ class Spotdl: fout.writelines(tracks[number-1:]) print("", file=sys.stderr) + """ def download_tracks_from_file_threaded(self, path): # FIXME: Can we make this function cleaner? @@ -430,4 +373,5 @@ class Spotdl: current_iteration += 1 with open(path, "w") as fout: fout.writelines(tracks) + """ diff --git a/spotdl/helpers/__init__.py b/spotdl/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/spotdl/lyrics/providers/genius.py b/spotdl/lyrics/providers/genius.py index 10c579e..3ba7581 100644 --- a/spotdl/lyrics/providers/genius.py +++ b/spotdl/lyrics/providers/genius.py @@ -5,6 +5,9 @@ import json from spotdl.lyrics.lyric_base import LyricBase from spotdl.lyrics.exceptions import LyricsNotFoundError +import logging +logger = logging.getLogger(__name__) + BASE_URL = "https://genius.com" BASE_SEARCH_URL = BASE_URL + "/api/search/multi?per_page=1&q=" @@ -40,7 +43,7 @@ class Genius(LyricBase): response = urllib.request.urlopen(request, timeout=timeout) except urllib.request.HTTPError: raise LyricsNotFoundError( - "Could not find lyrics at URL: {}".format(url) + "Could not find Genius lyrics at URL: {}".format(url) ) else: return response.read() @@ -55,7 +58,7 @@ class Genius(LyricBase): return lyrics_paragraph.get_text() else: raise LyricsNotFoundError( - "The lyrics for this track are yet to be released." + "The lyrics for this track are yet to be released on Genius." ) def _fetch_search_page(self, url, timeout=None): @@ -68,7 +71,7 @@ class Genius(LyricBase): metadata = json.loads(response.read()) if len(metadata["response"]["sections"][0]["hits"]) == 0: raise LyricsNotFoundError( - "Could not find any search results for URL: {}".format(url) + "Genius returned no lyric results for the search URL: {}".format(url) ) return metadata @@ -91,8 +94,8 @@ class Genius(LyricBase): if lyric_url is None: raise LyricsNotFoundError( - "Could not find any valid lyric paths in the " - "API response for the query {}".format(query) + "Could not find any valid lyric paths in Genius " + "lyrics API response for the query {}.".format(query) ) return self.base_url + lyric_url @@ -102,8 +105,14 @@ class Genius(LyricBase): Returns the lyric string for the track best matching the given query. """ - lyric_url = self.best_matching_lyric_url_from_query(query) - return self.from_url(lyric_url, linesep, timeout=timeout) + try: + lyric_url = self.best_matching_lyric_url_from_query(query) + except LyricsNotFoundError: + raise LyricsNotFoundError( + 'Genius returned no lyric results for the search query "{}".'.format(query) + ) + else: + return self.from_url(lyric_url, linesep, timeout=timeout) def from_artist_and_track(self, artist, track, linesep="\n", timeout=None): """ diff --git a/spotdl/metadata/embedders/default_embedder.py b/spotdl/metadata/embedders/default_embedder.py index 43dc0e9..7e6a2b4 100644 --- a/spotdl/metadata/embedders/default_embedder.py +++ b/spotdl/metadata/embedders/default_embedder.py @@ -50,7 +50,7 @@ class EmbedderDefault(EmbedderBase): def as_mp3(self, path, metadata, cached_albumart=None): """ Embed metadata to MP3 files. """ - logger.debug('Writing MP3 metadata to "{path}"'.format(path=path)) + logger.debug('Writing MP3 metadata to "{path}".'.format(path=path)) # EasyID3 is fun to use ;) # For supported easyid3 tags: # https://github.com/quodlibet/mutagen/blob/master/mutagen/easyid3.py @@ -108,7 +108,7 @@ class EmbedderDefault(EmbedderBase): def as_m4a(self, path, metadata, cached_albumart=None): """ Embed metadata to M4A files. """ - logger.debug('Writing M4A metadata to "{path}"'.format(path=path)) + logger.debug('Writing M4A metadata to "{path}".'.format(path=path)) audiofile = MP4(path) self._embed_basic_metadata(audiofile, metadata, "m4a", preset=M4A_TAG_PRESET) if metadata["year"]: @@ -132,7 +132,7 @@ class EmbedderDefault(EmbedderBase): audiofile.save() def as_flac(self, path, metadata, cached_albumart=None): - logger.debug('Writing FLAC metadata to "{path}"'.format(path=path)) + logger.debug('Writing FLAC metadata to "{path}".'.format(path=path)) audiofile = FLAC(path) self._embed_basic_metadata(audiofile, metadata, "flac") if metadata["year"]: diff --git a/spotdl/metadata/providers/spotify.py b/spotdl/metadata/providers/spotify.py index 9d72ed6..c72d18e 100644 --- a/spotdl/metadata/providers/spotify.py +++ b/spotdl/metadata/providers/spotify.py @@ -5,6 +5,7 @@ from spotdl.metadata import ProviderBase from spotdl.metadata.exceptions import SpotifyMetadataNotFoundError from spotdl.authorize.services import AuthorizeSpotify +import spotdl.util import logging logger = logging.getLogger(__name__) @@ -26,15 +27,18 @@ class ProviderSpotify(ProviderBase): return self.metadata_to_standard_form(metadata) def from_query(self, query): - tracks = self.spotify.search(query, limit=1)["tracks"]["items"] + tracks = self.search(query)["tracks"]["items"] if not tracks: raise SpotifyMetadataNotFoundError( - 'Could not find any tracks matching the given search query ("{}")'.format( + 'Spotify returned no tracks for the search query "{}".'.format( query, ) ) return self.metadata_to_standard_form(tracks[0]) + def search(self, query): + return self.spotify.search(query) + def _generate_token(self, client_id, client_secret): """ Generate the token. """ credentials = oauth2.SpotifyClientCredentials( @@ -43,15 +47,12 @@ class ProviderSpotify(ProviderBase): token = credentials.get_access_token() return token - def _titlecase(self, string): - return " ".join(word.capitalize() for word in string.split()) - def metadata_to_standard_form(self, metadata): artist = self.spotify.artist(metadata["artists"][0]["id"]) album = self.spotify.album(metadata["album"]["id"]) try: - metadata[u"genre"] = self._titlecase(artist["genres"][0]) + metadata[u"genre"] = spotdl.util.titlecase(artist["genres"][0]) except IndexError: metadata[u"genre"] = None try: @@ -78,3 +79,4 @@ class ProviderSpotify(ProviderBase): del metadata["album"]["available_markets"] return metadata + diff --git a/spotdl/metadata/providers/youtube.py b/spotdl/metadata/providers/youtube.py index e7d4fd4..aa58c8b 100644 --- a/spotdl/metadata/providers/youtube.py +++ b/spotdl/metadata/providers/youtube.py @@ -108,10 +108,6 @@ class YouTubeSearch: logger.debug('Fetching YouTube results for "{}".'.format(search_url)) html = self._fetch_response_html(search_url) videos = self._fetch_search_results(html) - # print(html) - # print("") - # print(videos) - # exit() return YouTubeVideos(videos) @@ -198,7 +194,7 @@ class YouTubeStreams(StreamsBase): class ProviderYouTube(ProviderBase): def from_query(self, query): - watch_urls = YouTubeSearch().search(query) + watch_urls = self.search(query) if not watch_urls: raise YouTubeMetadataNotFoundError( 'YouTube returned nothing for the given search ' @@ -214,6 +210,9 @@ class ProviderYouTube(ProviderBase): def from_pytube_object(self, content): return self.metadata_to_standard_form(content) + def search(self, query): + return YouTubeSearch().search(query) + def _fetch_publish_date(self, content): # FIXME: This needs to be supported in PyTube itself # See https://github.com/nficano/pytube/issues/595 diff --git a/spotdl/metadata_search.py b/spotdl/metadata_search.py new file mode 100644 index 0000000..54c74c3 --- /dev/null +++ b/spotdl/metadata_search.py @@ -0,0 +1,275 @@ +from spotdl.metadata.providers import ProviderSpotify +from spotdl.metadata.providers import ProviderYouTube +from spotdl.lyrics.providers import Genius +from spotdl.lyrics.exceptions import LyricsNotFoundError + +import spotdl.metadata +import spotdl.util +from spotdl.metadata.exceptions import SpotifyMetadataNotFoundError + +from spotdl.command_line.exceptions import NoYouTubeVideoFoundError +from spotdl.command_line.exceptions import NoYouTubeVideoMatchError + +import sys +import logging +logger = logging.getLogger(__name__) + +PROVIDERS = { + "spotify": ProviderSpotify, + "youtube": ProviderYouTube, +} + + +def prompt_for_youtube_search_result(videos): + max_index_length = len(str(len(videos))) + max_title_length = max(len(v["title"]) for v in videos) + print(" 0. Skip downloading this track", file=sys.stderr) + for index, video in enumerate(videos, 1): + vid_details = "{index:>{max_index}}. {title:<{max_title}}\n{new_line_gap} {url} [{duration}]".format( + index=index, + max_index=max_index_length, + title=video["title"], + max_title=max_title_length, + new_line_gap=" " * max_index_length, + url=video["url"], + duration=video["duration"], + ) + print(vid_details, file=sys.stderr) + print("", file=sys.stderr) + + selection = spotdl.util.prompt_user_for_selection(range(1, len(videos)+1)) + + if selection is None: + return None + return videos[selection-1] + + +class MetadataSearch: + def __init__(self, track, lyrics=False, yt_search_format="{artist} - {track-name}", yt_manual=False, providers=PROVIDERS): + self.track = track + self.track_type = spotdl.util.track_type(track) + self.lyrics = lyrics + self.yt_search_format = yt_search_format + self.yt_manual = yt_manual + self.providers = {} + for provider, parent in providers.items(): + self.providers[provider] = parent() + self.lyric_provider = Genius() + + def get_lyrics(self, query): + try: + lyrics = self.lyric_provider.from_query(query) + except LyricsNotFoundError as e: + logger.warning(e.args[0]) + lyrics = None + return lyrics + + def _make_lyric_search_query(self, metadata): + if self.track_type == "query": + lyric_query = self.track + else: + lyric_search_format = "{artist} - {track-name}" + lyric_query = spotdl.metadata.format_string( + lyric_search_format, + metadata + ) + return lyric_query + + def on_youtube_and_spotify(self): + track_type_mapper = { + "spotify": self._on_youtube_and_spotify_for_type_spotify, + "youtube": self._on_youtube_and_spotify_for_type_youtube, + "query": self._on_youtube_and_spotify_for_type_query, + } + caller = track_type_mapper[self.track_type] + metadata = caller() + + if not self.lyrics: + return metadata + + lyric_query = self._make_lyric_search_query(metadata) + metadata["lyrics"] = spotdl.util.ThreadWithReturnValue( + target=self.get_lyrics, + args=(lyric_query,), + ) + metadata["lyrics"].start() + + return metadata + + def on_youtube(self): + track_type_mapper = { + "spotify": self._on_youtube_for_type_spotify, + "youtube": self._on_youtube_for_type_youtube, + "query": self._on_youtube_for_type_query, + } + caller = track_type_mapper[self.track_type] + metadata = caller(self.track) + + if not self.lyrics: + return metadata + + lyric_query = self._make_lyric_search_query(metadata) + metadata["lyrics"] = spotdl.util.ThreadWithReturnValue( + target=self.get_lyrics, + arguments=(lyric_query,), + ) + metadata["lyrics"].start() + + return metadata + + def on_spotify(self): + track_type_mapper = { + "spotify": self._on_spotify_for_type_spotify, + "youtube": self._on_spotify_for_type_youtube, + "query": self._on_spotify_for_type_query, + } + caller = track_type_mapper[self.track_type] + metadata = caller(self.track) + + if not self.lyrics: + return metadata + + lyric_query = self._make_lyric_search_query(metadata) + metadata["lyrics"] = spotdl.util.ThreadWithReturnValue( + target=self.get_lyrics, + arguments=(lyric_query,), + ) + metadata["lyrics"].start() + + return metadata + + def _on_youtube_and_spotify_for_type_spotify(self): + logger.debug("Extracting YouTube and Spotify metadata for input Spotify URI.") + spotify_metadata = self._on_spotify_for_type_spotify(self.track) + lyric_query = spotdl.metadata.format_string( + "{artist} - {track-name}", + spotify_metadata, + ) + search_query = spotdl.metadata.format_string(self.yt_search_format, spotify_metadata) + videos = self.providers["youtube"].search(search_query) + if not videos: + raise NoYouTubeVideoFoundError( + 'YouTube returned no videos for the search query "{}".'.format(search_query) + ) + if self.yt_manual: + youtube_video = prompt_for_youtube_search_result(videos) + else: + youtube_video = videos.bestmatch() + + if youtube_video is None: + raise NoYouTubeVideoMatchError( + 'No matching videos found on YouTube for the search query "{}".'.format( + search_query + ) + ) + + youtube_metadata = self.providers["youtube"].from_url(youtube_video["url"]) + metadata = spotdl.util.merge( + youtube_metadata, + spotify_metadata + ) + return metadata + + def _on_youtube_and_spotify_for_type_youtube(self): + logger.debug("Extracting YouTube and Spotify metadata for input YouTube URL.") + youtube_metadata = self._on_youtube_for_type_youtube(self.track) + search_query = spotdl.metadata.format_string(self.yt_search_format, youtube_metadata) + spotify_metadata = self._on_spotify_for_type_query(search_query) + metadata = spotdl.util.merge( + youtube_metadata, + spotify_metadata + ) + return metadata + + def _on_youtube_and_spotify_for_type_query(self): + logger.debug("Extracting YouTube and Spotify metadata for input track query.") + search_query = self.track + lyric_query = search_query + # Make use of threads here to search on both YouTube & Spotify + # at the same time. + spotify_metadata = spotdl.util.ThreadWithReturnValue( + target=self._on_spotify_for_type_query, + args=(search_query,) + ) + spotify_metadata.start() + youtube_metadata = self._on_youtube_for_type_query(search_query) + metadata = spotdl.util.merge( + youtube_metadata, + spotify_metadata.join() + ) + return metadata + + def _on_youtube_for_type_spotify(self): + logger.debug("Extracting YouTube metadata for input Spotify URI.") + spotify_metadata = self._on_spotify_for_type_spotify(self.track) + lyric_query = spotdl.metadata.format_string( + "{artist} - {track-name}", + spotify_metadata, + ) + search_query = spotdl.metadata.format_string(self.yt_search_format, spotify_metadata) + videos = self.providers["youtube"].search(search_query) + if not videos: + raise NoYouTubeVideoFoundError( + 'YouTube returned no videos for the search query "{}".'.format(search_query) + ) + if self.yt_manual: + youtube_video = prompt_for_youtube_search_result(videos) + else: + youtube_video = videos.bestmatch() + + if youtube_video is None: + raise NoYouTubeVideoMatchError( + 'No matching videos found on YouTube for the search query "{}".'.format( + search_query + ) + ) + + youtube_metadata = self.providers["youtube"].from_url(youtube_video["url"]) + return youtube_metadata + + def _on_youtube_for_type_youtube(self, url): + logger.debug("Extracting YouTube metadata for input YouTube URL.") + youtube_metadata = self.providers["youtube"].from_url(url) + return youtube_metadata + + def _on_youtube_for_type_query(self, query): + logger.debug("Extracting YouTube metadata for input track query.") + videos = self.providers["youtube"].search(query) + if not videos: + raise NoYouTubeVideoFoundError( + 'YouTube returned no videos for the search query "{}".'.format(query) + ) + if self.yt_manual: + youtube_video = prompt_for_youtube_search_result(videos) + else: + youtube_video = videos.bestmatch() + if youtube_video is None: + raise NoYouTubeVideoMatchError( + 'No matching videos found on YouTube for the search query "{}".'.format( + query + ) + ) + youtube_metadata = self.providers["youtube"].from_url(youtube_video["url"]) + return youtube_metadata + + def _on_spotify_for_type_youtube(self, url): + logger.debug("Extracting Spotify metadata for input YouTube URL.") + youtube_metadata = self.providers["youtube"].from_url(url) + search_query = spotdl.metadata.format_string(self.yt_search_format, youtube_metadata) + spotify_metadata = self.providers["spotify"].from_query(search_query) + return spotify_metadata + + def _on_spotify_for_type_spotify(self, url): + logger.debug("Extracting Spotify metadata for input Spotify URI.") + spotify_metadata = self.providers["spotify"].from_url(url) + return spotify_metadata + + def _on_spotify_for_type_query(self, query): + logger.debug("Extracting Spotify metadata for input track query.") + try: + spotify_metadata = self.providers["spotify"].from_query(query) + except SpotifyMetadataNotFoundError as e: + logger.warn(e.args[0]) + spotify_metadata = {} + return spotify_metadata + diff --git a/spotdl/util.py b/spotdl/util.py index 647ae69..008abe7 100644 --- a/spotdl/util.py +++ b/spotdl/util.py @@ -66,22 +66,33 @@ def prompt_user_for_selection(items): logger.warning("Choose a valid number!") -def is_spotify(raw_song): +def is_spotify(track): """ Check if the input song is a Spotify link. """ - status = len(raw_song) == 22 and raw_song.replace(" ", "%20") == raw_song - status = status or raw_song.find("spotify") > -1 + status = len(track) == 22 and track.replace(" ", "%20") == track + status = status or track.find("spotify") > -1 return status -def is_youtube(raw_song): +def is_youtube(track): """ Check if the input song is a YouTube link. """ - status = len(raw_song) == 11 and raw_song.replace(" ", "%20") == raw_song - status = status and not raw_song.lower() == raw_song - status = status or "youtube.com/watch?v=" in raw_song + status = len(track) == 11 and track.replace(" ", "%20") == track + status = status and not track.lower() == track + status = status or "youtube.com/watch?v=" in track return status -def sanitize(string, ok="-_()[]{}", spaces_to_underscores=False): +def track_type(track): + track_types = { + "spotify": is_spotify, + "youtube": is_youtube, + } + for provider, fn in track_types.items(): + if fn(track): + return provider + return "query" + + +def sanitize(string, ok="&-_()[]{}", spaces_to_underscores=False): """ Generate filename of the song to be downloaded. """ if spaces_to_underscores: string = string.replace(" ", "_") @@ -197,3 +208,7 @@ def content_available(url): else: return response.getcode() < 300 + +def titlecase(string): + return " ".join(word.capitalize() for word in string.split()) +