From 7d81eb5cc6fef6b789bc4f76b84b627d0dafb0c5 Mon Sep 17 00:00:00 2001 From: yiannisha Date: Sat, 23 Oct 2021 16:06:34 +0300 Subject: [PATCH 01/41] Rewrote the search function It still works in the same way and no other functions need to be changed. Just changed it to store needed data about each track/album/playlist in a dictionary so tracking it in the end is simpler. --- zspotify.py | 169 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 112 insertions(+), 57 deletions(-) diff --git a/zspotify.py b/zspotify.py index 25538e15..d031ea9c 100755 --- a/zspotify.py +++ b/zspotify.py @@ -351,88 +351,143 @@ def search(search_term): """ Searches Spotify's API for relevant data """ token = SESSION.tokens().get("user-read-email") + params = { + "limit" : "10", + "q" : search_term, + "type" : ["track","album","playlist"] + } + + # ADD BLOCK FOR READING OPTIONS AND CLEAN SEARCH_TERM + resp = requests.get( "https://api.spotify.com/v1/search", { - "limit": "10", + "limit": params["limit"], "offset": "0", - "q": search_term, - "type": "track,album,playlist" + "q": params["q"], + "type": ",".join(params["type"]) }, headers={"Authorization": "Bearer %s" % token}, ) # print(resp.json()) - i = 1 - tracks = resp.json()["tracks"]["items"] - if len(tracks) > 0: - print("### TRACKS ###") - for track in tracks: - if track["explicit"]: - explicit = "[E]" - else: - explicit = "" - print("%d, %s %s | %s" % ( - i, - track["name"], - explicit, - ",".join([artist["name"] for artist in track["artists"]]), - )) - i += 1 - total_tracks = i - 1 + enum = 1 + dics = [] + + # add all returned tracks to dics + if "track" in params["type"]: + tracks = resp.json()["tracks"]["items"] + if len(tracks) > 0: + print("### TRACKS ###") + for track in tracks: + if track["explicit"]: + explicit = "[E]" + else: + explicit = "" + # collect needed data + dic = { + "id" : track["id"], + "name" : track["name"], + "artists/owner" : [artist["name"] for artist in track["artists"]], + "type" : "track", + } + dics.append(dic) + + print("{}, {} {} | {}".format( + enum, + dic["name"], + explicit, + ",".join(dic["artists/owner"]), + )) + enum += 1 + total_tracks = enum - 1 print("\n") + # free up memory + del tracks else: total_tracks = 0 - albums = resp.json()["albums"]["items"] - if len(albums) > 0: - print("### ALBUMS ###") - for album in albums: - print("%d, %s | %s" % ( - i, - album["name"], - ",".join([artist["name"] for artist in album["artists"]]), - )) - i += 1 - total_albums = i - total_tracks - 1 + if "album" in params["type"]: + albums = resp.json()["albums"]["items"] + if len(albums) > 0: + print("### ALBUMS ###") + for album in albums: + # collect needed data + dic = { + "id" : album["id"], + "name" : album["name"], + "artists/owner" : [artist["name"] for artist in album["artists"]], + "type" : "album", + } + dics.append(dic) + + print("{}, {} | {}".format( + enum, + dic["name"], + ",".join(dic["artists/owner"]), + )) + enum += 1 + total_albums = enum - total_tracks - 1 print("\n") + # free up memory + del albums else: total_albums = 0 - playlists = resp.json()["playlists"]["items"] - print("### PLAYLISTS ###") - for playlist in playlists: - print("%d, %s | %s" % ( - i, - playlist["name"], - playlist['owner']['display_name'], - )) - i += 1 - print("\n") + if "playlist" in params["type"]: + playlists = resp.json()["playlists"]["items"] + print("### PLAYLISTS ###") + if len(playlists) > 0: + for playlist in playlists: + # collect needed data + dic = { + "id" : playlist["id"], + "name" : playlist["name"], + "artists/owner" : [playlist["owner"]["display_name"]], + "type" : "playlist", + } + dics.append(dic) - if len(tracks) + len(albums) + len(playlists) == 0: + print("{}, {} | {}".format( + enum, + dic["name"], + ",".join(dic['artists/owner']), + )) + + enum += 1 + total_playlists = enum - total_tracks - total_albums - 1 + print("\n") + # free up memory + del playlists + else: + total_playlists = 0 + + if total_tracks + total_albums + total_playlists == 0: print("NO RESULTS FOUND - EXITING...") else: selection = str(input("SELECT ITEM(S) BY ID: ")) inputs = split_input(selection) for pos in inputs: position = int(pos) - if position <= total_tracks: - track_id = tracks[position - 1]["id"] - download_track(track_id) - elif position <= total_albums + total_tracks: - download_album(albums[position - total_tracks - 1]["id"]) - else: - playlist_choice = playlists[position - - total_tracks - total_albums - 1] - playlist_songs = get_playlist_songs( - token, playlist_choice['id']) - for song in playlist_songs: - if song['track']['id'] is not None: - download_track(song['track']['id'], sanitize_data( - playlist_choice['name'].strip()) + "/") - print("\n") + for dic in dics: + # find dictionary + print_pos = dics.index(dic) + 1 + if print_pos == position: + # if request is for track + if dic["type"] == "track": + download_track(dic["id"]) + # if request is for album + if dic["type"] == "album": + download_album(dic["id"]) + # if request is for playlist + if dic["type"] == "playlist": + playlist_songs = get_playlist_songs(token, dic["id"]) + for song in playlist_songs: + if song["track"]["id"] is not None: + download_track(song["track"]["id"], + sanitize_data(dic["name"].strip()) + "/") + print("\n") def get_song_info(song_id): From e9ba63b9d0da14c5f125f852baeebc548bc00dfc Mon Sep 17 00:00:00 2001 From: yiannisha Date: Sat, 23 Oct 2021 17:15:41 +0300 Subject: [PATCH 02/41] Added argument parsing in search function Added a block for parsing arguments passed with options -l -limit -t -type in the search() function. Arguments are passed inside search_term. They work as follows: * -l -limit : sets the limit of results to that number and raises a ValueError if that number exceeds 50. Default is 10. * -t -type : sets the type that is requested from the API about the search query. Raises a ValueError if an arguments passed is different than track, album, playlist. Default is all three. Example: Enter search or URL: -l 30 -t track album This will result with 30 tracks and 30 albums associated with query. Options can be passed in any order but the query must be first. --- zspotify.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/zspotify.py b/zspotify.py index d031ea9c..a58790f8 100755 --- a/zspotify.py +++ b/zspotify.py @@ -354,10 +354,55 @@ def search(search_term): params = { "limit" : "10", "q" : search_term, - "type" : ["track","album","playlist"] + "type" : set(), } - # ADD BLOCK FOR READING OPTIONS AND CLEAN SEARCH_TERM + # Block for parsing passed arguments + splits = search_term.split() + for split in splits: + index = splits.index(split) + + if split[0] == "-" and len(split) > 1: + if len(splits)-1 == index: + raise IndexError("No parameters passed after option: {}\n". + format(split)) + + if split == "-l" or split == "-limit": + try: + int(splits[index+1]) + except ValueError: + raise ValueError("Paramater passed after {} option must be an integer.\n". + format(split)) + if int(splits[index+1]) > 50: + raise ValueError("Invalid limit passed. Max is 50.\n") + params["limit"] = splits[index+1] + + + if split == "-t" or split == "-type": + + allowed_types = ["track", "playlist", "album"] + for i in range(index+1, len(splits)): + if splits[i][0] == "-": + break + + if splits[i] not in allowed_types: + raise ValueError("Parameters passed after {} option must be from this list:\n{}". + format(split, '\n'.join(allowed_types))) + + params["type"].add(splits[i]) + + if len(params["type"]) == 0: + params["type"] = {"track", "album", "playlist"} + + # Clean search term + search_term_list = [] + for split in splits: + if split[0] == "-": + break + search_term_list.append(split) + if not search_term_list: + raise ValueError("Invalid query.") + params["q"] = ' '.join(search_term_list) resp = requests.get( "https://api.spotify.com/v1/search", @@ -437,8 +482,8 @@ def search(search_term): if "playlist" in params["type"]: playlists = resp.json()["playlists"]["items"] - print("### PLAYLISTS ###") if len(playlists) > 0: + print("### PLAYLISTS ###") for playlist in playlists: # collect needed data dic = { From efed684e593fe04b470fe994818bb6dd0f2beb27 Mon Sep 17 00:00:00 2001 From: yiannisha Date: Sat, 23 Oct 2021 19:13:04 +0300 Subject: [PATCH 03/41] Fixed minor bugs --- zspotify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zspotify.py b/zspotify.py index 56339034..3c04c673 100755 --- a/zspotify.py +++ b/zspotify.py @@ -515,7 +515,7 @@ def search(search_term): # pylint: disable=too-many-locals,too-many-branches else: total_artists = 0 - if total_tracks + total_albums + total_playlists == 0: + if total_tracks + total_albums + total_playlists + total_artists == 0: print("NO RESULTS FOUND - EXITING...") else: selection = str(input("SELECT ITEM(S) BY ID: ")) From b4b2985a5b4de9819c6b71232359bef18c7e035f Mon Sep 17 00:00:00 2001 From: Mazzya Date: Sun, 24 Oct 2021 14:23:26 +0200 Subject: [PATCH 04/41] CHANGELOG.md has been created and a link to view the CONTRIBUTING.md file has been added. --- CHANGELOG.md | 84 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 88 ++-------------------------------------------------- 2 files changed, 87 insertions(+), 85 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..008e0f71 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,84 @@ +## **Changelog:** +**v2.2 (24 Oct 2021):** +- Added basic support for downloading an entire podcast series. +- Split code into multiple files for easier maintenance. +- Changed initial launch script to app.py +- Simplified audio formats. +- Added prebuild exe for Windows users. +- Added Docker file. +- Added CONTRIBUTING.md. +- Fixed artist names getting cutoff in metadata. +- Removed data sanitization of metadata tags. + +**v2.1 (23 Oct 2021):** +- Moved configuration from hard-coded values to separate zs_config.json file. +- Add subfolders for each disc. +- Can now search and download all songs by artist. +- Show single progress bar for entire album. +- Added song number at start of track name in albums. + +**v2.0 (22 Oct 2021):** +- Added progress bar for downloads. +- Added multi-select support for all results when searching. +- Added GPLv3 Licence. +- Changed welcome banner and removed unnecessary debug print statements. + +**v1.9 (22 Oct 2021):** +- Added Gitea mirror for when the Spotify Glowies come to DMCA the shit out of this. +- Changed the discord server invite to a matrix server so that won't get swatted either. +- Added option to select multiple of our saved playlists to download at once. +- Added support for downloading an entire show at once. + +**v1.8 (21 Oct 2021):** +- Improved podcast downloading a bit. +- Simplified the code that catches crashes while downloading. +- Cleaned up code using linter again. +- Added option to just paste a url in the search bar to download it. +- Added a small delay between downloading each track when downloading in bulk to help with downloading issues and potential bans. + +**v1.7 (21 Oct 2021):** +- Rewrote README.md to look a lot more professional. +- Added patch to fix edge case crash when downloading liked songs. +- Made premium account check a lot more reliable. +- Added experimental podcast support for specific episodes! + +**v1.6 (20 Oct 2021):** +- Added Pillow to requirements.txt. +- Removed websocket-client from requirements.txt because librespot-python added it to their dependency list. +- Made it hide your password when you type it in. +- Added manual override to force premium quality if zspotify cannot auto detect it. +- Added option to just download the raw audio with no re-encoding at all. +- Added Shebang line so it runs smoother on Linux. +- Made it download the entire track at once now so it is more efficient and fixed a bug users encountered. + +**v1.5 (19 Oct 2021):** +- Made downloading a lot more efficient and probably faster. +- Made the sanitizer more efficient. +- Formatted and linted all the code. + +**v1.4 (19 Oct 2021):** +- Added option to encode the downloaded tracks in the "ogg" format rather than "mp3". +- Added small improvement to sanitation function so it catches another edge case. + +**v1.3 (19 Oct 2021):** +- Added auto detection about if the current account is premium or not. If it is a premium account it automatically sets the quality to VERY_HIGH and otherwise HIGH if we are using a free account. +- Fixed conversion function so it now exports to the correct bitrate. +- Added sanitation to playlist names to help catch an edge case crash. +- Added option to download all your liked songs into a sub-folder. + +**v1.2 (18 Oct 2021):** +- Added .gitignore. +- Replaced dependency list in README.md with a proper requirements.txt file. +- Improved the readability of README.md. + +**v1.1 (16 Oct 2021):** +- Added try/except to help catch crashes where a very few specific tracks would crash either the downloading or conversion part. + +**v1.0 (14 Oct 2021):** +- Adjusted some functions so it runs again with the newer version of librespot-python. +- Improved my sanitization function so it catches more edge cases. +- Fixed an issue where sometimes spotify wouldn't provide a song id for a track we are trying to download. It will now detect and skip these invalid tracks. +- Added additional check for tracks that cannot be "played" due to licence(and similar) issues. These tracks will be skipped. + +**v0.9 (13 Oct 2021):** +- Initial upload, needs adjustments to get working again after backend rewrite. \ No newline at end of file diff --git a/README.md b/README.md index 7b8f1b0e..b0b94279 100644 --- a/README.md +++ b/README.md @@ -59,89 +59,7 @@ This isn't to say _you_ won't get banned as it is technically against Spotify's If you see this, don't worry! Just try logging back in. If you see the incorrect username or password error, reset your password and you should be able to log back in and continue using Spotify. ### Contributing -Please refer to CONTRIBUTING.md +Please refer to [CONTRIBUTING.md](CONTRIBUTING.md) -## **Changelog:** -**v2.2 (24 Oct 2021):** -- Added basic support for downloading an entire podcast series. -- Split code into multiple files for easier maintenance. -- Changed initial launch script to app.py -- Simplified audio formats. -- Added prebuild exe for Windows users. -- Added Docker file. -- Added CONTRIBUTING.md. -- Fixed artist names getting cutoff in metadata. -- Removed data sanitization of metadata tags. - -**v2.1 (23 Oct 2021):** -- Moved configuration from hard-coded values to separate zs_config.json file. -- Add subfolders for each disc. -- Can now search and download all songs by artist. -- Show single progress bar for entire album. -- Added song number at start of track name in albums. - -**v2.0 (22 Oct 2021):** -- Added progress bar for downloads. -- Added multi-select support for all results when searching. -- Added GPLv3 Licence. -- Changed welcome banner and removed unnecessary debug print statements. - -**v1.9 (22 Oct 2021):** -- Added Gitea mirror for when the Spotify Glowies come to DMCA the shit out of this. -- Changed the discord server invite to a matrix server so that won't get swatted either. -- Added option to select multiple of our saved playlists to download at once. -- Added support for downloading an entire show at once. - -**v1.8 (21 Oct 2021):** -- Improved podcast downloading a bit. -- Simplified the code that catches crashes while downloading. -- Cleaned up code using linter again. -- Added option to just paste a url in the search bar to download it. -- Added a small delay between downloading each track when downloading in bulk to help with downloading issues and potential bans. - -**v1.7 (21 Oct 2021):** -- Rewrote README.md to look a lot more professional. -- Added patch to fix edge case crash when downloading liked songs. -- Made premium account check a lot more reliable. -- Added experimental podcast support for specific episodes! - -**v1.6 (20 Oct 2021):** -- Added Pillow to requirements.txt. -- Removed websocket-client from requirements.txt because librespot-python added it to their dependency list. -- Made it hide your password when you type it in. -- Added manual override to force premium quality if zspotify cannot auto detect it. -- Added option to just download the raw audio with no re-encoding at all. -- Added Shebang line so it runs smoother on Linux. -- Made it download the entire track at once now so it is more efficient and fixed a bug users encountered. - -**v1.5 (19 Oct 2021):** -- Made downloading a lot more efficient and probably faster. -- Made the sanitizer more efficient. -- Formatted and linted all the code. - -**v1.4 (19 Oct 2021):** -- Added option to encode the downloaded tracks in the "ogg" format rather than "mp3". -- Added small improvement to sanitation function so it catches another edge case. - -**v1.3 (19 Oct 2021):** -- Added auto detection about if the current account is premium or not. If it is a premium account it automatically sets the quality to VERY_HIGH and otherwise HIGH if we are using a free account. -- Fixed conversion function so it now exports to the correct bitrate. -- Added sanitation to playlist names to help catch an edge case crash. -- Added option to download all your liked songs into a sub-folder. - -**v1.2 (18 Oct 2021):** -- Added .gitignore. -- Replaced dependency list in README.md with a proper requirements.txt file. -- Improved the readability of README.md. - -**v1.1 (16 Oct 2021):** -- Added try/except to help catch crashes where a very few specific tracks would crash either the downloading or conversion part. - -**v1.0 (14 Oct 2021):** -- Adjusted some functions so it runs again with the newer version of librespot-python. -- Improved my sanitization function so it catches more edge cases. -- Fixed an issue where sometimes spotify wouldn't provide a song id for a track we are trying to download. It will now detect and skip these invalid tracks. -- Added additional check for tracks that cannot be "played" due to licence(and similar) issues. These tracks will be skipped. - -**v0.9 (13 Oct 2021):** -- Initial upload, needs adjustments to get working again after backend rewrite. +### Changelog +Please refer to [CHANGELOG](CHANGELOG.md) From 51abecda74c8af8e5e60afa11cd9a7aaa4bb835d Mon Sep 17 00:00:00 2001 From: Mazzya <41023812+Mazzya@users.noreply.github.com> Date: Sun, 24 Oct 2021 14:24:47 +0200 Subject: [PATCH 05/41] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b0b94279..d25068fe 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ This isn't to say _you_ won't get banned as it is technically against Spotify's If you see this, don't worry! Just try logging back in. If you see the incorrect username or password error, reset your password and you should be able to log back in and continue using Spotify. ### Contributing -Please refer to [CONTRIBUTING.md](CONTRIBUTING.md) +Please refer to [CONTRIBUTING](CONTRIBUTING.md) ### Changelog Please refer to [CHANGELOG](CHANGELOG.md) From beff9627a8f9786748b0d75f588caae39d777501 Mon Sep 17 00:00:00 2001 From: Raju komati Date: Sun, 24 Oct 2021 18:35:40 +0530 Subject: [PATCH 06/41] added m3u playlist file creation fixed sonar issues --- src/app.py | 110 ++++++++++++++--------------------- src/const.py | 14 +++++ src/playlist.py | 15 +++-- src/podcast.py | 22 ++++--- src/track.py | 152 +++++++++++++++++++++++++++++++----------------- src/utils.py | 56 ++++++------------ 6 files changed, 193 insertions(+), 176 deletions(-) diff --git a/src/app.py b/src/app.py index abd8b876..d1c234d5 100644 --- a/src/app.py +++ b/src/app.py @@ -6,10 +6,11 @@ from tabulate import tabulate from album import download_album, download_artist_albums from const import TRACK, NAME, ID, ARTISTS, ITEMS, TRACKS, EXPLICIT, ALBUMS, OWNER, \ PLAYLISTS, DISPLAY_NAME -from playlist import get_playlist_songs, get_playlist_info, download_from_user_playlist, download_playlist +from playlist import download_from_user_playlist, download_playlist, \ + download_playlist_with_id from podcast import download_episode, get_show_episodes from track import download_track, get_saved_tracks -from utils import sanitize_data, splash, split_input, regex_input_for_urls +from utils import splash, split_input, regex_input_for_urls from zspotify import ZSpotify SEARCH_URL = 'https://api.spotify.com/v1/search' @@ -29,89 +30,68 @@ def client() -> None: while True: if len(sys.argv) > 1: - if sys.argv[1] == '-p' or sys.argv[1] == '--playlist': - download_from_user_playlist() - elif sys.argv[1] == '-ls' or sys.argv[1] == '--liked-songs': - for song in get_saved_tracks(): - if not song[TRACK][NAME]: - print('### SKIPPING: SONG DOES NOT EXISTS ON SPOTIFY ANYMORE ###') - else: - download_track(song[TRACK][ID], 'Liked Songs/') - print('\n') - else: - track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls(sys.argv[1]) - - if track_id is not None: - download_track(track_id) - elif artist_id is not None: - download_artist_albums(artist_id) - elif album_id is not None: - download_album(album_id) - elif playlist_id is not None: - playlist_songs = get_playlist_songs(playlist_id) - name, _ = get_playlist_info(playlist_id) - for song in playlist_songs: - download_track(song[TRACK][ID], - sanitize_data(name) + '/') - print('\n') - elif episode_id is not None: - download_episode(episode_id) - elif show_id is not None: - for episode in get_show_episodes(show_id): - download_episode(episode) - + process_args_input() else: search_text = '' while len(search_text) == 0: search_text = input('Enter search or URL: ') - - track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls(search_text) - - if track_id is not None: - download_track(track_id) - elif artist_id is not None: - download_artist_albums(artist_id) - elif album_id is not None: - download_album(album_id) - elif playlist_id is not None: - playlist_songs = get_playlist_songs(playlist_id) - name, _ = get_playlist_info(playlist_id) - for song in playlist_songs: - download_track(song[TRACK][ID], sanitize_data(name) + '/') - print('\n') - elif episode_id is not None: - download_episode(episode_id) - elif show_id is not None: - for episode in get_show_episodes(show_id): - download_episode(episode) - else: - search(search_text) + process_url_input(search_text, call_search=True) # wait() +def process_args_input(): + if sys.argv[1] == '-p' or sys.argv[1] == '--playlist': + download_from_user_playlist() + elif sys.argv[1] == '-ls' or sys.argv[1] == '--liked-songs': + for song in get_saved_tracks(): + if not song[TRACK][NAME]: + print('### SKIPPING: SONG DOES NOT EXISTS ON SPOTIFY ANYMORE ###') + else: + download_track(song[TRACK][ID], 'Liked Songs/') + print('\n') + else: + process_url_input(sys.argv[1]) + + +def process_url_input(url, call_search=False): + track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls(url) + + if track_id: + download_track(track_id) + elif artist_id: + download_artist_albums(artist_id) + elif album_id: + download_album(album_id) + elif playlist_id: + download_playlist_with_id(playlist_id) + elif episode_id: + download_episode(episode_id) + elif show_id: + for episode in get_show_episodes(show_id): + download_episode(episode) + elif call_search: + search(url) + + def search(search_term): """ Searches Spotify's API for relevant data """ params = {'limit': '10', 'offset': '0', 'q': search_term, 'type': 'track,album,artist,playlist'} resp = ZSpotify.invoke_url_with_params(SEARCH_URL, **params) + total_tracks = total_albums = total_artists = 0 counter = 1 tracks = resp[TRACKS][ITEMS] if len(tracks) > 0: print('### TRACKS ###') track_data = [] for track in tracks: - if track[EXPLICIT]: - explicit = '[E]' - else: - explicit = '' + explicit = '[E]' if track[EXPLICIT] else '' track_data.append([counter, f'{track[NAME]} {explicit}', ','.join([artist[NAME] for artist in track[ARTISTS]])]) counter += 1 total_tracks = counter - 1 print(tabulate(track_data, headers=['S.NO', 'Name', 'Artists'], tablefmt='pretty')) print('\n') - else: - total_tracks = 0 albums = resp[ALBUMS][ITEMS] if len(albums) > 0: @@ -123,8 +103,6 @@ def search(search_term): total_albums = counter - total_tracks - 1 print(tabulate(album_data, headers=['S.NO', 'Album', 'Artists'], tablefmt='pretty')) print('\n') - else: - total_albums = 0 artists = resp[ARTISTS][ITEMS] if len(artists) > 0: @@ -136,8 +114,6 @@ def search(search_term): total_artists = counter - total_tracks - total_albums - 1 print(tabulate(artist_data, headers=['S.NO', 'Name'], tablefmt='pretty')) print('\n') - else: - total_artists = 0 playlists = resp[PLAYLISTS][ITEMS] print('### PLAYLISTS ###') @@ -147,7 +123,11 @@ def search(search_term): counter += 1 print(tabulate(playlist_data, headers=['S.NO', 'Name', 'Owner'], tablefmt='pretty')) print('\n') + perform_action(tracks, albums, playlists, artists, total_tracks, total_albums, total_artists) + +def perform_action(tracks: list, albums: list, playlists: list, artists: list, total_tracks: int, total_albums: int, + total_artists: int): if len(tracks) + len(albums) + len(playlists) == 0: print('NO RESULTS FOUND - EXITING...') else: diff --git a/src/const.py b/src/const.py index 0c9a8545..44d7e688 100644 --- a/src/const.py +++ b/src/const.py @@ -93,3 +93,17 @@ OVERRIDE_AUTO_WAIT = 'OVERRIDE_AUTO_WAIT' CHUNK_SIZE = 'CHUNK_SIZE' SPLIT_ALBUM_DISCS = 'SPLIT_ALBUM_DISCS' + +DURATION_MS = 'duration_ms' + +ARTIST_ID = 'ArtistID' + +SHOW_ID = 'ShowID' + +EPISODE_ID = 'EpisodeID' + +PLAYLIST_ID = 'PlaylistID' + +ALBUM_ID = 'AlbumID' + +TRACK_ID = 'TrackID' diff --git a/src/playlist.py b/src/playlist.py index 8a4357a3..ed51673c 100644 --- a/src/playlist.py +++ b/src/playlist.py @@ -47,17 +47,20 @@ def get_playlist_info(playlist_id): return resp['name'].strip(), resp['owner']['display_name'].strip() -def download_playlist(playlists, playlist_number): - """Downloads all the songs from a playlist""" - - playlist_songs = [song for song in get_playlist_songs(playlists[int(playlist_number) - 1][ID]) if song[TRACK][ID]] +def download_playlist_with_id(playlist_id): + name, _ = get_playlist_info(playlist_id) + playlist_songs = [song for song in get_playlist_songs(playlist_id) if song[TRACK][ID]] p_bar = tqdm(playlist_songs, unit='song', total=len(playlist_songs), unit_scale=True) for song in p_bar: - download_track(song[TRACK][ID], sanitize_data(playlists[int(playlist_number) - 1][NAME].strip()) + '/', - disable_progressbar=True) + download_track(song[TRACK][ID], sanitize_data(name.strip()) + '/', disable_progressbar=True, create_m3u_file=True) p_bar.set_description(song[TRACK][NAME]) +def download_playlist(playlists, playlist_number): + """Downloads all the songs from a playlist""" + download_playlist_with_id(playlists[int(playlist_number) - 1][ID]) + + def download_from_user_playlist(): """ Select which playlist(s) to download """ playlists = get_all_playlists() diff --git a/src/podcast.py b/src/podcast.py index ccd8a694..1ff29105 100644 --- a/src/podcast.py +++ b/src/podcast.py @@ -1,6 +1,5 @@ from typing import Optional -from librespot.audio.decoders import VorbisOnlyAudioQuality from librespot.metadata import EpisodeId from tqdm import tqdm @@ -8,25 +7,24 @@ from const import NAME, ERROR, SHOW, ITEMS, ID, ROOT_PODCAST_PATH, CHUNK_SIZE from utils import sanitize_data, create_download_directory, MusicFormat from zspotify import ZSpotify - EPISODE_INFO_URL = 'https://api.spotify.com/v1/episodes' SHOWS_URL = 'https://api.spotify.com/v1/shows' -def get_episode_info(episode_id_str) -> tuple[Optional[str], Optional[str]]: - info = ZSpotify.invoke_url(f'{EPISODE_INFO_URL}/{episode_id_str}') +def get_episode_info(episode_id) -> tuple[Optional[str], Optional[str]]: + info = ZSpotify.invoke_url(f'{EPISODE_INFO_URL}/{episode_id}') if ERROR in info: return None, None return sanitize_data(info[SHOW][NAME]), sanitize_data(info[NAME]) -def get_show_episodes(show_id_str) -> list: +def get_show_episodes(show_id) -> list: episodes = [] offset = 0 limit = 50 while True: - resp = ZSpotify.invoke_url_with_params(f'{SHOWS_URL}/{show_id_str}/episodes', limit=limit, offset=offset) + resp = ZSpotify.invoke_url_with_params(f'{SHOWS_URL}/{show_id}/episodes', limit=limit, offset=offset) offset += limit for episode in resp[ITEMS]: episodes.append(episode[ID]) @@ -41,7 +39,7 @@ def download_episode(episode_id) -> None: extra_paths = podcast_name + '/' - if podcast_name is None: + if not podcast_name: print('### SKIPPING: (EPISODE NOT FOUND) ###') else: filename = podcast_name + ' - ' + episode_name @@ -54,11 +52,11 @@ def download_episode(episode_id) -> None: total_size = stream.input_stream.size with open(ZSpotify.get_config(ROOT_PODCAST_PATH) + extra_paths + filename + MusicFormat.OGG.value, 'wb') as file, tqdm( - desc=filename, - total=total_size, - unit='B', - unit_scale=True, - unit_divisor=1024 + desc=filename, + total=total_size, + unit='B', + unit_scale=True, + unit_divisor=1024 ) as bar: for _ in range(int(total_size / ZSpotify.get_config(CHUNK_SIZE)) + 1): bar.update(file.write( diff --git a/src/track.py b/src/track.py index dc148b0d..734fcb6a 100644 --- a/src/track.py +++ b/src/track.py @@ -1,3 +1,4 @@ +import math import os import time from typing import Any @@ -9,7 +10,7 @@ from tqdm import tqdm from const import TRACKS, ALBUM, NAME, ITEMS, DISC_NUMBER, TRACK_NUMBER, IS_PLAYABLE, ARTISTS, IMAGES, URL, \ RELEASE_DATE, ID, TRACKS_URL, SAVED_TRACKS_URL, SPLIT_ALBUM_DISCS, ROOT_PATH, DOWNLOAD_FORMAT, CHUNK_SIZE, \ - SKIP_EXISTING_FILES, ANTI_BAN_WAIT_TIME, OVERRIDE_AUTO_WAIT + SKIP_EXISTING_FILES, ANTI_BAN_WAIT_TIME, OVERRIDE_AUTO_WAIT, DURATION_MS from utils import sanitize_data, set_audio_tags, set_music_thumbnail, create_download_directory, \ MusicFormat from zspotify import ZSpotify @@ -31,7 +32,7 @@ def get_saved_tracks() -> list: return songs -def get_song_info(song_id) -> tuple[list[str], str, str, Any, Any, Any, Any, Any, Any]: +def get_song_info(song_id) -> tuple[list[str], str, str, Any, Any, Any, Any, Any, Any, Any]: """ Retrieves metadata for downloaded songs """ info = ZSpotify.invoke_url(f'{TRACKS_URL}?ids={song_id}&market=from_token') @@ -46,75 +47,118 @@ def get_song_info(song_id) -> tuple[list[str], str, str, Any, Any, Any, Any, Any track_number = info[TRACKS][0][TRACK_NUMBER] scraped_song_id = info[TRACKS][0][ID] is_playable = info[TRACKS][0][IS_PLAYABLE] + duration = math.ceil(info[TRACKS][0][DURATION_MS] / 1000) - return artists, album_name, name, image_url, release_year, disc_number, track_number, scraped_song_id, is_playable + return (artists, album_name, name, image_url, release_year, disc_number, track_number, scraped_song_id, is_playable, + duration) # noinspection PyBroadException -def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', disable_progressbar=False) -> None: +def download_track(track_id: str, extra_paths: str = '', prefix: bool = False, prefix_value='', + disable_progressbar: bool = False, + create_m3u_file: bool = False) -> None: """ Downloads raw song audio from Spotify """ try: (artists, album_name, name, image_url, release_year, disc_number, - track_number, scraped_song_id, is_playable) = get_song_info(track_id) - - song_name = sanitize_data(artists[0]) + ' - ' + sanitize_data(name) - if prefix: - song_name = f'{prefix_value.zfill(2)} - {song_name}' if prefix_value.isdigit( - ) else f'{prefix_value} - {song_name}' - - if ZSpotify.get_config(SPLIT_ALBUM_DISCS): - filename = os.path.join(ZSpotify.get_config(ROOT_PATH), extra_paths, 'Disc ' + str( - disc_number) + '/' + song_name + '.' + ZSpotify.get_config(DOWNLOAD_FORMAT)) - else: - filename = os.path.join(ZSpotify.get_config(ROOT_PATH), extra_paths, - song_name + '.' + ZSpotify.get_config(DOWNLOAD_FORMAT)) + track_number, scraped_song_id, is_playable, duration) = get_song_info(track_id) + song_name, filename, m3u_filename = process_track_metadata(artists, name, disc_number, extra_paths, prefix, + prefix_value, create_m3u_file) except Exception: print('### SKIPPING SONG - FAILED TO QUERY METADATA ###') else: try: - if not is_playable: - print('\n### SKIPPING:', song_name, - '(SONG IS UNAVAILABLE) ###') - else: - if os.path.isfile(filename) and os.path.getsize(filename) and ZSpotify.get_config(SKIP_EXISTING_FILES): - print('\n### SKIPPING:', song_name, - '(SONG ALREADY EXISTS) ###') - else: - if track_id != scraped_song_id: - track_id = scraped_song_id - track_id = TrackId.from_base62(track_id) - stream = ZSpotify.get_content_stream(track_id, ZSpotify.DOWNLOAD_QUALITY) - create_download_directory(ZSpotify.get_config(ROOT_PATH) + extra_paths) - total_size = stream.input_stream.size - - with open(filename, 'wb') as file, tqdm( - desc=song_name, - total=total_size, - unit='B', - unit_scale=True, - unit_divisor=1024, - disable=disable_progressbar - ) as p_bar: - for _ in range(int(total_size / ZSpotify.get_config(CHUNK_SIZE)) + 1): - p_bar.update(file.write( - stream.input_stream.stream().read(ZSpotify.get_config(CHUNK_SIZE)))) - - if ZSpotify.get_config(DOWNLOAD_FORMAT) == 'mp3': - convert_audio_format(filename) - set_audio_tags(filename, artists, name, album_name, - release_year, disc_number, track_number) - set_music_thumbnail(filename, image_url) - - if not ZSpotify.get_config(OVERRIDE_AUTO_WAIT): - time.sleep(ZSpotify.get_config(ANTI_BAN_WAIT_TIME)) - except Exception as e: + track_info = ( + artists, album_name, name, image_url, release_year, disc_number, track_number, scraped_song_id, + is_playable, duration) + playlist_info = (create_m3u_file, m3u_filename) + creat_track(track_id, extra_paths, song_name, filename, disable_progressbar, track_info, playlist_info) + except Exception: print('### SKIPPING:', song_name, '(GENERAL DOWNLOAD ERROR) ###') - print(e) if os.path.exists(filename): os.remove(filename) +def process_track_metadata(artists: list, name: str, disc_number: Any, extra_paths: str, prefix: bool, + prefix_value: str, + create_m3u_file: bool): + m3u_filename = None + + song_name = sanitize_data(artists[0]) + ' - ' + sanitize_data(name) + if prefix: + song_name = f'{prefix_value.zfill(2)} - {song_name}' if prefix_value.isdigit( + ) else f'{prefix_value} - {song_name}' + if create_m3u_file: + m3u_filename = f'{os.path.join(ZSpotify.get_config(ROOT_PATH), extra_paths, extra_paths)[:-1]}.m3u' + if ZSpotify.get_config(SPLIT_ALBUM_DISCS): + filename = os.path.join(ZSpotify.get_config(ROOT_PATH), extra_paths, 'Disc ' + str( + disc_number) + '/' + song_name + '.' + ZSpotify.get_config(DOWNLOAD_FORMAT)) + else: + filename = os.path.join(ZSpotify.get_config(ROOT_PATH), extra_paths, + song_name + '.' + ZSpotify.get_config(DOWNLOAD_FORMAT)) + return song_name, filename, m3u_filename + + +def creat_track(track_id, extra_paths: str, song_name: str, filename: str, disable_progressbar: bool, track_info: tuple, + playlist_info: tuple): + (artists, album_name, name, image_url, release_year, disc_number, + track_number, scraped_song_id, is_playable, duration) = track_info + create_m3u, m3u_filename = playlist_info + if not is_playable: + print(f'\n### SKIPPING: {song_name} (SONG IS UNAVAILABLE) ###') + else: + if os.path.isfile(filename) and os.path.getsize(filename) and ZSpotify.get_config(SKIP_EXISTING_FILES): + playlist_data = f'#EXTINF:{duration}, {artists[0]} - {name}\n{os.path.abspath(filename)}' + create_playlist_file(create_m3u, playlist_data, m3u_filename) + print(f'\n### SKIPPING: {song_name} (SONG ALREADY EXISTS) ###') + else: + if track_id != scraped_song_id: + track_id = scraped_song_id + track_id = TrackId.from_base62(track_id) + stream = ZSpotify.get_content_stream(track_id, ZSpotify.DOWNLOAD_QUALITY) + create_download_directory(ZSpotify.get_config(ROOT_PATH) + extra_paths) + total_size = stream.input_stream.size + + write_stream_to_file(stream, filename, song_name, total_size, disable_progressbar, track_info, + playlist_info) + + +def write_stream_to_file(stream, filename: str, song_name: str, total_size: Any, disable_progressbar: bool, + track_info: tuple, playlist_info: tuple): + (artists, album_name, name, image_url, release_year, disc_number, + track_number, scraped_song_id, is_playable, duration) = track_info + create_m3u, m3u_filename = playlist_info + with open(filename, 'wb') as file, tqdm( + desc=song_name, + total=total_size, + unit='B', + unit_scale=True, + unit_divisor=1024, + disable=disable_progressbar + ) as p_bar: + for _ in range(int(total_size / ZSpotify.get_config(CHUNK_SIZE)) + 1): + p_bar.update(file.write( + stream.input_stream.stream().read(ZSpotify.get_config(CHUNK_SIZE)))) + playlist_data = f'#EXTINF:{duration}, {artists[0]} - {name}\n{os.path.abspath(filename)}' + if ZSpotify.get_config(DOWNLOAD_FORMAT) == MusicFormat.MP3.value: + convert_audio_format(filename) + set_audio_tags(filename, artists, name, album_name, + release_year, disc_number, track_number) + set_music_thumbnail(filename, image_url) + + if not ZSpotify.get_config(OVERRIDE_AUTO_WAIT): + time.sleep(ZSpotify.get_config(ANTI_BAN_WAIT_TIME)) + create_playlist_file(create_m3u, playlist_data, m3u_filename) + + +def create_playlist_file(create_m3u: bool, playlist_data: str, m3u_filename: str): + if create_m3u and m3u_filename and playlist_data: + with open(m3u_filename, 'a+') as pfile: + if os.path.getsize(m3u_filename) == 0: + pfile.write('#EXTM3U\n') + pfile.write(playlist_data + '\n') + + def convert_audio_format(filename) -> None: """ Converts raw audio into playable mp3 """ # print('### CONVERTING TO ' + MUSIC_FORMAT.upper() + ' ###') diff --git a/src/utils.py b/src/utils.py index 3fd91612..dc4f19fe 100644 --- a/src/utils.py +++ b/src/utils.py @@ -3,18 +3,19 @@ import platform import re import time from enum import Enum +from typing import Match import music_tag import requests from const import SANITIZE, ARTIST, TRACKTITLE, ALBUM, YEAR, DISCNUMBER, TRACKNUMBER, ARTWORK, \ - WINDOWS_SYSTEM + WINDOWS_SYSTEM, TRACK_ID, ALBUM_ID, PLAYLIST_ID, EPISODE_ID, SHOW_ID, ARTIST_ID class MusicFormat(str, Enum): MP3 = 'mp3', OGG = 'ogg', - + def create_download_directory(download_path: str) -> None: os.makedirs(download_path, exist_ok=True) @@ -135,46 +136,23 @@ def regex_input_for_urls(search_input) -> tuple[str, str, str, str, str, str]: search_input, ) - if track_uri_search is not None or track_url_search is not None: - track_id_str = (track_uri_search - if track_uri_search is not None else - track_url_search).group('TrackID') - else: - track_id_str = None + return ( + extract_info_from_regex_response(TRACK_ID, track_uri_search, track_url_search), - if album_uri_search is not None or album_url_search is not None: - album_id_str = (album_uri_search - if album_uri_search is not None else - album_url_search).group('AlbumID') - else: - album_id_str = None + extract_info_from_regex_response(ALBUM_ID, album_uri_search, album_url_search), - if playlist_uri_search is not None or playlist_url_search is not None: - playlist_id_str = (playlist_uri_search - if playlist_uri_search is not None else - playlist_url_search).group('PlaylistID') - else: - playlist_id_str = None + extract_info_from_regex_response(PLAYLIST_ID, playlist_uri_search, playlist_url_search), - if episode_uri_search is not None or episode_url_search is not None: - episode_id_str = (episode_uri_search - if episode_uri_search is not None else - episode_url_search).group('EpisodeID') - else: - episode_id_str = None + extract_info_from_regex_response(EPISODE_ID, episode_uri_search, episode_url_search), - if show_uri_search is not None or show_url_search is not None: - show_id_str = (show_uri_search - if show_uri_search is not None else - show_url_search).group('ShowID') - else: - show_id_str = None + extract_info_from_regex_response(SHOW_ID, show_uri_search, show_url_search), - if artist_uri_search is not None or artist_url_search is not None: - artist_id_str = (artist_uri_search - if artist_uri_search is not None else - artist_url_search).group('ArtistID') - else: - artist_id_str = None + extract_info_from_regex_response(ARTIST_ID, artist_uri_search, artist_url_search) + ) - return track_id_str, album_id_str, playlist_id_str, episode_id_str, show_id_str, artist_id_str + +def extract_info_from_regex_response(key, uri_data: Match[str], url_data: Match[str]): + if uri_data or url_data: + return (uri_data if uri_data else url_data).group(key) + else: + return None From 18fca559b30baa306c5e7d7cb5e4c57b71284c6f Mon Sep 17 00:00:00 2001 From: yiannisha Date: Sun, 24 Oct 2021 18:07:12 +0300 Subject: [PATCH 07/41] Added argument parsing in search function Added a block for parsing arguments passed with options -l -limit -t -type in the search_term. They work as follows: * -l -limit : sets the limit of results to that number and raises a ValueError if that number exceeds 50. Default is 10. * -t -type : sets the type that is requested from the API about the search query. Raises a ValueError if an arguments passed are different than track, album, playlist. Default is all three. Example: Enter search or URL: -l 30 -t track album This will result with 30 tracks and 30 albums associated with query. Options can be passed in any order but the query must be first. --- src/app.py | 209 +++++++++++++++++++++++++++++++++++------------- src/const.py | 2 + src/playlist.py | 6 +- 3 files changed, 158 insertions(+), 59 deletions(-) diff --git a/src/app.py b/src/app.py index abd8b876..d34fdfec 100644 --- a/src/app.py +++ b/src/app.py @@ -4,8 +4,8 @@ from librespot.audio.decoders import AudioQuality from tabulate import tabulate from album import download_album, download_artist_albums -from const import TRACK, NAME, ID, ARTISTS, ITEMS, TRACKS, EXPLICIT, ALBUMS, OWNER, \ - PLAYLISTS, DISPLAY_NAME +from const import TRACK, NAME, ID, ARTIST, ARTISTS, ITEMS, TRACKS, EXPLICIT, ALBUM, ALBUMS, \ + OWNER, PLAYLIST, PLAYLISTS, DISPLAY_NAME from playlist import get_playlist_songs, get_playlist_info, download_from_user_playlist, download_playlist from podcast import download_episode, get_show_episodes from track import download_track, get_saved_tracks @@ -91,64 +91,159 @@ def client() -> None: def search(search_term): """ Searches Spotify's API for relevant data """ - params = {'limit': '10', 'offset': '0', 'q': search_term, 'type': 'track,album,artist,playlist'} + params = {'limit': '10', + 'offset': '0', + 'q': search_term, + 'type': 'track,album,artist,playlist'} + + # Parse args + splits = search_term.split() + for split in splits: + index = splits.index(split) + + if split[0] == '-' and len(split) > 1: + if len(splits)-1 == index: + raise IndexError('No parameters passed after option: {}\n'. + format(split)) + + if split == '-l' or split == '-limit': + try: + int(splits[index+1]) + except ValueError: + raise ValueError('Paramater passed after {} option must be an integer.\n'. + format(split)) + if int(splits[index+1]) > 50: + raise ValueError('Invalid limit passed. Max is 50.\n') + params['limit'] = splits[index+1] + + + if split == '-t' or split == '-type': + + allowed_types = ['track', 'playlist', 'album', 'artist'] + passed_types = [] + for i in range(index+1, len(splits)): + if splits[i][0] == '-': + break + + if splits[i] not in allowed_types: + raise ValueError('Parameters passed after {} option must be from this list:\n{}'. + format(split, '\n'.join(allowed_types))) + + passed_types.append(splits[i]) + params['type'] = ','.join(passed_types) + + if len(params['type']) == 0: + params['type'] = 'track,album,artist,playlist' + + # Clean search term + search_term_list = [] + for split in splits: + if split[0] == "-": + break + search_term_list.append(split) + if not search_term_list: + raise ValueError("Invalid query.") + params["q"] = ' '.join(search_term_list) + resp = ZSpotify.invoke_url_with_params(SEARCH_URL, **params) counter = 1 - tracks = resp[TRACKS][ITEMS] - if len(tracks) > 0: - print('### TRACKS ###') - track_data = [] - for track in tracks: - if track[EXPLICIT]: - explicit = '[E]' - else: - explicit = '' - track_data.append([counter, f'{track[NAME]} {explicit}', - ','.join([artist[NAME] for artist in track[ARTISTS]])]) - counter += 1 - total_tracks = counter - 1 - print(tabulate(track_data, headers=['S.NO', 'Name', 'Artists'], tablefmt='pretty')) - print('\n') + dics = [] + + if TRACK in params['type'].split(','): + tracks = resp[TRACKS][ITEMS] + if len(tracks) > 0: + print('### TRACKS ###') + track_data = [] + for track in tracks: + if track[EXPLICIT]: + explicit = '[E]' + else: + explicit = '' + + track_data.append([counter, f'{track[NAME]} {explicit}', + ','.join([artist[NAME] for artist in track[ARTISTS]])]) + dics.append({ + ID : track[ID], + NAME : track[NAME], + 'type' : TRACK, + }) + + counter += 1 + total_tracks = counter - 1 + print(tabulate(track_data, headers=['S.NO', 'Name', 'Artists'], tablefmt='pretty')) + print('\n') + del tracks + del track_data else: total_tracks = 0 - albums = resp[ALBUMS][ITEMS] - if len(albums) > 0: - print('### ALBUMS ###') - album_data = [] - for album in albums: - album_data.append([counter, album[NAME], ','.join([artist[NAME] for artist in album[ARTISTS]])]) - counter += 1 - total_albums = counter - total_tracks - 1 - print(tabulate(album_data, headers=['S.NO', 'Album', 'Artists'], tablefmt='pretty')) - print('\n') + if ALBUM in params['type'].split(','): + albums = resp[ALBUMS][ITEMS] + if len(albums) > 0: + print('### ALBUMS ###') + album_data = [] + for album in albums: + album_data.append([counter, album[NAME], + ','.join([artist[NAME] for artist in album[ARTISTS]])]) + dics.append({ + ID : album[ID], + NAME : album[NAME], + 'type' : ALBUM, + }) + + counter += 1 + total_albums = counter - total_tracks - 1 + print(tabulate(album_data, headers=['S.NO', 'Album', 'Artists'], tablefmt='pretty')) + print('\n') + del albums + del album_data else: total_albums = 0 - artists = resp[ARTISTS][ITEMS] - if len(artists) > 0: - print('### ARTISTS ###') - artist_data = [] - for artist in artists: - artist_data.append([counter, artist[NAME]]) - counter += 1 - total_artists = counter - total_tracks - total_albums - 1 - print(tabulate(artist_data, headers=['S.NO', 'Name'], tablefmt='pretty')) - print('\n') + if ARTIST in params['type'].split(','): + artists = resp[ARTISTS][ITEMS] + if len(artists) > 0: + print('### ARTISTS ###') + artist_data = [] + for artist in artists: + artist_data.append([counter, artist[NAME]]) + dics.append({ + ID : artist[ID], + NAME : artist[NAME], + 'type' : ARTIST, + }) + counter += 1 + total_artists = counter - total_tracks - total_albums - 1 + print(tabulate(artist_data, headers=['S.NO', 'Name'], tablefmt='pretty')) + print('\n') + del artists + del artist_data else: total_artists = 0 - playlists = resp[PLAYLISTS][ITEMS] - print('### PLAYLISTS ###') - playlist_data = [] - for playlist in playlists: - playlist_data.append([counter, playlist[NAME], playlist[OWNER][DISPLAY_NAME]]) - counter += 1 - print(tabulate(playlist_data, headers=['S.NO', 'Name', 'Owner'], tablefmt='pretty')) - print('\n') + if PLAYLIST in params['type'].split(','): + playlists = resp[PLAYLISTS][ITEMS] + if len(playlists) > 0: + print('### PLAYLISTS ###') + playlist_data = [] + for playlist in playlists: + playlist_data.append([counter, playlist[NAME], playlist[OWNER][DISPLAY_NAME]]) + dics.append({ + ID : playlist[ID], + NAME : playlist[NAME], + 'type' : PLAYLIST, + }) + counter += 1 + total_playlists = counter - total_artists - total_tracks - total_albums - 1 + print(tabulate(playlist_data, headers=['S.NO', 'Name', 'Owner'], tablefmt='pretty')) + print('\n') + del playlists + del playlist_data + else: + total_playlists = 0 - if len(tracks) + len(albums) + len(playlists) == 0: + if total_tracks + total_albums + total_artists + total_playlists == 0: print('NO RESULTS FOUND - EXITING...') else: selection = '' @@ -157,15 +252,17 @@ def search(search_term): inputs = split_input(selection) for pos in inputs: position = int(pos) - if position <= total_tracks: - track_id = tracks[position - 1][ID] - download_track(track_id) - elif position <= total_albums + total_tracks: - download_album(albums[position - total_tracks - 1][ID]) - elif position <= total_artists + total_tracks + total_albums: - download_artist_albums(artists[position - total_tracks - total_albums - 1][ID]) - else: - download_playlist(playlists, position - total_tracks - total_albums - total_artists) + for dic in dics: + print_pos = dics.index(dic) + 1 + if print_pos == position: + if dic['type'] == TRACK: + download_track(dic[ID]) + elif dic['type'] == ALBUM: + download_album(dic[ID]) + elif dic['type'] == ARTIST: + download_artist_albums(dic[ID]) + else: + download_playlist(dic) if __name__ == '__main__': diff --git a/src/const.py b/src/const.py index 0c9a8545..f44d8694 100644 --- a/src/const.py +++ b/src/const.py @@ -54,6 +54,8 @@ ERROR = 'error' EXPLICIT = 'explicit' +PLAYLIST = 'playlist' + PLAYLISTS = 'playlists' OWNER = 'owner' diff --git a/src/playlist.py b/src/playlist.py index 8a4357a3..9a1304c2 100644 --- a/src/playlist.py +++ b/src/playlist.py @@ -47,13 +47,13 @@ def get_playlist_info(playlist_id): return resp['name'].strip(), resp['owner']['display_name'].strip() -def download_playlist(playlists, playlist_number): +def download_playlist(playlist): """Downloads all the songs from a playlist""" - playlist_songs = [song for song in get_playlist_songs(playlists[int(playlist_number) - 1][ID]) if song[TRACK][ID]] + playlist_songs = [song for song in get_playlist_songs(playlist[ID]) if song[TRACK][ID]] p_bar = tqdm(playlist_songs, unit='song', total=len(playlist_songs), unit_scale=True) for song in p_bar: - download_track(song[TRACK][ID], sanitize_data(playlists[int(playlist_number) - 1][NAME].strip()) + '/', + download_track(song[TRACK][ID], sanitize_data(playlist[NAME].strip()) + '/', disable_progressbar=True) p_bar.set_description(song[TRACK][NAME]) From 40bc2d612fcb1932516bf2e5dcd4d1d2c7c93c54 Mon Sep 17 00:00:00 2001 From: Jared Rossberg Date: Sun, 24 Oct 2021 09:49:55 -0600 Subject: [PATCH 08/41] Fix spelling errors --- CONTRIBUTING.md | 2 +- README.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1ee5eef8..0020b813 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ ZSpotify is a community-driven project. There are many different ways to contrib ### What we aren't looking for -Please don't use the issues section to request help installing or setting up the project. It should be reserved for bugs when running the code, and feature requqests. Instead use the support channel in either our Discord or Matrix server. +Please don't use the issues section to request help installing or setting up the project. It should be reserved for bugs when running the code, and feature requests. Instead use the support channel in either our Discord or Matrix server. # Ground rules diff --git a/README.md b/README.md index 7b8f1b0e..a0ecfa9f 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,9 @@ Python packages: ``` -\*ffmpeg can be installed via apt for Debian-based distros or by downloading the binaries from [ffmpeg.org](https://ffmpeg.org) and placing them in your %PATH% in Windows. +\*ffmpeg can be installed via apt for Debian-based distros or by downloading the binaries from [ffmpeg.org](https://ffmpeg.org) and placing them in your %PATH% in Windows. Mac users can intall it using [Homebrew](https://brew.sh). -\*\*Git can be installed via apt for Debian-based distros or by downloading the binaries from [git-scm.com](https://git-scm.com/download/win) for Windows. +\*\*Git can be installed via apt for Debian-based distros or by downloading the binaries from [git-scm.com](https://git-scm.com/download/win) for Windows. Mac users can intall it using [Homebrew](https://brew.sh). ``` Command line usage: python app.py Loads search prompt to find then download a specific track, album or playlist @@ -43,7 +43,7 @@ Options that can be configured in zs_config.json: SKIP_EXISTING_FILES Set this to false if you want ZSpotify to overwrite files with the same name rather than skipping the song - MUSIC_FORMAT Can be "mp3" or "ogg", mp3 is required for track metadata however ogg is slightly higer quality as it is not trsnacoded. + MUSIC_FORMAT Can be "mp3" or "ogg", mp3 is required for track metadata however ogg is slightly higer quality as it is not transcoded. FORCE_PREMIUM Set this to true if ZSpotify isn't automatically detecting that you are using a premium account From 41b09712e55954a1cc2f5b8080b4e1f31339e3d5 Mon Sep 17 00:00:00 2001 From: yiannisha Date: Sun, 24 Oct 2021 19:09:58 +0300 Subject: [PATCH 09/41] Can now download all of an artists songs fixes#88 --- src/album.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/album.py b/src/album.py index 12f86eb6..19004d11 100644 --- a/src/album.py +++ b/src/album.py @@ -35,7 +35,13 @@ def get_artist_albums(artist_id): """ Returns artist's albums """ resp = ZSpotify.invoke_url(f'{ARTIST_URL}/{artist_id}/albums') # Return a list each album's id - return [resp[ITEMS][i][ID] for i in range(len(resp[ITEMS]))] + album_ids = [resp[ITEMS][i][ID] for i in range(len(resp[ITEMS]))] + # Recursive requests to get all albums including singles an EPs + while resp['next'] is not None: + resp = ZSpotify.invoke_url(resp['next']) + album_ids.extend([resp[ITEMS][i][ID] for i in range(len(resp[ITEMS]))]) + + return album_ids def download_album(album): From 40086384b0bf67a842787d686d4ea05d06deb402 Mon Sep 17 00:00:00 2001 From: yiannisha Date: Sun, 24 Oct 2021 19:18:18 +0300 Subject: [PATCH 10/41] Can now download all of an artists songs fixes#88 --- src/album.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/album.py b/src/album.py index 12f86eb6..19004d11 100644 --- a/src/album.py +++ b/src/album.py @@ -35,7 +35,13 @@ def get_artist_albums(artist_id): """ Returns artist's albums """ resp = ZSpotify.invoke_url(f'{ARTIST_URL}/{artist_id}/albums') # Return a list each album's id - return [resp[ITEMS][i][ID] for i in range(len(resp[ITEMS]))] + album_ids = [resp[ITEMS][i][ID] for i in range(len(resp[ITEMS]))] + # Recursive requests to get all albums including singles an EPs + while resp['next'] is not None: + resp = ZSpotify.invoke_url(resp['next']) + album_ids.extend([resp[ITEMS][i][ID] for i in range(len(resp[ITEMS]))]) + + return album_ids def download_album(album): From dc37d6157e34b1266ec52a2670de47f2985374cf Mon Sep 17 00:00:00 2001 From: Jared Rossberg Date: Sun, 24 Oct 2021 10:22:20 -0600 Subject: [PATCH 11/41] Fix readme cli usage --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a0ecfa9f..ad49bf7e 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ Python packages: \*\*Git can be installed via apt for Debian-based distros or by downloading the binaries from [git-scm.com](https://git-scm.com/download/win) for Windows. Mac users can intall it using [Homebrew](https://brew.sh). ``` Command line usage: - python app.py Loads search prompt to find then download a specific track, album or playlist - python app.py Downloads the track, album, playlist or podcast episode specified as a command line argument - python app.py Downloads all albums by specified artist + python src/app.py Loads search prompt to find then download a specific track, album or playlist + python src/app.py Downloads the track, album, playlist or podcast episode specified as a command line argument + python src/app.py Downloads all albums by specified artist Extra command line options: -p, --playlist Downloads a saved playlist from your account From 36be67b827bd2192380b3cfc98160bfa7eddd094 Mon Sep 17 00:00:00 2001 From: yiannisha Date: Sun, 24 Oct 2021 19:32:32 +0300 Subject: [PATCH 12/41] Revert "Can now download all of an artists songs fixes#88" This reverts commit 41b09712e55954a1cc2f5b8080b4e1f31339e3d5. --- src/album.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/album.py b/src/album.py index 19004d11..12f86eb6 100644 --- a/src/album.py +++ b/src/album.py @@ -35,13 +35,7 @@ def get_artist_albums(artist_id): """ Returns artist's albums """ resp = ZSpotify.invoke_url(f'{ARTIST_URL}/{artist_id}/albums') # Return a list each album's id - album_ids = [resp[ITEMS][i][ID] for i in range(len(resp[ITEMS]))] - # Recursive requests to get all albums including singles an EPs - while resp['next'] is not None: - resp = ZSpotify.invoke_url(resp['next']) - album_ids.extend([resp[ITEMS][i][ID] for i in range(len(resp[ITEMS]))]) - - return album_ids + return [resp[ITEMS][i][ID] for i in range(len(resp[ITEMS]))] def download_album(album): From 7c2b8fc1c997c215218a489ba9ef69faa4638a08 Mon Sep 17 00:00:00 2001 From: Jared Rossberg Date: Sun, 24 Oct 2021 10:35:04 -0600 Subject: [PATCH 13/41] Fix one more spelling error --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad49bf7e..8007922a 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Options that can be configured in zs_config.json: SKIP_EXISTING_FILES Set this to false if you want ZSpotify to overwrite files with the same name rather than skipping the song - MUSIC_FORMAT Can be "mp3" or "ogg", mp3 is required for track metadata however ogg is slightly higer quality as it is not transcoded. + MUSIC_FORMAT Can be "mp3" or "ogg", mp3 is required for track metadata however ogg is slightly higher quality as it is not transcoded. FORCE_PREMIUM Set this to true if ZSpotify isn't automatically detecting that you are using a premium account From 96128de4a7b8f79324edcbdb05dc8e068012d311 Mon Sep 17 00:00:00 2001 From: Jared Rossberg Date: Sun, 24 Oct 2021 10:37:59 -0600 Subject: [PATCH 14/41] Fix more spelling errors --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8007922a..7113c936 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,9 @@ Python packages: ``` -\*ffmpeg can be installed via apt for Debian-based distros or by downloading the binaries from [ffmpeg.org](https://ffmpeg.org) and placing them in your %PATH% in Windows. Mac users can intall it using [Homebrew](https://brew.sh). +\*ffmpeg can be installed via apt for Debian-based distros or by downloading the binaries from [ffmpeg.org](https://ffmpeg.org) and placing them in your %PATH% in Windows. Mac users can install it using [Homebrew](https://brew.sh). -\*\*Git can be installed via apt for Debian-based distros or by downloading the binaries from [git-scm.com](https://git-scm.com/download/win) for Windows. Mac users can intall it using [Homebrew](https://brew.sh). +\*\*Git can be installed via apt for Debian-based distros or by downloading the binaries from [git-scm.com](https://git-scm.com/download/win) for Windows. Mac users can install it using [Homebrew](https://brew.sh). ``` Command line usage: python src/app.py Loads search prompt to find then download a specific track, album or playlist From 5b6a41ef707cfc09d72b260b6128449b5b9df6bd Mon Sep 17 00:00:00 2001 From: Jared Rossberg Date: Sun, 24 Oct 2021 11:09:15 -0600 Subject: [PATCH 15/41] Explain homebrew better in readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7113c936..91c6349e 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,9 @@ Python packages: ``` -\*ffmpeg can be installed via apt for Debian-based distros or by downloading the binaries from [ffmpeg.org](https://ffmpeg.org) and placing them in your %PATH% in Windows. Mac users can install it using [Homebrew](https://brew.sh). +\*ffmpeg can be installed via apt for Debian-based distros or by downloading the binaries from [ffmpeg.org](https://ffmpeg.org) and placing them in your %PATH% in Windows. Mac users can install it with [Homebrew](https://brew.sh) by running `brew install ffmpeg`. -\*\*Git can be installed via apt for Debian-based distros or by downloading the binaries from [git-scm.com](https://git-scm.com/download/win) for Windows. Mac users can install it using [Homebrew](https://brew.sh). +\*\*Git can be installed via apt for Debian-based distros or by downloading the binaries from [git-scm.com](https://git-scm.com/download/win) for Windows. ``` Command line usage: python src/app.py Loads search prompt to find then download a specific track, album or playlist From 85344f573ca253737b79d309457d5aec714f4654 Mon Sep 17 00:00:00 2001 From: davidsalido <46091673+davidsalido@users.noreply.github.com> Date: Sun, 24 Oct 2021 17:11:01 +0000 Subject: [PATCH 16/41] Fix infinite loop --- src/app.py | 82 ++++++++++++++++++++++++++---------------------------- 1 file changed, 40 insertions(+), 42 deletions(-) diff --git a/src/app.py b/src/app.py index abd8b876..11f75069 100644 --- a/src/app.py +++ b/src/app.py @@ -27,45 +27,18 @@ def client() -> None: print('[ DETECTED FREE ACCOUNT - USING HIGH QUALITY ]\n\n') ZSpotify.DOWNLOAD_QUALITY = AudioQuality.HIGH - while True: - if len(sys.argv) > 1: - if sys.argv[1] == '-p' or sys.argv[1] == '--playlist': - download_from_user_playlist() - elif sys.argv[1] == '-ls' or sys.argv[1] == '--liked-songs': - for song in get_saved_tracks(): - if not song[TRACK][NAME]: - print('### SKIPPING: SONG DOES NOT EXISTS ON SPOTIFY ANYMORE ###') - else: - download_track(song[TRACK][ID], 'Liked Songs/') - print('\n') - else: - track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls(sys.argv[1]) - - if track_id is not None: - download_track(track_id) - elif artist_id is not None: - download_artist_albums(artist_id) - elif album_id is not None: - download_album(album_id) - elif playlist_id is not None: - playlist_songs = get_playlist_songs(playlist_id) - name, _ = get_playlist_info(playlist_id) - for song in playlist_songs: - download_track(song[TRACK][ID], - sanitize_data(name) + '/') - print('\n') - elif episode_id is not None: - download_episode(episode_id) - elif show_id is not None: - for episode in get_show_episodes(show_id): - download_episode(episode) - + if len(sys.argv) > 1: + if sys.argv[1] == '-p' or sys.argv[1] == '--playlist': + download_from_user_playlist() + elif sys.argv[1] == '-ls' or sys.argv[1] == '--liked-songs': + for song in get_saved_tracks(): + if not song[TRACK][NAME]: + print('### SKIPPING: SONG DOES NOT EXISTS ON SPOTIFY ANYMORE ###') + else: + download_track(song[TRACK][ID], 'Liked Songs/') + print('\n') else: - search_text = '' - while len(search_text) == 0: - search_text = input('Enter search or URL: ') - - track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls(search_text) + track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls(sys.argv[1]) if track_id is not None: download_track(track_id) @@ -77,16 +50,41 @@ def client() -> None: playlist_songs = get_playlist_songs(playlist_id) name, _ = get_playlist_info(playlist_id) for song in playlist_songs: - download_track(song[TRACK][ID], sanitize_data(name) + '/') + download_track(song[TRACK][ID], + sanitize_data(name) + '/') print('\n') elif episode_id is not None: download_episode(episode_id) elif show_id is not None: for episode in get_show_episodes(show_id): download_episode(episode) - else: - search(search_text) - # wait() + + else: + search_text = '' + while len(search_text) == 0: + search_text = input('Enter search or URL: ') + + track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls(search_text) + + if track_id is not None: + download_track(track_id) + elif artist_id is not None: + download_artist_albums(artist_id) + elif album_id is not None: + download_album(album_id) + elif playlist_id is not None: + playlist_songs = get_playlist_songs(playlist_id) + name, _ = get_playlist_info(playlist_id) + for song in playlist_songs: + download_track(song[TRACK][ID], sanitize_data(name) + '/') + print('\n') + elif episode_id is not None: + download_episode(episode_id) + elif show_id is not None: + for episode in get_show_episodes(show_id): + download_episode(episode) + else: + search(search_text) def search(search_term): From 69f59bb0f6f5c24c3ad69a04a1a64844cc94a4f2 Mon Sep 17 00:00:00 2001 From: Jared Rossberg Date: Sun, 24 Oct 2021 11:15:41 -0600 Subject: [PATCH 17/41] Create main module --- README.md | 6 +++--- src/__main__.py | 4 ++++ src/app.py | 3 --- 3 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 src/__main__.py diff --git a/README.md b/README.md index 91c6349e..8a78b481 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ Python packages: \*\*Git can be installed via apt for Debian-based distros or by downloading the binaries from [git-scm.com](https://git-scm.com/download/win) for Windows. ``` Command line usage: - python src/app.py Loads search prompt to find then download a specific track, album or playlist - python src/app.py Downloads the track, album, playlist or podcast episode specified as a command line argument - python src/app.py Downloads all albums by specified artist + python src Loads search prompt to find then download a specific track, album or playlist + python src Downloads the track, album, playlist or podcast episode specified as a command line argument + python src Downloads all albums by specified artist Extra command line options: -p, --playlist Downloads a saved playlist from your account diff --git a/src/__main__.py b/src/__main__.py new file mode 100644 index 00000000..50dd3890 --- /dev/null +++ b/src/__main__.py @@ -0,0 +1,4 @@ +from app import client + +if __name__ == '__main__': + client() diff --git a/src/app.py b/src/app.py index abd8b876..2a0898a4 100644 --- a/src/app.py +++ b/src/app.py @@ -167,6 +167,3 @@ def search(search_term): else: download_playlist(playlists, position - total_tracks - total_albums - total_artists) - -if __name__ == '__main__': - client() From a0bb3c3843a0fc49add8b3d144fe77e1ac82541f Mon Sep 17 00:00:00 2001 From: Alpha Date: Sun, 24 Oct 2021 13:45:52 -0400 Subject: [PATCH 18/41] Fix typing hints --- src/podcast.py | 4 ++-- src/track.py | 4 ++-- src/utils.py | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/podcast.py b/src/podcast.py index 43dae318..eea36c1b 100644 --- a/src/podcast.py +++ b/src/podcast.py @@ -1,5 +1,5 @@ import os -from typing import Optional +from typing import Optional, Tuple from librespot.audio.decoders import VorbisOnlyAudioQuality from librespot.metadata import EpisodeId @@ -14,7 +14,7 @@ EPISODE_INFO_URL = 'https://api.spotify.com/v1/episodes' SHOWS_URL = 'https://api.spotify.com/v1/shows' -def get_episode_info(episode_id_str) -> tuple[Optional[str], Optional[str]]: +def get_episode_info(episode_id_str) -> Tuple[Optional[str], Optional[str]]: info = ZSpotify.invoke_url(f'{EPISODE_INFO_URL}/{episode_id_str}') if ERROR in info: return None, None diff --git a/src/track.py b/src/track.py index 3fb088f7..9a928eca 100644 --- a/src/track.py +++ b/src/track.py @@ -1,6 +1,6 @@ import os import time -from typing import Any +from typing import Any, Tuple, List from librespot.audio.decoders import AudioQuality from librespot.metadata import TrackId @@ -31,7 +31,7 @@ def get_saved_tracks() -> list: return songs -def get_song_info(song_id) -> tuple[list[str], str, str, Any, Any, Any, Any, Any, Any]: +def get_song_info(song_id) -> Tuple[List[str], str, str, Any, Any, Any, Any, Any, Any]: """ Retrieves metadata for downloaded songs """ info = ZSpotify.invoke_url(f'{TRACKS_URL}?ids={song_id}&market=from_token') diff --git a/src/utils.py b/src/utils.py index 3fd91612..b8188d95 100644 --- a/src/utils.py +++ b/src/utils.py @@ -3,6 +3,7 @@ import platform import re import time from enum import Enum +from typing import List, Tuple import music_tag import requests @@ -27,7 +28,7 @@ def wait(seconds: int = 3) -> None: time.sleep(1) -def split_input(selection) -> list[str]: +def split_input(selection) -> List[str]: """ Returns a list of inputted strings """ inputs = [] if '-' in selection: @@ -91,7 +92,7 @@ def set_music_thumbnail(filename, image_url) -> None: tags.save() -def regex_input_for_urls(search_input) -> tuple[str, str, str, str, str, str]: +def regex_input_for_urls(search_input) -> Tuple[str, str, str, str, str, str]: """ Since many kinds of search may be passed at the command line, process them all here. """ track_uri_search = re.search( r'^spotify:track:(?P[0-9a-zA-Z]{22})$', search_input) From 798b25f3e6e1da7a1abd58e58a7c6d04c15c5bc3 Mon Sep 17 00:00:00 2001 From: Jared Rossberg Date: Sun, 24 Oct 2021 14:25:09 -0600 Subject: [PATCH 19/41] Fix dockerfile entrypoint --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index ceb88cf2..dcba36ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,4 +16,4 @@ COPY --from=builder /install /usr/local COPY src /app COPY zs_config.json / WORKDIR /app -ENTRYPOINT ["/usr/local/bin/python", "app.py"] +ENTRYPOINT ["/usr/local/bin/python", "__main__.py"] From e5da26c81abdb9b41c7aae0d31f1d15e4686d321 Mon Sep 17 00:00:00 2001 From: Jared Rossberg Date: Sun, 24 Oct 2021 14:32:50 -0600 Subject: [PATCH 20/41] Update README with Docker usage --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index ac87b944..80cfde88 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,13 @@ Options that can be configured in zs_config.json: ANTI_BAN_WAIT_TIME Change this setting if the time waited between bulk downloads is too high or low OVERRIDE_AUTO_WAIT Change this to true if you want to completely disable the wait between songs for faster downloads with the risk of instability ``` + +``` +Docker usage: + docker build -t zspotify . Builds the docker image from the Dockerfile + docker run --name zspotify zspotify Creates and runs a container from the image +``` + ### Will my account get banned if I use this tool? Currently no user has reported their account getting banned after using ZSpotify. This isn't to say _you_ won't get banned as it is technically against Spotify's TOS. From 5c0b02b0b83794dd863a67291b4b08a928fef1f7 Mon Sep 17 00:00:00 2001 From: Logykk <35679186+logykk@users.noreply.github.com> Date: Mon, 25 Oct 2021 14:06:26 +1300 Subject: [PATCH 21/41] Fix docker run command --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 80cfde88..9e6ea1f9 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,13 @@ Options that can be configured in zs_config.json: OVERRIDE_AUTO_WAIT Change this to true if you want to completely disable the wait between songs for faster downloads with the risk of instability ``` -``` -Docker usage: - docker build -t zspotify . Builds the docker image from the Dockerfile - docker run --name zspotify zspotify Creates and runs a container from the image +### Docker Usage + +``` +Build the docker image from the Dockerfile: + docker build -t zspotify . +Create and run a container from the image: + docker run --rm -v "$PWD/ZSpotify Music:/ZSpotify Music" -v "$PWD/ZSpotify Podcasts:/ZSpotify Podcasts" -it zspotify ``` ### Will my account get banned if I use this tool? From d7351dc95c5ddb1adf7374ea8f6a1c7b3e222a75 Mon Sep 17 00:00:00 2001 From: xx-05 Date: Sun, 24 Oct 2021 21:27:33 -0400 Subject: [PATCH 22/41] fix typo --- src/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index 5684b994..a2433a57 100644 --- a/src/app.py +++ b/src/app.py @@ -33,7 +33,7 @@ def client() -> None: elif sys.argv[1] == '-ls' or sys.argv[1] == '--liked-songs': for song in get_saved_tracks(): if not song[TRACK][NAME]: - print('### SKIPPING: SONG DOES NOT EXISTS ON SPOTIFY ANYMORE ###') + print('### SKIPPING: SONG DOES NOT EXIST ON SPOTIFY ANYMORE ###') else: download_track(song[TRACK][ID], 'Liked Songs/') print('\n') From 03b0b108b8a6f5f25cab88f67855043e7d420957 Mon Sep 17 00:00:00 2001 From: Footsiefat <12180913+Footsiefat@users.noreply.github.com> Date: Mon, 25 Oct 2021 14:51:12 +1300 Subject: [PATCH 23/41] renamed src to zspotify --- {src => zspotify}/__main__.py | 0 {src => zspotify}/album.py | 0 {src => zspotify}/app.py | 0 {src => zspotify}/const.py | 0 {src => zspotify}/playlist.py | 0 {src => zspotify}/podcast.py | 0 {src => zspotify}/track.py | 0 {src => zspotify}/utils.py | 0 {src => zspotify}/zspotify.py | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename {src => zspotify}/__main__.py (100%) rename {src => zspotify}/album.py (100%) rename {src => zspotify}/app.py (100%) rename {src => zspotify}/const.py (100%) rename {src => zspotify}/playlist.py (100%) rename {src => zspotify}/podcast.py (100%) rename {src => zspotify}/track.py (100%) rename {src => zspotify}/utils.py (100%) rename {src => zspotify}/zspotify.py (100%) diff --git a/src/__main__.py b/zspotify/__main__.py similarity index 100% rename from src/__main__.py rename to zspotify/__main__.py diff --git a/src/album.py b/zspotify/album.py similarity index 100% rename from src/album.py rename to zspotify/album.py diff --git a/src/app.py b/zspotify/app.py similarity index 100% rename from src/app.py rename to zspotify/app.py diff --git a/src/const.py b/zspotify/const.py similarity index 100% rename from src/const.py rename to zspotify/const.py diff --git a/src/playlist.py b/zspotify/playlist.py similarity index 100% rename from src/playlist.py rename to zspotify/playlist.py diff --git a/src/podcast.py b/zspotify/podcast.py similarity index 100% rename from src/podcast.py rename to zspotify/podcast.py diff --git a/src/track.py b/zspotify/track.py similarity index 100% rename from src/track.py rename to zspotify/track.py diff --git a/src/utils.py b/zspotify/utils.py similarity index 100% rename from src/utils.py rename to zspotify/utils.py diff --git a/src/zspotify.py b/zspotify/zspotify.py similarity index 100% rename from src/zspotify.py rename to zspotify/zspotify.py From c4ff812ac3382ddc96b50bdef984d5af1d89d022 Mon Sep 17 00:00:00 2001 From: logykk Date: Mon, 25 Oct 2021 14:59:40 +1300 Subject: [PATCH 24/41] change docker dir from src to zspotify --- CONTRIBUTING.md | 1 + Dockerfile | 2 +- README.md | 10 ++++++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0020b813..ca78ddd6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,6 +15,7 @@ ZSpotify is a community-driven project. There are many different ways to contrib ### What we aren't looking for Please don't use the issues section to request help installing or setting up the project. It should be reserved for bugs when running the code, and feature requests. Instead use the support channel in either our Discord or Matrix server. +Please do not make a new pull request just to fix a typo or any small issue like that. We'd rather you just make an issue reporting it and we will fix it in the next commit. This helps to prevent commit spamming. # Ground rules diff --git a/Dockerfile b/Dockerfile index dcba36ca..c24ed245 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN apk add gcc libc-dev zlib zlib-dev jpeg-dev \ FROM base COPY --from=builder /install /usr/local -COPY src /app +COPY zspotify /app COPY zs_config.json / WORKDIR /app ENTRYPOINT ["/usr/local/bin/python", "__main__.py"] diff --git a/README.md b/README.md index 9e6ea1f9..634a450a 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,13 @@ Python packages: \*ffmpeg can be installed via apt for Debian-based distros or by downloading the binaries from [ffmpeg.org](https://ffmpeg.org) and placing them in your %PATH% in Windows. Mac users can install it with [Homebrew](https://brew.sh) by running `brew install ffmpeg`. \*\*Git can be installed via apt for Debian-based distros or by downloading the binaries from [git-scm.com](https://git-scm.com/download/win) for Windows. + +### Command line usage: ``` -Command line usage: - python src Loads search prompt to find then download a specific track, album or playlist - python src Downloads the track, album, playlist or podcast episode specified as a command line argument - python src Downloads all albums by specified artist +Basic usage: + python zspotify Loads search prompt to find then download a specific track, album or playlist + python zspotify Downloads the track, album, playlist or podcast episode specified as a command line argument + python zspotify Downloads all albums by specified artist Extra command line options: -p, --playlist Downloads a saved playlist from your account From bcdb07593a7e7fe43540fdf9840a70773d0bc9e2 Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Sun, 24 Oct 2021 22:14:22 -0400 Subject: [PATCH 25/41] Generate config file if it doesn't exist --- zspotify/const.py | 12 ++++++++++++ zspotify/zspotify.py | 12 +++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/zspotify/const.py b/zspotify/const.py index f44d8694..3e4d9b0f 100644 --- a/zspotify/const.py +++ b/zspotify/const.py @@ -95,3 +95,15 @@ OVERRIDE_AUTO_WAIT = 'OVERRIDE_AUTO_WAIT' CHUNK_SIZE = 'CHUNK_SIZE' SPLIT_ALBUM_DISCS = 'SPLIT_ALBUM_DISCS' + +CONFIG_DEFAULT_SETTINGS = { + 'ROOT_PATH': '../ZSpotify Music/', + 'ROOT_PODCAST_PATH': '../ZSpotify Podcasts/', + 'SKIP_EXISTING_FILES': True, + 'DOWNLOAD_FORMAT': 'mp3', + 'FORCE_PREMIUM': False, + 'ANTI_BAN_WAIT_TIME': 1, + 'OVERRIDE_AUTO_WAIT': False, + 'CHUNK_SIZE': 50000, + 'SPLIT_ALBUM_DISCS': False +} diff --git a/zspotify/zspotify.py b/zspotify/zspotify.py index f206a80d..5739dfa5 100644 --- a/zspotify/zspotify.py +++ b/zspotify/zspotify.py @@ -18,7 +18,7 @@ from librespot.core import Session from const import CREDENTIALS_JSON, TYPE, \ PREMIUM, USER_READ_EMAIL, AUTHORIZATION, OFFSET, LIMIT, CONFIG_FILE_PATH, FORCE_PREMIUM, \ - PLAYLIST_READ_PRIVATE + PLAYLIST_READ_PRIVATE, CONFIG_DEFAULT_SETTINGS from utils import MusicFormat @@ -55,8 +55,14 @@ class ZSpotify: @classmethod def load_config(cls) -> None: app_dir = os.path.dirname(__file__) - with open(os.path.join(app_dir, CONFIG_FILE_PATH), encoding='utf-8') as config_file: - cls.CONFIG = json.load(config_file) + true_config_file_path = os.path.join(app_dir, CONFIG_FILE_PATH) + if not os.path.exists(true_config_file_path): + with open(true_config_file_path, 'w', encoding='utf-8') as config_file: + json.dump(CONFIG_DEFAULT_SETTINGS, config_file, indent=4) + cls.CONFIG = CONFIG_DEFAULT_SETTINGS + else: + with open(true_config_file_path, encoding='utf-8') as config_file: + cls.CONFIG = json.load(config_file) @classmethod def get_config(cls, key) -> Any: From 23c53247f7bf28df85b11ff9d9d4c33eb8ac376a Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Sun, 24 Oct 2021 22:24:05 -0400 Subject: [PATCH 26/41] Config file is removed from the repository --- .gitignore | 3 +++ zs_config.json | 11 ----------- 2 files changed, 3 insertions(+), 11 deletions(-) delete mode 100644 zs_config.json diff --git a/.gitignore b/.gitignore index ea24040c..65696cee 100644 --- a/.gitignore +++ b/.gitignore @@ -151,3 +151,6 @@ ZSpotify\ Podcasts/ # Intellij .idea + +# Config file +zs_config.json diff --git a/zs_config.json b/zs_config.json deleted file mode 100644 index b5dbb919..00000000 --- a/zs_config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "ROOT_PATH": "../ZSpotify Music/", - "ROOT_PODCAST_PATH": "../ZSpotify Podcasts/", - "SKIP_EXISTING_FILES": true, - "DOWNLOAD_FORMAT": "mp3", - "FORCE_PREMIUM": false, - "ANTI_BAN_WAIT_TIME": 1, - "OVERRIDE_AUTO_WAIT": false, - "CHUNK_SIZE": 50000, - "SPLIT_ALBUM_DISCS": false -} \ No newline at end of file From 0b51951b5f73204ba6daf0263041112d8dcec63b Mon Sep 17 00:00:00 2001 From: Footsiefat <12180913+Footsiefat@users.noreply.github.com> Date: Mon, 25 Oct 2021 16:44:42 +1300 Subject: [PATCH 27/41] Fixed crash from variable reference before assignment --- .gitignore | 3 +++ zs_config.json | 20 ++++++++-------- zspotify/app.py | 64 +++++++++++++++++++++++++------------------------ 3 files changed, 46 insertions(+), 41 deletions(-) diff --git a/.gitignore b/.gitignore index ea24040c..5295e936 100644 --- a/.gitignore +++ b/.gitignore @@ -151,3 +151,6 @@ ZSpotify\ Podcasts/ # Intellij .idea + +#Configuration json file +zs_config.json diff --git a/zs_config.json b/zs_config.json index b5dbb919..5e555afa 100644 --- a/zs_config.json +++ b/zs_config.json @@ -1,11 +1,11 @@ { - "ROOT_PATH": "../ZSpotify Music/", - "ROOT_PODCAST_PATH": "../ZSpotify Podcasts/", - "SKIP_EXISTING_FILES": true, - "DOWNLOAD_FORMAT": "mp3", - "FORCE_PREMIUM": false, - "ANTI_BAN_WAIT_TIME": 1, - "OVERRIDE_AUTO_WAIT": false, - "CHUNK_SIZE": 50000, - "SPLIT_ALBUM_DISCS": false -} \ No newline at end of file + "ROOT_PATH": "../ZSpotify Music/", + "ROOT_PODCAST_PATH": "../ZSpotify Podcasts/", + "SKIP_EXISTING_FILES": true, + "DOWNLOAD_FORMAT": "mp3", + "FORCE_PREMIUM": false, + "ANTI_BAN_WAIT_TIME": 1, + "OVERRIDE_AUTO_WAIT": false, + "CHUNK_SIZE": 50000, + "SPLIT_ALBUM_DISCS": false +} diff --git a/zspotify/app.py b/zspotify/app.py index a2433a57..3ee926b1 100644 --- a/zspotify/app.py +++ b/zspotify/app.py @@ -33,12 +33,14 @@ def client() -> None: elif sys.argv[1] == '-ls' or sys.argv[1] == '--liked-songs': for song in get_saved_tracks(): if not song[TRACK][NAME]: - print('### SKIPPING: SONG DOES NOT EXIST ON SPOTIFY ANYMORE ###') + print( + '### SKIPPING: SONG DOES NOT EXIST ON SPOTIFY ANYMORE ###') else: download_track(song[TRACK][ID], 'Liked Songs/') print('\n') else: - track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls(sys.argv[1]) + track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls( + sys.argv[1]) if track_id is not None: download_track(track_id) @@ -51,7 +53,7 @@ def client() -> None: name, _ = get_playlist_info(playlist_id) for song in playlist_songs: download_track(song[TRACK][ID], - sanitize_data(name) + '/') + sanitize_data(name) + '/') print('\n') elif episode_id is not None: download_episode(episode_id) @@ -64,7 +66,8 @@ def client() -> None: while len(search_text) == 0: search_text = input('Enter search or URL: ') - track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls(search_text) + track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls( + search_text) if track_id is not None: download_track(track_id) @@ -114,7 +117,6 @@ def search(search_term): raise ValueError('Invalid limit passed. Max is 50.\n') params['limit'] = splits[index+1] - if split == '-t' or split == '-type': allowed_types = ['track', 'playlist', 'album', 'artist'] @@ -148,6 +150,7 @@ def search(search_term): counter = 1 dics = [] + total_tracks = 0 if TRACK in params['type'].split(','): tracks = resp[TRACKS][ITEMS] if len(tracks) > 0: @@ -162,20 +165,20 @@ def search(search_term): track_data.append([counter, f'{track[NAME]} {explicit}', ','.join([artist[NAME] for artist in track[ARTISTS]])]) dics.append({ - ID : track[ID], - NAME : track[NAME], - 'type' : TRACK, + ID: track[ID], + NAME: track[NAME], + 'type': TRACK, }) counter += 1 total_tracks = counter - 1 - print(tabulate(track_data, headers=['S.NO', 'Name', 'Artists'], tablefmt='pretty')) + print(tabulate(track_data, headers=[ + 'S.NO', 'Name', 'Artists'], tablefmt='pretty')) print('\n') del tracks del track_data - else: - total_tracks = 0 + total_albums = 0 if ALBUM in params['type'].split(','): albums = resp[ALBUMS][ITEMS] if len(albums) > 0: @@ -185,20 +188,20 @@ def search(search_term): album_data.append([counter, album[NAME], ','.join([artist[NAME] for artist in album[ARTISTS]])]) dics.append({ - ID : album[ID], - NAME : album[NAME], - 'type' : ALBUM, + ID: album[ID], + NAME: album[NAME], + 'type': ALBUM, }) counter += 1 total_albums = counter - total_tracks - 1 - print(tabulate(album_data, headers=['S.NO', 'Album', 'Artists'], tablefmt='pretty')) + print(tabulate(album_data, headers=[ + 'S.NO', 'Album', 'Artists'], tablefmt='pretty')) print('\n') del albums del album_data - else: - total_albums = 0 + total_artists = 0 if ARTIST in params['type'].split(','): artists = resp[ARTISTS][ITEMS] if len(artists) > 0: @@ -207,39 +210,39 @@ def search(search_term): for artist in artists: artist_data.append([counter, artist[NAME]]) dics.append({ - ID : artist[ID], - NAME : artist[NAME], - 'type' : ARTIST, + ID: artist[ID], + NAME: artist[NAME], + 'type': ARTIST, }) counter += 1 total_artists = counter - total_tracks - total_albums - 1 - print(tabulate(artist_data, headers=['S.NO', 'Name'], tablefmt='pretty')) + print(tabulate(artist_data, headers=[ + 'S.NO', 'Name'], tablefmt='pretty')) print('\n') del artists del artist_data - else: - total_artists = 0 + total_playlists = 0 if PLAYLIST in params['type'].split(','): playlists = resp[PLAYLISTS][ITEMS] if len(playlists) > 0: print('### PLAYLISTS ###') playlist_data = [] for playlist in playlists: - playlist_data.append([counter, playlist[NAME], playlist[OWNER][DISPLAY_NAME]]) + playlist_data.append( + [counter, playlist[NAME], playlist[OWNER][DISPLAY_NAME]]) dics.append({ - ID : playlist[ID], - NAME : playlist[NAME], - 'type' : PLAYLIST, + ID: playlist[ID], + NAME: playlist[NAME], + 'type': PLAYLIST, }) counter += 1 total_playlists = counter - total_artists - total_tracks - total_albums - 1 - print(tabulate(playlist_data, headers=['S.NO', 'Name', 'Owner'], tablefmt='pretty')) + print(tabulate(playlist_data, headers=[ + 'S.NO', 'Name', 'Owner'], tablefmt='pretty')) print('\n') del playlists del playlist_data - else: - total_playlists = 0 if total_tracks + total_albums + total_artists + total_playlists == 0: print('NO RESULTS FOUND - EXITING...') @@ -261,4 +264,3 @@ def search(search_term): download_artist_albums(dic[ID]) else: download_playlist(dic) - From 2f717c4bd5ee960b3f5e11a896162fb9d9ffd2e5 Mon Sep 17 00:00:00 2001 From: Footsiefat <12180913+Footsiefat@users.noreply.github.com> Date: Mon, 25 Oct 2021 16:44:58 +1300 Subject: [PATCH 28/41] Delete zs_config.json --- zs_config.json | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 zs_config.json diff --git a/zs_config.json b/zs_config.json deleted file mode 100644 index 5e555afa..00000000 --- a/zs_config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "ROOT_PATH": "../ZSpotify Music/", - "ROOT_PODCAST_PATH": "../ZSpotify Podcasts/", - "SKIP_EXISTING_FILES": true, - "DOWNLOAD_FORMAT": "mp3", - "FORCE_PREMIUM": false, - "ANTI_BAN_WAIT_TIME": 1, - "OVERRIDE_AUTO_WAIT": false, - "CHUNK_SIZE": 50000, - "SPLIT_ALBUM_DISCS": false -} From dab823d09efb2065d0fbdc82aff6c02fff6815c5 Mon Sep 17 00:00:00 2001 From: Raju komati Date: Mon, 25 Oct 2021 09:22:53 +0530 Subject: [PATCH 29/41] updated folder name --- {src => zspotify}/album.py | 9 ++++++--- {src => zspotify}/app.py | 33 ++++++++++++++++++++++++--------- {src => zspotify}/const.py | 3 +++ {src => zspotify}/playlist.py | 6 ++++-- {src => zspotify}/podcast.py | 0 {src => zspotify}/track.py | 0 {src => zspotify}/utils.py | 0 {src => zspotify}/zspotify.py | 0 8 files changed, 37 insertions(+), 14 deletions(-) rename {src => zspotify}/album.py (78%) rename {src => zspotify}/app.py (81%) rename {src => zspotify}/const.py (95%) rename {src => zspotify}/playlist.py (93%) rename {src => zspotify}/podcast.py (100%) rename {src => zspotify}/track.py (100%) rename {src => zspotify}/utils.py (100%) rename {src => zspotify}/zspotify.py (100%) diff --git a/src/album.py b/zspotify/album.py similarity index 78% rename from src/album.py rename to zspotify/album.py index 12f86eb6..a331a222 100644 --- a/src/album.py +++ b/zspotify/album.py @@ -1,3 +1,4 @@ +"""It's provides functions for downloading the albums""" from tqdm import tqdm from const import ITEMS, ARTISTS, NAME, ID @@ -16,7 +17,8 @@ def get_album_tracks(album_id): limit = 50 while True: - resp = ZSpotify.invoke_url_with_params(f'{ALBUM_URL}/{album_id}/tracks', limit=limit, offset=offset) + resp = ZSpotify.invoke_url_with_params(f'{ALBUM_URL}/{album_id}/tracks', + limit=limit, offset=offset) offset += limit songs.extend(resp[ITEMS]) if len(resp[ITEMS]) < limit: @@ -42,9 +44,10 @@ def download_album(album): """ Downloads songs from an album """ artist, album_name = get_album_name(album) tracks = get_album_tracks(album) - for n, track in tqdm(enumerate(tracks, start=1), unit_scale=True, unit='Song', total=len(tracks)): + for album_number, track in tqdm(enumerate(tracks, start=1), unit_scale=True, + unit='Song', total=len(tracks)): download_track(track[ID], f'{artist}/{album_name}', - prefix=True, prefix_value=str(n), disable_progressbar=True) + prefix=True, prefix_value=str(album_number), disable_progressbar=True) def download_artist_albums(artist): diff --git a/src/app.py b/zspotify/app.py similarity index 81% rename from src/app.py rename to zspotify/app.py index d1c234d5..97ea7b43 100644 --- a/src/app.py +++ b/zspotify/app.py @@ -1,3 +1,4 @@ +"""Entrypoint of ZSpotify app. It provides functions for searching""" import sys from librespot.audio.decoders import AudioQuality @@ -5,7 +6,7 @@ from tabulate import tabulate from album import download_album, download_artist_albums from const import TRACK, NAME, ID, ARTISTS, ITEMS, TRACKS, EXPLICIT, ALBUMS, OWNER, \ - PLAYLISTS, DISPLAY_NAME + PLAYLISTS, DISPLAY_NAME, LIMIT, OFFSET, TYPE, S_NO, ALBUM from playlist import download_from_user_playlist, download_playlist, \ download_playlist_with_id from podcast import download_episode, get_show_episodes @@ -40,6 +41,9 @@ def client() -> None: def process_args_input(): + """ + process the sys args + """ if sys.argv[1] == '-p' or sys.argv[1] == '--playlist': download_from_user_playlist() elif sys.argv[1] == '-ls' or sys.argv[1] == '--liked-songs': @@ -54,6 +58,11 @@ def process_args_input(): def process_url_input(url, call_search=False): + """ + process the input and calls appropriate download method + @param url: input url + @param call_search: boolean variable to notify calling search method + """ track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls(url) if track_id: @@ -75,7 +84,7 @@ def process_url_input(url, call_search=False): def search(search_term): """ Searches Spotify's API for relevant data """ - params = {'limit': '10', 'offset': '0', 'q': search_term, 'type': 'track,album,artist,playlist'} + params = {LIMIT: '10', OFFSET: '0', 'q': search_term, TYPE: 'track,album,artist,playlist'} resp = ZSpotify.invoke_url_with_params(SEARCH_URL, **params) total_tracks = total_albums = total_artists = 0 @@ -90,7 +99,8 @@ def search(search_term): ','.join([artist[NAME] for artist in track[ARTISTS]])]) counter += 1 total_tracks = counter - 1 - print(tabulate(track_data, headers=['S.NO', 'Name', 'Artists'], tablefmt='pretty')) + print(tabulate(track_data, headers=[S_NO, NAME.title(), ARTISTS.title()], + tablefmt='pretty')) print('\n') albums = resp[ALBUMS][ITEMS] @@ -98,10 +108,12 @@ def search(search_term): print('### ALBUMS ###') album_data = [] for album in albums: - album_data.append([counter, album[NAME], ','.join([artist[NAME] for artist in album[ARTISTS]])]) + album_data.append([counter, album[NAME], ','.join([artist[NAME] + for artist in album[ARTISTS]])]) counter += 1 total_albums = counter - total_tracks - 1 - print(tabulate(album_data, headers=['S.NO', 'Album', 'Artists'], tablefmt='pretty')) + print(tabulate(album_data, headers=[S_NO, ALBUM.title(), ARTISTS.title()], + tablefmt='pretty')) print('\n') artists = resp[ARTISTS][ITEMS] @@ -112,7 +124,7 @@ def search(search_term): artist_data.append([counter, artist[NAME]]) counter += 1 total_artists = counter - total_tracks - total_albums - 1 - print(tabulate(artist_data, headers=['S.NO', 'Name'], tablefmt='pretty')) + print(tabulate(artist_data, headers=[S_NO, NAME.title()], tablefmt='pretty')) print('\n') playlists = resp[PLAYLISTS][ITEMS] @@ -121,13 +133,16 @@ def search(search_term): for playlist in playlists: playlist_data.append([counter, playlist[NAME], playlist[OWNER][DISPLAY_NAME]]) counter += 1 - print(tabulate(playlist_data, headers=['S.NO', 'Name', 'Owner'], tablefmt='pretty')) + print(tabulate(playlist_data, headers=[S_NO, NAME.title(), OWNER.title()], tablefmt='pretty')) print('\n') perform_action(tracks, albums, playlists, artists, total_tracks, total_albums, total_artists) -def perform_action(tracks: list, albums: list, playlists: list, artists: list, total_tracks: int, total_albums: int, - total_artists: int): +def perform_action(tracks: list, albums: list, playlists: list, artists: list, + total_tracks: int, total_albums: int, total_artists: int): + """ + process and downloads the user selection + """ if len(tracks) + len(albums) + len(playlists) == 0: print('NO RESULTS FOUND - EXITING...') else: diff --git a/src/const.py b/zspotify/const.py similarity index 95% rename from src/const.py rename to zspotify/const.py index 44d7e688..d9ec2b20 100644 --- a/src/const.py +++ b/zspotify/const.py @@ -1,3 +1,4 @@ +""" provides commonly used string across different modules""" SANITIZE = ('\\', '/', ':', '*', '?', '\'', '<', '>', '"') SAVED_TRACKS_URL = 'https://api.spotify.com/v1/me/tracks' @@ -107,3 +108,5 @@ PLAYLIST_ID = 'PlaylistID' ALBUM_ID = 'AlbumID' TRACK_ID = 'TrackID' + +S_NO = 'S.NO' \ No newline at end of file diff --git a/src/playlist.py b/zspotify/playlist.py similarity index 93% rename from src/playlist.py rename to zspotify/playlist.py index ed51673c..26bd4d4a 100644 --- a/src/playlist.py +++ b/zspotify/playlist.py @@ -32,7 +32,8 @@ def get_playlist_songs(playlist_id): limit = 100 while True: - resp = ZSpotify.invoke_url_with_params(f'{PLAYLISTS_URL}/{playlist_id}/tracks', limit=limit, offset=offset) + resp = ZSpotify.invoke_url_with_params(f'{PLAYLISTS_URL}/{playlist_id}/tracks', + limit=limit, offset=offset) offset += limit songs.extend(resp[ITEMS]) if len(resp[ITEMS]) < limit: @@ -52,7 +53,8 @@ def download_playlist_with_id(playlist_id): playlist_songs = [song for song in get_playlist_songs(playlist_id) if song[TRACK][ID]] p_bar = tqdm(playlist_songs, unit='song', total=len(playlist_songs), unit_scale=True) for song in p_bar: - download_track(song[TRACK][ID], sanitize_data(name.strip()) + '/', disable_progressbar=True, create_m3u_file=True) + download_track(song[TRACK][ID], sanitize_data(name.strip()) + '/', disable_progressbar=True, + create_m3u_file=True) p_bar.set_description(song[TRACK][NAME]) diff --git a/src/podcast.py b/zspotify/podcast.py similarity index 100% rename from src/podcast.py rename to zspotify/podcast.py diff --git a/src/track.py b/zspotify/track.py similarity index 100% rename from src/track.py rename to zspotify/track.py diff --git a/src/utils.py b/zspotify/utils.py similarity index 100% rename from src/utils.py rename to zspotify/utils.py diff --git a/src/zspotify.py b/zspotify/zspotify.py similarity index 100% rename from src/zspotify.py rename to zspotify/zspotify.py From 4576985b43a6f9b9ed1410588e748ffd09c9dbc7 Mon Sep 17 00:00:00 2001 From: Footsiefat <12180913+Footsiefat@users.noreply.github.com> Date: Mon, 25 Oct 2021 17:07:11 +1300 Subject: [PATCH 30/41] Reuploaded deleted file cos im a dumbass it would be fine if i merged a speicifc PR but I hadnt yet so this completely fucks things up :/ --- zs_config.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 zs_config.json diff --git a/zs_config.json b/zs_config.json new file mode 100644 index 00000000..5e555afa --- /dev/null +++ b/zs_config.json @@ -0,0 +1,11 @@ +{ + "ROOT_PATH": "../ZSpotify Music/", + "ROOT_PODCAST_PATH": "../ZSpotify Podcasts/", + "SKIP_EXISTING_FILES": true, + "DOWNLOAD_FORMAT": "mp3", + "FORCE_PREMIUM": false, + "ANTI_BAN_WAIT_TIME": 1, + "OVERRIDE_AUTO_WAIT": false, + "CHUNK_SIZE": 50000, + "SPLIT_ALBUM_DISCS": false +} From 1993fc05e13024dd26b481cce0821f9d553f079d Mon Sep 17 00:00:00 2001 From: Footsiefat <12180913+Footsiefat@users.noreply.github.com> Date: Mon, 25 Oct 2021 17:08:56 +1300 Subject: [PATCH 31/41] Fixed multiple issues including split disks breaking --- .gitignore | 5 +---- zspotify/track.py | 30 +++++++++++++++++++----------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 5295e936..9834e1b0 100644 --- a/.gitignore +++ b/.gitignore @@ -150,7 +150,4 @@ ZSpotify\ Music/ ZSpotify\ Podcasts/ # Intellij -.idea - -#Configuration json file -zs_config.json +.idea \ No newline at end of file diff --git a/zspotify/track.py b/zspotify/track.py index 9a928eca..423cce0c 100644 --- a/zspotify/track.py +++ b/zspotify/track.py @@ -22,7 +22,8 @@ def get_saved_tracks() -> list: limit = 50 while True: - resp = ZSpotify.invoke_url_with_params(SAVED_TRACKS_URL, limit=limit, offset=offset) + resp = ZSpotify.invoke_url_with_params( + SAVED_TRACKS_URL, limit=limit, offset=offset) offset += limit songs.extend(resp[ITEMS]) if len(resp[ITEMS]) < limit: @@ -53,24 +54,29 @@ def get_song_info(song_id) -> Tuple[List[str], str, str, Any, Any, Any, Any, Any # noinspection PyBroadException def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', disable_progressbar=False) -> None: """ Downloads raw song audio from Spotify """ - download_directory = os.path.join(os.path.dirname(__file__), ZSpotify.get_config(ROOT_PATH), extra_paths) + try: (artists, album_name, name, image_url, release_year, disc_number, track_number, scraped_song_id, is_playable) = get_song_info(track_id) + if ZSpotify.get_config(SPLIT_ALBUM_DISCS): + download_directory = os.path.join(os.path.dirname( + __file__), ZSpotify.get_config(ROOT_PATH), extra_paths, f'Disc {disc_number}') + else: + download_directory = os.path.join(os.path.dirname( + __file__), ZSpotify.get_config(ROOT_PATH), extra_paths) + song_name = artists[0] + ' - ' + name if prefix: song_name = f'{prefix_value.zfill(2)} - {song_name}' if prefix_value.isdigit( ) else f'{prefix_value} - {song_name}' - if ZSpotify.get_config(SPLIT_ALBUM_DISCS): - filename = os.path.join(download_directory, f'Disc {disc_number}', - f'{song_name}.{ZSpotify.get_config(DOWNLOAD_FORMAT)}') - else: - filename = os.path.join(download_directory, - f'{song_name}.{ZSpotify.get_config(DOWNLOAD_FORMAT)}') - except Exception: + filename = os.path.join( + download_directory, f'{song_name}.{ZSpotify.get_config(DOWNLOAD_FORMAT)}') + + except Exception as e: print('### SKIPPING SONG - FAILED TO QUERY METADATA ###') + print(e) else: try: if not is_playable: @@ -84,7 +90,8 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', if track_id != scraped_song_id: track_id = scraped_song_id track_id = TrackId.from_base62(track_id) - stream = ZSpotify.get_content_stream(track_id, ZSpotify.DOWNLOAD_QUALITY) + stream = ZSpotify.get_content_stream( + track_id, ZSpotify.DOWNLOAD_QUALITY) create_download_directory(download_directory) total_size = stream.input_stream.size @@ -125,4 +132,5 @@ def convert_audio_format(filename) -> None: bitrate = '320k' else: bitrate = '160k' - raw_audio.export(filename, format=ZSpotify.get_config(DOWNLOAD_FORMAT), bitrate=bitrate) + raw_audio.export(filename, format=ZSpotify.get_config( + DOWNLOAD_FORMAT), bitrate=bitrate) From 460ed6722003d2c0d12c68febc5226aa516dcb36 Mon Sep 17 00:00:00 2001 From: Footsiefat <12180913+Footsiefat@users.noreply.github.com> Date: Mon, 25 Oct 2021 17:20:08 +1300 Subject: [PATCH 32/41] tryna fix merge conflicts the lazy way for PR #115 --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9834e1b0..65696cee 100644 --- a/.gitignore +++ b/.gitignore @@ -150,4 +150,7 @@ ZSpotify\ Music/ ZSpotify\ Podcasts/ # Intellij -.idea \ No newline at end of file +.idea + +# Config file +zs_config.json From 179946498690d9768bb025a821699f74aca34753 Mon Sep 17 00:00:00 2001 From: Footsiefat <12180913+Footsiefat@users.noreply.github.com> Date: Mon, 25 Oct 2021 17:20:20 +1300 Subject: [PATCH 33/41] tryna fix merge conflicts the lazy way for PR #155 --- zs_config.json | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 zs_config.json diff --git a/zs_config.json b/zs_config.json deleted file mode 100644 index 5e555afa..00000000 --- a/zs_config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "ROOT_PATH": "../ZSpotify Music/", - "ROOT_PODCAST_PATH": "../ZSpotify Podcasts/", - "SKIP_EXISTING_FILES": true, - "DOWNLOAD_FORMAT": "mp3", - "FORCE_PREMIUM": false, - "ANTI_BAN_WAIT_TIME": 1, - "OVERRIDE_AUTO_WAIT": false, - "CHUNK_SIZE": 50000, - "SPLIT_ALBUM_DISCS": false -} From f034fc82bf236a538e8b377dbd5de6ead64fd77e Mon Sep 17 00:00:00 2001 From: Jared Rossberg Date: Sun, 24 Oct 2021 22:52:14 -0600 Subject: [PATCH 34/41] Add DS_Store to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 65696cee..d0a29a9f 100644 --- a/.gitignore +++ b/.gitignore @@ -154,3 +154,6 @@ ZSpotify\ Podcasts/ # Config file zs_config.json + +# MacOS file +.DS_Store From 89b74ce51197eb674babca4f1554be17a949259b Mon Sep 17 00:00:00 2001 From: Footsiefat <12180913+Footsiefat@users.noreply.github.com> Date: Mon, 25 Oct 2021 18:03:52 +1300 Subject: [PATCH 35/41] Updated changelog to v2.3 --- CHANGELOG.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 008e0f71..92732fca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,15 @@ ## **Changelog:** +**v2.3 (25 Oct 2021):** +- Moved changelog to seperate file. +- Added argument parsing in search function (query results limit and query result types). +- Fixed spelling errors. +- Added mac specific install guide stuff. +- Fixed infinite loop. +- Fixed issue where zspotify could'nt run on python 3.8/3.9. +- Changed it so you can just run zspotify from the root folder again. +- Added function to auto generate config file if it doesnt exist. +- Fixed issue where if you enabled splitting discs into seperate folders downloading would fail. + **v2.2 (24 Oct 2021):** - Added basic support for downloading an entire podcast series. - Split code into multiple files for easier maintenance. @@ -81,4 +92,4 @@ - Added additional check for tracks that cannot be "played" due to licence(and similar) issues. These tracks will be skipped. **v0.9 (13 Oct 2021):** -- Initial upload, needs adjustments to get working again after backend rewrite. \ No newline at end of file +- Initial upload, needs adjustments to get working again after backend rewrite. From 8e8d2b1c4baa3b2310922dd5433afd486778212f Mon Sep 17 00:00:00 2001 From: Jared Rossberg Date: Sun, 24 Oct 2021 23:11:09 -0600 Subject: [PATCH 36/41] Allows pylint to run correctly --- .github/workflows/pylint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 0805af74..c7d2a599 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -19,4 +19,4 @@ jobs: pip install pylint - name: Analysing the code with pylint run: | - pylint `ls -R|grep .py$|xargs` + pylint `ls */** | grep .py$ | xargs` From ab3f7e4ffa882f6c04ed9a92c7fdca3077690e9e Mon Sep 17 00:00:00 2001 From: Footsiefat <12180913+Footsiefat@users.noreply.github.com> Date: Mon, 25 Oct 2021 18:14:09 +1300 Subject: [PATCH 37/41] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92732fca..9f93e54e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Changed it so you can just run zspotify from the root folder again. - Added function to auto generate config file if it doesnt exist. - Fixed issue where if you enabled splitting discs into seperate folders downloading would fail. +- Added playlist file(m3u) creation for playlist download. **v2.2 (24 Oct 2021):** - Added basic support for downloading an entire podcast series. From afeb84de39032bd355423b2fef20ad2176ca55f9 Mon Sep 17 00:00:00 2001 From: Footsiefat <12180913+Footsiefat@users.noreply.github.com> Date: Mon, 25 Oct 2021 18:36:17 +1300 Subject: [PATCH 38/41] Revert "added m3u playlist file creation" --- zspotify/album.py | 9 +- zspotify/app.py | 328 ++++++++++++++++++++++++++++--------------- zspotify/const.py | 16 --- zspotify/playlist.py | 19 +-- zspotify/podcast.py | 22 +-- zspotify/track.py | 169 +++++++++------------- zspotify/utils.py | 67 ++++++--- 7 files changed, 346 insertions(+), 284 deletions(-) diff --git a/zspotify/album.py b/zspotify/album.py index 932c91e6..19004d11 100644 --- a/zspotify/album.py +++ b/zspotify/album.py @@ -1,4 +1,3 @@ -"""It's provides functions for downloading the albums""" from tqdm import tqdm from const import ITEMS, ARTISTS, NAME, ID @@ -17,8 +16,7 @@ def get_album_tracks(album_id): limit = 50 while True: - resp = ZSpotify.invoke_url_with_params(f'{ALBUM_URL}/{album_id}/tracks', - limit=limit, offset=offset) + resp = ZSpotify.invoke_url_with_params(f'{ALBUM_URL}/{album_id}/tracks', limit=limit, offset=offset) offset += limit songs.extend(resp[ITEMS]) if len(resp[ITEMS]) < limit: @@ -50,10 +48,9 @@ def download_album(album): """ Downloads songs from an album """ artist, album_name = get_album_name(album) tracks = get_album_tracks(album) - for album_number, track in tqdm(enumerate(tracks, start=1), unit_scale=True, - unit='Song', total=len(tracks)): + for n, track in tqdm(enumerate(tracks, start=1), unit_scale=True, unit='Song', total=len(tracks)): download_track(track[ID], f'{artist}/{album_name}', - prefix=True, prefix_value=str(album_number), disable_progressbar=True) + prefix=True, prefix_value=str(n), disable_progressbar=True) def download_artist_albums(artist): diff --git a/zspotify/app.py b/zspotify/app.py index e84e11de..3ee926b1 100644 --- a/zspotify/app.py +++ b/zspotify/app.py @@ -1,17 +1,15 @@ -"""Entrypoint of ZSpotify app. It provides functions for searching""" import sys from librespot.audio.decoders import AudioQuality from tabulate import tabulate from album import download_album, download_artist_albums -from const import TRACK, NAME, ID, ARTISTS, ITEMS, TRACKS, EXPLICIT, ALBUMS, OWNER, \ - PLAYLISTS, DISPLAY_NAME, LIMIT, OFFSET, TYPE, S_NO, ALBUM -from playlist import download_from_user_playlist, download_playlist, \ - download_playlist_with_id +from const import TRACK, NAME, ID, ARTIST, ARTISTS, ITEMS, TRACKS, EXPLICIT, ALBUM, ALBUMS, \ + OWNER, PLAYLIST, PLAYLISTS, DISPLAY_NAME +from playlist import get_playlist_songs, get_playlist_info, download_from_user_playlist, download_playlist from podcast import download_episode, get_show_episodes from track import download_track, get_saved_tracks -from utils import splash, split_input, regex_input_for_urls +from utils import sanitize_data, splash, split_input, regex_input_for_urls from zspotify import ZSpotify SEARCH_URL = 'https://api.spotify.com/v1/search' @@ -29,121 +27,224 @@ def client() -> None: print('[ DETECTED FREE ACCOUNT - USING HIGH QUALITY ]\n\n') ZSpotify.DOWNLOAD_QUALITY = AudioQuality.HIGH - while True: - if len(sys.argv) > 1: - process_args_input() + if len(sys.argv) > 1: + if sys.argv[1] == '-p' or sys.argv[1] == '--playlist': + download_from_user_playlist() + elif sys.argv[1] == '-ls' or sys.argv[1] == '--liked-songs': + for song in get_saved_tracks(): + if not song[TRACK][NAME]: + print( + '### SKIPPING: SONG DOES NOT EXIST ON SPOTIFY ANYMORE ###') + else: + download_track(song[TRACK][ID], 'Liked Songs/') + print('\n') else: - search_text = '' - while len(search_text) == 0: - search_text = input('Enter search or URL: ') - process_url_input(search_text, call_search=True) - # wait() + track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls( + sys.argv[1]) + if track_id is not None: + download_track(track_id) + elif artist_id is not None: + download_artist_albums(artist_id) + elif album_id is not None: + download_album(album_id) + elif playlist_id is not None: + playlist_songs = get_playlist_songs(playlist_id) + name, _ = get_playlist_info(playlist_id) + for song in playlist_songs: + download_track(song[TRACK][ID], + sanitize_data(name) + '/') + print('\n') + elif episode_id is not None: + download_episode(episode_id) + elif show_id is not None: + for episode in get_show_episodes(show_id): + download_episode(episode) -def process_args_input(): - """ - process the sys args - """ - if sys.argv[1] == '-p' or sys.argv[1] == '--playlist': - download_from_user_playlist() - elif sys.argv[1] == '-ls' or sys.argv[1] == '--liked-songs': - for song in get_saved_tracks(): - if not song[TRACK][NAME]: - print('### SKIPPING: SONG DOES NOT EXISTS ON SPOTIFY ANYMORE ###') - else: - download_track(song[TRACK][ID], 'Liked Songs/') - print('\n') else: - process_url_input(sys.argv[1]) + search_text = '' + while len(search_text) == 0: + search_text = input('Enter search or URL: ') + track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls( + search_text) -def process_url_input(url, call_search=False): - """ - process the input and calls appropriate download method - @param url: input url - @param call_search: boolean variable to notify calling search method - """ - track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls(url) - - if track_id: - download_track(track_id) - elif artist_id: - download_artist_albums(artist_id) - elif album_id: - download_album(album_id) - elif playlist_id: - download_playlist_with_id(playlist_id) - elif episode_id: - download_episode(episode_id) - elif show_id: - for episode in get_show_episodes(show_id): - download_episode(episode) - elif call_search: - search(url) + if track_id is not None: + download_track(track_id) + elif artist_id is not None: + download_artist_albums(artist_id) + elif album_id is not None: + download_album(album_id) + elif playlist_id is not None: + playlist_songs = get_playlist_songs(playlist_id) + name, _ = get_playlist_info(playlist_id) + for song in playlist_songs: + download_track(song[TRACK][ID], sanitize_data(name) + '/') + print('\n') + elif episode_id is not None: + download_episode(episode_id) + elif show_id is not None: + for episode in get_show_episodes(show_id): + download_episode(episode) + else: + search(search_text) def search(search_term): """ Searches Spotify's API for relevant data """ - params = {LIMIT: '10', OFFSET: '0', 'q': search_term, TYPE: 'track,album,artist,playlist'} + params = {'limit': '10', + 'offset': '0', + 'q': search_term, + 'type': 'track,album,artist,playlist'} + + # Parse args + splits = search_term.split() + for split in splits: + index = splits.index(split) + + if split[0] == '-' and len(split) > 1: + if len(splits)-1 == index: + raise IndexError('No parameters passed after option: {}\n'. + format(split)) + + if split == '-l' or split == '-limit': + try: + int(splits[index+1]) + except ValueError: + raise ValueError('Paramater passed after {} option must be an integer.\n'. + format(split)) + if int(splits[index+1]) > 50: + raise ValueError('Invalid limit passed. Max is 50.\n') + params['limit'] = splits[index+1] + + if split == '-t' or split == '-type': + + allowed_types = ['track', 'playlist', 'album', 'artist'] + passed_types = [] + for i in range(index+1, len(splits)): + if splits[i][0] == '-': + break + + if splits[i] not in allowed_types: + raise ValueError('Parameters passed after {} option must be from this list:\n{}'. + format(split, '\n'.join(allowed_types))) + + passed_types.append(splits[i]) + params['type'] = ','.join(passed_types) + + if len(params['type']) == 0: + params['type'] = 'track,album,artist,playlist' + + # Clean search term + search_term_list = [] + for split in splits: + if split[0] == "-": + break + search_term_list.append(split) + if not search_term_list: + raise ValueError("Invalid query.") + params["q"] = ' '.join(search_term_list) + resp = ZSpotify.invoke_url_with_params(SEARCH_URL, **params) - total_tracks = total_albums = total_artists = 0 counter = 1 - tracks = resp[TRACKS][ITEMS] - if len(tracks) > 0: - print('### TRACKS ###') - track_data = [] - for track in tracks: - explicit = '[E]' if track[EXPLICIT] else '' - track_data.append([counter, f'{track[NAME]} {explicit}', - ','.join([artist[NAME] for artist in track[ARTISTS]])]) - counter += 1 - total_tracks = counter - 1 - print(tabulate(track_data, headers=[S_NO, NAME.title(), ARTISTS.title()], - tablefmt='pretty')) - print('\n') + dics = [] - albums = resp[ALBUMS][ITEMS] - if len(albums) > 0: - print('### ALBUMS ###') - album_data = [] - for album in albums: - album_data.append([counter, album[NAME], ','.join([artist[NAME] - for artist in album[ARTISTS]])]) - counter += 1 - total_albums = counter - total_tracks - 1 - print(tabulate(album_data, headers=[S_NO, ALBUM.title(), ARTISTS.title()], - tablefmt='pretty')) - print('\n') + total_tracks = 0 + if TRACK in params['type'].split(','): + tracks = resp[TRACKS][ITEMS] + if len(tracks) > 0: + print('### TRACKS ###') + track_data = [] + for track in tracks: + if track[EXPLICIT]: + explicit = '[E]' + else: + explicit = '' - artists = resp[ARTISTS][ITEMS] - if len(artists) > 0: - print('### ARTISTS ###') - artist_data = [] - for artist in artists: - artist_data.append([counter, artist[NAME]]) - counter += 1 - total_artists = counter - total_tracks - total_albums - 1 - print(tabulate(artist_data, headers=[S_NO, NAME.title()], tablefmt='pretty')) - print('\n') + track_data.append([counter, f'{track[NAME]} {explicit}', + ','.join([artist[NAME] for artist in track[ARTISTS]])]) + dics.append({ + ID: track[ID], + NAME: track[NAME], + 'type': TRACK, + }) - playlists = resp[PLAYLISTS][ITEMS] - print('### PLAYLISTS ###') - playlist_data = [] - for playlist in playlists: - playlist_data.append([counter, playlist[NAME], playlist[OWNER][DISPLAY_NAME]]) - counter += 1 - print(tabulate(playlist_data, headers=[S_NO, NAME.title(), OWNER.title()], tablefmt='pretty')) - print('\n') - perform_action(tracks, albums, playlists, artists, total_tracks, total_albums, total_artists) + counter += 1 + total_tracks = counter - 1 + print(tabulate(track_data, headers=[ + 'S.NO', 'Name', 'Artists'], tablefmt='pretty')) + print('\n') + del tracks + del track_data + total_albums = 0 + if ALBUM in params['type'].split(','): + albums = resp[ALBUMS][ITEMS] + if len(albums) > 0: + print('### ALBUMS ###') + album_data = [] + for album in albums: + album_data.append([counter, album[NAME], + ','.join([artist[NAME] for artist in album[ARTISTS]])]) + dics.append({ + ID: album[ID], + NAME: album[NAME], + 'type': ALBUM, + }) -def perform_action(tracks: list, albums: list, playlists: list, artists: list, - total_tracks: int, total_albums: int, total_artists: int): - """ - process and downloads the user selection - """ - if len(tracks) + len(albums) + len(playlists) == 0: + counter += 1 + total_albums = counter - total_tracks - 1 + print(tabulate(album_data, headers=[ + 'S.NO', 'Album', 'Artists'], tablefmt='pretty')) + print('\n') + del albums + del album_data + + total_artists = 0 + if ARTIST in params['type'].split(','): + artists = resp[ARTISTS][ITEMS] + if len(artists) > 0: + print('### ARTISTS ###') + artist_data = [] + for artist in artists: + artist_data.append([counter, artist[NAME]]) + dics.append({ + ID: artist[ID], + NAME: artist[NAME], + 'type': ARTIST, + }) + counter += 1 + total_artists = counter - total_tracks - total_albums - 1 + print(tabulate(artist_data, headers=[ + 'S.NO', 'Name'], tablefmt='pretty')) + print('\n') + del artists + del artist_data + + total_playlists = 0 + if PLAYLIST in params['type'].split(','): + playlists = resp[PLAYLISTS][ITEMS] + if len(playlists) > 0: + print('### PLAYLISTS ###') + playlist_data = [] + for playlist in playlists: + playlist_data.append( + [counter, playlist[NAME], playlist[OWNER][DISPLAY_NAME]]) + dics.append({ + ID: playlist[ID], + NAME: playlist[NAME], + 'type': PLAYLIST, + }) + counter += 1 + total_playlists = counter - total_artists - total_tracks - total_albums - 1 + print(tabulate(playlist_data, headers=[ + 'S.NO', 'Name', 'Owner'], tablefmt='pretty')) + print('\n') + del playlists + del playlist_data + + if total_tracks + total_albums + total_artists + total_playlists == 0: print('NO RESULTS FOUND - EXITING...') else: selection = '' @@ -152,13 +253,14 @@ def perform_action(tracks: list, albums: list, playlists: list, artists: list, inputs = split_input(selection) for pos in inputs: position = int(pos) - if position <= total_tracks: - track_id = tracks[position - 1][ID] - download_track(track_id) - elif position <= total_albums + total_tracks: - download_album(albums[position - total_tracks - 1][ID]) - elif position <= total_artists + total_tracks + total_albums: - download_artist_albums(artists[position - total_tracks - total_albums - 1][ID]) - else: - download_playlist(playlists, position - total_tracks - total_albums - total_artists) - + for dic in dics: + print_pos = dics.index(dic) + 1 + if print_pos == position: + if dic['type'] == TRACK: + download_track(dic[ID]) + elif dic['type'] == ALBUM: + download_album(dic[ID]) + elif dic['type'] == ARTIST: + download_artist_albums(dic[ID]) + else: + download_playlist(dic) diff --git a/zspotify/const.py b/zspotify/const.py index 4ff031b6..3e4d9b0f 100644 --- a/zspotify/const.py +++ b/zspotify/const.py @@ -1,4 +1,3 @@ -""" provides commonly used string across different modules""" SANITIZE = ('\\', '/', ':', '*', '?', '\'', '<', '>', '"') SAVED_TRACKS_URL = 'https://api.spotify.com/v1/me/tracks' @@ -97,21 +96,6 @@ CHUNK_SIZE = 'CHUNK_SIZE' SPLIT_ALBUM_DISCS = 'SPLIT_ALBUM_DISCS' -DURATION_MS = 'duration_ms' - -ARTIST_ID = 'ArtistID' - -SHOW_ID = 'ShowID' - -EPISODE_ID = 'EpisodeID' - -PLAYLIST_ID = 'PlaylistID' - -ALBUM_ID = 'AlbumID' - -TRACK_ID = 'TrackID' - -S_NO = 'S.NO' CONFIG_DEFAULT_SETTINGS = { 'ROOT_PATH': '../ZSpotify Music/', 'ROOT_PODCAST_PATH': '../ZSpotify Podcasts/', diff --git a/zspotify/playlist.py b/zspotify/playlist.py index 26bd4d4a..9a1304c2 100644 --- a/zspotify/playlist.py +++ b/zspotify/playlist.py @@ -32,8 +32,7 @@ def get_playlist_songs(playlist_id): limit = 100 while True: - resp = ZSpotify.invoke_url_with_params(f'{PLAYLISTS_URL}/{playlist_id}/tracks', - limit=limit, offset=offset) + resp = ZSpotify.invoke_url_with_params(f'{PLAYLISTS_URL}/{playlist_id}/tracks', limit=limit, offset=offset) offset += limit songs.extend(resp[ITEMS]) if len(resp[ITEMS]) < limit: @@ -48,21 +47,17 @@ def get_playlist_info(playlist_id): return resp['name'].strip(), resp['owner']['display_name'].strip() -def download_playlist_with_id(playlist_id): - name, _ = get_playlist_info(playlist_id) - playlist_songs = [song for song in get_playlist_songs(playlist_id) if song[TRACK][ID]] +def download_playlist(playlist): + """Downloads all the songs from a playlist""" + + playlist_songs = [song for song in get_playlist_songs(playlist[ID]) if song[TRACK][ID]] p_bar = tqdm(playlist_songs, unit='song', total=len(playlist_songs), unit_scale=True) for song in p_bar: - download_track(song[TRACK][ID], sanitize_data(name.strip()) + '/', disable_progressbar=True, - create_m3u_file=True) + download_track(song[TRACK][ID], sanitize_data(playlist[NAME].strip()) + '/', + disable_progressbar=True) p_bar.set_description(song[TRACK][NAME]) -def download_playlist(playlists, playlist_number): - """Downloads all the songs from a playlist""" - download_playlist_with_id(playlists[int(playlist_number) - 1][ID]) - - def download_from_user_playlist(): """ Select which playlist(s) to download """ playlists = get_all_playlists() diff --git a/zspotify/podcast.py b/zspotify/podcast.py index cc7f0614..eea36c1b 100644 --- a/zspotify/podcast.py +++ b/zspotify/podcast.py @@ -1,6 +1,7 @@ import os from typing import Optional, Tuple +from librespot.audio.decoders import VorbisOnlyAudioQuality from librespot.metadata import EpisodeId from tqdm import tqdm @@ -8,24 +9,25 @@ from const import NAME, ERROR, SHOW, ITEMS, ID, ROOT_PODCAST_PATH, CHUNK_SIZE from utils import sanitize_data, create_download_directory, MusicFormat from zspotify import ZSpotify + EPISODE_INFO_URL = 'https://api.spotify.com/v1/episodes' SHOWS_URL = 'https://api.spotify.com/v1/shows' -def get_episode_info(episode_id) -> Tuple[Optional[str], Optional[str]]: - info = ZSpotify.invoke_url(f'{EPISODE_INFO_URL}/{episode_id}') +def get_episode_info(episode_id_str) -> Tuple[Optional[str], Optional[str]]: + info = ZSpotify.invoke_url(f'{EPISODE_INFO_URL}/{episode_id_str}') if ERROR in info: return None, None return sanitize_data(info[SHOW][NAME]), sanitize_data(info[NAME]) -def get_show_episodes(show_id) -> list: +def get_show_episodes(show_id_str) -> list: episodes = [] offset = 0 limit = 50 while True: - resp = ZSpotify.invoke_url_with_params(f'{SHOWS_URL}/{show_id}/episodes', limit=limit, offset=offset) + resp = ZSpotify.invoke_url_with_params(f'{SHOWS_URL}/{show_id_str}/episodes', limit=limit, offset=offset) offset += limit for episode in resp[ITEMS]: episodes.append(episode[ID]) @@ -40,7 +42,7 @@ def download_episode(episode_id) -> None: extra_paths = podcast_name + '/' - if not podcast_name: + if podcast_name is None: print('### SKIPPING: (EPISODE NOT FOUND) ###') else: filename = podcast_name + ' - ' + episode_name @@ -54,11 +56,11 @@ def download_episode(episode_id) -> None: total_size = stream.input_stream.size with open(download_directory + filename + MusicFormat.OGG.value, 'wb') as file, tqdm( - desc=filename, - total=total_size, - unit='B', - unit_scale=True, - unit_divisor=1024 + desc=filename, + total=total_size, + unit='B', + unit_scale=True, + unit_divisor=1024 ) as bar: for _ in range(int(total_size / ZSpotify.get_config(CHUNK_SIZE)) + 1): bar.update(file.write( diff --git a/zspotify/track.py b/zspotify/track.py index 4416fae9..423cce0c 100644 --- a/zspotify/track.py +++ b/zspotify/track.py @@ -1,4 +1,3 @@ -import math import os import time from typing import Any, Tuple, List @@ -10,7 +9,7 @@ from tqdm import tqdm from const import TRACKS, ALBUM, NAME, ITEMS, DISC_NUMBER, TRACK_NUMBER, IS_PLAYABLE, ARTISTS, IMAGES, URL, \ RELEASE_DATE, ID, TRACKS_URL, SAVED_TRACKS_URL, SPLIT_ALBUM_DISCS, ROOT_PATH, DOWNLOAD_FORMAT, CHUNK_SIZE, \ - SKIP_EXISTING_FILES, ANTI_BAN_WAIT_TIME, OVERRIDE_AUTO_WAIT, DURATION_MS + SKIP_EXISTING_FILES, ANTI_BAN_WAIT_TIME, OVERRIDE_AUTO_WAIT from utils import sanitize_data, set_audio_tags, set_music_thumbnail, create_download_directory, \ MusicFormat from zspotify import ZSpotify @@ -33,7 +32,7 @@ def get_saved_tracks() -> list: return songs -def get_song_info(song_id) -> Tuple[List[str], str, str, Any, Any, Any, Any, Any, Any, Any]: +def get_song_info(song_id) -> Tuple[List[str], str, str, Any, Any, Any, Any, Any, Any]: """ Retrieves metadata for downloaded songs """ info = ZSpotify.invoke_url(f'{TRACKS_URL}?ids={song_id}&market=from_token') @@ -48,123 +47,82 @@ def get_song_info(song_id) -> Tuple[List[str], str, str, Any, Any, Any, Any, Any track_number = info[TRACKS][0][TRACK_NUMBER] scraped_song_id = info[TRACKS][0][ID] is_playable = info[TRACKS][0][IS_PLAYABLE] - duration = math.ceil(info[TRACKS][0][DURATION_MS] / 1000) - return (artists, album_name, name, image_url, release_year, disc_number, track_number, scraped_song_id, is_playable, - duration) + return artists, album_name, name, image_url, release_year, disc_number, track_number, scraped_song_id, is_playable # noinspection PyBroadException -def download_track(track_id: str, extra_paths: str = '', prefix: bool = False, prefix_value='', - disable_progressbar: bool = False, - create_m3u_file: bool = False) -> None: +def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', disable_progressbar=False) -> None: """ Downloads raw song audio from Spotify """ + try: (artists, album_name, name, image_url, release_year, disc_number, - track_number, scraped_song_id, is_playable, duration) = get_song_info(track_id) - song_name, filename, m3u_filename = process_track_metadata(artists, name, disc_number, extra_paths, prefix, - prefix_value, create_m3u_file) - except Exception: + track_number, scraped_song_id, is_playable) = get_song_info(track_id) + + if ZSpotify.get_config(SPLIT_ALBUM_DISCS): + download_directory = os.path.join(os.path.dirname( + __file__), ZSpotify.get_config(ROOT_PATH), extra_paths, f'Disc {disc_number}') + else: + download_directory = os.path.join(os.path.dirname( + __file__), ZSpotify.get_config(ROOT_PATH), extra_paths) + + song_name = artists[0] + ' - ' + name + if prefix: + song_name = f'{prefix_value.zfill(2)} - {song_name}' if prefix_value.isdigit( + ) else f'{prefix_value} - {song_name}' + + filename = os.path.join( + download_directory, f'{song_name}.{ZSpotify.get_config(DOWNLOAD_FORMAT)}') + + except Exception as e: print('### SKIPPING SONG - FAILED TO QUERY METADATA ###') + print(e) else: try: - track_info = ( - artists, album_name, name, image_url, release_year, disc_number, track_number, scraped_song_id, - is_playable, duration) - playlist_info = (create_m3u_file, m3u_filename) - creat_track(track_id, extra_paths, song_name, filename, disable_progressbar, track_info, playlist_info) - except Exception: + if not is_playable: + print('\n### SKIPPING:', song_name, + '(SONG IS UNAVAILABLE) ###') + else: + if os.path.isfile(filename) and os.path.getsize(filename) and ZSpotify.get_config(SKIP_EXISTING_FILES): + print('\n### SKIPPING:', song_name, + '(SONG ALREADY EXISTS) ###') + else: + if track_id != scraped_song_id: + track_id = scraped_song_id + track_id = TrackId.from_base62(track_id) + stream = ZSpotify.get_content_stream( + track_id, ZSpotify.DOWNLOAD_QUALITY) + create_download_directory(download_directory) + total_size = stream.input_stream.size + + with open(filename, 'wb') as file, tqdm( + desc=song_name, + total=total_size, + unit='B', + unit_scale=True, + unit_divisor=1024, + disable=disable_progressbar + ) as p_bar: + for _ in range(int(total_size / ZSpotify.get_config(CHUNK_SIZE)) + 1): + p_bar.update(file.write( + stream.input_stream.stream().read(ZSpotify.get_config(CHUNK_SIZE)))) + + if ZSpotify.get_config(DOWNLOAD_FORMAT) == 'mp3': + convert_audio_format(filename) + set_audio_tags(filename, artists, name, album_name, + release_year, disc_number, track_number) + set_music_thumbnail(filename, image_url) + + if not ZSpotify.get_config(OVERRIDE_AUTO_WAIT): + time.sleep(ZSpotify.get_config(ANTI_BAN_WAIT_TIME)) + except Exception as e: print('### SKIPPING:', song_name, '(GENERAL DOWNLOAD ERROR) ###') + print(e) if os.path.exists(filename): os.remove(filename) -def process_track_metadata(artists: list, name: str, disc_number: Any, extra_paths: str, prefix: bool, - prefix_value: str, - create_m3u_file: bool): - m3u_filename = None - - if create_m3u_file: - download_directory = os.path.join(os.path.dirname( - __file__), ZSpotify.get_config(ROOT_PATH), extra_paths, extra_paths[:-1]) - m3u_filename = f'{download_directory}.m3u' - if ZSpotify.get_config(SPLIT_ALBUM_DISCS): - download_directory = os.path.join(os.path.dirname( - __file__), ZSpotify.get_config(ROOT_PATH), extra_paths, f'Disc {disc_number}') - else: - download_directory = os.path.join(os.path.dirname( - __file__), ZSpotify.get_config(ROOT_PATH), extra_paths) - song_name = artists[0] + ' - ' + name - if prefix: - song_name = f'{prefix_value.zfill(2)} - {song_name}' if prefix_value.isdigit( - ) else f'{prefix_value} - {song_name}' - - filename = os.path.join( - download_directory, f'{song_name}.{ZSpotify.get_config(DOWNLOAD_FORMAT)}') - return song_name, filename, m3u_filename - - -def creat_track(track_id, extra_paths: str, song_name: str, filename: str, disable_progressbar: bool, track_info: tuple, - playlist_info: tuple): - (artists, album_name, name, image_url, release_year, disc_number, - track_number, scraped_song_id, is_playable, duration) = track_info - create_m3u, m3u_filename = playlist_info - if not is_playable: - print(f'\n### SKIPPING: {song_name} (SONG IS UNAVAILABLE) ###') - else: - if os.path.isfile(filename) and os.path.getsize(filename) and ZSpotify.get_config(SKIP_EXISTING_FILES): - playlist_data = f'#EXTINF:{duration}, {artists[0]} - {name}\n{os.path.abspath(filename)}' - create_playlist_file(create_m3u, playlist_data, m3u_filename) - print(f'\n### SKIPPING: {song_name} (SONG ALREADY EXISTS) ###') - else: - if track_id != scraped_song_id: - track_id = scraped_song_id - track_id = TrackId.from_base62(track_id) - stream = ZSpotify.get_content_stream(track_id, ZSpotify.DOWNLOAD_QUALITY) - create_download_directory(ZSpotify.get_config(ROOT_PATH) + extra_paths) - total_size = stream.input_stream.size - - write_stream_to_file(stream, filename, song_name, total_size, disable_progressbar, track_info, - playlist_info) - - -def write_stream_to_file(stream, filename: str, song_name: str, total_size: Any, disable_progressbar: bool, - track_info: tuple, playlist_info: tuple): - (artists, album_name, name, image_url, release_year, disc_number, - track_number, scraped_song_id, is_playable, duration) = track_info - create_m3u, m3u_filename = playlist_info - with open(filename, 'wb') as file, tqdm( - desc=song_name, - total=total_size, - unit='B', - unit_scale=True, - unit_divisor=1024, - disable=disable_progressbar - ) as p_bar: - for _ in range(int(total_size / ZSpotify.get_config(CHUNK_SIZE)) + 1): - p_bar.update(file.write( - stream.input_stream.stream().read(ZSpotify.get_config(CHUNK_SIZE)))) - playlist_data = f'#EXTINF:{duration}, {artists[0]} - {name}\n{os.path.abspath(filename)}' - if ZSpotify.get_config(DOWNLOAD_FORMAT) == MusicFormat.MP3.value: - convert_audio_format(filename) - set_audio_tags(filename, artists, name, album_name, - release_year, disc_number, track_number) - set_music_thumbnail(filename, image_url) - - if not ZSpotify.get_config(OVERRIDE_AUTO_WAIT): - time.sleep(ZSpotify.get_config(ANTI_BAN_WAIT_TIME)) - create_playlist_file(create_m3u, playlist_data, m3u_filename) - - -def create_playlist_file(create_m3u: bool, playlist_data: str, m3u_filename: str): - if create_m3u and m3u_filename and playlist_data: - with open(m3u_filename, 'a+') as pfile: - if os.path.getsize(m3u_filename) == 0: - pfile.write('#EXTM3U\n') - pfile.write(playlist_data + '\n') - - def convert_audio_format(filename) -> None: """ Converts raw audio into playable mp3 """ # print('### CONVERTING TO ' + MUSIC_FORMAT.upper() + ' ###') @@ -174,4 +132,5 @@ def convert_audio_format(filename) -> None: bitrate = '320k' else: bitrate = '160k' - raw_audio.export(filename, format=ZSpotify.get_config(DOWNLOAD_FORMAT), bitrate=bitrate) + raw_audio.export(filename, format=ZSpotify.get_config( + DOWNLOAD_FORMAT), bitrate=bitrate) diff --git a/zspotify/utils.py b/zspotify/utils.py index 8632ebe3..b8188d95 100644 --- a/zspotify/utils.py +++ b/zspotify/utils.py @@ -3,19 +3,19 @@ import platform import re import time from enum import Enum -from typing import List, Tuple, Match +from typing import List, Tuple import music_tag import requests from const import SANITIZE, ARTIST, TRACKTITLE, ALBUM, YEAR, DISCNUMBER, TRACKNUMBER, ARTWORK, \ - WINDOWS_SYSTEM, TRACK_ID, ALBUM_ID, PLAYLIST_ID, EPISODE_ID, SHOW_ID, ARTIST_ID + WINDOWS_SYSTEM class MusicFormat(str, Enum): MP3 = 'mp3', OGG = 'ogg', - + def create_download_directory(download_path: str) -> None: os.makedirs(download_path, exist_ok=True) @@ -136,23 +136,46 @@ def regex_input_for_urls(search_input) -> Tuple[str, str, str, str, str, str]: search_input, ) - return ( - extract_info_from_regex_response(TRACK_ID, track_uri_search, track_url_search), - - extract_info_from_regex_response(ALBUM_ID, album_uri_search, album_url_search), - - extract_info_from_regex_response(PLAYLIST_ID, playlist_uri_search, playlist_url_search), - - extract_info_from_regex_response(EPISODE_ID, episode_uri_search, episode_url_search), - - extract_info_from_regex_response(SHOW_ID, show_uri_search, show_url_search), - - extract_info_from_regex_response(ARTIST_ID, artist_uri_search, artist_url_search) - ) - - -def extract_info_from_regex_response(key, uri_data: Match[str], url_data: Match[str]): - if uri_data or url_data: - return (uri_data if uri_data else url_data).group(key) + if track_uri_search is not None or track_url_search is not None: + track_id_str = (track_uri_search + if track_uri_search is not None else + track_url_search).group('TrackID') else: - return None + track_id_str = None + + if album_uri_search is not None or album_url_search is not None: + album_id_str = (album_uri_search + if album_uri_search is not None else + album_url_search).group('AlbumID') + else: + album_id_str = None + + if playlist_uri_search is not None or playlist_url_search is not None: + playlist_id_str = (playlist_uri_search + if playlist_uri_search is not None else + playlist_url_search).group('PlaylistID') + else: + playlist_id_str = None + + if episode_uri_search is not None or episode_url_search is not None: + episode_id_str = (episode_uri_search + if episode_uri_search is not None else + episode_url_search).group('EpisodeID') + else: + episode_id_str = None + + if show_uri_search is not None or show_url_search is not None: + show_id_str = (show_uri_search + if show_uri_search is not None else + show_url_search).group('ShowID') + else: + show_id_str = None + + if artist_uri_search is not None or artist_url_search is not None: + artist_id_str = (artist_uri_search + if artist_uri_search is not None else + artist_url_search).group('ArtistID') + else: + artist_id_str = None + + return track_id_str, album_id_str, playlist_id_str, episode_id_str, show_id_str, artist_id_str From ce5e2be0100e8e3d2a499ab93ecddf61f0ddc776 Mon Sep 17 00:00:00 2001 From: Raju komati Date: Mon, 25 Oct 2021 11:10:53 +0530 Subject: [PATCH 39/41] updated command in pylint github action --- .github/workflows/pylint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index c7d2a599..01c4ffd9 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -19,4 +19,4 @@ jobs: pip install pylint - name: Analysing the code with pylint run: | - pylint `ls */** | grep .py$ | xargs` + pylint $(git ls-files '*.py') From d744b6f40a9d10e60a005b3c1cd3156b22f49255 Mon Sep 17 00:00:00 2001 From: Raju komati Date: Mon, 25 Oct 2021 11:13:46 +0530 Subject: [PATCH 40/41] added requirements install command to pylint action --- .github/workflows/pylint.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 01c4ffd9..73ea43ae 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -17,6 +17,7 @@ jobs: run: | python -m pip install --upgrade pip pip install pylint + pip install -r requirements.txt - name: Analysing the code with pylint run: | pylint $(git ls-files '*.py') From 9f841bf00f2a02250f30d82d0a7bb314bdefdb93 Mon Sep 17 00:00:00 2001 From: LeithC Date: Mon, 25 Oct 2021 16:54:38 +1030 Subject: [PATCH 41/41] Modified zs_config.json to only copy if exists If zs_config.json does not exist it no longer forces a copy of the file. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c24ed245..40838a46 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,6 @@ FROM base COPY --from=builder /install /usr/local COPY zspotify /app -COPY zs_config.json / +COPY *zs_config.json / WORKDIR /app ENTRYPOINT ["/usr/local/bin/python", "__main__.py"]