From c3b1dad8520ed3b1fe105c58a572e91b4c8b070b Mon Sep 17 00:00:00 2001 From: kuba Date: Mon, 23 Jan 2023 16:44:25 +0100 Subject: [PATCH] rewritten most of the song list class --- spotdl/download/downloader.py | 4 +- spotdl/types/album.py | 109 ++++++++++----------- spotdl/types/artist.py | 174 +++++++--------------------------- spotdl/types/playlist.py | 128 ++++++++++++------------- spotdl/types/saved.py | 100 +++++++++---------- spotdl/types/song.py | 166 ++++++++++++-------------------- spotdl/utils/search.py | 126 +++++------------------- 7 files changed, 277 insertions(+), 530 deletions(-) diff --git a/spotdl/download/downloader.py b/spotdl/download/downloader.py index 11d5c551d..e026cef2d 100644 --- a/spotdl/download/downloader.py +++ b/spotdl/download/downloader.py @@ -384,7 +384,9 @@ def search_and_download(self, song: Song) -> Tuple[Song, Optional[Path]]: # and that the song object is not a placeholder # If it's None extract the current metadata # And reinitialize the song object - if song.name is None and song.url: + # Force song reinitialization if we are fetching albums + # they have most metadata but not all + if (song.name is None and song.url) or self.settings["fetch_albums"]: song = reinit_song(song, self.settings["playlist_numbering"]) # Initalize the progress tracker diff --git a/spotdl/types/album.py b/spotdl/types/album.py index 256b9e0c1..adbc65051 100644 --- a/spotdl/types/album.py +++ b/spotdl/types/album.py @@ -3,9 +3,9 @@ """ from dataclasses import dataclass -from typing import Any, Dict, List +from typing import Any, Dict, List, Tuple -from spotdl.types.song import SongList +from spotdl.types.song import Song, SongList from spotdl.utils.spotify import SpotifyClient __all__ = ["Album", "AlbumError"] @@ -25,43 +25,32 @@ class Album(SongList): artist: Dict[str, Any] - @classmethod - def search(cls, search_term: str): - """ - Searches for Album from a search term. - - ### Arguments - - search_term: The search term to use. - - ### Returns - - The raw search results - """ - - spotify_client = SpotifyClient() - raw_search_results = spotify_client.search(search_term, type="album") - - if ( - raw_search_results is None - or len(raw_search_results.get("albums", {}).get("items", [])) == 0 - ): - raise AlbumError("No album matches found on spotify") - - return raw_search_results - @staticmethod - def get_urls(url: str) -> List[str]: + def get_metadata(url: str) -> Tuple[Dict[str, Any], List[Song]]: """ - Get urls for all songs in album. + Get metadata for album. ### Arguments - url: The URL of the album. ### Returns - - A list of urls. + - A dictionary with metadata. """ spotify_client = SpotifyClient() + album_metadata = spotify_client.album(url) + if album_metadata is None: + raise AlbumError( + "Couldn't get metadata, check if you have passed correct album id" + ) + + metadata = { + "name": album_metadata["name"], + "artist": album_metadata["artists"][0], + "url": url, + } + album_response = spotify_client.album_tracks(url) if album_response is None: raise AlbumError( @@ -83,34 +72,40 @@ def get_urls(url: str) -> List[str]: if album_response is None: raise AlbumError(f"Failed to get album response: {url}") - return [ - track["external_urls"]["spotify"] - for track in tracks - if track and track.get("id") - ] - - @staticmethod - def get_metadata(url: str) -> Dict[str, Any]: - """ - Get metadata for album. - - ### Arguments - - url: The URL of the album. - - ### Returns - - A dictionary with metadata. - """ - - spotify_client = SpotifyClient() - - album_metadata = spotify_client.album(url) - if album_metadata is None: - raise AlbumError( - "Couldn't get metadata, check if you have passed correct album id" + songs = [] + for track in tracks: + if track["is_local"]: + continue + + release_date = album_metadata["release_date"] + artists = artists = [artist["name"] for artist in track["artists"]] + + song = Song.from_missing_data( + name=track["name"], + artists=artists, + artist=artists[0], + album_id=album_metadata["id"], + album_name=album_metadata["name"], + album_artist=album_metadata["artists"][0]["name"], + disc_number=track["disc_number"], + disc_count=int(album_metadata["tracks"]["items"][-1]["disc_number"]), + duration=track["duration_ms"], + year=release_date[:4], + date=release_date, + track_number=track["track_number"], + tracks_count=album_metadata["total_tracks"], + song_id=track["id"], + explicit=track["explicit"], + publisher=album_metadata["label"], + url=track["external_urls"]["spotify"], + cover_url=max( + album_metadata["images"], key=lambda i: i["width"] * i["height"] + ), + copyright_text=album_metadata["copyrights"][0]["text"] + if album_metadata["copyrights"] + else None, ) - return { - "name": album_metadata["name"], - "artist": album_metadata["artists"][0], - "url": url, - } + songs.append(song) + + return metadata, songs diff --git a/spotdl/types/artist.py b/spotdl/types/artist.py index 163f7501d..5d78aaec3 100644 --- a/spotdl/types/artist.py +++ b/spotdl/types/artist.py @@ -3,7 +3,7 @@ """ from dataclasses import dataclass -from typing import Any, Dict, List, Set +from typing import Any, Dict, List, Set, Tuple from spotdl.types.album import Album from spotdl.types.song import Song, SongList @@ -30,130 +30,29 @@ class Artist(SongList): genres: List[str] albums: List[Album] - @classmethod - def from_url(cls, url: str) -> "Artist": + @staticmethod + def get_metadata(url: str) -> Tuple[Dict[str, Any], List[Song]]: """ - Creates an Artist object from a URL. + Get metadata for artist. ### Arguments - url: The URL of the artist. ### Returns - - The Artist object. + - Dict with metadata for artist. """ - if "open.spotify.com" not in url or "artist" not in url: - raise ArtistError(f"Invalid URL: {url}") - - metadata = Artist.get_metadata(url) - album_urls = cls.get_albums(url) + # query spotify for artist details + spotify_client = SpotifyClient() - tracks: List[Song] = [] - albums: List[Album] = [] + # get artist info + raw_artist_meta = spotify_client.artist(url) - # get artist tracks - # same as above, but for tracks - known_tracks: Set[str] = set() - if len(album_urls) < 1: + if raw_artist_meta is None: raise ArtistError( - "Couldn't get albums, check if you have passed correct artist id" + "Couldn't get metadata, check if you have passed correct artist id" ) - # get all tracks from all albums - # ignore duplicates - urls = [] - for album_url in album_urls: - album = Album.from_url(album_url) - albums.append(album) - for track in album.songs: - track_name = slugify(track.name) # type: ignore - if track_name not in known_tracks: - tracks.append(track) - urls.append(track.url) - known_tracks.add(track_name) - - return cls( - **metadata, - songs=tracks, - albums=albums, - urls=urls, - ) - - @classmethod - def search(cls, search_term: str): - """ - Searches for Artist from a search term. - - ### Arguments - - search_term: The search term to use. - - ### Returns - - The raw search results - """ - - spotify_client = SpotifyClient() - raw_search_results = spotify_client.search(search_term, type="artist") - - if ( - raw_search_results is None - or len(raw_search_results.get("artists", {}).get("items", [])) == 0 - ): - raise ArtistError("No artist matches found on spotify") - - return raw_search_results - - @staticmethod - def get_urls(url: str) -> List[str]: - """ - Get urls for all songs for artist. - - ### Arguments - - url: The URL of the artist. - - ### Returns - - List of urls for all songs for artist. - """ - - albums = Artist.get_albums(url) - - urls = [] - for album in albums: - urls.extend(Album.get_urls(album)) - - return urls - - @classmethod - def create_basic_list(cls, url: str) -> "Artist": - """ - Create a basic list with only the required metadata and urls. - - ### Arguments - - url: The url of the list. - - ### Returns - - The SongList object. - """ - - metadata = Artist.get_metadata(url) - urls = Artist.get_urls(url) - - return cls(**metadata, urls=urls, songs=[], albums=[]) - - @staticmethod - def get_albums(url: str) -> List[str]: - """ - Returns a list with album urls. - - ### Arguments - - url: The URL of the artist. - - ### Returns - - List of album urls. - """ - - # query spotify for artist details - spotify_client = SpotifyClient() - artist_albums = spotify_client.artist_albums(url, album_type="album,single") # check if there is response @@ -165,10 +64,10 @@ def get_albums(url: str) -> List[str]: # get artist albums and remove duplicates # duplicates can occur if the artist has the same album available in # different countries - albums: Set[str] = set() + albums: List[str] = [] known_albums: Set[str] = set() for album in artist_albums["items"]: - albums.add(album["external_urls"]["spotify"]) + albums.append(album["external_urls"]["spotify"]) known_albums.add(slugify(album["name"])) # Fetch all artist albums @@ -181,37 +80,28 @@ def get_albums(url: str) -> List[str]: album_name = slugify(album["name"]) if album_name not in known_albums: - albums.add(album["external_urls"]["spotify"]) - + albums.append(album["external_urls"]["spotify"]) known_albums.add(album_name) - return list(albums) - - @staticmethod - def get_metadata(url: str) -> Dict[str, Any]: - """ - Get metadata for artist. - - ### Arguments - - url: The URL of the artist. - - ### Returns - - Dict with metadata for artist. - """ - - # query spotify for artist details - spotify_client = SpotifyClient() - - # get artist info - raw_artist_meta = spotify_client.artist(url) - - if raw_artist_meta is None: - raise ArtistError( - "Couldn't get metadata, check if you have passed correct artist id" - ) - - return { + songs = [] + for album in albums: + album_obj = Album.from_url(album, fetch_songs=False) + songs.extend(album_obj.songs) + + # Very aggressive deduplication + songs_list = [] + songs_names = set() + for song in songs: + slug_name = slugify(song.name) + if song.name not in songs_names: + songs_list.append(song) + songs_names.add(slug_name) + + metadata = { "name": raw_artist_meta["name"], "genres": raw_artist_meta["genres"], "url": url, + "albums": albums, } + + return metadata, songs_list diff --git a/spotdl/types/playlist.py b/spotdl/types/playlist.py index 823855f3d..f42445df0 100644 --- a/spotdl/types/playlist.py +++ b/spotdl/types/playlist.py @@ -3,9 +3,9 @@ """ from dataclasses import dataclass -from typing import Any, Dict, List +from typing import Any, Dict, List, Tuple -from spotdl.types.song import SongList +from spotdl.types.song import Song, SongList from spotdl.utils.spotify import SpotifyClient __all__ = ["Playlist", "PlaylistError"] @@ -28,72 +28,8 @@ class Playlist(SongList): author_name: str cover_url: str - @classmethod - def search(cls, search_term: str): - """ - Searches for Playlist from a search term. - - ### Arguments - - search_term: The search term to use. - - ### Returns - - The raw search results - """ - - spotify_client = SpotifyClient() - raw_search_results = spotify_client.search(search_term, type="playlist") - - if ( - raw_search_results is None - or len(raw_search_results.get("playlists", {}).get("items", [])) == 0 - ): - raise PlaylistError("No playlist matches found on spotify") - - return raw_search_results - @staticmethod - def get_urls(url: str) -> List[str]: - """ - Get URLs of all tracks in a playlist. - - ### Arguments - - url: The URL of the playlist. - - ### Returns - - A list of urls. - """ - - spotify_client = SpotifyClient() - tracks = [] - - playlist_response = spotify_client.playlist_items(url) - if playlist_response is None: - raise PlaylistError(f"Wrong playlist id: {url}") - - tracks = playlist_response["items"] - - # Get all tracks from playlist - while playlist_response["next"]: - playlist_response = spotify_client.next(playlist_response) - - # Failed to get response, break the loop - if playlist_response is None: - break - - # Add tracks to the list - tracks.extend(playlist_response["items"]) - - return [ - track["track"]["external_urls"]["spotify"] - for track in tracks - if track is not None - and track.get("track") is not None - and track.get("track").get("id") - and track.get("track").get("duration_ms") != 0 - ] - - @staticmethod - def get_metadata(url: str) -> Dict[str, Any]: + def get_metadata(url: str) -> Tuple[Dict[str, Any], List[Song]]: """ Get metadata for a playlist. @@ -110,7 +46,7 @@ def get_metadata(url: str) -> Dict[str, Any]: if playlist is None: raise PlaylistError("Invalid playlist URL.") - return { + metadata = { "name": playlist["name"], "url": url, "description": playlist["description"], @@ -127,3 +63,59 @@ def get_metadata(url: str) -> Dict[str, Any]: else "" ), } + + playlist_response = spotify_client.playlist_items(url) + if playlist_response is None: + raise PlaylistError(f"Wrong playlist id: {url}") + + # Get all tracks from playlist + tracks = playlist_response["items"] + while playlist_response["next"]: + playlist_response = spotify_client.next(playlist_response) + + # Failed to get response, break the loop + if playlist_response is None: + break + + # Add tracks to the list + tracks.extend(playlist_response["items"]) + + songs = [] + for track in tracks: + if track["track"]["is_local"]: + continue + + track_meta = track.get("track", {}) + track_id = track_meta.get("id") + if track_id is None or track_meta.get("duration_ms") == 0: + continue + + album_meta = track_meta["album"] + release_date = album_meta["release_date"] + + artists = [artist["name"] for artist in track_meta["artists"]] + song = Song.from_missing_data( + name=track_meta["name"], + artists=artists, + artist=artists[0], + album_id=album_meta["id"], + album_name=album_meta["name"], + album_artist=album_meta["artists"][0]["name"], + disc_number=track_meta["disc_number"], + duration=track_meta["duration_ms"], + year=release_date[:4], + date=release_date, + track_number=track_meta["track_number"], + tracks_count=album_meta["total_tracks"], + song_id=track_meta["id"], + explicit=track_meta["explicit"], + url=track_meta["external_urls"]["spotify"], + isrc=track_meta["external_ids"]["isrc"], + cover_url=max( + album_meta["images"], key=lambda i: i["width"] * i["height"] + )["url"], + ) + + songs.append(song) + + return metadata, songs diff --git a/spotdl/types/saved.py b/spotdl/types/saved.py index 86b6d7246..9a92d9142 100644 --- a/spotdl/types/saved.py +++ b/spotdl/types/saved.py @@ -3,9 +3,9 @@ """ from dataclasses import dataclass -from typing import Any, Dict, List +from typing import Any, Dict, List, Tuple -from spotdl.types.song import SongList +from spotdl.types.song import Song, SongList from spotdl.utils.spotify import SpotifyClient __all__ = ["Saved", "SavedError"] @@ -24,20 +24,23 @@ class Saved(SongList): """ @staticmethod - def get_urls(_: str = "saved") -> List[str]: + def get_metadata(url: str = "saved") -> Tuple[Dict[str, Any], List[Song]]: """ - Returns a list of urls of all saved tracks. + Returns metadata for a saved list. ### Arguments - - _: not required, but used to match the signature of the other get_urls methods. + - url: Not required, but used to match the signature of the other get_metadata methods. ### Returns - - A list of urls. + - metadata: A dictionary containing the metadata for the saved list. + - songs: A list of Song objects. """ + metadata = {"name": "Saved tracks", "url": url} + spotify_client = SpotifyClient() if spotify_client.user_auth is False: # type: ignore - raise SavedError("You must be logged in to use this function.") + raise SavedError("You must be logged in to use this function") saved_tracks_response = spotify_client.current_user_saved_tracks() if saved_tracks_response is None: @@ -48,56 +51,45 @@ def get_urls(_: str = "saved") -> List[str]: # Fetch all saved tracks while saved_tracks_response and saved_tracks_response["next"]: response = spotify_client.next(saved_tracks_response) - # response is wrong, break if response is None: break saved_tracks_response = response saved_tracks.extend(saved_tracks_response["items"]) - # Remove songs without id - # and return urls - return [ - "https://open.spotify.com/track/" + track["track"]["id"] - for track in saved_tracks - if track and track.get("track", {}).get("id") - ] - - @classmethod - def create_basic_list(cls, url: str = "saved") -> "Saved": - """ - Create a basic list with only the required metadata and urls. - - ### Returns - - The Saved object. - """ - - metadata = cls.get_metadata(url) - urls = cls.get_urls(url) - - return cls(**metadata, urls=urls, songs=[]) - - @staticmethod - def get_metadata(url: str = "saved") -> Dict[str, Any]: - """ - Returns metadata for a saved list. - - ### Arguments - - url: Not required, but used to match the signature of the other get_metadata methods. - """ - - return {"name": "Saved tracks", "url": url} - - @classmethod - def search(cls, _: str): - """ - Search for a saved list. - - ### Arguments - - search_term: The search term. - - ### Returns - - The Saved object. - """ - - return cls.create_basic_list() + songs = [] + for track in saved_tracks: + if track["track"]["is_local"]: + continue + + track_meta = track["track"] + album_meta = track_meta["album"] + + release_date = album_meta["release_date"] + artists = artists = [artist["name"] for artist in track_meta["artists"]] + + song = Song.from_missing_data( + name=track_meta["name"], + artists=artists, + artist=artists[0], + album_id=album_meta["id"], + album_name=album_meta["name"], + album_artist=album_meta["artists"][0]["name"], + disc_number=track_meta["disc_number"], + duration=track_meta["duration_ms"], + year=release_date[:4], + date=release_date, + track_number=track_meta["track_number"], + tracks_count=album_meta["total_tracks"], + song_id=track_meta["id"], + explicit=track_meta["explicit"], + url=track_meta["external_urls"]["spotify"], + isrc=track_meta["external_ids"]["isrc"], + cover_url=max( + album_meta["images"], key=lambda i: i["width"] * i["height"] + )["url"], + ) + + songs.append(song) + + return metadata, songs diff --git a/spotdl/types/song.py b/spotdl/types/song.py index 568260a00..07b242c6a 100644 --- a/spotdl/types/song.py +++ b/spotdl/types/song.py @@ -4,7 +4,7 @@ import json from dataclasses import asdict, dataclass -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple from spotdl.utils.spotify import SpotifyClient @@ -17,7 +17,13 @@ class SongError(Exception): """ -@dataclass() +class SongListError(Exception): + """ + Base class for all exceptions related to song lists. + """ + + +@dataclass class Song: """ Song class. Contains all the information about a song. @@ -47,6 +53,7 @@ class Song: song_list: Optional["SongList"] = None list_position: Optional[int] = None lyrics: Optional[str] = None + # Added in v4.1.0, not supported in previous versions # in the next major version, these will be made required album_id: Optional[str] = None @@ -237,6 +244,26 @@ def from_dict(cls, data: Dict[str, Any]) -> "Song": # Return product object return cls(**data) + @classmethod + def from_missing_data(cls, **kwargs) -> "Song": + """ + Create a Song object from a dictionary with missing data. + For example, data dict doesn't contain all the required + attributes for the Song class. + + ### Arguments + - data: The dictionary. + + ### Returns + - The Song object. + """ + + song_data = {} + for key in cls.__dataclass_fields__: # pylint: disable=E1101 + song_data.setdefault(key, kwargs.get(key)) + + return cls(**song_data) + @property def display_name(self) -> str: """ @@ -271,88 +298,26 @@ class SongList: urls: List[str] songs: List[Song] - @property - def length(self) -> int: - """ - Get list length (number of songs). - - ### Returns - - The list length. - """ - - return max(len(self.urls), len(self.songs)) - @classmethod - def create_basic_object(cls, url: str): + def from_url(cls, url: str, fetch_songs: bool = True): """ - Create a basic list with only the required metadata. + Create a SongList object from a url. ### Arguments - url: The url of the list. + - fetch_songs: Whether to fetch missing metadata for songs. ### Returns - The SongList object. """ - metadata = cls.get_metadata(url) - - return cls(**metadata, urls=[], songs=[]) - - @classmethod - def create_basic_list(cls, url: str): - """ - Create a basic list with only the required metadata and urls. - - ### Arguments - - url: The url of the list. - ### Returns - - The SongList object. - """ + metadata, songs = cls.get_metadata(url) + urls = [song.url for song in songs] - metadata = cls.get_metadata(url) - urls = cls.get_urls(url) + if fetch_songs: + songs = [Song.from_url(song.url) for song in songs] - return cls(**metadata, urls=urls, songs=[]) - - @classmethod - def from_url(cls, url: str): - """ - Parse an album from a Spotify URL. - - ### Arguments - - url: The URL of the album. - - ### Returns - - The Album object. - """ - - metadata = cls.get_metadata(url) - - urls = cls.get_urls(url) - - # Remove songs without id (country restricted/local tracks) - # And create song object for each track - songs: List[Song] = [Song.from_url(url) for url in urls] - - return cls( - **metadata, - songs=songs, - urls=urls, - ) - - @classmethod - def search(cls, search_term: str): - """ - Searches for SongList from a search term. - - ### Arguments - - search_term: The search term to use. - - ### Returns - - The raw search results - """ - - raise NotImplementedError + return cls(**metadata, urls=urls, songs=songs) @classmethod def from_search_term(cls, search_term: str): @@ -367,60 +332,51 @@ def from_search_term(cls, search_term: str): """ list_type = cls.__name__.lower() - raw_search_results = cls.search(search_term) + spotify_client = SpotifyClient() + raw_search_results = spotify_client.search(search_term, type="playlist") + + if ( + raw_search_results is None + or len(raw_search_results.get(f"{list_type}s", {}).get("items", [])) == 0 + ): + raise SongListError( + f"No {list_type} matches found on spotify for '{search_term}'" + ) - return cls.create_basic_list( + return cls.from_url( f"http://open.spotify.com/{list_type}/" + raw_search_results[f"{list_type}s"]["items"][0]["id"] ) - @classmethod - def list_from_search_term(cls, search_term: str): + @property + def length(self) -> int: """ - Creates a list of SongList objects from a search term. - - ### Arguments - - search_term: The search term to use. + Get list length (number of songs). ### Returns - - The list of SongList objects. + - The list length. """ - list_type = cls.__name__.lower() - raw_search_results = cls.search(search_term) - - songlist = [] - for idx, _ in enumerate(raw_search_results[f"{list_type}s"]["items"]): - songlist.append( - cls.create_basic_object( - f"http://open.spotify.com/{list_type}/" - + raw_search_results[f"{list_type}s"]["items"][idx]["id"] - ) - ) - - return songlist + return max(len(self.urls), len(self.songs)) - @staticmethod - def get_urls(url: str) -> List[str]: + @property + def json(self) -> Dict[str, Any]: """ - Get urls for all songs in url. - - ### Arguments - - url: The URL of the list. + Returns a dictionary of the song list's data. ### Returns - - The list of urls. + - The dictionary. """ - raise NotImplementedError + return asdict(self) @staticmethod - def get_metadata(url: str) -> Dict[str, Any]: + def get_metadata(url: str) -> Tuple[Dict[str, Any], List[Song]]: """ - Get metadata for list. + Get metadata for a song list. ### Arguments - - url: The URL of the list. + - url: The url of the song list. ### Returns - The metadata. diff --git a/spotdl/utils/search.py b/spotdl/utils/search.py index 161b8ef36..5a4f892d6 100644 --- a/spotdl/utils/search.py +++ b/spotdl/utils/search.py @@ -22,7 +22,6 @@ "QueryError", "get_search_results", "parse_query", - "create_empty_song", "get_simple_songs", "reinit_song", "get_song_from_file_metadata", @@ -77,93 +76,6 @@ def parse_query( return results -def create_empty_song( - name: Optional[str] = None, - artists: Optional[List[str]] = None, - artist: Optional[str] = None, - album_id: Optional[str] = None, - album_name: Optional[str] = None, - album_artist: Optional[str] = None, - genres: Optional[List[str]] = None, - disc_number: Optional[int] = None, - disc_count: Optional[int] = None, - duration: Optional[int] = None, - year: Optional[int] = None, - date: Optional[str] = None, - track_number: Optional[int] = None, - tracks_count: Optional[int] = None, - isrc: Optional[str] = None, - song_id: Optional[str] = None, - cover_url: Optional[str] = None, - explicit: Optional[bool] = None, - publisher: Optional[str] = None, - url: Optional[str] = None, - copyright_text: Optional[str] = None, - download_url: Optional[str] = None, - song_list: Optional["SongList"] = None, - lyrics: Optional[str] = None, -) -> Song: - """ - Create an empty song. - - ### Arguments - - name: Name of the song - - artists: List of artists - - artist: Name of the artist - - album_id: Spotify album ID - - album_name: Name of the album - - album_artist: Name of the album artist - - genres: List of genres - - disc_number: Disc number - - disc_count: Disc count - - duration: Duration of the song in seconds - - year: Year of release - - date: Date of release - - track_number: Track number - - tracks_count: Number of tracks - - isrc: ISRC code - - song_id: Spotify song ID - - cover_url: URL of the cover art - - explicit: Explicit flag - - publisher: Publisher - - url: URL of the song - - copyright_text: Copyright text - - download_url: Download URL - - song_list: Song list - - lyrics: Lyrics - - ### Returns - - Song object - """ - - return Song( - name=name, # type: ignore - artists=artists, # type: ignore - artist=artist if artist else (artists[0] if artists else None), # type: ignore - album_id=album_id, # type: ignore - album_name=album_name, # type: ignore - album_artist=album_artist, # type: ignore - genres=genres, # type: ignore - disc_number=disc_number, # type: ignore - disc_count=disc_count, # type: ignore - duration=duration, # type: ignore - year=year, # type: ignore - date=date, # type: ignore - track_number=track_number, # type: ignore - tracks_count=tracks_count, # type: ignore - isrc=isrc, # type: ignore - song_id=song_id, # type: ignore - cover_url=cover_url, # type: ignore - explicit=explicit, # type: ignore - publisher=publisher, # type: ignore - url=url, # type: ignore - copyright_text=copyright_text, - download_url=download_url, - song_list=song_list, - lyrics=lyrics, - ) - - def get_simple_songs( query: List[str], ) -> List[Song]: @@ -199,20 +111,20 @@ def get_simple_songs( ) songs.append( - create_empty_song(url=split_urls[1], download_url=split_urls[0]) + Song.from_missing_data(url=split_urls[1], download_url=split_urls[0]) ) elif "open.spotify.com" in request and "track" in request: - songs.append(create_empty_song(url=request)) + songs.append(Song.from_url(url=request)) elif "open.spotify.com" in request and "playlist" in request: - lists.append(Playlist.create_basic_list(request)) + lists.append(Playlist.from_url(request, fetch_songs=False)) elif "open.spotify.com" in request and "album" in request: - lists.append(Album.create_basic_list(request)) + lists.append(Album.from_url(request, fetch_songs=False)) elif "open.spotify.com" in request and "artist" in request: - lists.append(Artist.create_basic_list(request)) + lists.append(Artist.from_url(request, fetch_songs=False)) elif "album:" in request: lists.append(Album.from_search_term(request)) elif request == "saved": - lists.append(Saved.create_basic_list()) + lists.append(Saved.from_url(request, fetch_songs=False)) elif request.endswith(".spotdl"): with open(request, "r", encoding="utf-8") as save_file: for track in json.load(save_file): @@ -222,16 +134,20 @@ def get_simple_songs( songs.append(Song.from_search_term(request)) for song_list in lists: - logger.debug( + logger.info( "Found %s songs in %s (%s)", len(song_list.urls), song_list.name, song_list.__class__.__name__, ) - songs.extend( - [create_empty_song(url=url, song_list=song_list) for url in song_list.urls] - ) # type: ignore + for song in song_list.songs: + if song.song_list: + songs.append(Song.from_missing_data(**song.json)) + else: + song_data = song.json + song_data["song_list"] = song_list + songs.append(Song.from_missing_data(**song_data)) return songs @@ -249,11 +165,15 @@ def songs_from_albums(alubms: List[str]): songs = [] for album_id in alubms: - album = Album.create_basic_list(album_id) + album = Album.from_url(album_id, fetch_songs=False) - songs.extend( - [create_empty_song(url=url, song_list=album) for url in album.urls] - ) + for song in album.songs: + if song.song_list: + songs.append(Song.from_missing_data(**song.json)) + else: + song_data = song.json + song_data["song_list"] = album + songs.append(Song.from_missing_data(**song_data)) return songs @@ -311,7 +231,7 @@ def get_song_from_file_metadata(file: Path) -> Optional[Song]: if file_metadata is None: return None - return create_empty_song(**file_metadata) + return Song.from_missing_data(**file_metadata) def gather_known_songs(output: str, output_format: str) -> Dict[str, List[Path]]: