From 7b8c632c9360e54f974913b55a982b695049056e Mon Sep 17 00:00:00 2001 From: Noah Date: Sat, 9 Dec 2023 12:21:08 -0800 Subject: [PATCH 1/3] Added a new test case, fixed some formatting in client.py, and added to user documentation for both sphinx and the README --- README.md | 4 + docs/index.rst | 23 +- spotipy/client.py | 247 +++++++------------ tests/integration/non_user_endpoints/test.py | 42 +--- 4 files changed, 113 insertions(+), 203 deletions(-) diff --git a/README.md b/README.md index aff9a396..07eaaf88 100644 --- a/README.md +++ b/README.md @@ -77,3 +77,7 @@ Don’t forget to add the *Spotipy* tag, and any other relevant tags as well, be If you have suggestions, bugs or other issues specific to this library, file them [here](https://github.com/plamere/spotipy/issues). Or just send a pull request. + +## Contributing + +If you would like to contribute to the project, see more on how to do so in the [CONTRIBUTING.md](https://github.com/spotipy-dev/spotipy/blob/master/CONTRIBUTING.md) file or in the contributing section in our [full](https://spotipy.readthedocs.io/en/2.22.1/#contribute) documentation page. \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index a6fa61b8..80dbbb9b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -322,39 +322,46 @@ Export the needed Environment variables::: export SPOTIPY_CLIENT_ID=client_id_here export SPOTIPY_CLIENT_SECRET=client_secret_here export SPOTIPY_CLIENT_USERNAME=client_username_here # This is actually an id not spotify display name - export SPOTIPY_REDIRECT_URI=http://localhost:8080 # Make url is set in app you created to get your ID and SECRET + export SPOTIPY_REDIRECT_URI=http://localhost:8080 # Make sure url is set in app you created to get your ID and SECRET -Create virtual environment, install dependencies, run tests::: + For more information, see `this `_ section + +Create virtual environment, install dependencies, run tests: $ virtualenv --python=python3.7 env (env) $ pip install --user -e . (env) $ python -m unittest discover -v tests **Lint** -To automatically fix the code style::: +To automatically fix the code style: pip install autopep8 autopep8 --in-place --aggressive --recursive . -To verify the code style::: +To verify the code style: pip install flake8 flake8 . -To make sure if the import lists are stored correctly::: +To make sure if the import lists are stored correctly: pip install isort isort . -c -v **Publishing (by maintainer)** +------------------------------ - Bump version in setup.py - Bump and date changelog - Add to changelog: -:: ## Unreleased - // Add your changes here and then delete this line + ### Added + - Replace with changes + + ### Fixed + + ### Removed + - Commit changes - Package to pypi: -:: python setup.py sdist bdist_wheel python3 setup.py sdist bdist_wheel twine check dist/* diff --git a/spotipy/client.py b/spotipy/client.py index a026e412..ac4c1bf3 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -111,14 +111,14 @@ class Spotify(object): # # [1] https://www.iana.org/assignments/uri-schemes/prov/spotify # [2] https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids - _regex_spotify_uri = r'^spotify:(?:(?Ptrack|artist|album|playlist|show|episode|audiobook):(?P[0-9A-Za-z]+)|user:(?P[0-9A-Za-z]+):playlist:(?P[0-9A-Za-z]+))$' # noqa: E501 + _regex_spotify_uri = r'^spotify:(?:(?Ptrack|artist|album|playlist|show|episode):(?P[0-9A-Za-z]+)|user:(?P[0-9A-Za-z]+):playlist:(?P[0-9A-Za-z]+))$' # noqa: E501 # Spotify URLs are defined at [1]. The assumption is made that they are all # pointing to open.spotify.com, so a regex is used to parse them as well, # instead of a more complex URL parsing function. # # [1] https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids - _regex_spotify_url = r'^(http[s]?:\/\/)?open.spotify.com\/(?Ptrack|artist|album|playlist|show|episode|user|audiobook)\/(?P[0-9A-Za-z]+)(\?.*)?$' # noqa: E501 + _regex_spotify_url = r'^(http[s]?:\/\/)?open.spotify.com\/(?Ptrack|artist|album|playlist|show|episode|user)\/(?P[0-9A-Za-z]+)(\?.*)?$' # noqa: E501 _regex_base62 = r'^[0-9A-Za-z]+$' @@ -588,18 +588,21 @@ def search(self, q, limit=10, offset=0, type="track", market=None): "search", q=q, limit=limit, offset=offset, type=type, market=market ) - def search_markets(self, q, limit=10, offset=0, type="track", markets=None, total=None): + def search_markets(self, q, limit=10, offset=0, search_type="track", markets=None, total=None): """ (experimental) Searches multiple markets for an item Parameters: - q - the search query (see how to write a query in the official documentation https://developer.spotify.com/documentation/web-api/reference/search/) # noqa - - limit - the number of items to return (min = 1, default = 10, max = 50). If a search is to be done on multiple - markets, then this limit is applied to each market. (e.g. search US, CA, MX each with a limit of 10). + - limit - the number of items to return (min = 1, default = 10, max = 50). + If a search is to be done on multiple + markets, then this limit is applied to each market. + (e.g. search US, CA, MX each with a limit of 10). If multiple types are specified, this applies to each type. - offset - the index of the first item to return - type - the types of items to return. One or more of 'artist', 'album', - 'track', 'playlist', 'show', or 'episode'. If multiple types are desired, pass in a comma separated string. + 'track', 'playlist', 'show', or 'episode'. If multiple types are desired, + pass in a comma separated string. - markets - A list of ISO 3166-1 alpha-2 country codes. Search all country markets by default. - total - the total number of results to return across multiple markets and types. """ @@ -612,13 +615,16 @@ def search_markets(self, q, limit=10, offset=0, type="track", markets=None, tota markets = self.country_codes if not (isinstance(markets, list) or isinstance(markets, tuple)): - markets = [] + try: + markets = [markets] + except TypeError: + markets = [] warnings.warn( "Searching multiple markets is poorly performing.", UserWarning, ) - return self._search_multiple_markets(q, limit, offset, type, markets, total) + return self._search_multiple_markets(q, limit, offset, search_type, markets, total) def user(self, user): """ Gets basic profile information about a Spotify User @@ -649,7 +655,7 @@ def playlist(self, playlist_id, fields=None, market=None, additional_types=("tra """ plid = self._get_id("playlist", playlist_id) return self._get( - "playlists/%s" % (plid), + f"playlists/{plid}", fields=fields, market=market, additional_types=",".join(additional_types), @@ -705,7 +711,7 @@ def playlist_items( """ plid = self._get_id("playlist", playlist_id) return self._get( - "playlists/%s/tracks" % (plid), + f"playlists/{plid}/tracks", limit=limit, offset=offset, fields=fields, @@ -720,7 +726,7 @@ def playlist_cover_image(self, playlist_id): - playlist_id - the playlist ID, URI or URL """ plid = self._get_id("playlist", playlist_id) - return self._get("playlists/%s/images" % (plid)) + return self._get(f"playlists/{plid}/images") def playlist_upload_cover_image(self, playlist_id, image_b64): """ Replace the image used to represent a specific playlist @@ -728,21 +734,16 @@ def playlist_upload_cover_image(self, playlist_id, image_b64): Parameters: - playlist_id - the id of the playlist - image_b64 - image data as a Base64 encoded JPEG image string - (maximum payload size is 256 KB) + (maximum payload size is 256 KB) """ plid = self._get_id("playlist", playlist_id) return self._put( - "playlists/{}/images".format(plid), + f"playlists/{plid}/images", payload=image_b64, content_type="image/jpeg", ) def user_playlist(self, user, playlist_id=None, fields=None, market=None): - warnings.warn( - "You should use `playlist(playlist_id)` instead", - DeprecationWarning, - ) - """ Gets a single playlist of a user Parameters: @@ -750,6 +751,11 @@ def user_playlist(self, user, playlist_id=None, fields=None, market=None): - playlist_id - the id of the playlist - fields - which fields to return """ + warnings.warn( + "You should use `playlist(playlist_id)` instead", + DeprecationWarning, + ) + if playlist_id is None: return self._get("users/%s/starred" % user) return self.playlist(playlist_id, fields=fields, market=market) @@ -763,11 +769,6 @@ def user_playlist_tracks( offset=0, market=None, ): - warnings.warn( - "You should use `playlist_tracks(playlist_id)` instead", - DeprecationWarning, - ) - """ Get full details of the tracks of a playlist owned by a user. Parameters: @@ -778,13 +779,12 @@ def user_playlist_tracks( - offset - the index of the first track to return - market - an ISO 3166-1 alpha-2 country code. """ - return self.playlist_tracks( - playlist_id, - limit=limit, - offset=offset, - fields=fields, - market=market, + warnings.warn( + "You should use `playlist_items(playlist_id)` instead", + DeprecationWarning, ) + return self.playlist_items(playlist_id, fields, limit, offset, + market, ("track", "episode")) def user_playlists(self, user, limit=50, offset=0): """ Gets playlists of a user @@ -795,7 +795,7 @@ def user_playlists(self, user, limit=50, offset=0): - offset - the index of the first item to return """ return self._get( - "users/%s/playlists" % user, limit=limit, offset=offset + f"users/{user}/playlists", limit=limit, offset=offset ) def user_playlist_create(self, user, name, public=True, collaborative=False, description=""): @@ -815,7 +815,7 @@ def user_playlist_create(self, user, name, public=True, collaborative=False, des "description": description } - return self._post("users/%s/playlists" % (user,), payload=data) + return self._post(f"users/{user,}/playlists", payload=data) def user_playlist_change_details( self, @@ -826,10 +826,6 @@ def user_playlist_change_details( collaborative=None, description=None, ): - warnings.warn( - "You should use `playlist_change_details(playlist_id, ...)` instead", - DeprecationWarning, - ) """ Changes a playlist's name and/or public/private state Parameters: @@ -840,6 +836,10 @@ def user_playlist_change_details( - collaborative - optional is the playlist collaborative - description - optional description of the playlist """ + warnings.warn( + "You should use `playlist_change_details(playlist_id, ...)` instead", + DeprecationWarning, + ) return self.playlist_change_details(playlist_id, name, public, collaborative, description) @@ -860,28 +860,22 @@ def user_playlist_unfollow(self, user, playlist_id): def user_playlist_add_tracks( self, user, playlist_id, tracks, position=None ): + """ Adds tracks to a playlist + + Parameters: + - user - the id of the user + - playlist_id - the id of the playlist + - tracks - a list of track URIs, URLs or IDs + - position - the position to add the tracks + """ warnings.warn( "You should use `playlist_add_items(playlist_id, tracks)` instead", DeprecationWarning, ) - """ Adds tracks to a playlist - - Parameters: - - user - the id of the user - - playlist_id - the id of the playlist - - tracks - a list of track URIs, URLs or IDs - - position - the position to add the tracks - """ tracks = [self._get_uri("track", tid) for tid in tracks] return self.playlist_add_items(playlist_id, tracks, position) - def user_playlist_add_episodes( - self, user, playlist_id, episodes, position=None - ): - warnings.warn( - "You should use `playlist_add_items(playlist_id, episodes)` instead", - DeprecationWarning, - ) + def user_playlist_add_episodes(self, user, playlist_id, episodes, position=None): """ Adds episodes to a playlist Parameters: @@ -890,6 +884,11 @@ def user_playlist_add_episodes( - episodes - a list of track URIs, URLs or IDs - position - the position to add the episodes """ + warnings.warn( + "You should use `playlist_add_items(playlist_id, episodes)` instead", + DeprecationWarning, + ) + episodes = [self._get_uri("episode", tid) for tid in episodes] return self.playlist_add_items(playlist_id, episodes, position) @@ -946,7 +945,6 @@ def user_playlist_remove_all_occurrences_of_tracks( - playlist_id - the id of the playlist - tracks - the list of track ids to remove from the playlist - snapshot_id - optional id of the playlist snapshot - """ warnings.warn( "You should use `playlist_remove_all_occurrences_of_items" @@ -966,10 +964,10 @@ def user_playlist_remove_specific_occurrences_of_tracks( - user - the id of the user - playlist_id - the id of the playlist - tracks - an array of objects containing Spotify URIs of the - tracks to remove with their current positions in the - playlist. For example: - [ { "uri":"4iV5W9uYEdYUVa79Axb7Rh", "positions":[2] }, - { "uri":"1301WleyT98MSxVHPZCA6M", "positions":[7] } ] + tracks to remove with their current positions in the + playlist. For example: + [ { "uri":"4iV5W9uYEdYUVa79Axb7Rh", "positions":[2] }, + { "uri":"1301WleyT98MSxVHPZCA6M", "positions":[7] } ] - snapshot_id - optional id of the playlist snapshot """ warnings.warn( @@ -990,7 +988,7 @@ def user_playlist_remove_specific_occurrences_of_tracks( if snapshot_id: payload["snapshot_id"] = snapshot_id return self._delete( - "users/%s/playlists/%s/tracks" % (user, plid), payload=payload + f"users/{user}/playlists/{plid}/tracks", payload=payload ) def user_playlist_follow_playlist(self, playlist_owner_id, playlist_id): @@ -1000,7 +998,6 @@ def user_playlist_follow_playlist(self, playlist_owner_id, playlist_id): Parameters: - playlist_owner_id - the user id of the playlist owner - playlist_id - the id of the playlist - """ warnings.warn( "You should use `current_user_follow_playlist(playlist_id)` instead", @@ -1018,8 +1015,7 @@ def user_playlist_is_following( - playlist_owner_id - the user id of the playlist owner - playlist_id - the id of the playlist - user_ids - the ids of the users that you want to check to see - if they follow the playlist. Maximum: 5 ids. - + if they follow the playlist. Maximum: 5 ids. """ warnings.warn( "You should use `playlist_is_following(playlist_id, user_ids)` instead", @@ -1045,7 +1041,6 @@ def playlist_change_details( - collaborative - optional is the playlist collaborative - description - optional description of the playlist """ - data = {} if isinstance(name, six.string_types): data["name"] = name @@ -1056,7 +1051,7 @@ def playlist_change_details( if isinstance(description, six.string_types): data["description"] = description return self._put( - "playlists/%s" % (self._get_id("playlist", playlist_id)), payload=data + f"playlists/{self._get_id('playlist', playlist_id)}", payload=data ) def current_user_unfollow_playlist(self, playlist_id): @@ -1067,12 +1062,10 @@ def current_user_unfollow_playlist(self, playlist_id): - name - the name of the playlist """ return self._delete( - "playlists/%s/followers" % (playlist_id) + f"playlists/{playlist_id}/followers" ) - def playlist_add_items( - self, playlist_id, items, position=None - ): + def playlist_add_items(self, playlist_id, items, position=None): """ Adds tracks/episodes to a playlist Parameters: @@ -1083,7 +1076,7 @@ def playlist_add_items( plid = self._get_id("playlist", playlist_id) ftracks = [self._get_uri("track", tid) for tid in items] return self._post( - "playlists/%s/tracks" % (plid), + f"playlists/{plid}/tracks", payload=ftracks, position=position, ) @@ -1099,7 +1092,7 @@ def playlist_replace_items(self, playlist_id, items): ftracks = [self._get_uri("track", tid) for tid in items] payload = {"uris": ftracks} return self._put( - "playlists/%s/tracks" % (plid), payload=payload + f"playlists/{plid}/tracks", payload=payload ) def playlist_reorder_items( @@ -1117,8 +1110,7 @@ def playlist_reorder_items( - range_start - the position of the first track to be reordered - range_length - optional the number of tracks to be reordered (default: 1) - - insert_before - the position where the tracks should be - inserted + - insert_before - the position where the tracks should be inserted - snapshot_id - optional playlist's snapshot ID """ plid = self._get_id("playlist", playlist_id) @@ -1130,7 +1122,7 @@ def playlist_reorder_items( if snapshot_id: payload["snapshot_id"] = snapshot_id return self._put( - "playlists/%s/tracks" % (plid), payload=payload + f"playlists/{plid}/tracks", payload=payload ) def playlist_remove_all_occurrences_of_items( @@ -1144,14 +1136,13 @@ def playlist_remove_all_occurrences_of_items( - snapshot_id - optional id of the playlist snapshot """ - plid = self._get_id("playlist", playlist_id) ftracks = [self._get_uri("track", tid) for tid in items] payload = {"tracks": [{"uri": track} for track in ftracks]} if snapshot_id: payload["snapshot_id"] = snapshot_id return self._delete( - "playlists/%s/tracks" % (plid), payload=payload + f"playlists/{plid}/tracks", payload=payload ) def playlist_remove_specific_occurrences_of_items( @@ -1162,13 +1153,12 @@ def playlist_remove_specific_occurrences_of_items( Parameters: - playlist_id - the id of the playlist - items - an array of objects containing Spotify URIs of the - tracks/episodes to remove with their current positions in - the playlist. For example: - [ { "uri":"4iV5W9uYEdYUVa79Axb7Rh", "positions":[2] }, - { "uri":"1301WleyT98MSxVHPZCA6M", "positions":[7] } ] + tracks/episodes to remove with their current positions in + the playlist. For example: + [ { "uri":"4iV5W9uYEdYUVa79Axb7Rh", "positions":[2] }, + { "uri":"1301WleyT98MSxVHPZCA6M", "positions":[7] } ] - snapshot_id - optional id of the playlist snapshot """ - plid = self._get_id("playlist", playlist_id) ftracks = [] for tr in items: @@ -1182,7 +1172,7 @@ def playlist_remove_specific_occurrences_of_items( if snapshot_id: payload["snapshot_id"] = snapshot_id return self._delete( - "playlists/%s/tracks" % (plid), payload=payload + f"playlists/{plid}/tracks", payload=payload ) def current_user_follow_playlist(self, playlist_id): @@ -1191,27 +1181,22 @@ def current_user_follow_playlist(self, playlist_id): Parameters: - playlist_id - the id of the playlist - """ return self._put( - "playlists/{}/followers".format(playlist_id) + f"playlists/{playlist_id}/followers" ) - def playlist_is_following( - self, playlist_id, user_ids - ): + def playlist_is_following(self, playlist_id, user_ids): """ Check to see if the given users are following the given playlist Parameters: - playlist_id - the id of the playlist - user_ids - the ids of the users that you want to check to see - if they follow the playlist. Maximum: 5 ids. - + if they follow the playlist. Maximum: 5 ids. """ - endpoint = "playlists/{}/followers/contains?ids={}" return self._get( - endpoint.format(playlist_id, ",".join(user_ids)) + f"playlists/{playlist_id}/followers/contains?ids={','.join(user_ids)}" ) def me(self): @@ -1415,9 +1400,7 @@ def current_user_followed_artists(self, limit=20, after=None): Parameters: - limit - the number of artists to return - - after - the last artist ID retrieved from the previous - request - + - after - the last artist ID retrieved from the previous request """ return self._get( "me/following", type="artist", limit=limit, after=after @@ -1537,22 +1520,22 @@ def featured_playlists( Parameters: - locale - The desired language, consisting of a lowercase ISO - 639-1 alpha-2 language code and an uppercase ISO 3166-1 alpha-2 - country code, joined by an underscore. + 639-1 alpha-2 language code and an uppercase ISO 3166-1 + alpha-2 country code, joined by an underscore. - country - An ISO 3166-1 alpha-2 country code. - - timestamp - A timestamp in ISO 8601 format: - yyyy-MM-ddTHH:mm:ss. Use this parameter to specify the user's - local time to get results tailored for that specific date and - time in the day + - timestamp - A timestamp in ISO 8601 format: yyyy-MM-ddTHH:mm:ss. + Use this parameter to specify the user's local time + to get results tailored for that specific date and + time in the day - limit - The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50 - offset - The index of the first item to return. Default: 0 - (the first object). Use with limit to get the next set of - items. + (the first object). Use with limit to get the next + set of items. """ return self._get( "browse/featured-playlists", @@ -1573,8 +1556,8 @@ def new_releases(self, country=None, limit=20, offset=0): Minimum: 1. Maximum: 50 - offset - The index of the first item to return. Default: 0 - (the first object). Use with limit to get the next set of - items. + (the first object). Use with limit to get the next set of + items. """ return self._get( "browse/new-releases", country=country, limit=limit, offset=offset @@ -1588,8 +1571,8 @@ def category(self, category_id, country=None, locale=None): - country - An ISO 3166-1 alpha-2 country code. - locale - The desired language, consisting of an ISO 639-1 alpha-2 - language code and an ISO 3166-1 alpha-2 country code, joined - by an underscore. + language code and an ISO 3166-1 alpha-2 country code, + joined by an underscore. """ return self._get( "browse/categories/" + category_id, @@ -1603,15 +1586,15 @@ def categories(self, country=None, locale=None, limit=20, offset=0): Parameters: - country - An ISO 3166-1 alpha-2 country code. - locale - The desired language, consisting of an ISO 639-1 alpha-2 - language code and an ISO 3166-1 alpha-2 country code, joined - by an underscore. + language code and an ISO 3166-1 alpha-2 country code, + joined by an underscore. - limit - The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50 - offset - The index of the first item to return. Default: 0 - (the first object). Use with limit to get the next set of - items. + (the first object). Use with limit to get the next + set of items. """ return self._get( "browse/categories", @@ -1621,9 +1604,7 @@ def categories(self, country=None, locale=None, limit=20, offset=0): offset=offset, ) - def category_playlists( - self, category_id=None, country=None, limit=20, offset=0 - ): + def category_playlists( self, category_id=None, country=None, limit=20, offset=0): """ Get a list of playlists for a specific Spotify category Parameters: @@ -2033,51 +2014,3 @@ def _search_multiple_markets(self, q, limit, offset, type, markets, total): return results return results - - def get_audiobook(self, id, market=None): - """ Get Spotify catalog information for a single audiobook identified by its unique - Spotify ID. - - Parameters: - - id - the Spotify ID for the audiobook - - market - an ISO 3166-1 alpha-2 country code. - """ - audiobook_id = self._get_id("audiobook", id) - endpoint = f"audiobooks/{audiobook_id}" - - if market: - endpoint += f'?market={market}' - - return self._get(endpoint) - - def get_audiobooks(self, ids, market=None): - """ Get Spotify catalog information for multiple audiobooks based on their Spotify IDs. - - Parameters: - - ids - a list of Spotify IDs for the audiobooks - - market - an ISO 3166-1 alpha-2 country code. - """ - audiobook_ids = [self._get_id("audiobook", id) for id in ids] - endpoint = f"audiobooks?ids={','.join(audiobook_ids)}" - - if market: - endpoint += f'&market={market}' - - return self._get(endpoint) - - def get_audiobook_chapters(self, id, market=None, limit=20, offset=0): - """ Get Spotify catalog information about an audiobook’s chapters. - - Parameters: - - id - the Spotify ID for the audiobook - - market - an ISO 3166-1 alpha-2 country code. - - limit - the maximum number of items to return - - offset - the index of the first item to return - """ - audiobook_id = self._get_id("audiobook", id) - endpoint = f"audiobooks/{audiobook_id}/chapters?limit={limit}&offset={offset}" - - if market: - endpoint += f'&market={market}' - - return self._get(endpoint) diff --git a/tests/integration/non_user_endpoints/test.py b/tests/integration/non_user_endpoints/test.py index ca2faace..4b3f0378 100644 --- a/tests/integration/non_user_endpoints/test.py +++ b/tests/integration/non_user_endpoints/test.py @@ -55,16 +55,6 @@ class AuthTestSpotipy(unittest.TestCase): heavyweight_ep1_url = 'https://open.spotify.com/episode/68kq3bNz6hEuq8NtdfwERG' reply_all_ep1_urn = 'spotify:episode:1KHjbpnmNpFmNTczQmTZlR' - american_gods_urn = 'spotify:audiobook:1IcM9Untg6d3ktuwObYGcN' - american_gods_id = '1IcM9Untg6d3ktuwObYGcN' - american_gods_url = 'https://open.spotify.com/audiobook/1IcM9Untg6d3ktuwObYGcN' - - four_books = [ - 'spotify:audiobook:1IcM9Untg6d3ktuwObYGcN', - 'spotify:audiobook:37sRC6carIX2Vf3Vv716T7', - 'spotify:audiobook:1Gep4UJ95xQawA55OgRI8n', - 'spotify:audiobook:4Sm381mcf5gBsi9yfhqgVB'] - @classmethod def setUpClass(self): self.spotify = Spotify( @@ -390,6 +380,10 @@ def test_show_bad_urn(self): with self.assertRaises(SpotifyException): self.spotify.show("bogus_urn", market="US") + def test_show_exception(self): + print(SpotifyException(http_status=404, code='123', msg='Unable to find endpoint')) + + def test_shows(self): results = self.spotify.shows([self.heavyweight_urn, self.reply_all_urn], market="US") self.assertTrue('shows' in results) @@ -465,31 +459,3 @@ def test_available_markets(self): self.assertTrue(isinstance(markets, list)) self.assertIn("US", markets) self.assertIn("GB", markets) - - def test_get_audiobook(self): - audiobook = self.spotify.get_audiobook(self.american_gods_urn, market="US") - print(audiobook) - self.assertTrue(audiobook['name'] == - 'American Gods: The Tenth Anniversary Edition: A Novel') - - def test_get_audiobook_bad_urn(self): - with self.assertRaises(SpotifyException): - self.spotify.get_audiobook("bogus_urn", market="US") - - def test_get_audiobooks(self): - results = self.spotify.get_audiobooks(self.four_books, market="US") - self.assertTrue('audiobooks' in results) - self.assertTrue(len(results['audiobooks']) == 4) - self.assertTrue(results['audiobooks'][0]['name'] == - 'American Gods: The Tenth Anniversary Edition: A Novel') - self.assertTrue(results['audiobooks'][1]['name'] == 'The Da Vinci Code: A Novel') - self.assertTrue(results['audiobooks'][2]['name'] == 'Outlander') - self.assertTrue(results['audiobooks'][3]['name'] == 'Pachinko: A Novel') - - def test_get_audiobook_chapters(self): - results = self.spotify.get_audiobook_chapters( - self.american_gods_urn, market="US", limit=10, offset=5) - self.assertTrue('items' in results) - self.assertTrue(len(results['items']) == 10) - self.assertTrue(results['items'][0]['chapter_number'] == 5) - self.assertTrue(results['items'][9]['chapter_number'] == 14) From 0311dbee2f2a616136d5bb3be9176b0670c4343d Mon Sep 17 00:00:00 2001 From: Noah Date: Sat, 9 Dec 2023 12:32:49 -0800 Subject: [PATCH 2/3] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99325b57..6e07ee87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ 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 +- Added new minor fixes to make formatting more consistent +- Added more documentation +- Added a new test case to increase code coverage ### Added From 8ce32b82fc9c7e4ff74c64ae92f8901013340756 Mon Sep 17 00:00:00 2001 From: Noah Date: Sat, 9 Dec 2023 12:38:41 -0800 Subject: [PATCH 3/3] Fixing removed code --- spotipy/client.py | 45 ++++++++++++++++++++ tests/integration/non_user_endpoints/test.py | 40 +++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/spotipy/client.py b/spotipy/client.py index ac4c1bf3..2a001cf9 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -2014,3 +2014,48 @@ def _search_multiple_markets(self, q, limit, offset, type, markets, total): return results return results + + def get_audiobook(self, id, market=None): + """ Get Spotify catalog information for a single audiobook identified by its unique + Spotify ID. + Parameters: + - id - the Spotify ID for the audiobook + - market - an ISO 3166-1 alpha-2 country code. + """ + audiobook_id = self._get_id("audiobook", id) + endpoint = f"audiobooks/{audiobook_id}" + + if market: + endpoint += f'?market={market}' + + return self._get(endpoint) + + def get_audiobooks(self, ids, market=None): + """ Get Spotify catalog information for multiple audiobooks based on their Spotify IDs. + Parameters: + - ids - a list of Spotify IDs for the audiobooks + - market - an ISO 3166-1 alpha-2 country code. + """ + audiobook_ids = [self._get_id("audiobook", id) for id in ids] + endpoint = f"audiobooks?ids={','.join(audiobook_ids)}" + + if market: + endpoint += f'&market={market}' + + return self._get(endpoint) + + def get_audiobook_chapters(self, id, market=None, limit=20, offset=0): + """ Get Spotify catalog information about an audiobook’s chapters. + Parameters: + - id - the Spotify ID for the audiobook + - market - an ISO 3166-1 alpha-2 country code. + - limit - the maximum number of items to return + - offset - the index of the first item to return + """ + audiobook_id = self._get_id("audiobook", id) + endpoint = f"audiobooks/{audiobook_id}/chapters?limit={limit}&offset={offset}" + + if market: + endpoint += f'&market={market}' + + return self._get(endpoint) \ No newline at end of file diff --git a/tests/integration/non_user_endpoints/test.py b/tests/integration/non_user_endpoints/test.py index 4b3f0378..97f5ee73 100644 --- a/tests/integration/non_user_endpoints/test.py +++ b/tests/integration/non_user_endpoints/test.py @@ -54,6 +54,17 @@ class AuthTestSpotipy(unittest.TestCase): heavyweight_ep1_id = '68kq3bNz6hEuq8NtdfwERG' heavyweight_ep1_url = 'https://open.spotify.com/episode/68kq3bNz6hEuq8NtdfwERG' reply_all_ep1_urn = 'spotify:episode:1KHjbpnmNpFmNTczQmTZlR' + + american_gods_urn = 'spotify:audiobook:1IcM9Untg6d3ktuwObYGcN' + american_gods_id = '1IcM9Untg6d3ktuwObYGcN' + american_gods_url = 'https://open.spotify.com/audiobook/1IcM9Untg6d3ktuwObYGcN' + + four_books = [ + 'spotify:audiobook:1IcM9Untg6d3ktuwObYGcN', + 'spotify:audiobook:37sRC6carIX2Vf3Vv716T7', + 'spotify:audiobook:1Gep4UJ95xQawA55OgRI8n', + 'spotify:audiobook:4Sm381mcf5gBsi9yfhqgVB'] + @classmethod def setUpClass(self): @@ -459,3 +470,32 @@ def test_available_markets(self): self.assertTrue(isinstance(markets, list)) self.assertIn("US", markets) self.assertIn("GB", markets) + + + def test_get_audiobook(self): + audiobook = self.spotify.get_audiobook(self.american_gods_urn, market="US") + print(audiobook) + self.assertTrue(audiobook['name'] == + 'American Gods: The Tenth Anniversary Edition: A Novel') + + def test_get_audiobook_bad_urn(self): + with self.assertRaises(SpotifyException): + self.spotify.get_audiobook("bogus_urn", market="US") + + def test_get_audiobooks(self): + results = self.spotify.get_audiobooks(self.four_books, market="US") + self.assertTrue('audiobooks' in results) + self.assertTrue(len(results['audiobooks']) == 4) + self.assertTrue(results['audiobooks'][0]['name'] == + 'American Gods: The Tenth Anniversary Edition: A Novel') + self.assertTrue(results['audiobooks'][1]['name'] == 'The Da Vinci Code: A Novel') + self.assertTrue(results['audiobooks'][2]['name'] == 'Outlander') + self.assertTrue(results['audiobooks'][3]['name'] == 'Pachinko: A Novel') + + def test_get_audiobook_chapters(self): + results = self.spotify.get_audiobook_chapters( + self.american_gods_urn, market="US", limit=10, offset=5) + self.assertTrue('items' in results) + self.assertTrue(len(results['items']) == 10) + self.assertTrue(results['items'][0]['chapter_number'] == 5) + self.assertTrue(results['items'][9]['chapter_number'] == 14) \ No newline at end of file