From 7d81eb5cc6fef6b789bc4f76b84b627d0dafb0c5 Mon Sep 17 00:00:00 2001 From: yiannisha Date: Sat, 23 Oct 2021 16:06:34 +0300 Subject: [PATCH 1/6] 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 25538e1..d031ea9 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 2/6] 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 d031ea9..a58790f 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 3/6] Fixed minor bugs --- zspotify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zspotify.py b/zspotify.py index 5633903..3c04c67 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 18fca559b30baa306c5e7d7cb5e4c57b71284c6f Mon Sep 17 00:00:00 2001 From: yiannisha Date: Sun, 24 Oct 2021 18:07:12 +0300 Subject: [PATCH 4/6] 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 abd8b87..d34fdfe 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 0c9a854..f44d869 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 8a4357a..9a1304c 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 41b09712e55954a1cc2f5b8080b4e1f31339e3d5 Mon Sep 17 00:00:00 2001 From: yiannisha Date: Sun, 24 Oct 2021 19:09:58 +0300 Subject: [PATCH 5/6] 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 12f86eb..19004d1 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 36be67b827bd2192380b3cfc98160bfa7eddd094 Mon Sep 17 00:00:00 2001 From: yiannisha Date: Sun, 24 Oct 2021 19:32:32 +0300 Subject: [PATCH 6/6] 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 19004d1..12f86eb 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):