diff --git a/CHANGELOG.md b/CHANGELOG.md index 74dd761..fece821 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,55 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased -Add your changes below. +## Unreleased [3.0.0-alpha] + +While this is unreleased, please only add v3 features here. +Rebasing master onto v3 doesn't require a changelog update. ### Added +* `Scope` - An enum which contains all of the authorization scopes (see [here](https://github.com/plamere/spotipy/issues/652#issuecomment-797461311)). + +### Changed + +* Made `CacheHandler` an abstract base class +* Modified the return structure of the `audio_features` function (wrapping the [Get Audio Features for Several Tracks](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-audio-features) API) to conform to the return structure of the similar methods listed below. The functions wrapping these APIs do not unwrap the single key JSON response, and this is currently the only function that does this. + * [Get Several Tracks](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-tracks) + * [Get Multiple Artists](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-artists) + * [Get Multiple Albums](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-albums) +* Renamed the `auth` parameter of `Spotify.__init__` to `access_token` for better clarity. +* Removed the `client_credentials_manager` and `oauth_manager` parameters because they are redundant. +* Replaced the `set_auth` and `auth_manager` properties with standard attributes. + +### Removed + +* Removed the following deprecated methods from `Spotify`: + * `playlist_tracks` + * `user_playlist` + * `user_playlist_tracks` + * `user_playlist_change_details` + * `user_playlist_unfollow` + * `user_playlist_add_tracks` + * `user_playlist_replace_tracks` + * `user_playlist_reorder_tracks` + * `user_playlist_remove_all_occurrences_of_tracks` + * `user_playlist_remove_specific_occurrences_of_tracks` + * `user_playlist_follow_playlist` + * `user_playlist_is_following` + +* Removed the deprecated `as_dict` parameter from the `get_access_token` method of `SpotifyOAuth` and `SpotifyPKCE`. +* Removed the deprecated `get_cached_token` and `_save_token_info` methods of `SpotifyOAuth` and `SpotifyPKCE`. +* Removed `SpotifyImplicitGrant`. +* Removed `prompt_for_user_token`. + +## Unreleased [2.x.x] + +### Added +- Added examples for audiobooks, shows and episodes methods to examples directory + ### Fixed +- Fixed scripts in examples directory that didn't run correctly +- Updated documentation for `Client.current_user_top_artists` to indicate maximum number of artists limit ### Removed diff --git a/examples/add_saved_episodes.py b/examples/add_saved_episodes.py new file mode 100644 index 0000000..5acc177 --- /dev/null +++ b/examples/add_saved_episodes.py @@ -0,0 +1,28 @@ +""" +Add episodes to current user's library +Usage: add_saved_episodes.py -e episode_id episode_id ... +""" + +import argparse +import spotipy +from spotipy.oauth2 import SpotifyOAuth + +scope = 'user-library-modify' + +def get_args(): + parser = argparse.ArgumentParser(description='Add episodes to library') + # Default args set to This American Life episodes 814 and 815 + parser.add_argument('-e', '--eids', nargs='+', + default=['6rxg9Lpt2ywNHFea8LxEBO', '7q8or6oYYRFQFYlA0remoy'], + help='Episode ids') + return parser.parse_args() + +def main(): + args = get_args() + print('Adding following episode ids to library: ' + str(args.eids)) + sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope)) + sp.current_user_saved_episodes_add(episodes=args.eids) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/examples/add_saved_shows.py b/examples/add_saved_shows.py new file mode 100644 index 0000000..ef16258 --- /dev/null +++ b/examples/add_saved_shows.py @@ -0,0 +1,28 @@ +""" +Add shows to current user's library +Usage: add_saved_shows.py -s show_id show_id ... +""" + +import argparse +import spotipy +from spotipy.oauth2 import SpotifyOAuth + +scope = 'user-library-modify' + +def get_args(): + parser = argparse.ArgumentParser(description='Add shows to library') + # Default args set to Radiolab and 99% invisible + parser.add_argument('-s', '--sids', nargs='+', + default=['2hmkzUtix0qTqvtpPcMzEL', '2VRS1IJCTn2Nlkg33ZVfkM'], + help='Show ids') + return parser.parse_args() + +def main(): + args = get_args() + print('Adding following show ids to library: ' + str(args.sids)) + sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope)) + sp.current_user_saved_shows_add(shows=args.sids) + + +if __name__ == '__main__': + main() diff --git a/examples/check_show_is_saved.py b/examples/check_show_is_saved.py new file mode 100644 index 0000000..eeeb56d --- /dev/null +++ b/examples/check_show_is_saved.py @@ -0,0 +1,31 @@ +""" +Check if shows are saved in user's library +Usage: check_show_is_saved -s show_id show_id ... +""" + +import argparse +import spotipy +from spotipy.oauth2 import SpotifyOAuth + +scope = 'user-library-read' + +def get_args(): + parser = argparse.ArgumentParser(description='Check that a show is saved') + # Default args set to Radiolab and 99% invisible + parser.add_argument('-s', '--sids', nargs='+', + default=['2hmkzUtix0qTqvtpPcMzEL', '2VRS1IJCTn2Nlkg33ZVfkM'], + help='Show ids') + return parser.parse_args() + +def main(): + args = get_args() + sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope)) + results = sp.current_user_saved_episodes_contains(episodes=args.sids) + show_names = sp.shows(shows=args.sids) + # Print show names and if show is saved by current user + for i, show in enumerate(show_names['shows']): + print(show['name'] + ': ' + str(results[i])) + + +if __name__ == '__main__': + main() diff --git a/examples/delete_saved_episodes.py b/examples/delete_saved_episodes.py new file mode 100644 index 0000000..b62f4f0 --- /dev/null +++ b/examples/delete_saved_episodes.py @@ -0,0 +1,28 @@ +""" +Delete episodes from current user's library +Usage: delete_saved_episodes.py -e episode_id episode_id ... +""" + +import argparse +import spotipy +from spotipy.oauth2 import SpotifyOAuth + +scope = 'user-library-modify' + +def get_args(): + parser = argparse.ArgumentParser(description='Delete episodes from library') + # Default args set to This American Life episodes 814 and 815 + parser.add_argument('-e', '--eids', nargs='+', + default=['6rxg9Lpt2ywNHFea8LxEBO', '7q8or6oYYRFQFYlA0remoy'], + help='Episode ids') + return parser.parse_args() + +def main(): + args = get_args() + print('Deleting following episode ids from library: ' + str(args.eids)) + sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope)) + sp.current_user_saved_episodes_delete(episodes=args.eids) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/examples/follow_playlist.py b/examples/follow_playlist.py index 6973468..20f4c32 100644 --- a/examples/follow_playlist.py +++ b/examples/follow_playlist.py @@ -1,27 +1,22 @@ -import argparse +# Follow a playlist +import argparse import spotipy from spotipy.oauth2 import SpotifyOAuth +scope = 'playlist-modify-public' +sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope)) def get_args(): parser = argparse.ArgumentParser(description='Follows a playlist based on playlist ID') - parser.add_argument('-p', '--playlist', required=True, help='Playlist ID') - + # Default to Top 50 Global if no playlist is provided + parser.add_argument('-p', '--playlist', help='Playlist ID', nargs='?', default='37i9dQZEVXbMDoHDwVN2tF') return parser.parse_args() def main(): args = get_args() - - if args.playlist is None: - # Uses the Spotify Global Top 50 playlist - spotipy.Spotify(auth_manager=SpotifyOAuth()).current_user_follow_playlist( - '37i9dQZEVXbMDoHDwVN2tF') - - else: - spotipy.Spotify(auth_manager=SpotifyOAuth()).current_user_follow_playlist(args.playlist) - + sp.current_user_follow_playlist(args.playlist) if __name__ == '__main__': - main() + main() \ No newline at end of file diff --git a/examples/get_audiobook_chapters_info.py b/examples/get_audiobook_chapters_info.py new file mode 100644 index 0000000..abbf37a --- /dev/null +++ b/examples/get_audiobook_chapters_info.py @@ -0,0 +1,29 @@ +""" +Print chapter titles and lengths for given audiobook +Usage: get_audiobooks_chapters_info.py -a audiobook_id +""" + +import argparse +import spotipy +from spotipy.oauth2 import SpotifyOAuth + + +def get_args(): + parser = argparse.ArgumentParser(description='Get chapter info for an audiobook') + # Default set to Dune + parser.add_argument('-a', '--audiobook', default='2h01INWMBvfpzNMpGFzhdF', help='Audiobook id') + return parser.parse_args() + + +def main(): + args = get_args() + print('Getting chapter info for follow audiobook id: ' + str(args.audiobook)) + sp = spotipy.Spotify(auth_manager=SpotifyOAuth()) + results = sp.get_audiobook_chapters(id=args.audiobook) + # Print chapter name and length + for item in results['items']: + print('Name: ' + item['name'] + ', length: ' + str(round(item['duration_ms']/60000,1)) + ' minutes') + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/examples/get_audiobooks_info.py b/examples/get_audiobooks_info.py new file mode 100644 index 0000000..72fe18d --- /dev/null +++ b/examples/get_audiobooks_info.py @@ -0,0 +1,29 @@ +""" +Print audiobook title and description for a list of audiobook ids +Usage: get_audiobooks_info.py -a audiobook_id audiobook_id ... +""" + +import argparse +import spotipy +from spotipy.oauth2 import SpotifyOAuth + +def get_args(): + parser = argparse.ArgumentParser(description='Get information for a list of audiobooks') + # Defaults set to The Great Gatsby, The Chronicles of Narnia and Dune + parser.add_argument('-a', '--aids', nargs='+', + default=['6qjpt1CUHhKXiNoeNoU7nu', '1ezmXd68LbDtxebvygEQ2U', '2h01INWMBvfpzNMpGFzhdF'], + help='Audiobook ids') + return parser.parse_args() + +def main(): + args = get_args() + print('Getting info for follow audiobook ids: ' + str(args.aids) + '\n') + sp = spotipy.Spotify(auth_manager=SpotifyOAuth()) + results = sp.get_audiobooks(ids=args.aids) + # Print book title and description + for book in results['audiobooks']: + print('Title: ' + book['name'] + '\n' + book['description'] + '\n') + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/examples/playlist_add_items.py b/examples/playlist_add_items.py deleted file mode 100644 index 8d10c5b..0000000 --- a/examples/playlist_add_items.py +++ /dev/null @@ -1,12 +0,0 @@ -# Add a list of items (URI) to a playlist (URI) - -import spotipy -from spotipy.oauth2 import SpotifyOAuth - -sp = spotipy.Spotify(auth_manager=SpotifyOAuth(client_id="YOUR_APP_CLIENT_ID", - client_secret="YOUR_APP_CLIENT_SECRET", - redirect_uri="YOUR_APP_REDIRECT_URI", - scope="playlist-modify-private" - )) - -sp.playlist_add_items('playlist_id', ['list_of_items']) diff --git a/examples/read_a_playlist.py b/examples/read_a_playlist.py index 506760b..9697f4a 100644 --- a/examples/read_a_playlist.py +++ b/examples/read_a_playlist.py @@ -5,6 +5,6 @@ import json auth_manager = SpotifyClientCredentials() sp = spotipy.Spotify(auth_manager=auth_manager) -playlist_id = 'spotify:user:spotifycharts:playlist:37i9dQZEVXbJiZcmkrIHGU' +playlist_id = '37i9dQZEVXbJiZcmkrIHGU' results = sp.playlist(playlist_id) print(json.dumps(results, indent=4)) diff --git a/examples/remove_tracks_from_playlist.py b/examples/remove_tracks_from_playlist.py index 4e011eb..59625cc 100644 --- a/examples/remove_tracks_from_playlist.py +++ b/examples/remove_tracks_from_playlist.py @@ -7,8 +7,8 @@ from spotipy.oauth2 import SpotifyOAuth if len(sys.argv) > 2: - playlist_id = sys.argv[2] - track_ids = sys.argv[3:] + playlist_id = sys.argv[1] + track_ids = sys.argv[2:] else: print(f"Usage: {sys.argv[0]} playlist_id track_id ...") sys.exit() diff --git a/examples/replace_tracks_in_playlist.py b/examples/replace_tracks_in_playlist.py index 6c76b05..2a46d46 100644 --- a/examples/replace_tracks_in_playlist.py +++ b/examples/replace_tracks_in_playlist.py @@ -6,7 +6,7 @@ import sys import spotipy from spotipy.oauth2 import SpotifyOAuth -if len(sys.argv) > 3: +if len(sys.argv) > 2: playlist_id = sys.argv[1] track_ids = sys.argv[2:] else: diff --git a/examples/tracks.py b/examples/show_artist_tracks.py similarity index 100% rename from examples/tracks.py rename to examples/show_artist_tracks.py diff --git a/examples/show_tracks.py b/examples/show_tracks.py index bf184e8..986ed8f 100644 --- a/examples/show_tracks.py +++ b/examples/show_tracks.py @@ -3,21 +3,26 @@ given a list of track IDs show the artist and track name ''' -from spotipy.oauth2 import SpotifyClientCredentials -import sys +from spotipy.oauth2 import SpotifyOAuth import spotipy +import argparse + + +def get_args(): + parser = argparse.ArgumentParser(description='Print artist and track name given a list of track IDs') + parser.add_argument('-u', '--uris', nargs='+', + required=True, help='Track ids') + return parser.parse_args() + + +def main(): + args = get_args() + sp = spotipy.Spotify(auth_manager=SpotifyOAuth()) + track_list = sp.tracks(args.uris) + for track in track_list['tracks']: + print(track['name'] + ' - ' + track['artists'][0]['name']) + if __name__ == '__main__': - max_tracks_per_call = 50 - if len(sys.argv) > 1: - file = open(sys.argv[1]) - else: - file = sys.stdin - tids = file.read().split() + main() - auth_manager = SpotifyClientCredentials() - sp = spotipy.Spotify(auth_manager=auth_manager) - for start in range(0, len(tids), max_tracks_per_call): - results = sp.tracks(tids[start: start + max_tracks_per_call]) - for track in results['tracks']: - print(track['name'] + ' - ' + track['artists'][0]['name']) diff --git a/examples/test.py b/examples/test.py deleted file mode 100644 index bffb19b..0000000 --- a/examples/test.py +++ /dev/null @@ -1,11 +0,0 @@ -import spotipy -from spotipy.oauth2 import SpotifyOAuth - -scope = "user-library-read" - -sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope)) - -results = sp.current_user_saved_tracks() -for idx, item in enumerate(results['items']): - track = item['track'] - print(idx, track['artists'][0]['name'], " – ", track['name']) diff --git a/examples/unfollow_playlist.py b/examples/unfollow_playlist.py index bf0466f..eebf216 100644 --- a/examples/unfollow_playlist.py +++ b/examples/unfollow_playlist.py @@ -4,6 +4,8 @@ import logging import spotipy from spotipy.oauth2 import SpotifyOAuth +scope = 'playlist-modify-public' + logger = logging.getLogger('examples.unfollow_playlist') logging.basicConfig(level='DEBUG') @@ -23,9 +25,9 @@ def get_args(): def main(): args = get_args() - sp = spotipy.Spotify(auth_manager=SpotifyOAuth()) + sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope)) sp.current_user_unfollow_playlist(args.playlist) if __name__ == '__main__': - main() + main() \ No newline at end of file diff --git a/examples/user_saved_episodes.py b/examples/user_saved_episodes.py new file mode 100644 index 0000000..4a10682 --- /dev/null +++ b/examples/user_saved_episodes.py @@ -0,0 +1,29 @@ +""" +List current user's saved episodes +Usage: user_saved_episodes -l -o +""" + +import argparse +import spotipy +from spotipy.oauth2 import SpotifyOAuth + +scope = 'user-library-read' + +def get_args(): + parser = argparse.ArgumentParser(description='Show user\'s saved episodes') + parser.add_argument('-l', '--limit', default=20, help='Num of episodes to return') + parser.add_argument('-o', '--offset', default=0, help='Index of first show to return') + + return parser.parse_args() + +def main(): + args = get_args() + sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope)) + results = sp.current_user_saved_episodes(limit=args.limit, offset=args.offset) + # Print episode names + for item in results['items']: + print(item['episode']['name']) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/examples/user_saved_shows.py b/examples/user_saved_shows.py new file mode 100644 index 0000000..ea60f4a --- /dev/null +++ b/examples/user_saved_shows.py @@ -0,0 +1,29 @@ +""" +List current user's saved shows +Usage: user_saved_shows -l -o +""" + +import argparse +import spotipy +from spotipy.oauth2 import SpotifyOAuth + +scope = 'user-library-read' + +def get_args(): + parser = argparse.ArgumentParser(description='Show user\'s saved shows') + parser.add_argument('-l', '--limit', default=20, help='Num of shows to return') + parser.add_argument('-o', '--offset', default=0, help='Index of first show to return') + + return parser.parse_args() + +def main(): + args = get_args() + sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope)) + results = sp.current_user_saved_shows(limit=args.limit, offset=args.offset) + # Print episode names + for item in results['items']: + print(item['show']['name']) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/spotipy/client.py b/spotipy/client.py index 79d6e47..c63822a 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -1178,7 +1178,7 @@ class Spotify: """ Get the current user's top artists Parameters: - - limit - the number of entities to return + - limit - the number of entities to return (max 50) - offset - the index of the first entity to return - time_range - Over what time frame are the affinities computed Valid-values: short_term, medium_term, long_term