diff --git a/README.md b/README.md index 10fcfb8a..1384fe50 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Downloads songs from any Spotify playlist, album or track. ### Tell me more! -I wanted an easy way to grab the songs present in my library so I can download it & use it offline. I no longer use this, but continue to maintain this. spotify-dl doesn't download anything from Spotify. It picks up the metadata from Spotify API and then uses [yt-dlp](https://github.com/yt-dlp/yt-dlp) to download the song. +I wanted an easy way to grab the songs present in my library so I can download it & use it offline. I no longer use this, but continue to maintain this. spotify-dl doesn't download anything from Spotify. It picks up the metadata from Spotify API and then uses [yt-dlp](https://github.com/yt-dlp/yt-dlp) to download the song. ### How do I get this thing running? @@ -24,7 +24,11 @@ Install using pip Run the program - spotify_dl -l spotify_playlist_link/s -o download_directory + spotify_dl -l spotify_playlist_link_1 spotify_playlist_link_2 + +If you want to make use of parallel download, pass `-mc `, where `` refers to number of cores. If this is too high, spotify-dl will set it to one lesser than max number of cores that you have. + + spotify_dl -mc 4 -l spotify_playlist_link_1 spotify_playlist_link_2 For running in verbose mode, append `-V` @@ -34,14 +38,9 @@ For more details and other arguments, issue `-h` spotify_dl -h -For downloading using multiple cores - - - spotify_dl -l [link] -mc [number of cores to use] - See [the getting started guide](https://github.com/SathyaBhat/spotify-dl/blob/master/GETTING_STARTED.md) for more details. -### Demo +### Demo [![asciicast](https://asciinema.org/a/488558.svg)](https://asciinema.org/a/488558) diff --git a/images/spotify-playlist.png b/images/spotify-playlist.png index 5387f2e1..c01974a3 100644 Binary files a/images/spotify-playlist.png and b/images/spotify-playlist.png differ diff --git a/spotify_dl/constants.py b/spotify_dl/constants.py index b15a6b7a..209bb521 100644 --- a/spotify_dl/constants.py +++ b/spotify_dl/constants.py @@ -9,3 +9,5 @@ SAVE_PATH = os.getenv("XDG_CACHE_HOME") + "/spotifydl" else: SAVE_PATH = str(Path.home()) + "/.cache/spotifydl" + +DOWNLOAD_LIST = "download_list.log" diff --git a/spotify_dl/scaffold.py b/spotify_dl/scaffold.py index a9d85f9e..5e2ee171 100644 --- a/spotify_dl/scaffold.py +++ b/spotify_dl/scaffold.py @@ -14,7 +14,9 @@ ) console = Console() log = logging.getLogger("sdl") -sentry_sdk.init("https://fc66a23d79634b9bba1690ea13e289f0@o321064.ingest.sentry.io/2383261") +sentry_sdk.init( + "https://fc66a23d79634b9bba1690ea13e289f0@o321064.ingest.sentry.io/2383261" +) def get_tokens(): diff --git a/spotify_dl/spotify.py b/spotify_dl/spotify.py index b587763d..90d5878f 100644 --- a/spotify_dl/spotify.py +++ b/spotify_dl/spotify.py @@ -30,7 +30,9 @@ def fetch_tracks(sp, item_type, url): offset=offset, ) total_songs = items.get("total") - track_info_task = progress.add_task(description="Fetching track info", total=len(items["items"])) + track_info_task = progress.add_task( + description="Fetching track info", total=len(items["items"]) + ) for item in items["items"]: track_info = item.get("track") # If the user has a podcast in their playlist, there will be no track @@ -42,11 +44,15 @@ def fetch_tracks(sp, item_type, url): track_num = track_info.get("track_number") spotify_id = track_info.get("id") track_name = track_info.get("name") - track_artist = ",".join([artist["name"] for artist in track_info.get("artists")]) + track_artist = ",".join( + [artist["name"] for artist in track_info.get("artists")] + ) if track_album_info: track_album = track_album_info.get("name") track_year = ( - track_album_info.get("release_date")[:4] if track_album_info.get("release_date") else "" + track_album_info.get("release_date")[:4] + if track_album_info.get("release_date") + else "" ) album_total = track_album_info.get("total_tracks") if len(item["track"]["album"]["images"]) > 0: @@ -55,8 +61,14 @@ def fetch_tracks(sp, item_type, url): cover = None artists = track_info.get("artists") - main_artist_id = artists[0].get("uri", None) if len(artists) > 0 else None - genres = sp.artist(artist_id=main_artist_id).get("genres", []) if main_artist_id else [] + main_artist_id = ( + artists[0].get("uri", None) if len(artists) > 0 else None + ) + genres = ( + sp.artist(artist_id=main_artist_id).get("genres", []) + if main_artist_id + else [] + ) if len(genres) > 0: genre = genres[0] else: @@ -95,25 +107,38 @@ def fetch_tracks(sp, item_type, url): elif item_type == "album": with Progress() as progress: - album_songs_task = progress.add_task(description="Fetching songs from the album..") + album_songs_task = progress.add_task( + description="Fetching songs from the album.." + ) while True: album_info = sp.album(album_id=url) items = sp.album_tracks(album_id=url, offset=offset) total_songs = items.get("total") track_album = album_info.get("name") - track_year = album_info.get("release_date")[:4] if album_info.get("release_date") else "" + track_year = ( + album_info.get("release_date")[:4] + if album_info.get("release_date") + else "" + ) album_total = album_info.get("total_tracks") if len(album_info["images"]) > 0: cover = album_info["images"][0]["url"] else: cover = None - if len(sp.artist(artist_id=album_info["artists"][0]["uri"])["genres"]) > 0: - genre = sp.artist(artist_id=album_info["artists"][0]["uri"])["genres"][0] + if ( + len(sp.artist(artist_id=album_info["artists"][0]["uri"])["genres"]) + > 0 + ): + genre = sp.artist(artist_id=album_info["artists"][0]["uri"])[ + "genres" + ][0] else: genre = "" for item in items["items"]: track_name = item.get("name") - track_artist = ", ".join([artist["name"] for artist in item["artists"]]) + track_artist = ", ".join( + [artist["name"] for artist in item["artists"]] + ) track_num = item["track_number"] spotify_id = item.get("id") songs_list.append( @@ -149,7 +174,11 @@ def fetch_tracks(sp, item_type, url): track_artist = ", ".join([artist["name"] for artist in items["artists"]]) if album_info: track_album = album_info.get("name") - track_year = album_info.get("release_date")[:4] if album_info.get("release_date") else "" + track_year = ( + album_info.get("release_date")[:4] + if album_info.get("release_date") + else "" + ) album_total = album_info.get("total_tracks") track_num = items["track_number"] spotify_id = items["id"] diff --git a/spotify_dl/spotify_dl.py b/spotify_dl/spotify_dl.py index 2db06073..94b05198 100755 --- a/spotify_dl/spotify_dl.py +++ b/spotify_dl/spotify_dl.py @@ -107,8 +107,15 @@ def spotify_dl(): args = parser.parse_args() num_cores = os.cpu_count() args.multi_core = int(args.multi_core) + console.log(f"Starting spotify_dl [bold green]v{VERSION}[/bold green]") + if args.verbose: + log.setLevel(DEBUG) + log.debug("Setting debug mode on spotify_dl") + if args.multi_core > (num_cores - 1): - print(f"[!] too many cores requested , reverting to {num_cores - 1} cores") + console.log( + f"Requested cores [bold red]{args.multi_core}[/bold red] exceeds available [bold green]{num_cores}[/bold green], using [bold green]{num_cores - 1}[/bold green] cores." + ) args.multi_core = num_cores - 1 if args.version: console.print(f"spotify_dl [bold green]v{VERSION}[/bold green]") @@ -127,20 +134,19 @@ def spotify_dl(): else: setattr(args, key, value) - if args.verbose: - log.setLevel(DEBUG) if not args.url: raise (Exception("No playlist url provided:")) - console.log(f"Starting spotify_dl [bold green]v{VERSION}[/bold green]") - log.debug("Setting debug mode on spotify_dl") - tokens = get_tokens() if tokens is None: sys.exit(1) - C_ID, C_SECRET = tokens + client_id, client_secret = tokens - sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials(client_id=C_ID, client_secret=C_SECRET)) + sp = spotipy.Spotify( + auth_manager=SpotifyClientCredentials( + client_id=client_id, client_secret=client_secret + ) + ) log.debug("Arguments: %s ", args) valid_urls = validate_spotify_urls(args.url) @@ -153,9 +159,13 @@ def spotify_dl(): url_dict = {} item_type, item_id = parse_spotify_url(url) directory_name = get_item_name(sp, item_type, item_id) - url_dict["save_path"] = Path(PurePath.joinpath(Path(args.output), Path(directory_name))) + url_dict["save_path"] = Path( + PurePath.joinpath(Path(args.output), Path(directory_name)) + ) url_dict["save_path"].mkdir(parents=True, exist_ok=True) - console.print(f"Saving songs to [bold green]{directory_name}[/bold green] directory") + console.print( + f"Saving songs to [bold green]{directory_name}[/bold green] directory" + ) url_dict["songs"] = fetch_tracks(sp, item_type, url) url_data["urls"].append(url_dict.copy()) if args.download is True: @@ -177,6 +187,8 @@ def spotify_dl(): if __name__ == "__main__": - starttime = time.time() + start_time = time.time() spotify_dl() - print(f"[*] finished in {time.time() - starttime}") + console.log( + f"Download completed in [bold green]{time.time() - start_time} seconds.[/bold green]" + ) diff --git a/spotify_dl/youtube.py b/spotify_dl/youtube.py index bd2dea54..9538d1d7 100644 --- a/spotify_dl/youtube.py +++ b/spotify_dl/youtube.py @@ -10,11 +10,14 @@ from mutagen.mp3 import MP3 from spotify_dl.scaffold import log from spotify_dl.utils import sanitize +from spotify_dl.constants import DOWNLOAD_LIST def default_filename(**kwargs): """name without number""" - return sanitize(f"{kwargs['artist']} - {kwargs['name']}", "#") # youtube-dl automatically replaces with # + return sanitize( + f"{kwargs['artist']} - {kwargs['name']}", "#" + ) # youtube-dl automatically replaces with # def playlist_num_filename(**kwargs): @@ -30,8 +33,6 @@ def write_tracks(tracks_file, song_dict): :param song_dict: the songs to be written to tracks_file """ track_db = [] - if tracks_file != "All_Songs_For_This_Download.txt": - return "Invalid filename" with open(tracks_file, "w+", encoding="utf-8", newline="") as file_out: i = 0 @@ -81,14 +82,18 @@ def set_tags(temp, file_path, kwargs): song_file = MP3(mp3filename, ID3=EasyID3) except mutagen.MutagenError as e: log.debug(e) - print(f"Failed to download: {mp3filename}, please ensure YouTubeDL is up-to-date. ") + print( + f"Failed to download: {mp3filename}, please ensure YouTubeDL is up-to-date. " + ) return song_file["date"] = song.get("year") if kwargs["keep_playlist_order"]: song_file["tracknumber"] = str(song.get("playlist_num")) else: - song_file["tracknumber"] = str(song.get("num")) + "/" + str(song.get("num_tracks")) + song_file["tracknumber"] = ( + str(song.get("num")) + "/" + str(song.get("num_tracks")) + ) song_file["genre"] = song.get("genre") song_file.save() @@ -132,8 +137,12 @@ def find_and_download_songs(kwargs): query = f"{artist} - {name} Lyrics".replace(":", "").replace('"', "") print(f"Initiating download for {query}.") - file_name = kwargs["file_name_f"](name=name, artist=artist, track_num=kwargs["track_db"][i].get("num")) - sponsorblock_remove_list = ["music_offtopic"] if kwargs["skip_non_music_sections"] else [] + file_name = kwargs["file_name_f"]( + name=name, artist=artist, track_num=kwargs["track_db"][i].get("num") + ) + sponsorblock_remove_list = ( + ["music_offtopic"] if kwargs["skip_non_music_sections"] else [] + ) file_path = path.join(kwargs["track_db"][i]["save_path"], file_name) outtmpl = f"{file_path}.%(ext)s" @@ -214,7 +223,9 @@ def multicore_find_and_download_songs(kwargs): processes = [] segment_index = 0 for segment in file_segments: - p = multiprocessing.Process(target=multicore_handler, args=(segment_index, segment, kwargs.copy())) + p = multiprocessing.Process( + target=multicore_handler, args=(segment_index, segment, kwargs.copy()) + ) processes.append(p) segment_index += 1 @@ -249,7 +260,7 @@ def download_songs(**kwargs): """ for url in kwargs["songs"]["urls"]: log.debug("Downloading to %s", url["save_path"]) - reference_file = "All_Songs_For_This_Download.txt" + reference_file = DOWNLOAD_LIST track_db = write_tracks(reference_file, kwargs["songs"]) os.rename(reference_file, kwargs["output_dir"] + "/" + reference_file) reference_file = str(kwargs["output_dir"]) + "/" + reference_file @@ -259,3 +270,4 @@ def download_songs(**kwargs): multicore_find_and_download_songs(kwargs) else: find_and_download_songs(kwargs) + os.remove(reference_file) diff --git a/tests/test_spotify_fetch_tracks.py b/tests/test_spotify_fetch_tracks.py index 55bd1331..ee2673a1 100644 --- a/tests/test_spotify_fetch_tracks.py +++ b/tests/test_spotify_fetch_tracks.py @@ -119,7 +119,7 @@ def test_spotify_track_fetch_one(): assert { "album": "Hell Freezes Over (Remaster 2018)", "artist": "Eagles", - "cover": "https://i.scdn.co/image/ab67616d0000b27396d28597a5ae44ab66552183", + "cover": "https://i.scdn.co/image/ab67616d0000485196d28597a5ae44ab66552183", "genre": "album rock", "name": "Hotel California - Live On MTV, 1994", "num": 6,