From d0bbe67302cb89e0d071e3319fbcdd75d414936b Mon Sep 17 00:00:00 2001 From: kasowskc <72176947+kasowskc@users.noreply.github.com> Date: Tue, 27 Dec 2022 06:27:17 -0800 Subject: [PATCH 01/67] Add additional video tutorial reference to documentation. (#921) * Added additional video tutorial Added additional link to video tutorial series by Ian Annase. The original tutorial is very low resolution and is hard to follow. The tutorial linked in this commit is entirely in HD and goes further in depth, including examples of possible functionality. * Update index.rst Adjusted link formatting. * Update index.rst Updated link formatting again because I'm an idiot. * Update index.rst Edited formatting to fix missing period and double wording. --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index c9622921..debd9ffa 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,7 +9,7 @@ Welcome to Spotipy! you get full access to all of the music data provided by the Spotify platform. Assuming you set the ``SPOTIPY_CLIENT_ID`` and ``SPOTIPY_CLIENT_SECRET`` -environment variables (here is a `video `_ explaining how to do so), here's a quick example of using *Spotipy* to list the +environment variables (here is a `video `_ explaining how to do so). For a longer tutorial with examples included, refer to this `video playlist `_. Below is a quick example of using *Spotipy* to list the names of all the albums released by the artist 'Birdy':: import spotipy From 0b9062726c079958afd5c641ed7175c5c83a7b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bruckert?= Date: Sat, 7 Jan 2023 08:26:17 +0000 Subject: [PATCH 02/67] Create SECURITY.md --- .github/SECURITY.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/SECURITY.md diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 00000000..034e8480 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. From f669966a7221a25051e84d8e9eb5933ac4772aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bruckert?= Date: Sat, 7 Jan 2023 08:27:58 +0000 Subject: [PATCH 03/67] Update SECURITY.md --- .github/SECURITY.md | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 034e8480..b819ae48 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -2,20 +2,13 @@ ## Supported Versions -Use this section to tell people about which versions of your project are -currently being supported with security updates. - | Version | Supported | | ------- | ------------------ | -| 5.1.x | :white_check_mark: | -| 5.0.x | :x: | -| 4.0.x | :white_check_mark: | -| < 4.0 | :x: | +| 2.x | :white_check_mark: | +| 1.x | :x: | ## Reporting a Vulnerability -Use this section to tell people how to report a vulnerability. +Report via https://github.com/spotipy-dev/spotipy/security/advisories. -Tell them where to go, how often they can expect to get an update on a -reported vulnerability, what to expect if the vulnerability is accepted or -declined, etc. +Guidance: https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability. From d884ae13c24a28ff090576139a6b103bee6d0e8c Mon Sep 17 00:00:00 2001 From: Archie Baldry <33762310+archiebaldry@users.noreply.github.com> Date: Sun, 15 Jan 2023 23:30:37 +0000 Subject: [PATCH 04/67] Fix typo in start_playback function (#930) --- spotipy/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spotipy/client.py b/spotipy/client.py index d7025a95..c03bc497 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -1758,7 +1758,7 @@ def start_playback( ): """ Start or resume user's playback. - Provide a `context_uri` to start playback or an album, + Provide a `context_uri` to start playback of an album, artist, or playlist. Provide a `uris` list to start playback of one or more From 262e7a0443ece48ce6825f97ad9a3ed205ceb725 Mon Sep 17 00:00:00 2001 From: Mario Sessa <121885783+mase-git@users.noreply.github.com> Date: Mon, 23 Jan 2023 13:48:37 +0100 Subject: [PATCH 05/67] Rename simple files (#933) --- examples/{simple1.py => simple_artist_albums.py} | 0 examples/{simple2.py => simple_artist_top_tracks.py} | 0 examples/{simple4.py => simple_me.py} | 0 examples/{simple0.py => simple_search_artist.py} | 0 examples/{simple3.py => simple_search_artist_image_url.py} | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename examples/{simple1.py => simple_artist_albums.py} (100%) rename examples/{simple2.py => simple_artist_top_tracks.py} (100%) rename examples/{simple4.py => simple_me.py} (100%) rename examples/{simple0.py => simple_search_artist.py} (100%) rename examples/{simple3.py => simple_search_artist_image_url.py} (100%) diff --git a/examples/simple1.py b/examples/simple_artist_albums.py similarity index 100% rename from examples/simple1.py rename to examples/simple_artist_albums.py diff --git a/examples/simple2.py b/examples/simple_artist_top_tracks.py similarity index 100% rename from examples/simple2.py rename to examples/simple_artist_top_tracks.py diff --git a/examples/simple4.py b/examples/simple_me.py similarity index 100% rename from examples/simple4.py rename to examples/simple_me.py diff --git a/examples/simple0.py b/examples/simple_search_artist.py similarity index 100% rename from examples/simple0.py rename to examples/simple_search_artist.py diff --git a/examples/simple3.py b/examples/simple_search_artist_image_url.py similarity index 100% rename from examples/simple3.py rename to examples/simple_search_artist_image_url.py From b1db0b63d90eae40af32d7ee4c760c2fd581a1b4 Mon Sep 17 00:00:00 2001 From: Shaderbug <119610832+Shaderbug@users.noreply.github.com> Date: Mon, 23 Jan 2023 19:50:07 +0100 Subject: [PATCH 06/67] Merge pull request from GHSA-q764-g6fm-555v * Improve URL and URI handling * Back to SpotifyException for backward-compatibility * fix copy paste typo * TODO v3 comments Co-authored-by: Stephane Bruckert --- CHANGELOG.md | 8 ++++-- spotipy/client.py | 62 +++++++++++++++++++++++++++++++++++------------ 2 files changed, 53 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bdab119..58cd5466 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased // Add new changes below this line -- Modified docstring for playlist_add_items() to accept "only URIs or URLs", - with intended deprecation for IDs in v3 ### Added @@ -17,8 +15,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added Comment to README - Getting Started for user to add URI to app in Spotify Developer Dashboard. - Added playlist_add_tracks.py to example folder +### Changed + +- Modified docstring for playlist_add_items() to accept "only URIs or URLs", + with intended deprecation for IDs in v3 + ### Fixed +- Path traversal vulnerability that may lead to type confusion in URI handling code - Update contributing.md ### Removed diff --git a/spotipy/client.py b/spotipy/client.py index c03bc497..e2409568 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -6,6 +6,7 @@ import json import logging +import re import warnings import requests @@ -96,6 +97,29 @@ class Spotify(object): "US", "UY"] + # Spotify URI scheme defined in [1], and the ID format as base-62 in [2]. + # + # Unfortunately the IANA specification is out of date and doesn't include the new types + # show and episode. Additionally, for the user URI, it does not specify which characters + # are valid for usernames, so the assumption is alphanumeric which coincidentially are also + # the same ones base-62 uses. + # In limited manual exploration this seems to hold true, as newly accounts are assigned an + # identifier that looks like the base-62 of all other IDs, but some older accounts only have + # numbers and even older ones seemed to have been allowed to freely pick this name. + # + # [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|user):(?P[0-9A-Za-z]+)$' + + # 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)\/(?P[0-9A-Za-z]+)(\?.*)?$' + + _regex_base62 = r'^[0-9A-Za-z]+$' + def __init__( self, auth=None, @@ -1940,20 +1964,28 @@ def _append_device_id(self, path, device_id): return path def _get_id(self, type, id): - fields = id.split(":") - if len(fields) >= 3: - if type != fields[-2]: - logger.warning('Expected id of type %s but found type %s %s', - type, fields[-2], id) - return fields[-1].split("?")[0] - fields = id.split("/") - if len(fields) >= 3: - itype = fields[-2] - if type != itype: - logger.warning('Expected id of type %s but found type %s %s', - type, itype, id) - return fields[-1].split("?")[0] - return id + uri_match = re.search(Spotify._regex_spotify_uri, id) + if uri_match is not None: + uri_match_groups = uri_match.groupdict() + if uri_match_groups['type'] != type: + # TODO change to a ValueError in v3 + raise SpotifyException(400, -1, "Unexpected Spotify URI type.") + return uri_match_groups['id'] + + url_match = re.search(Spotify._regex_spotify_url, id) + if url_match is not None: + url_match_groups = url_match.groupdict() + if url_match_groups['type'] != type: + raise SpotifyException(400, -1, "Unexpected Spotify URL type.") + # TODO change to a ValueError in v3 + return url_match_groups['id'] + + # Raw identifiers might be passed, ensure they are also base-62 + if re.search(Spotify._regex_base62, id) is not None: + return id + + # TODO change to a ValueError in v3 + raise SpotifyException(400, -1, "Unsupported URL / URI.") def _get_uri(self, type, id): if self._is_uri(id): @@ -1962,7 +1994,7 @@ def _get_uri(self, type, id): return "spotify:" + type + ":" + self._get_id(type, id) def _is_uri(self, uri): - return uri.startswith("spotify:") and len(uri.split(':')) == 3 + return re.search(Spotify._regex_spotify_uri, uri) is not None def _search_multiple_markets(self, q, limit, offset, type, markets, total): if total and limit > total: From beec3dad1f886f1ad68502b619f40329cf766dc9 Mon Sep 17 00:00:00 2001 From: Stephane Bruckert Date: Mon, 23 Jan 2023 18:54:40 +0000 Subject: [PATCH 07/67] Fix flake8 --- spotipy/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spotipy/client.py b/spotipy/client.py index e2409568..a696b997 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -109,14 +109,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|user):(?P[0-9A-Za-z]+)$' + _regex_spotify_uri = r'^spotify:(?Ptrack|artist|album|playlist|show|episode|user):(?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)\/(?P[0-9A-Za-z]+)(\?.*)?$' + _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]+$' From c53511bbbe87f89c2aeaa65bef584539db440f97 Mon Sep 17 00:00:00 2001 From: Stephane Bruckert Date: Mon, 23 Jan 2023 19:00:55 +0000 Subject: [PATCH 08/67] Bump to 2.22.1 --- CHANGELOG.md | 10 +++++++--- setup.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58cd5466..f1f7617b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -// Add new changes below this line +### Added + +### Fixed + +### Removed + +## [2.22.1] - 2023-01-23 ### Added @@ -25,8 +31,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Path traversal vulnerability that may lead to type confusion in URI handling code - Update contributing.md -### Removed - ## [2.22.0] - 2022-12-10 ### Added diff --git a/setup.py b/setup.py index e20d30b7..08482ace 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ setup( name='spotipy', - version='2.22.0', + version='2.22.1', description='A light weight Python library for the Spotify Web API', long_description=long_description, long_description_content_type="text/markdown", From b3f308d2894d0d90538d5dc604a648ad1a9ca849 Mon Sep 17 00:00:00 2001 From: Shawn Cruz Date: Sun, 12 Feb 2023 17:58:55 -0500 Subject: [PATCH 09/67] Add encoder_cls argument to CacheFileHandler (#941) Fixes --- CHANGELOG.md | 2 ++ spotipy/cache_handler.py | 9 ++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1f7617b..76d4879e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Added optional `encoder_cls` argument to `CacheFileHandler`, which overwrite default encoder for token before writing to disk + ### Added ### Fixed diff --git a/spotipy/cache_handler.py b/spotipy/cache_handler.py index 2645f25b..9a6d703b 100644 --- a/spotipy/cache_handler.py +++ b/spotipy/cache_handler.py @@ -50,15 +50,18 @@ class CacheFileHandler(CacheHandler): def __init__(self, cache_path=None, - username=None): + username=None, + encoder_cls=None): """ Parameters: * cache_path: May be supplied, will otherwise be generated (takes precedence over `username`) * username: May be supplied or set as environment variable (will set `cache_path` to `.cache-{username}`) + * encoder_cls: May be supplied as a means of overwriting the + default serializer used for writing tokens to disk """ - + self.encoder_cls = encoder_cls if cache_path: self.cache_path = cache_path else: @@ -88,7 +91,7 @@ def get_cached_token(self): def save_token_to_cache(self, token_info): try: f = open(self.cache_path, "w") - f.write(json.dumps(token_info)) + f.write(json.dumps(token_info, cls=self.encoder_cls)) f.close() except IOError: logger.warning('Couldn\'t write token to cache at: %s', From 572195617b3d63f05f4083a4067fe6eb0dfe448b Mon Sep 17 00:00:00 2001 From: Andrii Yurchuk Date: Tue, 14 Feb 2023 15:00:59 +0100 Subject: [PATCH 10/67] Fix SpotifyPKCE docstring (#942) --- spotipy/oauth2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spotipy/oauth2.py b/spotipy/oauth2.py index 631a8658..125c87c9 100644 --- a/spotipy/oauth2.py +++ b/spotipy/oauth2.py @@ -629,7 +629,7 @@ class SpotifyPKCE(SpotifyAuthBase): """ Implements PKCE Authorization Flow for client apps This auth manager enables *user and non-user* endpoints with only - a client secret, redirect uri, and username. When the app requests + a client ID, redirect URI, and username. When the app requests an access token for the first time, the user is prompted to authorize the new client app. After authorizing the app, the client app is then given both access and refresh tokens. This is the From ca332a538ec7e23e74b73a7faf73e489527c6cfc Mon Sep 17 00:00:00 2001 From: Suyash Bajpai <7suyashbajpai@gmail.com> Date: Tue, 28 Feb 2023 13:11:22 +0530 Subject: [PATCH 11/67] Fixing a typo in the docs (#945) spotispy --> spotipy --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index debd9ffa..9c253f07 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -160,7 +160,7 @@ The redirect URI can be any valid URI (it does not need to be accessible) such as ``http://example.com``, ``http://localhost`` or ``http://127.0.0.1:9090``. .. note:: If you choose an `http`-scheme URL, and it's for `localhost` or - `127.0.0.1`, **AND** it specifies a port, then spotispy will instantiate + `127.0.0.1`, **AND** it specifies a port, then spotipy will instantiate a server on the indicated response to receive the access token from the response at the end of the oauth flow [see the code](https://github.com/plamere/spotipy/blob/master/spotipy/oauth2.py#L483-L490). From f2d23e2219cd490c880a64ead419f67dd37e2ee3 Mon Sep 17 00:00:00 2001 From: Ludwig Johansson Date: Wed, 15 Mar 2023 23:17:02 +0800 Subject: [PATCH 12/67] Fix regex to support detailed URI #947 (#949) * Fix regex to support detailed URI as #947 * Added changes to changelog --- CHANGELOG.md | 1 + spotipy/client.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76d4879e..fbb70438 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added ### Fixed +- Fixed the regex for matching playlist URIs with the format spotify:user:USERNAME:playlist:PLAYLISTID. ### Removed diff --git a/spotipy/client.py b/spotipy/client.py index a696b997..f68e0b34 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -109,7 +109,7 @@ 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|user):(?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, From fe438c0432b962e5f95aa463c3bba23522fd2a2b Mon Sep 17 00:00:00 2001 From: Richard Ngo-Lam <69094063+rngolam@users.noreply.github.com> Date: Wed, 15 Mar 2023 16:46:08 -0700 Subject: [PATCH 13/67] Update search_markets method to apply the total parameter to all types, add tests (#901) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update search_markets method to apply the total parameter to all types, fixes #534 * Add integration tests for searching multiple types in multiple markets * Update search_markets method to apply the total parameter to all types, add tests --------- Co-authored-by: Stéphane Bruckert --- CHANGELOG.md | 5 +- spotipy/client.py | 31 +++++--- tests/integration/non_user_endpoints/test.py | 81 ++++++++++++++++++++ 3 files changed, 104 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbb70438..608f0e75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -- Added optional `encoder_cls` argument to `CacheFileHandler`, which overwrite default encoder for token before writing to disk - ### Added +- Added optional `encoder_cls` argument to `CacheFileHandler`, which overwrite default encoder for token before writing to disk +- Integration tests for searching multiple types in multiple markets (non-user endpoints) ### Fixed - Fixed the regex for matching playlist URIs with the format spotify:user:USERNAME:playlist:PLAYLISTID. +- `search_markets` now factors the counts of all types in the `total` rather than just the first type ([#534](https://github.com/spotipy-dev/spotipy/issues/534)) ### Removed diff --git a/spotipy/client.py b/spotipy/client.py index f68e0b34..d3b918f0 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -15,6 +15,8 @@ from spotipy.exceptions import SpotifyException +from collections import defaultdict + logger = logging.getLogger(__name__) @@ -594,12 +596,12 @@ def search_markets(self, q, limit=10, offset=0, type="track", markets=None, tota 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). + 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. - 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 if multiple markets are supplied in the search. - If multiple types are specified, this only applies to the first type. + - total - the total number of results to return across multiple markets and types. """ warnings.warn( "Searching multiple markets is an experimental feature. " @@ -2005,22 +2007,29 @@ def _search_multiple_markets(self, q, limit, offset, type, markets, total): UserWarning, ) - results = {} - first_type = type.split(",")[0] + 's' + results = defaultdict(dict) + item_types = [item_type + "s" for item_type in type.split(",")] count = 0 for country in markets: result = self._get( "search", q=q, limit=limit, offset=offset, type=type, market=country ) - results[country] = result + for item_type in item_types: + results[country][item_type] = result[item_type] + + # Truncate the items list to the current limit + if len(results[country][item_type]['items']) > limit: + results[country][item_type]['items'] = \ + results[country][item_type]['items'][:limit] + + count += len(results[country][item_type]['items']) + if total and limit > total - count: + # when approaching `total` results, adjust `limit` to not request more + # items than needed + limit = total - count - count += len(result[first_type]['items']) if total and count >= total: - break - if total and limit > total - count: - # when approaching `total` results, adjust `limit` to not request more - # items than needed - limit = total - count + return results return results diff --git a/tests/integration/non_user_endpoints/test.py b/tests/integration/non_user_endpoints/test.py index 96ee4da9..fe58160d 100644 --- a/tests/integration/non_user_endpoints/test.py +++ b/tests/integration/non_user_endpoints/test.py @@ -221,6 +221,87 @@ def test_artist_search_with_multiple_markets(self): total_limited_results += len(results_limited[country]['artists']['items']) self.assertTrue(total_limited_results <= total) + def test_multiple_types_search_with_multiple_markets(self): + total = 14 + + countries_list = ['GB', 'US', 'AU'] + countries_tuple = ('GB', 'US', 'AU') + + results_multiple = self.spotify.search_markets(q='abba', type='artist,track', + markets=countries_list) + results_all = self.spotify.search_markets(q='abba', type='artist,track') + results_tuple = self.spotify.search_markets(q='abba', type='artist,track', + markets=countries_tuple) + results_limited = self.spotify.search_markets(q='abba', limit=3, type='artist,track', + markets=countries_list, total=total) + + # Asserts 'artists' property is present in all responses + self.assertTrue( + all('artists' in results_multiple[country] for country in results_multiple)) + self.assertTrue(all('artists' in results_all[country] for country in results_all)) + self.assertTrue(all('artists' in results_tuple[country] for country in results_tuple)) + self.assertTrue(all('artists' in results_limited[country] for country in results_limited)) + + # Asserts 'tracks' property is present in all responses + self.assertTrue( + all('tracks' in results_multiple[country] for country in results_multiple)) + self.assertTrue(all('tracks' in results_all[country] for country in results_all)) + self.assertTrue(all('tracks' in results_tuple[country] for country in results_tuple)) + self.assertTrue(all('tracks' in results_limited[country] for country in results_limited)) + + # Asserts 'artists' list is nonempty in unlimited searches + self.assertTrue( + all(len(results_multiple[country]['artists']['items']) > 0 for country in + results_multiple)) + self.assertTrue(all(len(results_all[country]['artists'] + ['items']) > 0 for country in results_all)) + self.assertTrue( + all(len(results_tuple[country]['artists']['items']) > 0 for country in results_tuple)) + + # Asserts 'tracks' list is nonempty in unlimited searches + self.assertTrue( + all(len(results_multiple[country]['tracks']['items']) > 0 for country in + results_multiple)) + self.assertTrue(all(len(results_all[country]['tracks'] + ['items']) > 0 for country in results_all)) + self.assertTrue(all(len(results_tuple[country]['tracks'] + ['items']) > 0 for country in results_tuple)) + + # Asserts artist name is the first artist result in all searches + self.assertTrue(all(results_multiple[country]['artists']['items'] + [0]['name'] == 'ABBA' for country in results_multiple)) + self.assertTrue(all(results_all[country]['artists']['items'] + [0]['name'] == 'ABBA' for country in results_all)) + self.assertTrue(all(results_tuple[country]['artists']['items'] + [0]['name'] == 'ABBA' for country in results_tuple)) + self.assertTrue(all(results_limited[country]['artists']['items'] + [0]['name'] == 'ABBA' for country in results_limited)) + + # Asserts track name is present in responses from specified markets + self.assertTrue(all('Dancing Queen' in + [item['name'] for item in results_multiple[country]['tracks']['items']] + for country in results_multiple)) + self.assertTrue(all('Dancing Queen' in + [item['name'] for item in results_tuple[country]['tracks']['items']] + for country in results_tuple)) + + # Asserts expected number of items are returned based on the total + # 3 artists + 3 tracks = 6 items returned from first market + # 3 artists + 3 tracks = 6 items returned from second market + # 2 artists + 0 tracks = 2 items returned from third market + # 14 items returned total + self.assertEqual(len(results_limited['GB']['artists']['items']), 3) + self.assertEqual(len(results_limited['GB']['tracks']['items']), 3) + self.assertEqual(len(results_limited['US']['artists']['items']), 3) + self.assertEqual(len(results_limited['US']['tracks']['items']), 3) + self.assertEqual(len(results_limited['AU']['artists']['items']), 2) + self.assertEqual(len(results_limited['AU']['tracks']['items']), 0) + + item_count = sum([len(market_result['artists']['items']) + len(market_result['tracks'] + ['items']) for market_result in results_limited.values()]) + + self.assertEqual(item_count, total) + def test_artist_albums(self): results = self.spotify.artist_albums(self.weezer_urn) self.assertTrue('items' in results) From b7baaabf7403b1e6c5daf57f4bb6a7ca64cdfef1 Mon Sep 17 00:00:00 2001 From: Beza Amare Amsalu Date: Thu, 30 Mar 2023 13:12:41 -0400 Subject: [PATCH 14/67] fixed contributing.md (#955) --- CHANGELOG.md | 3 +++ CONTRIBUTING.md | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 608f0e75..56bbc309 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -395,6 +395,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix typos in doc - Start following [SemVer](https://semver.org) properly +### Changed + +- Made instructions in the CONTRIBUTING.md file more clear such that it is easier to onboard and there are no conflicts with TUTORIAL.md ## [2.5.0] - 2020-01-11 Added follow and player endpoints diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e5701b50..688bebc6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ If you would like to contribute to spotipy follow these steps: # Linux or Mac 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_CLIENT_USERNAME=client_username_here # This is actually an id not spotify display name and can be found [here](https://www.spotify.com/us/account/overview/) export SPOTIPY_REDIRECT_URI=http://localhost:8080 # Make url is set in app you created to get your ID and SECRET # Windows @@ -21,9 +21,9 @@ $env:SPOTIPY_REDIRECT_URI="http://localhost:8080" ### Create virtual environment, install dependencies, run tests: ```bash -$ virtualenv --python=python3.7 env +$ virtualenv --python=python3 env $ source env/bin/activate -(env) $ pip install --user -e . +(env) $ pip install -e . (env) $ python -m unittest discover -v tests ``` From 3bac7250ec6dd91398887e3ee6d7d7709a688e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bruckert?= Date: Fri, 7 Apr 2023 12:33:32 +0100 Subject: [PATCH 15/67] Publish to PyPI action (#958) --- .github/workflows/publish.yml | 62 +++++++++++++++++++++++++++++++++++ CHANGELOG.md | 1 + 2 files changed, 63 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..d937059f --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,62 @@ +name: Publish to PyPI + +on: + push: + branches-ignore: + - '**' + tags: + - '*.*.*' + +jobs: + build-n-publish: + name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + - name: Install pypa/build + run: >- + python -m + pip install + build + --user + - name: Build a binary wheel and a source tarball + run: >- + python -m + build + --sdist + --wheel + --outdir dist/ + . + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "2.x" + - name: Install pypa/build + run: >- + python -m + pip install + build + --user + - name: Build a binary wheel and a source tarball + run: >- + python -m + build + --sdist + --wheel + --outdir dist/ + . + - name: Publish distribution 📦 to Test PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.TEST_PYPI_API_TOKEN }} + repository-url: https://test.pypi.org/legacy/ +# - name: Publish distribution 📦 to PyPI +# if: startsWith(github.ref, 'refs/tags') +# uses: pypa/gh-action-pypi-publish@release/v1 +# with: +# password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 56bbc309..fc7f7f0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added optional `encoder_cls` argument to `CacheFileHandler`, which overwrite default encoder for token before writing to disk - Integration tests for searching multiple types in multiple markets (non-user endpoints) +- Publish to PyPI action ### Fixed - Fixed the regex for matching playlist URIs with the format spotify:user:USERNAME:playlist:PLAYLISTID. From 796c03338f3dd4896133b36fcf4bd7dce395052e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bruckert?= Date: Fri, 7 Apr 2023 18:36:17 +0100 Subject: [PATCH 16/67] Bump to 2.23.0 (#959) --- .github/workflows/publish.yml | 11 +++-------- CHANGELOG.md | 9 +++++++++ CONTRIBUTING.md | 19 ++++++------------- setup.py | 2 +- 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d937059f..2c1fe1ff 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -50,13 +50,8 @@ jobs: --wheel --outdir dist/ . - - name: Publish distribution 📦 to Test PyPI + - name: Publish distribution 📦 to PyPI + if: startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@release/v1 with: - password: ${{ secrets.TEST_PYPI_API_TOKEN }} - repository-url: https://test.pypi.org/legacy/ -# - name: Publish distribution 📦 to PyPI -# if: startsWith(github.ref, 'refs/tags') -# uses: pypa/gh-action-pypi-publish@release/v1 -# with: -# password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file + password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index fc7f7f0d..e09a9c7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added +- Replace with changes + +### Fixed + +### Removed + +## [2.23.0] - 2023-04-07 + ### Added - Added optional `encoder_cls` argument to `CacheFileHandler`, which overwrite default encoder for token before writing to disk - Integration tests for searching multiple types in multiple markets (non-user endpoints) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 688bebc6..3dd2c276 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,6 +44,10 @@ To make sure if the import lists are stored correctly: pip install isort isort . -c -v +### Changelog + +Don't forget to add a short description of your change in the [CHANGELOG](CHANGELOG.md) + ### Publishing (by maintainer) - Bump version in setup.py @@ -52,26 +56,15 @@ To make sure if the import lists are stored correctly: ## Unreleased - // Add new changes below - ### 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/* - twine upload dist/* - + - Push tag to trigger PyPI build & release workflow - Create github release https://github.com/plamere/spotipy/releases with the changelog content for the version and a short name that describes the main addition - Verify doc uses latest https://readthedocs.org/projects/spotipy/ - -### Changelog - -Don't forget to add a short description of your change in the [CHANGELOG](CHANGELOG.md) diff --git a/setup.py b/setup.py index 08482ace..dd1ab177 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ setup( name='spotipy', - version='2.22.1', + version='2.23.0', description='A light weight Python library for the Spotify Web API', long_description=long_description, long_description_content_type="text/markdown", From 612b30efa3168008e7b092ba1f1b17802d5aed49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bruckert?= Date: Sun, 9 Apr 2023 15:52:22 +0100 Subject: [PATCH 17/67] Create .github/dependabot.yml --- .github/dependabot.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..ac6621f1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" From 45f78ce7a6861f6f43832b93fbf844756db11632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bruckert?= Date: Sun, 9 Apr 2023 15:53:17 +0100 Subject: [PATCH 18/67] Update dependabot.yml --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ac6621f1..91abb11f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,7 @@ version: 2 updates: - - package-ecosystem: "" # See documentation for possible values + - package-ecosystem: "pip" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" From 6cc817af85c167c5c0099fdd78635e6411c58266 Mon Sep 17 00:00:00 2001 From: John Cheng Date: Tue, 11 Apr 2023 23:29:37 -0400 Subject: [PATCH 19/67] ignore if _session does not exist --- spotipy/client.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spotipy/client.py b/spotipy/client.py index d3b918f0..91db14a3 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -211,8 +211,11 @@ def auth_manager(self, auth_manager): def __del__(self): """Make sure the connection (pool) gets closed""" - if isinstance(self._session, requests.Session): - self._session.close() + try: + if isinstance(self._session, requests.Session): + self._session.close() + except AttributeError: + pass def _build_session(self): self._session = requests.Session() From 3b6d14404e4c51226cbd2ed9ed704f701d8eaedb Mon Sep 17 00:00:00 2001 From: darwady2 Date: Mon, 24 Apr 2023 05:21:19 -0700 Subject: [PATCH 20/67] Updates the video link to a high definition video (#967) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updates the video link to a high definition video for setting the SPOTIPY_CLIENT_ID and SPOTIPY_CLIENT_SECRET values and authenticating. The old video was getting some complaints about people not being able to read the text (since it was low quality), so I've re-recorded it and replaced it with this new version. * Adds changes to Chanelog --------- Co-authored-by: Stéphane Bruckert --- CHANGELOG.md | 10 ++-------- docs/index.rst | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e09a9c7f..a575ac8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -### Added -- Replace with changes - -### Fixed - -### Removed +### Changed +- Changes the YouTube video link for authentication tutorial (the old video was in low definition, the new one is in high definition) ## [2.23.0] - 2023-04-07 @@ -25,8 +21,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed the regex for matching playlist URIs with the format spotify:user:USERNAME:playlist:PLAYLISTID. - `search_markets` now factors the counts of all types in the `total` rather than just the first type ([#534](https://github.com/spotipy-dev/spotipy/issues/534)) -### Removed - ## [2.22.1] - 2023-01-23 ### Added diff --git a/docs/index.rst b/docs/index.rst index 9c253f07..1ca18621 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,7 +9,7 @@ Welcome to Spotipy! you get full access to all of the music data provided by the Spotify platform. Assuming you set the ``SPOTIPY_CLIENT_ID`` and ``SPOTIPY_CLIENT_SECRET`` -environment variables (here is a `video `_ explaining how to do so). For a longer tutorial with examples included, refer to this `video playlist `_. Below is a quick example of using *Spotipy* to list the +environment variables (here is a `video `_ explaining how to do so). For a longer tutorial with examples included, refer to this `video playlist `_. Below is a quick example of using *Spotipy* to list the names of all the albums released by the artist 'Birdy':: import spotipy From d31969108d462c544f41aba4581a0d84a1e75d6f Mon Sep 17 00:00:00 2001 From: adhil0 <56094214+adhil0@users.noreply.github.com> Date: Tue, 2 May 2023 14:26:40 -0700 Subject: [PATCH 21/67] Update links in documentation (#969) * updating links to Spotify documentation * add changes to CHANGELOG.md --- CHANGELOG.md | 1 + FAQ.md | 2 +- docs/index.rst | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a575ac8d..f69d8fc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Changes the YouTube video link for authentication tutorial (the old video was in low definition, the new one is in high definition) +- Updated links to Spotify in documentation ## [2.23.0] - 2023-04-07 diff --git a/FAQ.md b/FAQ.md index 895e2938..63fa0521 100644 --- a/FAQ.md +++ b/FAQ.md @@ -36,7 +36,7 @@ Error: Solution: - You are likely missing a scope when requesting the endpoint, check -https://developer.spotify.com/web-api/using-scopes/ +https://developer.spotify.com/documentation/web-api/concepts/scopes/ ### Search doesn't find some tracks diff --git a/docs/index.rst b/docs/index.rst index 1ca18621..a6fa61b8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,7 +5,7 @@ Welcome to Spotipy! =================================== *Spotipy* is a lightweight Python library for the `Spotify Web API -`_. With *Spotipy* +`_. With *Spotipy* you get full access to all of the music data provided by the Spotify platform. Assuming you set the ``SPOTIPY_CLIENT_ID`` and ``SPOTIPY_CLIENT_SECRET`` @@ -71,7 +71,7 @@ Features *Spotipy* supports all of the features of the Spotify Web API including access to all end points, and support for user authorization. For details on the capabilities you are encouraged to review the `Spotify Web -API `_ documentation. +API `_ documentation. Installation ============ @@ -143,7 +143,7 @@ Scopes ------ See `Using -Scopes `_ for information +Scopes `_ for information about scopes. Redirect URI From f647ca75c2a1cde12a2c06703e117e7b7dab0857 Mon Sep 17 00:00:00 2001 From: John Cheng Date: Fri, 26 May 2023 19:51:59 -0400 Subject: [PATCH 22/67] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f69d8fc3..a6d4acec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Changes the YouTube video link for authentication tutorial (the old video was in low definition, the new one is in high definition) - Updated links to Spotify in documentation +- Fixed error obfuscation when Spotify class is being inherited and an error is raised in the Child's __init__ ## [2.23.0] - 2023-04-07 From 1416d47cba95428e3a22b22d105d39aa9c08d904 Mon Sep 17 00:00:00 2001 From: Dan Joseph Date: Mon, 30 Oct 2023 21:32:46 -0400 Subject: [PATCH 23/67] Audiobook Support and CI Workflow Update (#1036) * Implement audiobook endpoints * Update GitHub CI Workflow: Removed Python v2.7 * Update GitHub CI Workflow: Removed Python v3.6 * Add integration tests for audiobook endpoints --- .github/workflows/pythonapp.yml | 2 +- CHANGELOG.md | 7 +++ spotipy/client.py | 52 +++++++++++++++++++- tests/integration/non_user_endpoints/test.py | 38 ++++++++++++++ 4 files changed, 96 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index a3d5dcc7..7c83ab32 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} diff --git a/CHANGELOG.md b/CHANGELOG.md index f69d8fc3..99325b57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added support for audiobook endpoints: get_audiobook, get_audiobooks, and get_audiobook_chapters. +- Added integration tests for audiobook endpoints. +- Removed `python 2.7` from GitHub Actions CI workflow. Python v2.7 reached end of life support and is no longer supported by Ubuntu 20.04. +- Removed `python 3.6` from GitHub Actions CI workflow. Ubuntu 20.04 is not available in GitHub Actions for `python 3.6`. + ### Changed - Changes the YouTube video link for authentication tutorial (the old video was in low definition, the new one is in high definition) - Updated links to Spotify in documentation diff --git a/spotipy/client.py b/spotipy/client.py index d3b918f0..a026e412 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):(?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|audiobook):(?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)\/(?P[0-9A-Za-z]+)(\?.*)?$' # noqa: E501 + _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_base62 = r'^[0-9A-Za-z]+$' @@ -2033,3 +2033,51 @@ 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 fe58160d..ca2faace 100644 --- a/tests/integration/non_user_endpoints/test.py +++ b/tests/integration/non_user_endpoints/test.py @@ -55,6 +55,16 @@ 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( @@ -455,3 +465,31 @@ 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 a14a28e10c1889cce83eec7a7e1ad4b5944a452d Mon Sep 17 00:00:00 2001 From: chaisupt <72107485+chaisupt@users.noreply.github.com> Date: Fri, 29 Dec 2023 06:50:13 -0800 Subject: [PATCH 24/67] Improve usability on README.md (#983) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update README.md Improve usability of the README.md file * Update CHANGELOG.md change log following README changes * Update CHANGELOG.md * Update README.md Co-authored-by: Hugo van Kemenade * Update README.md Co-authored-by: Hugo van Kemenade * Update README.md Co-authored-by: Hugo van Kemenade * Update README.md Co-authored-by: Hugo van Kemenade * Update README.md • Removed the License Section from the table of contents due to the new GitHub tab UI • Updated link to not specify any specific version * Update CHANGELOG.md --------- Co-authored-by: Hugo van Kemenade --- CHANGELOG.md | 3 ++- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99325b57..a0601ddf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Changes the YouTube video link for authentication tutorial (the old video was in low definition, the new one is in high definition) -- Updated links to Spotify in documentation +- Updated links to Spotify in documentation +- Improve usability on README.md ## [2.23.0] - 2023-04-07 diff --git a/README.md b/README.md index aff9a396..c5f22f94 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,26 @@ # Spotipy -##### A light weight Python library for the Spotify Web API +##### Spotipy is a lightweight Python library for the [Spotify Web API](https://developer.spotify.com/documentation/web-api). With Spotipy you get full access to all of the music data provided by the Spotify platform. ![Tests](https://github.com/plamere/spotipy/workflows/Tests/badge.svg?branch=master) [![Documentation Status](https://readthedocs.org/projects/spotipy/badge/?version=latest)](https://spotipy.readthedocs.io/en/latest/?badge=latest) +## Table of Contents + +- [Features](#features) +- [Documentation](#documentation) +- [Installation](#installation) +- [Quick Start](#quick-start) +- [Reporting Issues](#reporting-issues) +- [Contributing](#contributing) +- [License](#license) + +## Features + +Spotipy supports all of the features of the Spotify Web API including access to all end points, and support for user authorization. For details on the capabilities you are encouraged to review the [Spotify Web API](https://developer.spotify.com/web-api/) documentation. + ## Documentation -Spotipy's full documentation is online at [Spotipy Documentation](http://spotipy.readthedocs.org/). +Spotipy's [full documentation is online](http://spotipy.readthedocs.org/). Some function may need a [specific scope](https://developer.spotify.com/documentation/web-api/concepts/scopes). If you do not define the scope properly `ERROR 401 Unauthorized, permission missing` may occur. ## Installation @@ -30,10 +44,9 @@ pip install spotipy --upgrade A full set of examples can be found in the [online documentation](http://spotipy.readthedocs.org/) and in the [Spotipy examples directory](https://github.com/plamere/spotipy/tree/master/examples). -To get started, install spotipy and create an app on https://developers.spotify.com/. -Add your new ID and SECRET to your environment: +To get started, [install spotipy](#installation), create a new account or log in on https://developers.spotify.com/. Go to the [dashboard](https://developer.spotify.com/dashboard), create an app and add your new ID and SECRET (ID and SECRET can be found on an app setting) to your environment ([step-by-step video](https://www.youtube.com/watch?v=kaBVN8uP358)): -### Without user authentication +### Example without user authentication ```python import spotipy @@ -46,8 +59,20 @@ results = sp.search(q='weezer', limit=20) for idx, track in enumerate(results['tracks']['items']): print(idx, track['name']) ``` +Expected result: +``` +0 Island In The Sun +1 Say It Ain't So +2 Buddy Holly +. +. +. +18 Troublemaker +19 Feels Like Summer +``` + -### With user authentication +### Example with user authentication A redirect URI must be added to your application at [My Dashboard](https://developer.spotify.com/dashboard/applications) to access user authenticated features. @@ -65,6 +90,12 @@ for idx, item in enumerate(results['items']): track = item['track'] print(idx, track['artists'][0]['name'], " – ", track['name']) ``` +Expected result will be the list of music that you liked. For example if you liked Red and Sunflower, the result will be: +``` +0 Post Malone – Sunflower - Spider-Man: Into the Spider-Verse +1 Taylor Swift – Red +``` + ## Reporting Issues @@ -77,3 +108,9 @@ 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 are a developer with Python experience, and you would like to contribute to Spotipy, please be sure to follow the guidelines listed on documentation page + +> #### [Visit the guideline](https://spotipy.readthedocs.io/en/#contribute) From e3629cdacbf92be45fd2bbcea43b53e0b6436e29 Mon Sep 17 00:00:00 2001 From: Colin Wong Date: Sat, 6 Jan 2024 00:20:53 -0600 Subject: [PATCH 25/67] Use unused description parameter in example --- examples/create_playlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/create_playlist.py b/examples/create_playlist.py index b9f38f9f..702c25c6 100644 --- a/examples/create_playlist.py +++ b/examples/create_playlist.py @@ -24,7 +24,7 @@ def main(): scope = "playlist-modify-public" sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope)) user_id = sp.me()['id'] - sp.user_playlist_create(user_id, args.playlist) + sp.user_playlist_create(user_id, args.playlist, description=args.description) if __name__ == '__main__': From d9a5f008ff214ac0a6eab4f19c2922b1282ff27e Mon Sep 17 00:00:00 2001 From: Colin Wong Date: Sat, 6 Jan 2024 00:25:18 -0600 Subject: [PATCH 26/67] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0601ddf..b0fdaad4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated links to Spotify in documentation - Improve usability on README.md +### Fixed +- Fixed unused description parameter in playlist creation example + ## [2.23.0] - 2023-04-07 ### Added From 97c9917a024022023ef574349b408e85e8e12e65 Mon Sep 17 00:00:00 2001 From: Jack Dane Date: Tue, 2 Jan 2024 14:06:18 +0000 Subject: [PATCH 27/67] Use the playlist_items function to retrieve tracks. * It will request the same endpoint as sp.next with the same fields for consistency. --- CHANGELOG.md | 1 + examples/user_playlists_contents.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0601ddf..34c1b41f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Changes the YouTube video link for authentication tutorial (the old video was in low definition, the new one is in high definition) - Updated links to Spotify in documentation - Improve usability on README.md +- Fix `user_playlists_contents` example. ## [2.23.0] - 2023-04-07 diff --git a/examples/user_playlists_contents.py b/examples/user_playlists_contents.py index 13b576f5..9379a0b8 100644 --- a/examples/user_playlists_contents.py +++ b/examples/user_playlists_contents.py @@ -25,8 +25,7 @@ def show_tracks(results): print(playlist['name']) print(' total tracks', playlist['tracks']['total']) - results = sp.playlist(playlist['id'], fields="tracks,next") - tracks = results['tracks'] + tracks = sp.playlist_items(playlist['id'], fields="items,next", additional_types=('tracks', )) show_tracks(tracks) while tracks['next']: From 17893a611539cb23306a4aeeb96b876c90819b98 Mon Sep 17 00:00:00 2001 From: Niko Date: Wed, 15 May 2024 16:14:30 +0200 Subject: [PATCH 28/67] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6d4acec..a4dbc7f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Changes the YouTube video link for authentication tutorial (the old video was in low definition, the new one is in high definition) - Updated links to Spotify in documentation -- Fixed error obfuscation when Spotify class is being inherited and an error is raised in the Child's __init__ +- Fixed error obfuscation when Spotify class is being inherited and an error is raised in the Child's `__init__` ## [2.23.0] - 2023-04-07 From 2e54f2c138e836caf1161e16507fef82ff855fba Mon Sep 17 00:00:00 2001 From: Niko Date: Fri, 17 May 2024 12:35:23 +0200 Subject: [PATCH 29/67] Replaced argument "album_type" with "include_groups" in "Spotify.artist_albums" --- CHANGELOG.md | 1 + spotipy/client.py | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cc825f7..50d812ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix `user_playlists_contents` example. - Updated links to Spotify in documentation - Fixed error obfuscation when Spotify class is being inherited and an error is raised in the Child's `__init__` +- Replaced `artist_albums(album_type=...)` with `artist_albums(include_groups=...)` due to an API change. ### Fixed - Fixed unused description parameter in playlist creation example diff --git a/spotipy/client.py b/spotipy/client.py index 7d81b1f2..1d6de4af 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -405,22 +405,32 @@ def artists(self, artists): return self._get("artists/?ids=" + ",".join(tlist)) def artist_albums( - self, artist_id, album_type=None, country=None, limit=20, offset=0 + self, artist_id, album_type=None, include_groups=None country=None, limit=20, offset=0 ): """ Get Spotify catalog information about an artist's albums Parameters: - artist_id - the artist ID, URI or URL - - album_type - 'album', 'single', 'appears_on', 'compilation' + - include_groups - the types of items to return. One or more of 'album', 'single', + 'appears_on', 'compilation'. If multiple types are desired, + pass in a comma separated string; e.g., 'album,single'. - country - limit the response to one particular country. - limit - the number of albums to return - offset - the index of the first album to return """ + if album_type: + warnings.warn( + "You're using `artist_albums(..., album_type='...')` which will be removed in future versions. " + "Please adjust your code accordingly by using `artist_albums(..., include_groups='...')` instead.", + DeprecationWarning, + ) + include_groups = album_type if include_groups is None else include_groups + trid = self._get_id("artist", artist_id) return self._get( "artists/" + trid + "/albums", - album_type=album_type, + include_groups=include_groups, country=country, limit=limit, offset=offset, From 0a8e7f635ec5e2ea877c7418597d5451a8806f68 Mon Sep 17 00:00:00 2001 From: Niko Date: Fri, 17 May 2024 12:37:54 +0200 Subject: [PATCH 30/67] Added missing comma to artist_albums --- spotipy/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spotipy/client.py b/spotipy/client.py index 1d6de4af..743c1160 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -405,7 +405,7 @@ def artists(self, artists): return self._get("artists/?ids=" + ",".join(tlist)) def artist_albums( - self, artist_id, album_type=None, include_groups=None country=None, limit=20, offset=0 + self, artist_id, album_type=None, include_groups=None, country=None, limit=20, offset=0 ): """ Get Spotify catalog information about an artist's albums From f4c2b90a29a512287a49c16059eedee9b41ca19c Mon Sep 17 00:00:00 2001 From: Niko Date: Fri, 17 May 2024 12:41:57 +0200 Subject: [PATCH 31/67] Made sure that the line column is not over 99. --- spotipy/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spotipy/client.py b/spotipy/client.py index 743c1160..dc71d64b 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -421,8 +421,9 @@ def artist_albums( if album_type: warnings.warn( - "You're using `artist_albums(..., album_type='...')` which will be removed in future versions. " - "Please adjust your code accordingly by using `artist_albums(..., include_groups='...')` instead.", + "You're using `artist_albums(..., album_type='...')` which will be removed in " + "future versions. Please adjust your code accordingly by using " + "`artist_albums(..., include_groups='...')` instead.", DeprecationWarning, ) include_groups = album_type if include_groups is None else include_groups From 52f2b923badd83c3d959bf848cf0d2b070a9dcbe Mon Sep 17 00:00:00 2001 From: Niko Date: Fri, 17 May 2024 12:46:23 +0200 Subject: [PATCH 32/67] Removing whitespace (new IDE, sorry) --- spotipy/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spotipy/client.py b/spotipy/client.py index dc71d64b..03abdece 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -427,7 +427,7 @@ def artist_albums( DeprecationWarning, ) include_groups = album_type if include_groups is None else include_groups - + trid = self._get_id("artist", artist_id) return self._get( "artists/" + trid + "/albums", From 3b5708f5a03b07a51c79dda99f041662f04f0f3a Mon Sep 17 00:00:00 2001 From: Niko Date: Fri, 17 May 2024 14:38:19 +0200 Subject: [PATCH 33/67] Update spotipy/client.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stéphane Bruckert --- spotipy/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spotipy/client.py b/spotipy/client.py index 03abdece..8a4d72fb 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -426,7 +426,7 @@ def artist_albums( "`artist_albums(..., include_groups='...')` instead.", DeprecationWarning, ) - include_groups = album_type if include_groups is None else include_groups + include_groups = include_groups or album_type trid = self._get_id("artist", artist_id) return self._get( From 74330dae6d3d6ad8a5aca7ef6ed47a34c39fb03c Mon Sep 17 00:00:00 2001 From: Dianna <97206862+dianna-SE@users.noreply.github.com> Date: Sat, 18 May 2024 12:46:03 -0700 Subject: [PATCH 34/67] Updated TUTORIAL.md with an additional instructional step and Troubleshooting Section (#1104) * updated TUTORIAL.md with Troubleshooting section and an added step to Prerequisite instructions * Update TUTORIAL.md Editing grammatical syntax and formatting * Update CHANGELOG.md --- CHANGELOG.md | 2 ++ TUTORIAL.md | 29 +++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50d812ba..59c8ee5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added integration tests for audiobook endpoints. - Removed `python 2.7` from GitHub Actions CI workflow. Python v2.7 reached end of life support and is no longer supported by Ubuntu 20.04. - Removed `python 3.6` from GitHub Actions CI workflow. Ubuntu 20.04 is not available in GitHub Actions for `python 3.6`. +- Added extra installation step to TUTORIAL.md for required installation packages. +- Added Troubleshooting Tips section to TUTORIAL.md to address common installation issues. ### Changed - Changes the YouTube video link for authentication tutorial (the old video was in low definition, the new one is in high definition) diff --git a/TUTORIAL.md b/TUTORIAL.md index 9bbe6ea8..d9a0c290 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -15,7 +15,14 @@ If you see a version number, pip is installed and you're ready to proceed. If no Spotipy is written in Python, so you'll need to have the lastest version of Python installed in order to use Spotipy. Check if you already have Python installed with the Terminal command: python --version If you see a version number, Python is already installed. If not, you can download it here: https://www.python.org/downloads/ -**3. experience with basic Linux commands** +**3. spotipy** + +You'll need to install the packages necessary for this project. Run the following command: +``` +pip install spotipy +``` + +**4. experience with basic Linux commands** This tutorial will be easiest if you have some knowledge of how to use Linux commands to create and navigate folders and files on your computer. If you're not sure how to create, edit and delete files and directories from Terminal, learn about basic Linux commands [here](https://ubuntu.com/tutorials/command-line-for-beginners#1-overview) before continuing. @@ -78,4 +85,22 @@ D. Close main.py and return to the directory that contains main.py. You can then E. You may see a window open in your browser asking you to authorize the application. Do so - you will only have to do this once. -F. Return to your terminal - you should see all of Taylor's albums printed out there. \ No newline at end of file +F. Return to your terminal - you should see all of Taylor's albums printed out there. + +## Troubleshooting Tips +A. Command not found running the application "zsh: command not found: python" + +Check which Python version that you have by running the command: +```python --version ``` or ```python3 --version```. + +In most cases, the recent Python version is Python 3. You may need to update Python. Once you have updated Python to the most recent version, run the command: +``` python3 main.py``` + +B. Encountering package error: + +If you are seeing an error "ModuleNotFoundError: No module named 'spotipy'", this means you have not installed the package. This may occur if you followed the installation and setup (up to Step 3, Part D) and attempted to run the app with the missing package. +Run the command: +``` +pip install spotipy +``` +After the package is installed, run the app again. From 958ff6ad2b1c48256a295657f9c887e859d3f1cf Mon Sep 17 00:00:00 2001 From: melissa <122661573+johnmel3@users.noreply.github.com> Date: Tue, 21 May 2024 01:28:38 -0700 Subject: [PATCH 35/67] Reorganized index.rst for improved logical flow (#1054) * reorganized sections without changing any header. Made a few small grammar fixes. * cleaning up examples section. * updating changelog for proposed edits --- CHANGELOG.md | 2 + docs/index.rst | 130 ++++++++++++++++++++++++++----------------------- 2 files changed, 71 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59c8ee5d..bf144e0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removed `python 3.6` from GitHub Actions CI workflow. Ubuntu 20.04 is not available in GitHub Actions for `python 3.6`. - Added extra installation step to TUTORIAL.md for required installation packages. - Added Troubleshooting Tips section to TUTORIAL.md to address common installation issues. +- Added link to Spotipy Tutorial for Beginners under Getting Started. ### Changed - Changes the YouTube video link for authentication tutorial (the old video was in low definition, the new one is in high definition) @@ -24,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated links to Spotify in documentation - Fixed error obfuscation when Spotify class is being inherited and an error is raised in the Child's `__init__` - Replaced `artist_albums(album_type=...)` with `artist_albums(include_groups=...)` due to an API change. +- Restructured the tutorial in `index.rst` to improve logical flow and made some minor edits. ### Fixed - Fixed unused description parameter in playlist creation example diff --git a/docs/index.rst b/docs/index.rst index a6fa61b8..46252f9a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,63 +8,6 @@ Welcome to Spotipy! `_. With *Spotipy* you get full access to all of the music data provided by the Spotify platform. -Assuming you set the ``SPOTIPY_CLIENT_ID`` and ``SPOTIPY_CLIENT_SECRET`` -environment variables (here is a `video `_ explaining how to do so). For a longer tutorial with examples included, refer to this `video playlist `_. Below is a quick example of using *Spotipy* to list the -names of all the albums released by the artist 'Birdy':: - - import spotipy - from spotipy.oauth2 import SpotifyClientCredentials - - birdy_uri = 'spotify:artist:2WX2uTcsvV5OnS0inACecP' - spotify = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials()) - - results = spotify.artist_albums(birdy_uri, album_type='album') - albums = results['items'] - while results['next']: - results = spotify.next(results) - albums.extend(results['items']) - - for album in albums: - print(album['name']) - -Here's another example showing how to get 30 second samples and cover art -for the top 10 tracks for Led Zeppelin:: - - import spotipy - from spotipy.oauth2 import SpotifyClientCredentials - - lz_uri = 'spotify:artist:36QJpDe2go2KgaRleHCDTp' - - spotify = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials()) - results = spotify.artist_top_tracks(lz_uri) - - for track in results['tracks'][:10]: - print('track : ' + track['name']) - print('audio : ' + track['preview_url']) - print('cover art: ' + track['album']['images'][0]['url']) - print() - -Finally, here's an example that will get the URL for an artist image given the -artist's name:: - - import spotipy - import sys - from spotipy.oauth2 import SpotifyClientCredentials - - spotify = spotipy.Spotify(auth_manager=SpotifyClientCredentials()) - - if len(sys.argv) > 1: - name = ' '.join(sys.argv[1:]) - else: - name = 'Radiohead' - - results = spotify.search(q='artist:' + name, type='artist') - items = results['artists']['items'] - if len(items) > 0: - artist = items[0] - print(artist['name'], artist['images'][0]['url']) - - Features ======== @@ -80,7 +23,8 @@ Install or upgrade *Spotipy* with:: pip install spotipy --upgrade -Or you can get the source from github at https://github.com/plamere/spotipy +You can also obtain the source code from the `Spotify GitHub repository `_. + Getting Started =============== @@ -90,20 +34,28 @@ All methods require user authorization. You will need to register your app at to get the credentials necessary to make authorized calls (a *client id* and *client secret*). + + *Spotipy* supports two authorization flows: - - The **Authorization Code flow** This method is suitable for long-running applications + - **Authorization Code flow** This method is suitable for long-running applications which the user logs into once. It provides an access token that can be refreshed. .. note:: Requires you to add a redirect URI to your application at `My Dashboard `_. See `Redirect URI`_ for more details. - - The **Client Credentials flow** The method makes it possible + - **Client Credentials flow** This method makes it possible to authenticate your requests to the Spotify Web API and to obtain a higher rate limit than you would with the Authorization Code flow. +For guidance on setting your app credentials watch this `video tutorial `_ or follow the +`Spotipy Tutorial for Beginners `_. + +For a longer tutorial with examples included, refer to this `video playlist `_. + + Authorization Code Flow ======================= @@ -139,6 +91,7 @@ on Windows):: export SPOTIPY_CLIENT_SECRET='your-spotify-client-secret' export SPOTIPY_REDIRECT_URI='your-app-redirect-url' + Scopes ------ @@ -242,9 +195,64 @@ Feel free to contribute new cache handlers to the repo. Examples ======================= + +Here is an example of using *Spotipy* to list the +names of all the albums released by the artist 'Birdy':: + + import spotipy + from spotipy.oauth2 import SpotifyClientCredentials + + birdy_uri = 'spotify:artist:2WX2uTcsvV5OnS0inACecP' + spotify = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials()) + + results = spotify.artist_albums(birdy_uri, album_type='album') + albums = results['items'] + while results['next']: + results = spotify.next(results) + albums.extend(results['items']) + + for album in albums: + print(album['name']) + +Here's another example showing how to get 30 second samples and cover art +for the top 10 tracks for Led Zeppelin:: + + import spotipy + from spotipy.oauth2 import SpotifyClientCredentials + + lz_uri = 'spotify:artist:36QJpDe2go2KgaRleHCDTp' + + spotify = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials()) + results = spotify.artist_top_tracks(lz_uri) + + for track in results['tracks'][:10]: + print('track : ' + track['name']) + print('audio : ' + track['preview_url']) + print('cover art: ' + track['album']['images'][0]['url']) + print() + +Finally, here's an example that will get the URL for an artist image given the +artist's name:: + + import spotipy + import sys + from spotipy.oauth2 import SpotifyClientCredentials + + spotify = spotipy.Spotify(auth_manager=SpotifyClientCredentials()) + + if len(sys.argv) > 1: + name = ' '.join(sys.argv[1:]) + else: + name = 'Radiohead' + + results = spotify.search(q='artist:' + name, type='artist') + items = results['artists']['items'] + if len(items) > 0: + artist = items[0] + print(artist['name'], artist['images'][0]['url']) There are many more examples of how to use *Spotipy* in the `Examples -Directory `_ on Github +Directory `_ on GitHub. API Reference ============== From 85c9d74dc1a75d4c1837f1b4bf167ee595ee8f99 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 21 May 2024 12:32:01 -0400 Subject: [PATCH 36/67] Drop support for EOL Python 3.7 (#1065) * Add python_requires to help pip * Update supported versions in tox.ini * Upgrade Python syntax with pyupgrade --py37-plus * Bump GitHub Actions * Add Python 3.11 and 3.12 to CI * Remove six dependency * Remove redundant dependencies * Remove redudant Python 3.5 code * Drop support for EOL Python 3.7 * Upgrade Python syntax with pyupgrade --py38-plus * Update CHANGELOG * More f-strings --------- Co-authored-by: Hugo van Kemenade --- .github/workflows/integration_tests.yml | 4 +- .github/workflows/publish.yml | 6 +- .github/workflows/pull_request.yml | 4 +- .github/workflows/pythonapp.yml | 6 +- CHANGELOG.md | 4 ++ docs/conf.py | 1 - docs/index.rst | 2 +- examples/audio_analysis_for_track.py | 3 +- examples/audio_features.py | 4 +- examples/audio_features_for_track.py | 5 +- examples/contains_a_saved_track.py | 2 +- examples/delete_a_saved_track.py | 2 +- .../remove_specific_tracks_from_playlist.py | 3 +- examples/remove_tracks_from_playlist.py | 2 +- examples/replace_tracks_in_playlist.py | 2 +- examples/show_album.py | 1 - examples/show_related.py | 1 - examples/simple_artist_albums.py | 2 +- examples/simple_artist_top_tracks.py | 1 - examples/title_chain.py | 2 +- setup.py | 5 +- spotipy/cache_handler.py | 4 +- spotipy/client.py | 65 +++++++++---------- spotipy/exceptions.py | 2 +- spotipy/oauth2.py | 60 ++++++----------- spotipy/util.py | 2 - tests/integration/non_user_endpoints/test.py | 2 - tests/unit/test_oauth.py | 9 +-- tox.ini | 4 +- 29 files changed, 85 insertions(+), 125 deletions(-) diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index c5450c21..f6b6eac9 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -11,9 +11,9 @@ jobs: SPOTIPY_CLIENT_SECRET: ${{ secrets.SPOTIPY_CLIENT_SECRET }} PYTHON_VERSION: "3.10" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install dependencies diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2c1fe1ff..fe9b7fa3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,9 +13,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.x" - name: Install pypa/build @@ -33,7 +33,7 @@ jobs: --outdir dist/ . - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "2.x" - name: Install pypa/build diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index d7871ef3..32f92a6f 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -8,8 +8,8 @@ jobs: changelog: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - uses: dangoslen/changelog-enforcer@v1.1.1 + - uses: actions/checkout@v4 + - uses: dangoslen/changelog-enforcer@v3.5.1 with: changeLogPath: 'CHANGELOG.md' skipLabel: 'skip-changelog' \ No newline at end of file diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index 7c83ab32..5f2ffc8f 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -8,11 +8,11 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/CHANGELOG.md b/CHANGELOG.md index bf144e0d..8362463b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed unused description parameter in playlist creation example +### Changed +- Drop support for EOL Python 3.7. + + ## [2.23.0] - 2023-04-07 ### Added diff --git a/docs/conf.py b/docs/conf.py index 3da5998b..5f74164e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # spotipy documentation build configuration file, created by # sphinx-quickstart on Thu Aug 21 11:04:39 2014. diff --git a/docs/index.rst b/docs/index.rst index 46252f9a..ef6fd2d6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -333,7 +333,7 @@ Export the needed Environment variables::: export SPOTIPY_REDIRECT_URI=http://localhost:8080 # Make url is set in app you created to get your ID and SECRET Create virtual environment, install dependencies, run tests::: - $ virtualenv --python=python3.7 env + $ virtualenv --python=python3.12 env (env) $ pip install --user -e . (env) $ python -m unittest discover -v tests diff --git a/examples/audio_analysis_for_track.py b/examples/audio_analysis_for_track.py index 1f728a5a..1bef5e9f 100644 --- a/examples/audio_analysis_for_track.py +++ b/examples/audio_analysis_for_track.py @@ -1,6 +1,5 @@ # shows audio analysis for the given track -from __future__ import print_function # (at top of module) from spotipy.oauth2 import SpotifyClientCredentials import json import spotipy @@ -20,4 +19,4 @@ analysis = sp.audio_analysis(tid) delta = time.time() - start print(json.dumps(analysis, indent=4)) -print("analysis retrieved in %.2f seconds" % (delta,)) +print(f"analysis retrieved in {delta:.2f} seconds") diff --git a/examples/audio_features.py b/examples/audio_features.py index 30caddb6..4657a972 100644 --- a/examples/audio_features.py +++ b/examples/audio_features.py @@ -1,7 +1,5 @@ - # shows acoustic features for tracks for the given artist -from __future__ import print_function # (at top of module) from spotipy.oauth2 import SpotifyClientCredentials import json import spotipy @@ -33,4 +31,4 @@ analysis = sp._get(feature['analysis_url']) print(json.dumps(analysis, indent=4)) print() -print("features retrieved in %.2f seconds" % (delta,)) +print(f"features retrieved in {delta:.2f} seconds") diff --git a/examples/audio_features_for_track.py b/examples/audio_features_for_track.py index 9e156d3f..e345ca6d 100644 --- a/examples/audio_features_for_track.py +++ b/examples/audio_features_for_track.py @@ -1,8 +1,5 @@ - - # shows acoustic features for tracks for the given artist -from __future__ import print_function # (at top of module) from spotipy.oauth2 import SpotifyClientCredentials import json import spotipy @@ -22,4 +19,4 @@ features = sp.audio_features(tids) delta = time.time() - start print(json.dumps(features, indent=4)) - print("features retrieved in %.2f seconds" % (delta,)) + print(f"features retrieved in {delta:.2f} seconds") diff --git a/examples/contains_a_saved_track.py b/examples/contains_a_saved_track.py index 41da4fd4..fb6175dc 100644 --- a/examples/contains_a_saved_track.py +++ b/examples/contains_a_saved_track.py @@ -11,7 +11,7 @@ if len(sys.argv) > 1: tid = sys.argv[1] else: - print("Usage: %s track-id ..." % (sys.argv[0],)) + print(f"Usage: {sys.argv[0]} track-id ...") sys.exit() sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope)) diff --git a/examples/delete_a_saved_track.py b/examples/delete_a_saved_track.py index 39525496..2f461531 100644 --- a/examples/delete_a_saved_track.py +++ b/examples/delete_a_saved_track.py @@ -11,7 +11,7 @@ if len(sys.argv) > 1: tid = sys.argv[1] else: - print("Usage: %s track-id ..." % (sys.argv[0],)) + print(f"Usage: {sys.argv[0]} track-id ...") sys.exit() sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope)) diff --git a/examples/remove_specific_tracks_from_playlist.py b/examples/remove_specific_tracks_from_playlist.py index 963eaefc..340f3795 100644 --- a/examples/remove_specific_tracks_from_playlist.py +++ b/examples/remove_specific_tracks_from_playlist.py @@ -15,8 +15,7 @@ track_ids.append({"uri": tid, "positions": [int(pos)]}) else: print( - "Usage: %s playlist_id track_id,pos track_id,pos ..." % - (sys.argv[0],)) + f"Usage: {sys.argv[0]} playlist_id track_id,pos track_id,pos ...") sys.exit() scope = 'playlist-modify-public' diff --git a/examples/remove_tracks_from_playlist.py b/examples/remove_tracks_from_playlist.py index 8a51c569..4e011eb3 100644 --- a/examples/remove_tracks_from_playlist.py +++ b/examples/remove_tracks_from_playlist.py @@ -10,7 +10,7 @@ playlist_id = sys.argv[2] track_ids = sys.argv[3:] else: - print("Usage: %s playlist_id track_id ..." % (sys.argv[0])) + print(f"Usage: {sys.argv[0]} playlist_id track_id ...") sys.exit() scope = 'playlist-modify-public' diff --git a/examples/replace_tracks_in_playlist.py b/examples/replace_tracks_in_playlist.py index 6d1c46fd..6c76b056 100644 --- a/examples/replace_tracks_in_playlist.py +++ b/examples/replace_tracks_in_playlist.py @@ -10,7 +10,7 @@ playlist_id = sys.argv[1] track_ids = sys.argv[2:] else: - print("Usage: %s playlist_id track_id ..." % (sys.argv[0],)) + print(f"Usage: {sys.argv[0]} playlist_id track_id ...") sys.exit() scope = 'playlist-modify-public' diff --git a/examples/show_album.py b/examples/show_album.py index 248e3055..8f5e617a 100644 --- a/examples/show_album.py +++ b/examples/show_album.py @@ -1,4 +1,3 @@ - # shows album info for a URN or URL from spotipy.oauth2 import SpotifyClientCredentials diff --git a/examples/show_related.py b/examples/show_related.py index 6fed03fd..87914045 100644 --- a/examples/show_related.py +++ b/examples/show_related.py @@ -1,4 +1,3 @@ - # shows related artists for the given seed artist from spotipy.oauth2 import SpotifyClientCredentials diff --git a/examples/simple_artist_albums.py b/examples/simple_artist_albums.py index c4cc5621..3f8323db 100644 --- a/examples/simple_artist_albums.py +++ b/examples/simple_artist_albums.py @@ -13,4 +13,4 @@ albums.extend(results['items']) for album in albums: - print((album['name'])) + print(album['name']) diff --git a/examples/simple_artist_top_tracks.py b/examples/simple_artist_top_tracks.py index 1a207b2a..caf5fed4 100644 --- a/examples/simple_artist_top_tracks.py +++ b/examples/simple_artist_top_tracks.py @@ -1,4 +1,3 @@ - from spotipy.oauth2 import SpotifyClientCredentials import spotipy diff --git a/examples/title_chain.py b/examples/title_chain.py index f3bc321c..6cf3b8e7 100644 --- a/examples/title_chain.py +++ b/examples/title_chain.py @@ -13,7 +13,7 @@ sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) -skiplist = set(['dm', 'remix']) +skiplist = {'dm', 'remix'} max_offset = 500 seen = set() diff --git a/setup.py b/setup.py index dd1ab177..069511a6 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -with open("README.md", "r") as f: +with open("README.md") as f: long_description = f.read() test_reqs = [ @@ -28,11 +28,10 @@ project_urls={ 'Source': 'https://github.com/plamere/spotipy', }, + python_requires='>3.8', install_requires=[ "redis>=3.5.3", - "redis<4.0.0;python_version<'3.4'", "requests>=2.25.0", - "six>=1.15.0", "urllib3>=1.26.0" ], tests_require=test_reqs, diff --git a/spotipy/cache_handler.py b/spotipy/cache_handler.py index 9a6d703b..0ab98e93 100644 --- a/spotipy/cache_handler.py +++ b/spotipy/cache_handler.py @@ -80,7 +80,7 @@ def get_cached_token(self): f.close() token_info = json.loads(token_info_string) - except IOError as error: + except OSError as error: if error.errno == errno.ENOENT: logger.debug("cache does not exist at: %s", self.cache_path) else: @@ -93,7 +93,7 @@ def save_token_to_cache(self, token_info): f = open(self.cache_path, "w") f.write(json.dumps(token_info, cls=self.encoder_cls)) f.close() - except IOError: + except OSError: logger.warning('Couldn\'t write token to cache at: %s', self.cache_path) diff --git a/spotipy/client.py b/spotipy/client.py index 8a4d72fb..00850e58 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """ A simple and thin Python library for the Spotify Web API """ __all__ = ["Spotify", "SpotifyException"] @@ -10,7 +8,6 @@ import warnings import requests -import six import urllib3 from spotipy.exceptions import SpotifyException @@ -20,7 +17,7 @@ logger = logging.getLogger(__name__) -class Spotify(object): +class Spotify: """ Example usage:: @@ -234,14 +231,14 @@ def _build_session(self): def _auth_headers(self): if self._auth: - return {"Authorization": "Bearer {0}".format(self._auth)} + return {"Authorization": f"Bearer {self._auth}"} if not self.auth_manager: return {} try: token = self.auth_manager.get_access_token(as_dict=False) except TypeError: token = self.auth_manager.get_access_token() - return {"Authorization": "Bearer {0}".format(token)} + return {"Authorization": f"Bearer {token}"} def _internal_call(self, method, url, payload, params): args = dict(params=params) @@ -296,7 +293,7 @@ def _internal_call(self, method, url, payload, params): raise SpotifyException( response.status_code, -1, - "%s:\n %s" % (response.url, msg), + f"{response.url}:\n {msg}", reason=reason, headers=response.headers, ) @@ -310,7 +307,7 @@ def _internal_call(self, method, url, payload, params): raise SpotifyException( 429, -1, - "%s:\n %s" % (request.path_url, "Max Retries"), + f"{request.path_url}:\n Max Retries", reason=reason ) except ValueError: @@ -663,7 +660,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), @@ -719,7 +716,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, @@ -734,7 +731,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 @@ -746,7 +743,7 @@ def playlist_upload_cover_image(self, playlist_id, image_b64): """ 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", ) @@ -765,7 +762,7 @@ def user_playlist(self, user, playlist_id=None, fields=None, market=None): - fields - which fields to return """ if playlist_id is None: - return self._get("users/%s/starred" % user) + return self._get(f"users/{user}/starred") return self.playlist(playlist_id, fields=fields, market=market) def user_playlist_tracks( @@ -809,7 +806,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=""): @@ -829,7 +826,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, @@ -1004,7 +1001,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): @@ -1061,16 +1058,16 @@ def playlist_change_details( """ data = {} - if isinstance(name, six.string_types): + if isinstance(name, str): data["name"] = name if isinstance(public, bool): data["public"] = public if isinstance(collaborative, bool): data["collaborative"] = collaborative - if isinstance(description, six.string_types): + if isinstance(description, str): 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): @@ -1081,7 +1078,7 @@ 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( @@ -1097,7 +1094,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, ) @@ -1113,7 +1110,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( @@ -1144,7 +1141,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( @@ -1165,7 +1162,7 @@ def playlist_remove_all_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 playlist_remove_specific_occurrences_of_items( @@ -1196,7 +1193,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): @@ -1208,7 +1205,7 @@ def current_user_follow_playlist(self, playlist_id): """ return self._put( - "playlists/{}/followers".format(playlist_id) + f"playlists/{playlist_id}/followers" ) def playlist_is_following( @@ -1874,7 +1871,7 @@ def seek_track(self, position_ms, device_id=None): return return self._put( self._append_device_id( - "me/player/seek?position_ms=%s" % position_ms, device_id + f"me/player/seek?position_ms={position_ms}", device_id ) ) @@ -1890,7 +1887,7 @@ def repeat(self, state, device_id=None): return self._put( self._append_device_id( - "me/player/repeat?state=%s" % state, device_id + f"me/player/repeat?state={state}", device_id ) ) @@ -1909,7 +1906,7 @@ def volume(self, volume_percent, device_id=None): return self._put( self._append_device_id( - "me/player/volume?volume_percent=%s" % volume_percent, + f"me/player/volume?volume_percent={volume_percent}", device_id, ) ) @@ -1927,7 +1924,7 @@ def shuffle(self, state, device_id=None): state = str(state).lower() self._put( self._append_device_id( - "me/player/shuffle?state=%s" % state, device_id + f"me/player/shuffle?state={state}", device_id ) ) @@ -1952,10 +1949,10 @@ def add_to_queue(self, uri, device_id=None): uri = self._get_uri("track", uri) - endpoint = "me/player/queue?uri=%s" % uri + endpoint = f"me/player/queue?uri={uri}" if device_id is not None: - endpoint += "&device_id=%s" % device_id + endpoint += f"&device_id={device_id}" return self._post(endpoint) @@ -1974,9 +1971,9 @@ def _append_device_id(self, path, device_id): """ if device_id: if "?" in path: - path += "&device_id=%s" % device_id + path += f"&device_id={device_id}" else: - path += "?device_id=%s" % device_id + path += f"?device_id={device_id}" return path def _get_id(self, type, id): diff --git a/spotipy/exceptions.py b/spotipy/exceptions.py index df503f10..28b91419 100644 --- a/spotipy/exceptions.py +++ b/spotipy/exceptions.py @@ -12,5 +12,5 @@ def __init__(self, http_status, code, msg, reason=None, headers=None): self.headers = headers def __str__(self): - return 'http status: {0}, code:{1} - {2}, reason: {3}'.format( + return 'http status: {}, code:{} - {}, reason: {}'.format( self.http_status, self.code, self.msg, self.reason) diff --git a/spotipy/oauth2.py b/spotipy/oauth2.py index 125c87c9..126f861b 100644 --- a/spotipy/oauth2.py +++ b/spotipy/oauth2.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - __all__ = [ "SpotifyClientCredentials", "SpotifyOAuth", @@ -17,11 +15,9 @@ import webbrowser import requests -# Workaround to support both python 2 & 3 -import six -import six.moves.urllib.parse as urllibparse -from six.moves.BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer -from six.moves.urllib_parse import parse_qsl, urlparse +import urllib.parse as urllibparse +from http.server import BaseHTTPRequestHandler, HTTPServer +from urllib.parse import parse_qsl, urlparse from spotipy.cache_handler import CacheFileHandler, CacheHandler from spotipy.util import CLIENT_CREDS_ENV_VARS, get_host_port, normalize_scope @@ -36,7 +32,7 @@ def __init__(self, message, error=None, error_description=None, *args, **kwargs) self.error = error self.error_description = error_description self.__dict__.update(kwargs) - super(SpotifyOauthError, self).__init__(message, *args, **kwargs) + super().__init__(message, *args, **kwargs) class SpotifyStateError(SpotifyOauthError): @@ -54,24 +50,21 @@ def __init__(self, local_state=None, remote_state=None, message=None, def _make_authorization_headers(client_id, client_secret): auth_header = base64.b64encode( - six.text_type(client_id + ":" + client_secret).encode("ascii") + str(client_id + ":" + client_secret).encode("ascii") ) - return {"Authorization": "Basic %s" % auth_header.decode("ascii")} + return {"Authorization": f"Basic {auth_header.decode('ascii')}"} def _ensure_value(value, env_key): env_val = CLIENT_CREDS_ENV_VARS[env_key] _val = value or os.getenv(env_val) if _val is None: - msg = "No %s. Pass it or set a %s environment variable." % ( - env_key, - env_val, - ) + msg = f"No {env_key}. Pass it or set a {env_val} environment variable." raise SpotifyOauthError(msg) return _val -class SpotifyAuthBase(object): +class SpotifyAuthBase: def __init__(self, requests_session): if isinstance(requests_session, requests.Session): self._session = requests_session @@ -144,9 +137,7 @@ def _handle_oauth_error(self, http_error): error_description = None raise SpotifyOauthError( - 'error: {0}, error_description: {1}'.format( - error, error_description - ), + f'error: {error}, error_description: {error_description}', error=error, error_description=error_description ) @@ -196,7 +187,7 @@ def __init__( """ - super(SpotifyClientCredentials, self).__init__(requests_session) + super().__init__(requests_session) self.client_id = client_id self.client_secret = client_secret @@ -327,7 +318,7 @@ def __init__( (takes precedence over `cache_path` and `username`) """ - super(SpotifyOAuth, self).__init__(requests_session) + super().__init__(requests_session) self.client_id = client_id self.client_secret = client_secret @@ -402,7 +393,7 @@ def get_authorize_url(self, state=None): urlparams = urllibparse.urlencode(payload) - return "%s?%s" % (self.OAUTH_AUTHORIZE_URL, urlparams) + return f"{self.OAUTH_AUTHORIZE_URL}?{urlparams}" def parse_response_code(self, url): """ Parse the response code in the given response url @@ -421,8 +412,7 @@ def parse_auth_response_url(url): query_s = urlparse(url).query form = dict(parse_qsl(query_s)) if "error" in form: - raise SpotifyOauthError("Received error from auth server: " - "{}".format(form["error"]), + raise SpotifyOauthError(f"Received error from auth server: {form['error']}", error=form["error"]) return tuple(form.get(param) for param in ["state", "code"]) @@ -677,7 +667,7 @@ def __init__(self, (takes precedence over `cache_path` and `username`) """ - super(SpotifyPKCE, self).__init__(requests_session) + super().__init__(requests_session) self.client_id = client_id self.redirect_uri = redirect_uri self.state = state @@ -727,15 +717,8 @@ def _get_code_verifier(self): length = random.randint(33, 96) # The seeded length generates between a 44 and 128 base64 characters encoded string - try: - import secrets - verifier = secrets.token_urlsafe(length) - except ImportError: # For python 3.5 support - import base64 - import os - rand_bytes = os.urandom(length) - verifier = base64.urlsafe_b64encode(rand_bytes).decode('utf-8').replace('=', '') - return verifier + import secrets + return secrets.token_urlsafe(length) def _get_code_challenge(self): """ Spotify PCKE code challenge - See step 1 of the reference guide below @@ -766,7 +749,7 @@ def get_authorize_url(self, state=None): if state is not None: payload["state"] = state urlparams = urllibparse.urlencode(payload) - return "%s?%s" % (self.OAUTH_AUTHORIZE_URL, urlparams) + return f"{self.OAUTH_AUTHORIZE_URL}?{urlparams}" def _open_auth_url(self, state=None): auth_url = self.get_authorize_url(state) @@ -817,7 +800,7 @@ def _get_auth_response_local_server(self, redirect_port): if server.auth_code is not None: return server.auth_code elif server.error is not None: - raise SpotifyOauthError("Received error from OAuth server: {}".format(server.error)) + raise SpotifyOauthError(f"Received error from OAuth server: {server.error}") else: raise SpotifyOauthError("Server listening on localhost has not been accessed") @@ -1160,7 +1143,7 @@ def get_authorize_url(self, state=None): urlparams = urllibparse.urlencode(payload) - return "%s?%s" % (self.OAUTH_AUTHORIZE_URL, urlparams) + return f"{self.OAUTH_AUTHORIZE_URL}?{urlparams}" def parse_response_token(self, url, state=None): """ Parse the response code in the given response url """ @@ -1180,8 +1163,7 @@ def parse_auth_response_url(url): form = dict(i.split('=') for i in (fragment_s or query_s or url).split('&')) if "error" in form: - raise SpotifyOauthError("Received error from auth server: " - "{}".format(form["error"]), + raise SpotifyOauthError(f"Received error from auth server: {form['error']}", state=form["state"]) if "expires_in" in form: form["expires_in"] = int(form["expires_in"]) @@ -1273,7 +1255,7 @@ def do_GET(self): if self.server.auth_code: status = "successful" elif self.server.error: - status = "failed ({})".format(self.server.error) + status = f"failed ({self.server.error})" else: self._write("

Invalid request

") return diff --git a/spotipy/util.py b/spotipy/util.py index b949a618..7e586734 100644 --- a/spotipy/util.py +++ b/spotipy/util.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """ Shows a user's playlists (need to be authenticated via oauth) """ __all__ = ["CLIENT_CREDS_ENV_VARS", "prompt_for_user_token"] diff --git a/tests/integration/non_user_endpoints/test.py b/tests/integration/non_user_endpoints/test.py index ca2faace..583cc3d5 100644 --- a/tests/integration/non_user_endpoints/test.py +++ b/tests/integration/non_user_endpoints/test.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from spotipy import ( Spotify, SpotifyClientCredentials, diff --git a/tests/unit/test_oauth.py b/tests/unit/test_oauth.py index fa58a162..10e1062b 100644 --- a/tests/unit/test_oauth.py +++ b/tests/unit/test_oauth.py @@ -1,20 +1,15 @@ -# -*- coding: utf-8 -*- import io import json import unittest -import six.moves.urllib.parse as urllibparse +import unittest.mock as mock +import urllib.parse as urllibparse from spotipy import SpotifyOAuth, SpotifyImplicitGrant, SpotifyPKCE from spotipy.cache_handler import MemoryCacheHandler from spotipy.oauth2 import SpotifyClientCredentials, SpotifyOauthError from spotipy.oauth2 import SpotifyStateError -try: - import unittest.mock as mock -except ImportError: - import mock - patch = mock.patch DEFAULT = mock.DEFAULT diff --git a/tox.ini b/tox.ini index b0f5bff0..bbf780fa 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,8 @@ [tox] -envlist = py27,py34 +envlist = py3{8,9,10,11,12} [testenv] deps= requests - six - py27: mock commands=python -m unittest discover -v tests [flake8] max-line-length = 99 From b109ca722cc93869b2d2088349dee4e1af2fdf99 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 21 May 2024 17:01:02 -0400 Subject: [PATCH 37/67] Bump GitHub Actions (#1113) Co-authored-by: Niko --- .github/workflows/pull_request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 32f92a6f..3c31d606 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: dangoslen/changelog-enforcer@v3.5.1 + - uses: dangoslen/changelog-enforcer@v3.6.1 with: changeLogPath: 'CHANGELOG.md' - skipLabel: 'skip-changelog' \ No newline at end of file + skipLabel: 'skip-changelog' From 939b7557a5ae01a42c368e4819abef0b7c0e344e Mon Sep 17 00:00:00 2001 From: Niko Date: Wed, 22 May 2024 12:43:04 +0200 Subject: [PATCH 38/67] Updated _regex_spotify_url to ignore /intl- in Spotify links (#1100) * Updated _regex_spotify_url to ignore /intl- in Spotify links * Updated documentation link and added some additional information --- CHANGELOG.md | 1 + spotipy/client.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8362463b..b0cfbc93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed error obfuscation when Spotify class is being inherited and an error is raised in the Child's `__init__` - Replaced `artist_albums(album_type=...)` with `artist_albums(include_groups=...)` due to an API change. - Restructured the tutorial in `index.rst` to improve logical flow and made some minor edits. +- Updated _regex_spotify_url to ignore `/intl-` in Spotify links ### Fixed - Fixed unused description parameter in playlist creation example diff --git a/spotipy/client.py b/spotipy/client.py index 00850e58..610c79c9 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -107,15 +107,19 @@ class Spotify: # numbers and even older ones seemed to have been allowed to freely pick this name. # # [1] https://www.iana.org/assignments/uri-schemes/prov/spotify - # [2] https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids + # [2] https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-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 # 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. + # Spotify recently added "/intl-" to their links. This change is undocumented. + # There is an assumption that the country code uses the ISO 3166-1 alpha-2 standard [2], + # but this has not been confirmed yet. Spotipy has no use for this, so it gets ignored. # - # [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 + # [1] https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids + # [2] https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 + _regex_spotify_url = r'^(http[s]?:\/\/)?open.spotify.com\/(intl-\w\w\/)?(?Ptrack|artist|album|playlist|show|episode|user|audiobook)\/(?P[0-9A-Za-z]+)(\?.*)?$' # noqa: E501 _regex_base62 = r'^[0-9A-Za-z]+$' From e153dabe7c90c4777ff39054dbc897b19fb1a13d Mon Sep 17 00:00:00 2001 From: jackattack825 Date: Wed, 22 May 2024 22:40:08 -0700 Subject: [PATCH 39/67] Update index.rst (#1087) spelling issue --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index ef6fd2d6..3b734527 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -293,7 +293,7 @@ You can ask questions about Spotipy on Stack Overflow. Don’t forget to add t http://stackoverflow.com/questions/ask If you think you've found a bug, let us know at -`Spotify Issues `_ +`Spotipy Issues `_ Contribute From 677841b417740a84cffb03d469fd9aa1aafbc382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bruckert?= Date: Mon, 27 May 2024 13:01:01 +0100 Subject: [PATCH 40/67] Create .readthedocs.yaml (#1120) * Create .readthedocs.yaml * Update CHANGELOG.md --- .readthedocs.yaml | 12 ++++++++++++ CHANGELOG.md | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..134784f5 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,12 @@ +# Read the Docs configuration file for Sphinx projects +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.12" + +sphinx: + configuration: docs/conf.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b0cfbc93..bf7b9e6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removed `python 3.6` from GitHub Actions CI workflow. Ubuntu 20.04 is not available in GitHub Actions for `python 3.6`. - Added extra installation step to TUTORIAL.md for required installation packages. - Added Troubleshooting Tips section to TUTORIAL.md to address common installation issues. -- Added link to Spotipy Tutorial for Beginners under Getting Started. +- Added link to Spotipy Tutorial for Beginners under Getting Started. +- Readthedocs config ### Changed - Changes the YouTube video link for authentication tutorial (the old video was in low definition, the new one is in high definition) From cc24b4c22fc8d430a66f08ce45be5f0718e12806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bruckert?= Date: Mon, 27 May 2024 13:06:21 +0100 Subject: [PATCH 41/67] Fix RTD build --- docs/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 5f74164e..79096330 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,7 +10,6 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import spotipy import sys import os From 2d1cb99be7289ce86d0ca4a971d3c3a7e6ed4d54 Mon Sep 17 00:00:00 2001 From: Stephane Bruckert Date: Tue, 28 May 2024 08:36:22 +0100 Subject: [PATCH 42/67] Use sphinx_rtd_theme --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 79096330..7b64c0b6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -92,7 +92,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the From 1ce8c4f06b3f29c4cf38536fce3c197262279599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bruckert?= Date: Tue, 28 May 2024 09:08:23 +0100 Subject: [PATCH 43/67] RTD theme fix (#1122) * pip sphinx-rtd-theme * Bump sphinx * Add extension * Add docs/requirements.txt * CL --- .readthedocs.yaml | 4 ++++ CHANGELOG.md | 2 +- docs/conf.py | 5 ++++- docs/requirements.txt | 2 ++ setup.py | 5 ----- 5 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 docs/requirements.txt diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 134784f5..be0738ca 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -10,3 +10,7 @@ build: sphinx: configuration: docs/conf.py + +python: + install: + - requirements: docs/requirements.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index bf7b9e6c..e48ee516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added extra installation step to TUTORIAL.md for required installation packages. - Added Troubleshooting Tips section to TUTORIAL.md to address common installation issues. - Added link to Spotipy Tutorial for Beginners under Getting Started. -- Readthedocs config ### Changed - Changes the YouTube video link for authentication tutorial (the old video was in low definition, the new one is in high definition) @@ -31,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed unused description parameter in playlist creation example +- Readthedocs ### Changed - Drop support for EOL Python 3.7. diff --git a/docs/conf.py b/docs/conf.py index 7b64c0b6..0f781410 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,7 +26,10 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc'] +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx_rtd_theme' +] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..609f7acd --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +Sphinx~=7.3.7 +sphinx-rtd-theme~=2.0.0 diff --git a/setup.py b/setup.py index 069511a6..c7d17525 100644 --- a/setup.py +++ b/setup.py @@ -7,12 +7,7 @@ 'mock==2.0.0' ] -doc_reqs = [ - 'Sphinx>=1.5.2' -] - extra_reqs = { - 'doc': doc_reqs, 'test': test_reqs } From 913ae572759f2e626442721c784ce36d7666bf53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bruckert?= Date: Wed, 29 May 2024 08:49:27 +0100 Subject: [PATCH 44/67] Fix docs badge Latest was removed on RTD and is equivalent to master --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c5f22f94..c6b62702 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ##### Spotipy is a lightweight Python library for the [Spotify Web API](https://developer.spotify.com/documentation/web-api). With Spotipy you get full access to all of the music data provided by the Spotify platform. -![Tests](https://github.com/plamere/spotipy/workflows/Tests/badge.svg?branch=master) [![Documentation Status](https://readthedocs.org/projects/spotipy/badge/?version=latest)](https://spotipy.readthedocs.io/en/latest/?badge=latest) +![Tests](https://github.com/plamere/spotipy/workflows/Tests/badge.svg?branch=master) [![Documentation Status](https://readthedocs.org/projects/spotipy/badge/?version=master)](https://spotipy.readthedocs.io/en/latest/?badge=master) ## Table of Contents From 62a27a20e0cc8d4fb015ab262ddafe70916c51ed Mon Sep 17 00:00:00 2001 From: andrewcara <64450522+andrewcara@users.noreply.github.com> Date: Thu, 30 May 2024 14:01:16 -0400 Subject: [PATCH 45/67] Added MemcacheCacheHandler (#1042) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added MemcacheCacheHandler * Import MemcacheError where used * Update index.rst --------- Co-authored-by: Stéphane Bruckert --- CHANGELOG.md | 2 +- docs/index.rst | 3 +++ setup.py | 9 +++++++-- spotipy/cache_handler.py | 34 +++++++++++++++++++++++++++++++++- 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e48ee516..0d48e49f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added - +- Added `MemcacheCacheHandler`, a cache handler that stores the token info using pymemcache. - Added support for audiobook endpoints: get_audiobook, get_audiobooks, and get_audiobook_chapters. - Added integration tests for audiobook endpoints. - Removed `python 2.7` from GitHub Actions CI workflow. Python v2.7 reached end of life support and is no longer supported by Ubuntu 20.04. diff --git a/docs/index.rst b/docs/index.rst index 3b734527..3534bfac 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -185,14 +185,17 @@ cache handler ``CacheHandler``. The default cache handler ``CacheFileHandler`` i An instance of that new class can then be passed as a parameter when creating ``SpotifyOAuth``, ``SpotifyPKCE`` or ``SpotifyImplicitGrant``. The following handlers are available and defined in the URL above. + - ``CacheFileHandler`` - ``MemoryCacheHandler`` - ``DjangoSessionCacheHandler`` - ``FlaskSessionCacheHandler`` - ``RedisCacheHandler`` + - ``MemcacheCacheHandler``: install with dependency using ``pip install "spotipy[pymemcache]"`` Feel free to contribute new cache handlers to the repo. + Examples ======================= diff --git a/setup.py b/setup.py index c7d17525..e1ad46e1 100644 --- a/setup.py +++ b/setup.py @@ -7,8 +7,13 @@ 'mock==2.0.0' ] +memcache_cache_reqs = [ + 'pymemcache>=3.5.2' +] + extra_reqs = { - 'test': test_reqs + 'test': test_reqs, + 'memcache': memcache_cache_reqs } setup( @@ -25,7 +30,7 @@ }, python_requires='>3.8', install_requires=[ - "redis>=3.5.3", + "redis>=3.5.3", # TODO: Move to extras_require in v3 "requests>=2.25.0", "urllib3>=1.26.0" ], diff --git a/spotipy/cache_handler.py b/spotipy/cache_handler.py index 0ab98e93..7ae94a23 100644 --- a/spotipy/cache_handler.py +++ b/spotipy/cache_handler.py @@ -4,7 +4,8 @@ 'DjangoSessionCacheHandler', 'FlaskSessionCacheHandler', 'MemoryCacheHandler', - 'RedisCacheHandler'] + 'RedisCacheHandler', + 'MemcacheCacheHandler'] import errno import json @@ -208,3 +209,34 @@ def save_token_to_cache(self, token_info): self.redis.set(self.key, json.dumps(token_info)) except RedisError as e: logger.warning('Error saving token to cache: ' + str(e)) + + +class MemcacheCacheHandler(CacheHandler): + """A Cache handler that stores the token info in Memcache using the pymemcache client + """ + def __init__(self, memcache, key=None) -> None: + """ + Parameters: + * memcache: memcache client object provided by pymemcache + (https://pymemcache.readthedocs.io/en/latest/getting_started.html) + * key: May be supplied, will otherwise be generated + (takes precedence over `token_info`) + """ + self.memcache = memcache + self.key = key if key else 'token_info' + + def get_cached_token(self): + from pymemcache import MemcacheError + try: + token_info = self.memcache.get(self.key) + if token_info: + return json.loads(token_info.decode()) + except MemcacheError as e: + logger.warning('Error getting token from cache' + str(e)) + + def save_token_to_cache(self, token_info): + from pymemcache import MemcacheError + try: + self.memcache.set(self.key, json.dumps(token_info)) + except MemcacheError as e: + logger.warning('Error saving token to cache' + str(e)) From df27fe93bfa2e26f298461b0b1e280d5da321474 Mon Sep 17 00:00:00 2001 From: Shukie Li <59763257+lishukie6588@users.noreply.github.com> Date: Fri, 31 May 2024 01:29:32 +0700 Subject: [PATCH 46/67] Combined assertion tests, fixes #1051 (#1052) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * addressed issue 936 * publish branch * 12/9/2023 * Update CHANGELOG.md --------- Co-authored-by: Stéphane Bruckert --- CHANGELOG.md | 3 +++ tests/integration/user_endpoints/test.py | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d48e49f..8c5d0649 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Drop support for EOL Python 3.7. +### Fixed +- Seperated the test_current_user_save_and_usave_tracks unit test into test_current_user_save_tracks and test_current_user_unsave_tracks in the user endpoint test suite to improve unit test clarity and effectiveness for their respective user endpoints methods (current_user_saved_tracks_add, current_user_saved_tracks). + ## [2.23.0] - 2023-04-07 ### Added diff --git a/tests/integration/user_endpoints/test.py b/tests/integration/user_endpoints/test.py index d2568499..93955e73 100644 --- a/tests/integration/user_endpoints/test.py +++ b/tests/integration/user_endpoints/test.py @@ -253,7 +253,7 @@ def test_current_user_saved_tracks(self): tracks = self.spotify.current_user_saved_tracks() self.assertGreaterEqual(len(tracks['items']), 0) - def test_current_user_save_and_unsave_tracks(self): + def test_current_user_save_tracks(self): tracks = self.spotify.current_user_saved_tracks() total = tracks['total'] self.spotify.current_user_saved_tracks_add(self.four_tracks) @@ -266,6 +266,19 @@ def test_current_user_save_and_unsave_tracks(self): self.four_tracks) tracks = self.spotify.current_user_saved_tracks() new_total = tracks['total'] + + def test_current_user_unsave_tracks(self): + tracks = self.spotify.current_user_saved_tracks() + total = tracks['total'] + self.spotify.current_user_saved_tracks_add(self.four_tracks) + + tracks = self.spotify.current_user_saved_tracks() + new_total = tracks['total'] + + self.spotify.current_user_saved_tracks_delete( + self.four_tracks) + tracks = self.spotify.current_user_saved_tracks() + new_total = tracks['total'] self.assertEqual(new_total, total) def test_current_user_saved_albums(self): From 126da62dd0646a3cc685c9a3d3dd1eabde588117 Mon Sep 17 00:00:00 2001 From: Niko Date: Thu, 30 May 2024 20:33:17 +0200 Subject: [PATCH 47/67] Fix API Reference (#1126) * add base directory to PATH * Check if build completes if redis is included --- docs/conf.py | 3 +++ docs/requirements.txt | 1 + 2 files changed, 4 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 0f781410..df657248 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,6 +18,9 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath("..")) + +import spotipy # -- General configuration ----------------------------------------------------- diff --git a/docs/requirements.txt b/docs/requirements.txt index 609f7acd..339adc95 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,3 @@ Sphinx~=7.3.7 sphinx-rtd-theme~=2.0.0 +redis>=3.5.3 From a810edf5da17820a38daad61c8a5568a76de41f6 Mon Sep 17 00:00:00 2001 From: wouldube <107458582+wouldube@users.noreply.github.com> Date: Thu, 30 May 2024 11:37:39 -0700 Subject: [PATCH 48/67] Corrections to Grammar Errors and Typos in Documentation (#1017) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed grammar errors and typos. * Update CHANGELOG.md to mention documentation typo corrections. --------- Co-authored-by: Stéphane Bruckert --- CHANGELOG.md | 28 ++++++++++++---------------- TUTORIAL.md | 12 ++++++------ examples/app.py | 2 +- spotipy/client.py | 6 +++--- spotipy/oauth2.py | 8 ++++---- 5 files changed, 26 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c5d0649..ca9b0bf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,16 +27,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Replaced `artist_albums(album_type=...)` with `artist_albums(include_groups=...)` due to an API change. - Restructured the tutorial in `index.rst` to improve logical flow and made some minor edits. - Updated _regex_spotify_url to ignore `/intl-` in Spotify links +- Drop support for EOL Python 3.7. ### Fixed - Fixed unused description parameter in playlist creation example - Readthedocs - -### Changed -- Drop support for EOL Python 3.7. - - -### Fixed +- Corrected various grammar errors and typos in the documentation. - Seperated the test_current_user_save_and_usave_tracks unit test into test_current_user_save_tracks and test_current_user_unsave_tracks in the user endpoint test suite to improve unit test clarity and effectiveness for their respective user endpoints methods (current_user_saved_tracks_add, current_user_saved_tracks). ## [2.23.0] - 2023-04-07 @@ -81,7 +77,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Incorrect `category_id` input for test_category - Assertion value for `test_categories_limit_low` and `test_categories_limit_high` -- Pin Github Actions Runner to Ubuntu 20 for Py27 +- Pin GitHub Actions Runner to Ubuntu 20 for Py27 - Fixed potential error where `found` variable in `test_artist_related_artists` is undefined if for loop never evaluates to true - Fixed false positive test `test_new_releases` which looks up the wrong property of the JSON response object and always evaluates to true @@ -106,13 +102,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added `RedisCacheHandler`, a cache handler that stores the token info in Redis. -- Changed URI handling in `client.Spotify._get_id()` to remove qureies if provided by error. +- Changed URI handling in `client.Spotify._get_id()` to remove queries if provided by error. - Added a new parameter to `RedisCacheHandler` to allow custom keys (instead of the default `token_info` key) - Simplify check for existing token in `RedisCacheHandler` ### Changed -- Removed Python 3.5 and added Python 3.9 in Github Action +- Removed Python 3.5 and added Python 3.9 in GitHub Action ## [2.19.0] - 2021-08-12 @@ -125,7 +121,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed a bug in `CacheFileHandler.__init__`: The documentation says that the username will be retrieved from the environment, but it wasn't. -- Fixed a bug in the initializers for the auth managers that produced a spurious warning message if you provide a cache handler and you set a value for the "SPOTIPY_CLIENT_USERNAME" environment variable. +- Fixed a bug in the initializers for the auth managers that produced a spurious warning message if you provide a cache handler, and you set a value for the "SPOTIPY_CLIENT_USERNAME" environment variable. - Use generated MIT license and fix license type in `pip show` ## [2.18.0] - 2021-04-13 @@ -166,7 +162,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The docs for the `auth` parameter of `Spotify.init` use the term "access token" instead of "authorization token" - Changed docs for `search` to mention that you can provide multiple types to search for - The query parameters of requests are now logged -- Deprecate specifing `cache_path` or `username` directly to `SpotifyOAuth`, `SpotifyPKCE`, and `SpotifyImplicitGrant` constructors, instead directing users to use the `CacheFileHandler` cache handler +- Deprecate specifying `cache_path` or `username` directly to `SpotifyOAuth`, `SpotifyPKCE`, and `SpotifyImplicitGrant` constructors, instead directing users to use the `CacheFileHandler` cache handler - Removed requirement for examples/app.py to specify port multiple times (only SPOTIPY_REDIRECT_URI needs to contain the port) ### Added @@ -267,7 +263,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 authorization/authentication web api errors details. - Added `SpotifyStateError` subclass of `SpotifyOauthError` - Allow extending `SpotifyClientCredentials` and `SpotifyOAuth` -- Added the market paramter to `album_tracks` +- Added the market parameter to `album_tracks` ### Deprecated @@ -318,7 +314,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - retries - status_retries - backoff_factor -- Spin up a local webserver to auto-fill authentication URL +- Spin up a local webserver to autofill authentication URL - Use session in SpotifyAuthBase - Logging used instead of print statements @@ -421,7 +417,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for `current_user_saved_albums_contains` - Support for `user_unfollow_artists` - Support for `user_unfollow_users` -- Lint with flake8 using Github action +- Lint with flake8 using GitHub action ### Changed @@ -473,7 +469,7 @@ Fixed bug in auto retry logic ## [2.3.3] - 2015-04-01 -Aadded client credential flow +Added client credential flow ## [2.3.2] - 2015-03-31 @@ -517,7 +513,7 @@ Support for "Your Music" tracks (add, delete, get), with examples ## [1.45.0] - 2014-07-07 -Support for related artists endpoint. Don't use cache auth codes when scope changes +Support for related artists' endpoint. Don't use cache auth codes when scope changes ## [1.44.0] - 2014-07-03 diff --git a/TUTORIAL.md b/TUTORIAL.md index d9a0c290..4ac94bd4 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -7,12 +7,12 @@ In order to complete this tutorial successfully, there are a few things that you **1. pip package manager** You can check to see if you have pip installed by opening up Terminal and typing the following command: pip --version -If you see a version number, pip is installed and you're ready to proceed. If not, instructions for downloading the latest version of pip can be found here: https://pip.pypa.io/en/stable/cli/pip_download/ +If you see a version number, pip is installed, and you're ready to proceed. If not, instructions for downloading the latest version of pip can be found here: https://pip.pypa.io/en/stable/cli/pip_download/ **2. python3** -Spotipy is written in Python, so you'll need to have the lastest version of Python installed in order to use Spotipy. Check if you already have Python installed with the Terminal command: python --version +Spotipy is written in Python, so you'll need to have the latest version of Python installed in order to use Spotipy. Check if you already have Python installed with the Terminal command: python --version If you see a version number, Python is already installed. If not, you can download it here: https://www.python.org/downloads/ **3. spotipy** @@ -37,13 +37,13 @@ B. Click the "Create an App" button. Enter any name and description you'd like f C. In your new app's Overview screen, click the "Edit Settings" button and scroll down to "Redirect URIs." Add "http://localhost:1234" (or any other port number of your choosing). Hit the "Save" button at the bottom of the Settings panel to return to you App Overview screen. -D. Underneath your app name and description on the lefthand side, you'll see a "Show Client Secret" link. Click that link to reveal your Client Secret, then copy both your Client Secret and your Client ID somewhere on your computer. You'll need to access them later. +D. Underneath your app name and description on the left-hand side, you'll see a "Show Client Secret" link. Click that link to reveal your Client Secret, then copy both your Client Secret and your Client ID somewhere on your computer. You'll need to access them later. ## Step 2. Installation and Setup A. Create a folder somewhere on your computer where you'd like to store the code for your Spotipy app. You can create a folder in terminal with this command: mkdir folder_name -B. In that folder, create a Python file named main.py. You can create the file directly from Terminal using a built in text editor like Vim, which comes preinstalled on Linux operating systems. To create the file with Vim, ensure that you are in your new directory, then run: vim main.py +B. In that folder, create a Python file named main.py. You can create the file directly from Terminal using a built-in text editor like Vim, which comes preinstalled on Linux operating systems. To create the file with Vim, ensure that you are in your new directory, then run: vim main.py C. Paste the following code into your main.py file: ``` @@ -61,11 +61,11 @@ D. Replace YOUR_APP_CLIENT_ID and YOUR_APP_CLIENT_SECRET with the values you cop After completing steps 1 and 2, your app is fully configured and ready to fetch data from the Spotify API. All that's left is to tell the API what data we're looking for, and we do that by adding some additional code to main.py. The code that follows is just an example - once you get it working, you should feel free to modify it in order to get different results. -For now, let's assume that we want to print the names of all of the albums on Spotify by Taylor Swift: +For now, let's assume that we want to print the names of all the albums on Spotify by Taylor Swift: A. First, we need to find Taylor Swift's Spotify URI (Uniform Resource Indicator). Every entity (artist, album, song, etc.) has a URI that can identify it. To find Taylor's URI, navigate to [her page on Spotify](https://open.spotify.com/artist/06HL4z0CvFAxyc27GXpf02) and look at the URI in your browser. Everything there that follows the last backslash in the URL path is Taylor's URI, in this case: 06HL4z0CvFAxyc27GXpf02 -B. Add the URI as a variable in main.py. Notice the prefix added the the URI: +B. Add the URI as a variable in main.py. Notice the prefix added the URI: ``` taylor_uri = 'spotify:artist:06HL4z0CvFAxyc27GXpf02' ``` diff --git a/examples/app.py b/examples/app.py index 635a132e..821f3fb6 100644 --- a/examples/app.py +++ b/examples/app.py @@ -11,7 +11,7 @@ OPTIONAL // in development environment for debug output export FLASK_ENV=development - // so that you can invoke the app outside of the file's directory include + // so that you can invoke the app outside the file's directory include export FLASK_APP=/path/to/spotipy/examples/app.py // on Windows, use `SET` instead of `export` diff --git a/spotipy/client.py b/spotipy/client.py index 610c79c9..50e1e199 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -100,7 +100,7 @@ class Spotify: # # Unfortunately the IANA specification is out of date and doesn't include the new types # show and episode. Additionally, for the user URI, it does not specify which characters - # are valid for usernames, so the assumption is alphanumeric which coincidentially are also + # are valid for usernames, so the assumption is alphanumeric which coincidentally are also # the same ones base-62 uses. # In limited manual exploration this seems to hold true, as newly accounts are assigned an # identifier that looks like the base-62 of all other IDs, but some older accounts only have @@ -1751,7 +1751,7 @@ def audio_features(self, tracks=[]): tlist = [self._get_id("track", t) for t in tracks] results = self._get("audio-features/?ids=" + ",".join(tlist)) # the response has changed, look for the new style first, and if - # its not there, fallback on the old style + # it's not there, fallback on the old style if "audio_features" in results: return results["audio_features"] else: @@ -1939,7 +1939,7 @@ def queue(self): def add_to_queue(self, uri, device_id=None): """ Adds a song to the end of a user's queue - If device A is currently playing music and you try to add to the queue + If device A is currently playing music, and you try to add to the queue and pass in the id for device B, you will get a 'Player command failed: Restriction violated' error I therefore recommend leaving device_id as None so that the active device is targeted diff --git a/spotipy/oauth2.py b/spotipy/oauth2.py index 126f861b..4c0cd906 100644 --- a/spotipy/oauth2.py +++ b/spotipy/oauth2.py @@ -41,7 +41,7 @@ class SpotifyStateError(SpotifyOauthError): def __init__(self, local_state=None, remote_state=None, message=None, error=None, error_description=None, *args, **kwargs): if not message: - message = ("Expected " + local_state + " but recieved " + message = ("Expected " + local_state + " but received " + remote_state) super(SpotifyOauthError, self).__init__(message, error, error_description, *args, @@ -310,7 +310,7 @@ def __init__( * requests_session: A Requests session * requests_timeout: Optional, tell Requests to stop waiting for a response after a given number of seconds - * open_browser: Optional, whether or not the web browser should be opened to + * open_browser: Optional, whether the web browser should be opened to authorize a user * cache_handler: An instance of the `CacheHandler` class to handle getting and saving cached authorization tokens. @@ -659,7 +659,7 @@ def __init__(self, * requests_timeout: Optional, tell Requests to stop waiting for a response after a given number of seconds * requests_session: A Requests session - * open_browser: Optional, whether or not the web browser should be opened to + * open_browser: Optional, whether the web browser should be opened to authorize a user * cache_handler: An instance of the `CacheHandler` class to handle getting and saving cached authorization tokens. @@ -995,7 +995,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase): Authentication Code flow. Use the SpotifyPKCE auth manager instead of SpotifyImplicitGrant. - SpotifyPKCE contains all of the functionality of + SpotifyPKCE contains all the functionality of SpotifyImplicitGrant, plus automatic response retrieval and refreshable tokens. Only a few replacements need to be made: From 22e3b7406d1865ee23ff6bb19986c351e310c2e3 Mon Sep 17 00:00:00 2001 From: Nick <24370162+deltaclock@users.noreply.github.com> Date: Thu, 30 May 2024 21:43:10 +0300 Subject: [PATCH 49/67] Update current_user_follow_playlist (#1040) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update current_user_follow_playlist The API specifies a `public` parameter as seen [here](https://developer.spotify.com/documentation/web-api/reference/follow-playlist) * Update CHANGELOG.md --------- Co-authored-by: Stéphane Bruckert --- CHANGELOG.md | 1 + spotipy/client.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca9b0bf9..bf308464 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added extra installation step to TUTORIAL.md for required installation packages. - Added Troubleshooting Tips section to TUTORIAL.md to address common installation issues. - Added link to Spotipy Tutorial for Beginners under Getting Started. +- Added `update` field to `current_user_follow_playlist`. ### Changed - Changes the YouTube video link for authentication tutorial (the old video was in low definition, the new one is in high definition) diff --git a/spotipy/client.py b/spotipy/client.py index 50e1e199..d83053ca 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -1200,7 +1200,7 @@ def playlist_remove_specific_occurrences_of_items( f"playlists/{plid}/tracks", payload=payload ) - def current_user_follow_playlist(self, playlist_id): + def current_user_follow_playlist(self, playlist_id, public=True): """ Add the current authenticated user as a follower of a playlist. @@ -1209,7 +1209,8 @@ def current_user_follow_playlist(self, playlist_id): """ return self._put( - f"playlists/{playlist_id}/followers" + f"playlists/{playlist_id}/followers", + payload={"public": public} ) def playlist_is_following( From 5fa234316273681dd9eb1fb928575c2879a816bb Mon Sep 17 00:00:00 2001 From: Allie <73832771+mountainchaser@users.noreply.github.com> Date: Thu, 30 May 2024 15:14:15 -0400 Subject: [PATCH 50/67] TUTORIAL.md updates (#1009) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update TUTORIAL.md Added instructions for installing Spotipy. * Update TUTORIAL.md Updated Spotify app creation steps. --------- Co-authored-by: Stéphane Bruckert --- TUTORIAL.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index 4ac94bd4..54d49b29 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -33,19 +33,21 @@ Spotipy relies on the Spotify API. In order to use the Spotify API, you'll need A. Visit the [Spotify developer portal](https://developer.spotify.com/dashboard/). If you already have a Spotify account, click "Log in" and enter your username and password. Otherwise, click "Sign up" and follow the steps to create an account. After you've signed in or signed up, you should be redirected to your developer dashboard. -B. Click the "Create an App" button. Enter any name and description you'd like for your new app. Accept the terms of service and click "Create." +B. Click the "Create an App" button. Enter any name and description you'd like for your new app. Add "http://localhost:1234" (or any other port number of your choosing) as your "Redirect URI". Accept the terms of service and click "Create." -C. In your new app's Overview screen, click the "Edit Settings" button and scroll down to "Redirect URIs." Add "http://localhost:1234" (or any other port number of your choosing). Hit the "Save" button at the bottom of the Settings panel to return to you App Overview screen. +C. In your new app's Overview screen, click the "Settings" button and then under the "Basic Information" tab click "View client secret", then copy both your Client Secret and your Client ID somewhere on your computer. You'll need to access them later. D. Underneath your app name and description on the left-hand side, you'll see a "Show Client Secret" link. Click that link to reveal your Client Secret, then copy both your Client Secret and your Client ID somewhere on your computer. You'll need to access them later. ## Step 2. Installation and Setup -A. Create a folder somewhere on your computer where you'd like to store the code for your Spotipy app. You can create a folder in terminal with this command: mkdir folder_name +A. Create a folder somewhere on your computer where you'd like to store the code for your Spotipy app. You can create a folder in terminal with this command: ```mkdir folder_name``` -B. In that folder, create a Python file named main.py. You can create the file directly from Terminal using a built-in text editor like Vim, which comes preinstalled on Linux operating systems. To create the file with Vim, ensure that you are in your new directory, then run: vim main.py +B. Install the Spotipy library. You can do this by using this command in the terminal: ```pip install spotipy``` -C. Paste the following code into your main.py file: +C. In that folder, create a Python file named main.py. You can create the file directly from Terminal using a built in text editor like Vim, which comes preinstalled on Linux operating systems. To create the file with Vim, ensure that you are in your new directory, then run: vim main.py + +D. Paste the following code into your main.py file: ``` import spotipy from spotipy.oauth2 import SpotifyOAuth @@ -55,7 +57,7 @@ sp = spotipy.Spotify(auth_manager=SpotifyOAuth(client_id="YOUR_APP_CLIENT_ID", redirect_uri="YOUR_APP_REDIRECT_URI", scope="user-library-read")) ``` -D. Replace YOUR_APP_CLIENT_ID and YOUR_APP_CLIENT_SECRET with the values you copied and saved in step 1D. Replace YOUR_APP_REDIRECT_URI with the URI you set in step 1C. +E. Replace YOUR_APP_CLIENT_ID and YOUR_APP_CLIENT_SECRET with the values you copied and saved in step 1D. Replace YOUR_APP_REDIRECT_URI with the URI you set in step 1C. ## Step 3. Start Using Spotipy From c0343b8b80028dc4e46543ac0e0939f8108e316e Mon Sep 17 00:00:00 2001 From: Stephane Bruckert Date: Thu, 30 May 2024 22:03:18 +0100 Subject: [PATCH 51/67] Bump to 2.24.0 --- CHANGELOG.md | 38 ++++++++++++++++++++------------------ CONTRIBUTING.md | 2 +- setup.py | 2 +- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf308464..b6080297 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,35 +6,37 @@ 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. + +### Added +- + +### Fixed +- + +### Removed +- + +## [2.24.0] - 2024-05-30 ### Added - Added `MemcacheCacheHandler`, a cache handler that stores the token info using pymemcache. -- Added support for audiobook endpoints: get_audiobook, get_audiobooks, and get_audiobook_chapters. +- Added support for audiobook endpoints: `get_audiobook`, `get_audiobooks`, and `get_audiobook_chapters`. - Added integration tests for audiobook endpoints. -- Removed `python 2.7` from GitHub Actions CI workflow. Python v2.7 reached end of life support and is no longer supported by Ubuntu 20.04. -- Removed `python 3.6` from GitHub Actions CI workflow. Ubuntu 20.04 is not available in GitHub Actions for `python 3.6`. -- Added extra installation step to TUTORIAL.md for required installation packages. -- Added Troubleshooting Tips section to TUTORIAL.md to address common installation issues. -- Added link to Spotipy Tutorial for Beginners under Getting Started. - Added `update` field to `current_user_follow_playlist`. ### Changed -- Changes the YouTube video link for authentication tutorial (the old video was in low definition, the new one is in high definition) -- Updated links to Spotify in documentation -- Improve usability on README.md -- Fix `user_playlists_contents` example. -- Updated links to Spotify in documentation - Fixed error obfuscation when Spotify class is being inherited and an error is raised in the Child's `__init__` - Replaced `artist_albums(album_type=...)` with `artist_albums(include_groups=...)` due to an API change. -- Restructured the tutorial in `index.rst` to improve logical flow and made some minor edits. -- Updated _regex_spotify_url to ignore `/intl-` in Spotify links -- Drop support for EOL Python 3.7. +- Updated `_regex_spotify_url` to ignore `/intl-` in Spotify links +- Improved README, docs and examples ### Fixed -- Fixed unused description parameter in playlist creation example -- Readthedocs -- Corrected various grammar errors and typos in the documentation. -- Seperated the test_current_user_save_and_usave_tracks unit test into test_current_user_save_tracks and test_current_user_unsave_tracks in the user endpoint test suite to improve unit test clarity and effectiveness for their respective user endpoints methods (current_user_saved_tracks_add, current_user_saved_tracks). +- Readthedocs build +- Split `test_current_user_save_and_usave_tracks` unit test + +### Removed +- Drop support for EOL Python 3.7 ## [2.23.0] - 2023-04-07 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3dd2c276..aa99374d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,9 +55,9 @@ Don't forget to add a short description of your change in the [CHANGELOG](CHANGE - Add to changelog: ## Unreleased + Add your changes below. ### Added - - Replace with changes ### Fixed diff --git a/setup.py b/setup.py index e1ad46e1..b99c4b89 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ setup( name='spotipy', - version='2.23.0', + version='2.24.0', description='A light weight Python library for the Spotify Web API', long_description=long_description, long_description_content_type="text/markdown", From 8a40e038b9cf0daac3c12cffa1b17d04e098038b Mon Sep 17 00:00:00 2001 From: Stephane Bruckert Date: Thu, 30 May 2024 22:06:13 +0100 Subject: [PATCH 52/67] Remove python2.7 from pypi build --- .github/workflows/publish.yml | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fe9b7fa3..94ecb5ef 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -32,26 +32,8 @@ jobs: --wheel --outdir dist/ . - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "2.x" - - name: Install pypa/build - run: >- - python -m - pip install - build - --user - - name: Build a binary wheel and a source tarball - run: >- - python -m - build - --sdist - --wheel - --outdir dist/ - . - name: Publish distribution 📦 to PyPI if: startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@release/v1 with: - password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file + password: ${{ secrets.PYPI_API_TOKEN }} From eee7ae7d0de94bba39787823015ac8005015b1ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Fri, 31 May 2024 09:08:24 +0200 Subject: [PATCH 53/67] Remove obsolete `mock` dependency (#1127) The package uses `unittest.mock` only, and that one is part of the Python standard library. --- CHANGELOG.md | 2 +- setup.py | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6080297..6d67ad09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ Add your changes below. - ### Removed -- +- `mock` no longer listed as a test dependency. Only built-in `unittest.mock` is actually used. ## [2.24.0] - 2024-05-30 diff --git a/setup.py b/setup.py index b99c4b89..be5a982c 100644 --- a/setup.py +++ b/setup.py @@ -3,16 +3,11 @@ with open("README.md") as f: long_description = f.read() -test_reqs = [ - 'mock==2.0.0' -] - memcache_cache_reqs = [ 'pymemcache>=3.5.2' ] extra_reqs = { - 'test': test_reqs, 'memcache': memcache_cache_reqs } @@ -34,7 +29,6 @@ "requests>=2.25.0", "urllib3>=1.26.0" ], - tests_require=test_reqs, extras_require=extra_reqs, license='MIT', packages=['spotipy']) From 9a8b8a7f0270d8e4a360c70bc180eaa9ed4c252b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bruckert?= Date: Fri, 31 May 2024 08:10:05 +0100 Subject: [PATCH 54/67] Update README.md --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index c6b62702..6ecf44f8 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,6 @@ Spotipy supports all of the features of the Spotify Web API including access to all end points, and support for user authorization. For details on the capabilities you are encouraged to review the [Spotify Web API](https://developer.spotify.com/web-api/) documentation. -## Documentation - -Spotipy's [full documentation is online](http://spotipy.readthedocs.org/). Some function may need a [specific scope](https://developer.spotify.com/documentation/web-api/concepts/scopes). If you do not define the scope properly `ERROR 401 Unauthorized, permission missing` may occur. - ## Installation ```bash From bb36290b8eebbeddc90b1aba5c544853f9ec2c5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bruckert?= Date: Fri, 31 May 2024 08:11:58 +0100 Subject: [PATCH 55/67] Fix TOC in readme --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 6ecf44f8..3af3c4cc 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,10 @@ ## Table of Contents - [Features](#features) -- [Documentation](#documentation) - [Installation](#installation) - [Quick Start](#quick-start) - [Reporting Issues](#reporting-issues) - [Contributing](#contributing) -- [License](#license) ## Features From cb36133ca9f90e9c5d12f8ce50f5ba78adb76745 Mon Sep 17 00:00:00 2001 From: Stephane Bruckert Date: Fri, 31 May 2024 17:42:28 +0100 Subject: [PATCH 56/67] Comment out test failing with payment_required --- tests/integration/non_user_endpoints/test.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/integration/non_user_endpoints/test.py b/tests/integration/non_user_endpoints/test.py index 583cc3d5..5767ba2f 100644 --- a/tests/integration/non_user_endpoints/test.py +++ b/tests/integration/non_user_endpoints/test.py @@ -355,11 +355,11 @@ def test_album_search(self): self.assertTrue(results['albums']['items'][0] ['name'].find('Pinkerton') >= 0) - def test_track_search(self): - results = self.spotify.search(q='el scorcho weezer', type='track') - self.assertTrue('tracks' in results) - self.assertTrue(len(results['tracks']['items']) > 0) - self.assertTrue(results['tracks']['items'][0]['name'] == 'El Scorcho') + # def test_track_search(self): + # results = self.spotify.search(q='el scorcho weezer', type='track') + # self.assertTrue('tracks' in results) + # self.assertTrue(len(results['tracks']['items']) > 0) + # self.assertTrue(results['tracks']['items'][0]['name'] == 'El Scorcho') def test_user(self): user = self.spotify.user(user='plamere') From f94a89a995d755e7fd8a665994c5f6bb6d2f2d2f Mon Sep 17 00:00:00 2001 From: Brandon Parrott <107897092+bparrott3@users.noreply.github.com> Date: Fri, 31 May 2024 09:46:19 -0700 Subject: [PATCH 57/67] Add unit tests for queue functions (#1059) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update test.py Add tests for queue manipulation/retrieval functions * Update CHANGELOG.md * flake8 --------- Co-authored-by: Stéphane Bruckert --- CHANGELOG.md | 2 +- tests/integration/user_endpoints/test.py | 41 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d67ad09..3d498943 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Add your changes below. ### Added -- +- Added unit tests for queue functions ### Fixed - diff --git a/tests/integration/user_endpoints/test.py b/tests/integration/user_endpoints/test.py index 93955e73..fb1fbb21 100644 --- a/tests/integration/user_endpoints/test.py +++ b/tests/integration/user_endpoints/test.py @@ -558,3 +558,44 @@ def test_current_user(self): c_user = self.spotify.current_user() user = self.spotify.user(c_user['id']) self.assertEqual(c_user['display_name'], user['display_name']) + + +class SpotifyQueueApiTests(unittest.TestCase): + + @classmethod + def setUp(self): + self.spotify = Spotify(auth="test_token") + + def test_get_queue(self, mock_get): + # Mock the response from _get + mock_get.return_value = {'songs': ['song1', 'song2']} + + # Call the queue function + response = self.spotify.queue() + + # Check if the correct endpoint is called + mock_get.assert_called_with("me/player/queue") + + # Check if the response is as expected + self.assertEqual(response, {'songs': ['song1', 'song2']}) + + def test_add_to_queue(self, mock_post): + test_uri = 'spotify:track:123' + + # Call the add_to_queue function + self.spotify.add_to_queue(test_uri) + + # Check if the correct endpoint is called + endpoint = "me/player/queue?uri=%s" % test_uri + mock_post.assert_called_with(endpoint) + + def test_add_to_queue_with_device_id(self, mock_post): + test_uri = 'spotify:track:123' + device_id = 'device123' + + # Call the add_to_queue function with a device_id + self.spotify.add_to_queue(test_uri, device_id=device_id) + + # Check if the correct endpoint is called + endpoint = "me/player/queue?uri=%s&device_id=%s" % (test_uri, device_id) + mock_post.assert_called_with(endpoint) From c5a0943016e7de0d073624995b1897dbe9e168a0 Mon Sep 17 00:00:00 2001 From: Stephane Bruckert Date: Sat, 1 Jun 2024 19:34:44 +0100 Subject: [PATCH 58/67] Uncomment temporarily failing test --- tests/integration/non_user_endpoints/test.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/integration/non_user_endpoints/test.py b/tests/integration/non_user_endpoints/test.py index 5767ba2f..e4f222a9 100644 --- a/tests/integration/non_user_endpoints/test.py +++ b/tests/integration/non_user_endpoints/test.py @@ -355,11 +355,11 @@ def test_album_search(self): self.assertTrue(results['albums']['items'][0] ['name'].find('Pinkerton') >= 0) - # def test_track_search(self): - # results = self.spotify.search(q='el scorcho weezer', type='track') - # self.assertTrue('tracks' in results) - # self.assertTrue(len(results['tracks']['items']) > 0) - # self.assertTrue(results['tracks']['items'][0]['name'] == 'El Scorcho') + def test_track_search(self): + results = self.spotify.search(q='el scorcho weezer', type='track') + self.assertTrue('tracks' in results) + self.assertTrue(len(results['tracks']['items']) > 0) + self.assertTrue(results['tracks']['items'][0]['name'] == 'El Scorcho') def test_user(self): user = self.spotify.user(user='plamere') @@ -466,7 +466,6 @@ def test_available_markets(self): 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') From 8f003147f7649e957223373b2da4deae2bcdec36 Mon Sep 17 00:00:00 2001 From: Dianna <97206862+dianna-SE@users.noreply.github.com> Date: Tue, 18 Jun 2024 01:30:04 -0700 Subject: [PATCH 59/67] Added and revised function docstrings for util.py (#1130) * Added and revised function docstrings for util.py * added credentials to gitignore * Addressed request changes by reverting/modifying function docstrings to match the format used in client.py --- CHANGELOG.md | 1 + spotipy/util.py | 41 ++++++++++++++++++++++++++--------------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d498943..7924ecf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Add your changes below. ### Added - Added unit tests for queue functions +- Added detailed function docstrings to 'util.py', including descriptions and special sections that lists arguments, returns, and raises. ### Fixed - diff --git a/spotipy/util.py b/spotipy/util.py index 7e586734..1c91d742 100644 --- a/spotipy/util.py +++ b/spotipy/util.py @@ -1,4 +1,4 @@ -""" Shows a user's playlists (need to be authenticated via oauth) """ +""" Shows a user's playlists. This needs to be authenticated via OAuth. """ __all__ = ["CLIENT_CREDS_ENV_VARS", "prompt_for_user_token"] @@ -35,21 +35,18 @@ def prompt_for_user_token( " spotipy.Spotify(auth_manager=auth_manager)", DeprecationWarning ) - """ prompts the user to login if necessary and returns - the user token suitable for use with the spotipy.Spotify - constructor + """Prompt the user to login if necessary and returns a user token + suitable for use with the spotipy.Spotify constructor. Parameters: - - - username - the Spotify username (optional) - - scope - the desired scope of the request (optional) - - client_id - the client id of your app (required) - - client_secret - the client secret of your app (required) - - redirect_uri - the redirect URI of your app (required) - - cache_path - path to location to save tokens (optional) - - oauth_manager - Oauth manager object (optional) - - show_dialog - If true, a login prompt always shows (optional, defaults to False) - + - username - the Spotify username. (optional) + - scope - the desired scope of the request. (optional) + - client_id - the client ID of your app. (required) + - client_secret - the client secret of your app. (required) + - redirect_uri - the redirect URI of your app. (required) + - cache_path - path to location to save tokens. (required) + - oauth_manager - OAuth manager object. (optional) + - show_dialog - If True, a login prompt always shows or defaults to False. (optional) """ if not oauth_manager: if not client_id: @@ -107,6 +104,12 @@ def prompt_for_user_token( def get_host_port(netloc): + """ Split the network location string into host and port and returns a tuple + where the host is a string and the the port is an integer. + + Parameters: + - netloc - a string representing the network location. + """ if ":" in netloc: host, port = netloc.split(":", 1) port = int(port) @@ -118,6 +121,14 @@ def get_host_port(netloc): def normalize_scope(scope): + """Normalize the scope to verify that it is a list or tuple. A string + input will split the string by commas to create a list of scopes. + A list or tuple input is used directly. + + Parameters: + - scope - a string representing scopes separated by commas, + or a list/tuple of scopes. + """ if scope: if isinstance(scope, str): scopes = scope.split(',') @@ -126,7 +137,7 @@ def normalize_scope(scope): else: raise Exception( "Unsupported scope value, please either provide a list of scopes, " - "or a string of scopes separated by commas" + "or a string of scopes separated by commas." ) return " ".join(sorted(scopes)) else: From d92951b356797ddabf4661faa67273f2c8b1d97e Mon Sep 17 00:00:00 2001 From: Brennan Pate <114464003+brennanpate@users.noreply.github.com> Date: Sat, 22 Jun 2024 10:35:53 -0500 Subject: [PATCH 60/67] Update Directions In TUTORIAL.md; Add Unit Tests To non_user_endpoints test.py (#1136) * Update navigational directions in Step 1(A) * Combine directions in Step 1(B) and Step 2(C) and remove Step 2(C) * Update navigational directions and verbage in Step 1(C) * Change reference from Step 1(C) to Step 1(B) in Step 2(D) * Update capitalization in Prerequisites Step 3 * Add directions for installing Spotipy in Prerequisites Step 1(A) * List updates to TUTORIAL.md file in CHANGELOG.md * Update docstrings for funcs in lines 340-585 * Add unit tests for artist ID and URL * Add test_artists_mixed_ids * Updated CHANGELOG.md and TUTORIAL.md as requested * Update client.py and test.py * Fix linting issue * Remove duplicate line; Change order of prerequisites * Update local repo * Add test_artists_mixed_ids * Add Radiohead ID and URL; Add qotsa URL * Add test_artist_url * Comment out three failing tests * Fix linting errors * Uncommenting out failed tests * Add test_artist_id * List changes in CHANGELOG.md * Add line breaks at the end of files. * Remove multiple spaces Also I've removed a sentence that just doesn't make sense in my eyes, but was added before this PR. --------- Co-authored-by: Niko --- CHANGELOG.md | 3 + CONTRIBUTING.md | 1 + TUTORIAL.md | 34 ++++----- docs/conf.py | 78 ++++++++++---------- docs/index.rst | 1 - tests/integration/non_user_endpoints/test.py | 20 +++++ 6 files changed, 76 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7924ecf6..e0d3fa5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ Add your changes below. ### Added - Added unit tests for queue functions - Added detailed function docstrings to 'util.py', including descriptions and special sections that lists arguments, returns, and raises. +- Updated order of instructions for Python and pip package manager installation in TUTORIAL.md +- Updated TUTORIAL.md instructions to match current layout of Spotify Developer Dashboard +- Added test_artist_id, test_artist_url, and test_artists_mixed_ids to non_user_endpoints test.py ### Fixed - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aa99374d..11558a0c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -68,3 +68,4 @@ Don't forget to add a short description of your change in the [CHANGELOG](CHANGE - Create github release https://github.com/plamere/spotipy/releases with the changelog content for the version and a short name that describes the main addition - Verify doc uses latest https://readthedocs.org/projects/spotipy/ + \ No newline at end of file diff --git a/TUTORIAL.md b/TUTORIAL.md index 54d49b29..b3dd15dc 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -4,25 +4,19 @@ Hello and welcome to the Spotipy Tutorial for Beginners. If you have limited exp ## Prerequisites In order to complete this tutorial successfully, there are a few things that you should already have installed: -**1. pip package manager** - -You can check to see if you have pip installed by opening up Terminal and typing the following command: pip --version -If you see a version number, pip is installed, and you're ready to proceed. If not, instructions for downloading the latest version of pip can be found here: https://pip.pypa.io/en/stable/cli/pip_download/ - - -**2. python3** +**1. python3** Spotipy is written in Python, so you'll need to have the latest version of Python installed in order to use Spotipy. Check if you already have Python installed with the Terminal command: python --version If you see a version number, Python is already installed. If not, you can download it here: https://www.python.org/downloads/ -**3. spotipy** +**2. pip package manager** -You'll need to install the packages necessary for this project. Run the following command: -``` -pip install spotipy -``` +You can check to see if you have pip installed by opening up Terminal and typing the following command: pip --version +If you see a version number, pip is installed, and you're ready to proceed. If not, instructions for downloading the latest version of pip can be found here: https://pip.pypa.io/en/stable/cli/pip_download/ -**4. experience with basic Linux commands** +A. After ensuring that pip is installed, run the following command in Terminal to install Spotipy: pip install spotipy --upgrade + +**3. Experience with Basic Linux Commands** This tutorial will be easiest if you have some knowledge of how to use Linux commands to create and navigate folders and files on your computer. If you're not sure how to create, edit and delete files and directories from Terminal, learn about basic Linux commands [here](https://ubuntu.com/tutorials/command-line-for-beginners#1-overview) before continuing. @@ -31,19 +25,17 @@ Once those three setup items are taken care of, you're ready to start learning h ## Step 1. Creating a Spotify Account Spotipy relies on the Spotify API. In order to use the Spotify API, you'll need to create a Spotify developer account. -A. Visit the [Spotify developer portal](https://developer.spotify.com/dashboard/). If you already have a Spotify account, click "Log in" and enter your username and password. Otherwise, click "Sign up" and follow the steps to create an account. After you've signed in or signed up, you should be redirected to your developer dashboard. - -B. Click the "Create an App" button. Enter any name and description you'd like for your new app. Add "http://localhost:1234" (or any other port number of your choosing) as your "Redirect URI". Accept the terms of service and click "Create." +A. Visit the [Spotify developer portal](https://developer.spotify.com/dashboard/). If you already have a Spotify account, click "Log in" and enter your username and password. Otherwise, click "Sign up" and follow the steps to create an account. After you've signed in or signed up, begin by clicking on your profile name at the top right of your screen and then click “Dashboard” to go to Spotify’s Developer Dashboard. -C. In your new app's Overview screen, click the "Settings" button and then under the "Basic Information" tab click "View client secret", then copy both your Client Secret and your Client ID somewhere on your computer. You'll need to access them later. +B. Check the box "Accept the Spotify Developer Terms of Service" and then click "Accept the terms". On the next page, verify your email address if you haven't already. Click the "Create an App" button. Enter any name and description you'd like for your new app. Next, add "http://localhost:1234" (or any other port number of your choosing) to the "Redirect URI" secction. Check the box "I understand and agree with Spotify's Developer Terms of Service and Design Guidelines" and then click the "Save" button. -D. Underneath your app name and description on the left-hand side, you'll see a "Show Client Secret" link. Click that link to reveal your Client Secret, then copy both your Client Secret and your Client ID somewhere on your computer. You'll need to access them later. +C. Click on "Settings". Underneath "Client ID", you'll see a "View Client Secret" link. Click the link to reveal your Client secret and copy both your Client secret and your Client ID somewhere so that you can access them later. ## Step 2. Installation and Setup A. Create a folder somewhere on your computer where you'd like to store the code for your Spotipy app. You can create a folder in terminal with this command: ```mkdir folder_name``` -B. Install the Spotipy library. You can do this by using this command in the terminal: ```pip install spotipy``` +B. In your new folder, create a Python file named main.py. You can create the file directly from Terminal using a built in text editor like Vim, which comes preinstalled on Linux operating systems. To create the file with Vim, ensure that you are in your new directory, then run: vim main.py C. In that folder, create a Python file named main.py. You can create the file directly from Terminal using a built in text editor like Vim, which comes preinstalled on Linux operating systems. To create the file with Vim, ensure that you are in your new directory, then run: vim main.py @@ -57,7 +49,7 @@ sp = spotipy.Spotify(auth_manager=SpotifyOAuth(client_id="YOUR_APP_CLIENT_ID", redirect_uri="YOUR_APP_REDIRECT_URI", scope="user-library-read")) ``` -E. Replace YOUR_APP_CLIENT_ID and YOUR_APP_CLIENT_SECRET with the values you copied and saved in step 1D. Replace YOUR_APP_REDIRECT_URI with the URI you set in step 1C. +D. Replace YOUR_APP_CLIENT_ID and YOUR_APP_CLIENT_SECRET with the values you copied and saved in step 1D. Replace YOUR_APP_REDIRECT_URI with the URI you set in step 1B. ## Step 3. Start Using Spotipy @@ -100,7 +92,7 @@ In most cases, the recent Python version is Python 3. You may need to update Pyt B. Encountering package error: -If you are seeing an error "ModuleNotFoundError: No module named 'spotipy'", this means you have not installed the package. This may occur if you followed the installation and setup (up to Step 3, Part D) and attempted to run the app with the missing package. +If you are seeing an error "ModuleNotFoundError: No module named 'spotipy'", this means you have not installed the package. Run the command: ``` pip install spotipy diff --git a/docs/conf.py b/docs/conf.py index df657248..69d99433 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,7 +16,7 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.abspath("..")) @@ -25,7 +25,7 @@ # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. @@ -41,7 +41,7 @@ source_suffix = '.rst' # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' @@ -61,37 +61,37 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- @@ -103,26 +103,26 @@ # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -131,44 +131,44 @@ # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'spotipydoc' @@ -196,23 +196,23 @@ # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- @@ -225,7 +225,7 @@ ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -240,10 +240,10 @@ ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' diff --git a/docs/index.rst b/docs/index.rst index 3534bfac..e8c037d8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -402,4 +402,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/tests/integration/non_user_endpoints/test.py b/tests/integration/non_user_endpoints/test.py index e4f222a9..c67703a6 100644 --- a/tests/integration/non_user_endpoints/test.py +++ b/tests/integration/non_user_endpoints/test.py @@ -37,12 +37,19 @@ class AuthTestSpotipy(unittest.TestCase): creep_urn = 'spotify:track:6b2oQwSGFkzsMtQruIWm2p' creep_id = '6b2oQwSGFkzsMtQruIWm2p' creep_url = 'http://open.spotify.com/track/6b2oQwSGFkzsMtQruIWm2p' + el_scorcho_urn = 'spotify:track:0Svkvt5I79wficMFgaqEQJ' el_scorcho_bad_urn = 'spotify:track:0Svkvt5I79wficMFgaqEQK' pinkerton_urn = 'spotify:album:04xe676vyiTeYNXw15o9jT' weezer_urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu' + pablo_honey_urn = 'spotify:album:6AZv3m27uyRxi8KyJSfUxL' radiohead_urn = 'spotify:artist:4Z8W4fKeB5YxbusRsdQVPb' + radiohead_id = "4Z8W4fKeB5YxbusRsdQVPb" + radiohead_url = "https://open.spotify.com/artist/4Z8W4fKeB5YxbusRsdQVPb" + + qotsa_url = "https://open.spotify.com/artist/4pejUc4iciQfgdX6OKulQn" + angeles_haydn_urn = 'spotify:album:1vAbqAeuJVWNAe7UR00bdM' heavyweight_urn = 'spotify:show:5c26B28vZMN8PG0Nppmn5G' heavyweight_id = '5c26B28vZMN8PG0Nppmn5G' @@ -101,11 +108,24 @@ def test_artist_urn(self): artist = self.spotify.artist(self.radiohead_urn) self.assertTrue(artist['name'] == 'Radiohead') + def test_artist_url(self): + artist = self.spotify.artist(self.radiohead_url) + self.assertTrue(artist['name'] == 'Radiohead') + + def test_artist_id(self): + artist = self.spotify.artist(self.radiohead_id) + self.assertTrue(artist['name'] == 'Radiohead') + def test_artists(self): results = self.spotify.artists([self.weezer_urn, self.radiohead_urn]) self.assertTrue('artists' in results) self.assertTrue(len(results['artists']) == 2) + def test_artists_mixed_ids(self): + results = self.spotify.artists([self.weezer_urn, self.radiohead_id, self.qotsa_url]) + self.assertTrue('artists' in results) + self.assertTrue(len(results['artists']) == 3) + def test_album_urn(self): album = self.spotify.album(self.pinkerton_urn) self.assertTrue(album['name'] == 'Pinkerton') From c7856d0120086c8628a14a56555037b950c244bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bruckert?= Date: Sun, 23 Jun 2024 20:39:17 +0100 Subject: [PATCH 61/67] Don't run integration tests twice on PR --- .github/workflows/integration_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index f6b6eac9..a203147a 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -1,6 +1,6 @@ name: Integration tests -on: [push, pull_request_target] +on: [push] jobs: build: From c90ce4a8753571fa29335c0d120537a85abbb3e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bruckert?= Date: Sun, 23 Jun 2024 22:13:44 +0100 Subject: [PATCH 62/67] Fix audiobook integration tests (#1141) --- CHANGELOG.md | 2 +- tests/integration/non_user_endpoints/test.py | 35 +++++++++----------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0d3fa5a..55810481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ Add your changes below. - Added test_artist_id, test_artist_url, and test_artists_mixed_ids to non_user_endpoints test.py ### Fixed -- +- Audiobook integration tests ### Removed - `mock` no longer listed as a test dependency. Only built-in `unittest.mock` is actually used. diff --git a/tests/integration/non_user_endpoints/test.py b/tests/integration/non_user_endpoints/test.py index c67703a6..2aa878b5 100644 --- a/tests/integration/non_user_endpoints/test.py +++ b/tests/integration/non_user_endpoints/test.py @@ -60,15 +60,12 @@ 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'] + dune_urn = 'spotify:audiobook:7iHfbu1YPACw6oZPAFJtqe' + dune_id = '7iHfbu1YPACw6oZPAFJtqe' + dune_url = 'https://open.spotify.com/audiobook/7iHfbu1YPACw6oZPAFJtqe' + two_books = [ + 'spotify:audiobook:7iHfbu1YPACw6oZPAFJtqe', + 'spotify:audiobook:67VtmjZitn25TWocsyAEyh'] @classmethod def setUpClass(self): @@ -337,7 +334,7 @@ def test_artist_albums(self): def find_album(): for album in results['items']: - if album['name'] == 'Death to False Metal': + if 'Weezer' in album['name']: # Weezer has many albums containing Weezer return True return False @@ -485,27 +482,25 @@ def test_available_markets(self): self.assertIn("GB", markets) def test_get_audiobook(self): - audiobook = self.spotify.get_audiobook(self.american_gods_urn, market="US") + audiobook = self.spotify.get_audiobook(self.dune_urn, market="US") self.assertTrue(audiobook['name'] == - 'American Gods: The Tenth Anniversary Edition: A Novel') + 'Dune: Book One in the Dune Chronicles') 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") + results = self.spotify.get_audiobooks(self.two_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') + self.assertTrue(len(results['audiobooks']) == 2) + self.assertTrue(results['audiobooks'][0]['name'] + == 'Dune: Book One in the Dune Chronicles') + self.assertTrue(results['audiobooks'][1]['name'] == 'The Helper') def test_get_audiobook_chapters(self): results = self.spotify.get_audiobook_chapters( - self.american_gods_urn, market="US", limit=10, offset=5) + self.dune_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) From d7640404a567130e1281cb2f47d307f8e9c213c3 Mon Sep 17 00:00:00 2001 From: Brandon Parrott <107897092+bparrott3@users.noreply.github.com> Date: Sun, 23 Jun 2024 14:29:28 -0700 Subject: [PATCH 63/67] client.py docstring updates (#1060) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update client.py Added warnings in docstrings for functions that have been replaced and call other new functions. * Update CHANGELOG.md --------- Co-authored-by: Stéphane Bruckert --- CHANGELOG.md | 1 + spotipy/client.py | 83 ++++++++++++++++++++++++++++------------------- 2 files changed, 51 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55810481..715b8153 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Add your changes below. ### Fixed - Audiobook integration tests +- Edited docstrings for certain functions in client.py for functions that are no longer in use and have been replaced. ### Removed - `mock` no longer listed as a test dependency. Only built-in `unittest.mock` is actually used. diff --git a/spotipy/client.py b/spotipy/client.py index d83053ca..f53c79af 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -841,11 +841,9 @@ 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 + """ This function is no longer in use, please use the recommended function in the warning! + + Changes a playlist's name and/or public/private state Parameters: - user - the id of the user @@ -855,12 +853,18 @@ 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) def user_playlist_unfollow(self, user, playlist_id): - """ Unfollows (deletes) a playlist for a user + """ This function is no longer in use, please use the recommended function in the warning! + + Unfollows (deletes) a playlist for a user Parameters: - user - the id of the user @@ -875,11 +879,9 @@ def user_playlist_unfollow(self, user, playlist_id): def user_playlist_add_tracks( self, user, playlist_id, tracks, position=None ): - warnings.warn( - "You should use `playlist_add_items(playlist_id, tracks)` instead", - DeprecationWarning, - ) - """ Adds tracks to a playlist + """ This function is no longer in use, please use the recommended function in the warning! + + Adds tracks to a playlist Parameters: - user - the id of the user @@ -887,17 +889,20 @@ def user_playlist_add_tracks( - 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, + ) + 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, - ) - """ Adds episodes to a playlist + """ This function is no longer in use, please use the recommended function in the warning! + + Adds episodes to a playlist Parameters: - user - the id of the user @@ -905,11 +910,18 @@ 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) def user_playlist_replace_tracks(self, user, playlist_id, tracks): - """ Replace all tracks in a playlist for a user + """ This function is no longer in use, please use the recommended function in the warning! + + Replace all tracks in a playlist for a user Parameters: - user - the id of the user @@ -931,7 +943,9 @@ def user_playlist_reorder_tracks( range_length=1, snapshot_id=None, ): - """ Reorder tracks in a playlist from a user + """ This function is no longer in use, please use the recommended function in the warning! + + Reorder tracks in a playlist from a user Parameters: - user - the id of the user @@ -954,14 +968,15 @@ def user_playlist_reorder_tracks( def user_playlist_remove_all_occurrences_of_tracks( self, user, playlist_id, tracks, snapshot_id=None ): - """ Removes all occurrences of the given tracks from the given playlist + """ This function is no longer in use, please use the recommended function in the warning! + + Removes all occurrences of the given tracks from the given playlist Parameters: - user - the id of the user - 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" @@ -975,7 +990,9 @@ def user_playlist_remove_all_occurrences_of_tracks( def user_playlist_remove_specific_occurrences_of_tracks( self, user, playlist_id, tracks, snapshot_id=None ): - """ Removes all occurrences of the given tracks from the given playlist + """ This function is no longer in use, please use the recommended function in the warning! + + Removes all occurrences of the given tracks from the given playlist Parameters: - user - the id of the user @@ -1009,13 +1026,13 @@ def user_playlist_remove_specific_occurrences_of_tracks( ) def user_playlist_follow_playlist(self, playlist_owner_id, playlist_id): - """ - Add the current authenticated user as a follower of a playlist. + """ This function is no longer in use, please use the recommended function in the warning! - Parameters: - - playlist_owner_id - the user id of the playlist owner - - playlist_id - the id of the playlist + Add the current authenticated user as a follower of a playlist. + 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", @@ -1026,15 +1043,15 @@ def user_playlist_follow_playlist(self, playlist_owner_id, playlist_id): def user_playlist_is_following( self, playlist_owner_id, playlist_id, user_ids ): - """ - Check to see if the given users are following the given playlist + """ This function is no longer in use, please use the recommended function in the warning! - Parameters: - - 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. + Check to see if the given users are following the given playlist + Parameters: + - 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. """ warnings.warn( "You should use `playlist_is_following(playlist_id, user_ids)` instead", From ef282e2423d3ffaff19119e5f36636b29724e86e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bruckert?= Date: Sun, 23 Jun 2024 22:30:54 +0100 Subject: [PATCH 64/67] Removed outdated contributors from RTD --- docs/index.rst | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index e8c037d8..c7ef2bc8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -302,30 +302,6 @@ If you think you've found a bug, let us know at Contribute ========== -Spotipy authored by Paul Lamere (plamere) with contributions by: - - - Daniel Beaudry (`danbeaudry on Github `_) - - Faruk Emre Sahin (`fsahin on Github `_) - - George (`rogueleaderr on Github `_) - - Henry Greville (`sethaurus on Github `_) - - Hugo van Kemanade (`hugovk on Github `_) - - José Manuel Pérez (`JMPerez on Github `_) - - Lucas Nunno (`lnunno on Github `_) - - Lynn Root (`econchick on Github `_) - - Matt Dennewitz (`mattdennewitz on Github `_) - - Matthew Duck (`mattduck on Github `_) - - Michael Thelin (`thelinmichael on Github `_) - - Ryan Choi (`ryankicks on Github `_) - - Simon Metson (`drsm79 on Github `_) - - Steve Winton (`swinton on Github `_) - - Tim Balzer (`timbalzer on Github `_) - - `corycorycory on Github `_ - - Nathan Coleman (`nathancoleman on Github `_) - - Michael Birtwell (`mbirtwell on Github `_) - - Harrison Hayes (`Harrison97 on Github `_) - - Stephane Bruckert (`stephanebruckert on Github `_) - - Ritiek Malhotra (`ritiek on Github `_) - If you are a developer with Python experience, and you would like to contribute to Spotipy, please be sure to follow the guidelines listed below: From 5e09c78ccf9cb5450da50e5a984e21cce39ee7a2 Mon Sep 17 00:00:00 2001 From: Niko Date: Mon, 24 Jun 2024 23:43:54 +0200 Subject: [PATCH 65/67] Added an explanation for rate/request limits to FAQ (#1133) --- CHANGELOG.md | 1 + FAQ.md | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 715b8153..67692919 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Add your changes below. - Updated order of instructions for Python and pip package manager installation in TUTORIAL.md - Updated TUTORIAL.md instructions to match current layout of Spotify Developer Dashboard - Added test_artist_id, test_artist_url, and test_artists_mixed_ids to non_user_endpoints test.py +- Added rate/request limit to FAQ ### Fixed - Audiobook integration tests diff --git a/FAQ.md b/FAQ.md index 63fa0521..f34fa19f 100644 --- a/FAQ.md +++ b/FAQ.md @@ -51,4 +51,26 @@ must be specified: `search("abba", market="DE")`. If you cannot open a browser, set `open_browser=False` when instantiating SpotifyOAuth or SpotifyPKCE. You will be prompted to open the authorization URI manually. -See the [headless auth example](examples/headless.py). \ No newline at end of file +See the [headless auth example](examples/headless.py). + +### My application is not responding + +This is still speculation, but it seems that Spotify has two limits. A rate limit and a request limit. + +- The rate limit prevents a script from requesting too much from the API in a short period of time. +- The request limit limits how many requests you can make in a 24 hour window. +The limits appear to be endpoint-specific, so each endpoint has its own limits. + +If your application stops responding, it's likely that you've reached the request limit. +There's nothing Spotipy can do to prevent this, but you can follow Spotify's [Rate Limits](https://developer.spotify.com/documentation/web-api/concepts/rate-limits) guide to learn how rate limiting works and what you can do to avoid ever hitting a limit. + +#### *Why* is the application not responding? +Spotipy (or more precisely `urllib3`) has a backoff-retry strategy built in, which is waiting until the rate limit is gone. +If you want to receive an error instead, then you can pass `retries=0` to `Spotify` like this: +```python +sp = spotipy.Spotify( + retries=0, + ... +) +``` +The error raised is a `spotipy.exceptions.SpotifyException` From 66ad716595befa689a1447c5579fc6dc056519c4 Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 25 Jun 2024 09:26:19 +0200 Subject: [PATCH 66/67] Print warnings when a rate/request limit is reached (#1134) * create custom urllib3.Retry class for printing warnings on rate/request limits * move import urllib3 from client.py to util.py * Using Retry.increment instead of Retry.is_retry. Shows the Retry-After value in the warning as well * Making sure that max column <= 99 * add types.TracebackType * Change warning in request/rate limit warning * adding all parameters, just to make sure * fixing length of line * making sure that response is not None --- CHANGELOG.md | 1 + spotipy/client.py | 4 ++-- spotipy/util.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67692919..e602185c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Add your changes below. - Updated TUTORIAL.md instructions to match current layout of Spotify Developer Dashboard - Added test_artist_id, test_artist_url, and test_artists_mixed_ids to non_user_endpoints test.py - Added rate/request limit to FAQ +- Added custom `urllib3.Retry` class for printing a warning when a rate/request limit is reached. ### Fixed - Audiobook integration tests diff --git a/spotipy/client.py b/spotipy/client.py index f53c79af..caaf9ca9 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -8,9 +8,9 @@ import warnings import requests -import urllib3 from spotipy.exceptions import SpotifyException +from spotipy.util import Retry from collections import defaultdict @@ -220,7 +220,7 @@ def __del__(self): def _build_session(self): self._session = requests.Session() - retry = urllib3.Retry( + retry = Retry( total=self.retries, connect=None, read=False, diff --git a/spotipy/util.py b/spotipy/util.py index 1c91d742..2d9e012d 100644 --- a/spotipy/util.py +++ b/spotipy/util.py @@ -1,3 +1,5 @@ +from __future__ import annotations + """ Shows a user's playlists. This needs to be authenticated via OAuth. """ __all__ = ["CLIENT_CREDS_ENV_VARS", "prompt_for_user_token"] @@ -5,9 +7,12 @@ import logging import os import warnings +from types import TracebackType import spotipy +import urllib3 + LOGGER = logging.getLogger(__name__) CLIENT_CREDS_ENV_VARS = { @@ -142,3 +147,29 @@ def normalize_scope(scope): return " ".join(sorted(scopes)) else: return None + + +class Retry(urllib3.Retry): + """ + Custom class for printing a warning when a rate/request limit is reached. + """ + def increment( + self, + method: str | None = None, + url: str | None = None, + response: urllib3.BaseHTTPResponse | None = None, + error: Exception | None = None, + _pool: urllib3.connectionpool.ConnectionPool | None = None, + _stacktrace: TracebackType | None = None, + ) -> urllib3.Retry: + if response: + retry_header = response.headers.get("Retry-After") + if self.is_retry(method, response.status, bool(retry_header)): + logging.warning("Your application has reached a rate/request limit. " + f"Retry will occur after: {retry_header}") + return super().increment(method, + url, + response=response, + error=error, + _pool=_pool, + _stacktrace=_stacktrace) From 1e05bdba67f8b7e8fcd4b419176a0048c329b1b7 Mon Sep 17 00:00:00 2001 From: JackDyre <83248599+JackDyre@users.noreply.github.com> Date: Sun, 30 Jun 2024 14:34:14 -0500 Subject: [PATCH 67/67] ID support for `current_user_unfollow_playlist()` (#1143) * ID support for `current_user_unfollow_playlist()` * turned double quotes into single quotes * Clarified changelog entry --- CHANGELOG.md | 1 + spotipy/client.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e602185c..f9d46ffc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Add your changes below. ### Fixed - Audiobook integration tests - Edited docstrings for certain functions in client.py for functions that are no longer in use and have been replaced. +- `current_user_unfollow_playlist()` now supports playlist IDs, URLs, and URIs rather than previously where it only supported playlist IDs. ### Removed - `mock` no longer listed as a test dependency. Only built-in `unittest.mock` is actually used. diff --git a/spotipy/client.py b/spotipy/client.py index caaf9ca9..61924a55 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -1096,10 +1096,10 @@ def current_user_unfollow_playlist(self, playlist_id): user Parameters: - - name - the name of the playlist + - playlist_id - the id of the playlist """ return self._delete( - f"playlists/{playlist_id}/followers" + f"playlists/{self._get_id('playlist', playlist_id)}/followers" ) def playlist_add_items(