Use f-strings + code simplification (#961)

Resolves #960
This commit is contained in:
Atharva Shah 2025-01-16 06:21:26 +05:30 committed by GitHub
parent 3c75886229
commit 5d0b8edff4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 334 additions and 329 deletions

View File

@ -12,46 +12,49 @@ Rebasing master onto v3 doesn't require a changelog update.
### Added
* `Scope` - An enum which contains all of the authorization scopes (see [here](https://github.com/plamere/spotipy/issues/652#issuecomment-797461311)).
- `Scope` - An enum which contains all of the authorization scopes (see [here](https://github.com/plamere/spotipy/issues/652#issuecomment-797461311)).
### Changed
* Made `CacheHandler` an abstract base class
* Modified the return structure of the `audio_features` function (wrapping the [Get Audio Features for Several Tracks](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-audio-features) API) to conform to the return structure of the similar methods listed below. The functions wrapping these APIs do not unwrap the single key JSON response, and this is currently the only function that does this.
* [Get Several Tracks](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-tracks)
* [Get Multiple Artists](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-artists)
* [Get Multiple Albums](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-albums)
* Renamed the `auth` parameter of `Spotify.__init__` to `access_token` for better clarity.
* Removed the `client_credentials_manager` and `oauth_manager` parameters because they are redundant.
* Replaced the `set_auth` and `auth_manager` properties with standard attributes.
- Made `CacheHandler` an abstract base class
- Modified the return structure of the `audio_features` function (wrapping the [Get Audio Features for Several Tracks](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-audio-features) API) to conform to the return structure of the similar methods listed below. The functions wrapping these APIs do not unwrap the single key JSON response, and this is currently the only function that does this.
- [Get Several Tracks](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-tracks)
- [Get Multiple Artists](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-artists)
- [Get Multiple Albums](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-albums)
- Renamed the `auth` parameter of `Spotify.__init__` to `access_token` for better clarity.
- Removed the `client_credentials_manager` and `oauth_manager` parameters because they are redundant.
- Replaced the `set_auth` and `auth_manager` properties with standard attributes.
- Replaced string concatenations and `str.format()` with f-strings
### Removed
* Removed the following deprecated methods from `Spotify`:
* `playlist_tracks`
* `user_playlist`
* `user_playlist_tracks`
* `user_playlist_change_details`
* `user_playlist_unfollow`
* `user_playlist_add_tracks`
* `user_playlist_replace_tracks`
* `user_playlist_reorder_tracks`
* `user_playlist_remove_all_occurrences_of_tracks`
* `user_playlist_remove_specific_occurrences_of_tracks`
* `user_playlist_follow_playlist`
* `user_playlist_is_following`
- Removed the following deprecated methods from `Spotify`:
- `playlist_tracks`
- `user_playlist`
- `user_playlist_tracks`
- `user_playlist_change_details`
- `user_playlist_unfollow`
- `user_playlist_add_tracks`
- `user_playlist_replace_tracks`
- `user_playlist_reorder_tracks`
- `user_playlist_remove_all_occurrences_of_tracks`
- `user_playlist_remove_specific_occurrences_of_tracks`
- `user_playlist_follow_playlist`
- `user_playlist_is_following`
* Removed the deprecated `as_dict` parameter from the `get_access_token` method of `SpotifyOAuth` and `SpotifyPKCE`.
* Removed the deprecated `get_cached_token` and `_save_token_info` methods of `SpotifyOAuth` and `SpotifyPKCE`.
* Removed `SpotifyImplicitGrant`.
* Removed `prompt_for_user_token`.
- Removed the deprecated `as_dict` parameter from the `get_access_token` method of `SpotifyOAuth` and `SpotifyPKCE`.
- Removed the deprecated `get_cached_token` and `_save_token_info` methods of `SpotifyOAuth` and `SpotifyPKCE`.
- Removed `SpotifyImplicitGrant`.
- Removed `prompt_for_user_token`.
## Unreleased [2.x.x]
### Added
- Added examples for audiobooks, shows and episodes methods to examples directory
### Fixed
- Fixed scripts in examples directory that didn't run correctly
- Updated documentation for `Client.current_user_top_artists` to indicate maximum number of artists limit
@ -60,6 +63,7 @@ Rebasing master onto v3 doesn't require a changelog update.
## [2.25.0] - 2025-03-01
### 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
@ -80,42 +84,50 @@ Rebasing master onto v3 doesn't require a changelog update.
- Added FAQ entry for inaccessible playlists
### 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.
## [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 integration tests for audiobook endpoints.
- Added `update` field to `current_user_follow_playlist`.
### Changed
- 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.
- Updated `_regex_spotify_url` to ignore `/intl-<countrycode>` in Spotify links
- Improved README, docs and examples
### Fixed
- 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
### 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.
- `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))
@ -197,10 +209,6 @@ Rebasing master onto v3 doesn't require a changelog update.
- 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`
### Fixed
* Fixed a bug in `CacheFileHandler.__init__`: The documentation says that the username will be retrieved from the environment, but it wasn't.
## [2.18.0] - 2021-04-13
### Added
@ -297,7 +305,7 @@ Rebasing master onto v3 doesn't require a changelog update.
- Support to test whether the current user is following certain
users or artists
- Proper replacements for all deprecated playlist endpoints
(See https://developer.spotify.com/community/news/2018/06/12/changes-to-playlist-uris/ and below)
(See <https://developer.spotify.com/community/news/2018/06/12/changes-to-playlist-uris/> and below)
- Allow for OAuth 2.0 authorization by instructing the user to open the URL in a browser instead of opening the browser.
- Reason for 403 error in SpotifyException
- Support for the PKCE Auth Flow
@ -504,6 +512,7 @@ Rebasing master onto v3 doesn't require a changelog update.
### 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

View File

@ -18,7 +18,7 @@ $env:SPOTIPY_CLIENT_USERNAME="client_username_here"
$env:SPOTIPY_REDIRECT_URI="http://localhost:8080"
```
### Create virtual environment, install dependencies, run tests:
### Create virtual environment, install dependencies, run tests
```bash
$ virtualenv --python=python3 env
@ -50,9 +50,9 @@ Don't forget to add a short description of your change in the [CHANGELOG](CHANGE
### Publishing (by maintainer)
- Bump version in setup.py
- Bump and date changelog
- Add to changelog:
- Bump version in setup.py
- Bump and date changelog
- Add to changelog:
## Unreleased
Add your changes below.
@ -63,9 +63,8 @@ Don't forget to add a short description of your change in the [CHANGELOG](CHANGE
### Removed
- Commit changes
- Push tag to trigger PyPI build & release workflow
- Create github release https://github.com/plamere/spotipy/releases with the changelog content
- Commit changes
- 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/
- Verify doc uses latest <https://readthedocs.org/projects/spotipy/>

View File

@ -1,6 +1,6 @@
# Spotipy
##### 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.
##### 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
![Integration tests](https://github.com/spotipy-dev/spotipy/actions/workflows/integration_tests.yml/badge.svg?branch=master) [![Documentation Status](https://readthedocs.org/projects/spotipy/badge/?version=master)](https://spotipy.readthedocs.io/en/latest/?badge=master) [![Discord server](https://img.shields.io/discord/1244611850700849183?style=flat&logo=discord&logoColor=7289DA&color=7289DA)](https://discord.gg/HP6xcPsTPJ)
@ -38,7 +38,7 @@ 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](#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)):
To get started, [install spotipy](#installation), create a new account or log in on [developers.spotify.com](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)):
### Example without user authentication
@ -53,7 +53,9 @@ 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
@ -65,7 +67,6 @@ Expected result:
19 Feels Like Summer
```
### 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.
@ -84,13 +85,14 @@ 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
For common questions please check our [FAQ](FAQ.md).

View File

@ -1,18 +1,20 @@
# Spotipy Tutorial for Beginners
Hello and welcome to the Spotipy Tutorial for Beginners. If you have limited experience coding in Python and have never used Spotipy or the Spotify API before, you've come to the right place. This tutorial will walk you through all the steps necessary to set up Spotipy and use it to accomplish a simple task.
## Prerequisites
In order to complete this tutorial successfully, there are a few things that you should already have installed:
**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/
If you see a version number, Python is already installed. If not, you can download it here: <https://www.python.org/downloads/>
**2. 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/>
A. After ensuring that pip is installed, run the following command in Terminal to install Spotipy: pip install spotipy --upgrade
@ -23,11 +25,12 @@ This tutorial will be easiest if you have some knowledge of how to use Linux com
Once those three setup items are taken care of, you're ready to start learning how to use Spotipy!
## 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, begin by clicking on your profile name at the top right of your screen and then click “Dashboard” to go to Spotifys Developer Dashboard.
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.
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.
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.
@ -40,6 +43,7 @@ B. In your new folder, create a Python file named main.py. You can create the fi
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
@ -49,6 +53,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 1B.
## Step 3. Start Using Spotipy
@ -60,10 +65,13 @@ For now, let's assume that we want to print the names of all the albums on Spoti
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 URI:
```
taylor_uri = 'spotify:artist:06HL4z0CvFAxyc27GXpf02'
```
C. Add the following code that will get all of Taylor's album names from Spotify and iterate through them to print them all to standard output.
```
results = sp.artist_albums(taylor_uri, album_type='album')
albums = results['items']
@ -82,19 +90,22 @@ E. You may see a window open in your browser asking you to authorize the applica
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```.
```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```
```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.
Run the command:
```
pip install spotipy
```
After the package is installed, run the app again.

View File

@ -88,9 +88,7 @@ def currently_playing():
return redirect('/')
spotify = spotipy.Spotify(auth_manager=auth_manager)
track = spotify.current_user_playing_track()
if not track is None:
return track
return "No track currently playing."
return track if track is not None else "No track currently playing."
@app.route('/current_user')

View File

@ -18,12 +18,9 @@ def get_args():
def get_artist(name):
results = sp.search(q='artist:' + name, type='artist')
results = sp.search(q=f'artist:{name}', type='artist')
items = results['artists']['items']
if len(items) > 0:
return items[0]
else:
return None
return items[0] if len(items) > 0 else None
def show_artist_albums(artist):

View File

@ -18,12 +18,9 @@ def get_args():
def get_artist(name):
results = sp.search(q='artist:' + name, type='artist')
results = sp.search(q=f'artist:{name}', type='artist')
items = results['artists']['items']
if len(items) > 0:
return items[0]
else:
return None
return items[0] if len(items) > 0 else None
def show_album_tracks(album):

View File

@ -20,12 +20,9 @@ def get_args():
def get_artist(name):
results = sp.search(q='artist:' + name, type='artist')
results = sp.search(q=f'artist:{name}', type='artist')
items = results['artists']['items']
if len(items) > 0:
return items[0]
else:
return None
return items[0] if len(items) > 0 else None
def show_recommendations_for_artist(artist):

View File

@ -0,0 +1,12 @@
# Add a list of items (URI) to a playlist (URI)
import spotipy
from spotipy.oauth2 import SpotifyOAuth
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(client_id="YOUR_APP_CLIENT_ID",
client_secret="YOUR_APP_CLIENT_SECRET",
redirect_uri="YOUR_APP_REDIRECT_URI",
scope="playlist-modify-private"
))
sp.playlist_add_items('playlist_id', ['list_of_items'])

View File

@ -25,6 +25,5 @@ for item in tracks:
tracks.remove(item)
i += 1
# print result
print("Playlist length: " + str(len(tracks)) + "\nExcluding: " + str(i))
print(f"Playlist length: {len(tracks)}\nExcluding: {i}")

View File

@ -14,8 +14,7 @@ if len(sys.argv) > 2:
tid, pos = t_pos.split(',')
track_ids.append({"uri": tid, "positions": [int(pos)]})
else:
print(
f"Usage: {sys.argv[0]} playlist_id track_id,pos track_id,pos ...")
print(f"Usage: {sys.argv[0]} playlist_id track_id,pos track_id,pos ...")
sys.exit()
scope = 'playlist-modify-public'

View File

@ -5,11 +5,7 @@ import spotipy
import sys
import pprint
if len(sys.argv) > 1:
search_str = sys.argv[1]
else:
search_str = 'Radiohead'
search_str = sys.argv[1] if len(sys.argv) > 1 else 'Radiohead'
sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
result = sp.search(search_str)
pprint.pprint(result)

View File

@ -13,7 +13,4 @@ while response:
for i, item in enumerate(playlists['items']):
print(playlists['offset'] + i, item['name'])
if playlists['next']:
response = sp.next(playlists)
else:
response = None
response = sp.next(playlists) if playlists['next'] else None

View File

@ -12,7 +12,4 @@ while response:
for i, item in enumerate(albums['items']):
print(albums['offset'] + i, item['name'])
if albums['next']:
response = sp.next(albums)
else:
response = None
response = sp.next(albums) if albums['next'] else None

View File

@ -4,14 +4,10 @@ from spotipy.oauth2 import SpotifyClientCredentials
import spotipy
import sys
if len(sys.argv) > 1:
artist_name = sys.argv[1]
else:
artist_name = 'weezer'
artist_name = sys.argv[1] if len(sys.argv) > 1 else 'weezer'
auth_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(auth_manager=auth_manager)
result = sp.search(q='artist:' + artist_name, type='artist')
result = sp.search(q=f'artist:{artist_name}', type='artist')
try:
name = result['artists']['items'][0]['name']
uri = result['artists']['items'][0]['uri']

View File

@ -5,11 +5,7 @@ import spotipy
import sys
import pprint
if len(sys.argv) > 1:
username = sys.argv[1]
else:
username = 'plamere'
username = sys.argv[1] if len(sys.argv) > 1 else 'plamere'
auth_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(auth_manager=auth_manager)
sp.trace = True

View File

@ -6,12 +6,8 @@ import spotipy
sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
if len(sys.argv) > 1:
name = ' '.join(sys.argv[1:])
else:
name = 'Radiohead'
results = sp.search(q='artist:' + name, type='artist')
name = ' '.join(sys.argv[1:]) if len(sys.argv) > 1 else 'Radiohead'
results = sp.search(q=f'artist:{name}', type='artist')
items = results['artists']['items']
if len(items) > 0:
artist = items[0]

View File

@ -39,7 +39,5 @@ recommendations = sp.recommendations(
# Display the recommendations
for i, track in enumerate(recommendations['tracks']):
print(
"{}. {} by {}"
.format(i+1, track['name'], ', '
.join([artist['name'] for artist in track['artists']]))
f"{i+1}. {track['name']} by {', '.join([artist['name'] for artist in track['artists']])}"
)

View File

@ -9,11 +9,7 @@ from spotipy.oauth2 import SpotifyClientCredentials
auth_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(auth_manager=auth_manager)
user = 'spotify'
if len(sys.argv) > 1:
user = sys.argv[1]
user = sys.argv[1] if len(sys.argv) > 1 else 'spotify'
playlists = sp.user_playlists(user)
while playlists:
@ -25,7 +21,4 @@ while playlists:
playlists['offset'],
playlist['uri'],
playlist['name']))
if playlists['next']:
playlists = sp.next(playlists)
else:
playlists = None
playlists = sp.next(playlists) if playlists['next'] else None

View File

@ -68,7 +68,7 @@ class CacheFileHandler(CacheHandler):
cache_path = ".cache"
username = (username or os.getenv(CLIENT_CREDS_ENV_VARS["client_username"]))
if username:
cache_path += "-" + str(username)
cache_path += f"-{username}"
self.cache_path = cache_path
def get_cached_token(self):
@ -82,9 +82,9 @@ class CacheFileHandler(CacheHandler):
except OSError as error:
if error.errno == errno.ENOENT:
logger.debug("cache does not exist at: %s", self.cache_path)
logger.debug(f"cache does not exist at: {self.cache_path}")
else:
logger.warning("Couldn't read cache at: %s", self.cache_path)
logger.warning(f"Couldn't read cache at: {self.cache_path}")
return token_info
@ -94,8 +94,7 @@ class CacheFileHandler(CacheHandler):
f.write(json.dumps(token_info, cls=self.encoder_cls))
f.close()
except OSError:
logger.warning('Couldn\'t write token to cache at: %s',
self.cache_path)
logger.warning(f'Couldn\'t write token to cache at: {self.cache_path}')
class MemoryCacheHandler(CacheHandler):
@ -148,7 +147,7 @@ class DjangoSessionCacheHandler(CacheHandler):
try:
self.request.session['token_info'] = token_info
except Exception as e:
logger.warning("Error saving token to cache: " + str(e))
logger.warning(f"Error saving token to cache: {str(e)}")
class FlaskSessionCacheHandler(CacheHandler):
@ -173,7 +172,7 @@ class FlaskSessionCacheHandler(CacheHandler):
try:
self.session["token_info"] = token_info
except Exception as e:
logger.warning("Error saving token to cache: " + str(e))
logger.warning(f"Error saving token to cache: {str(e)}")
class RedisCacheHandler(CacheHandler):
@ -199,7 +198,7 @@ class RedisCacheHandler(CacheHandler):
if token_info:
return json.loads(token_info)
except RedisError as e:
logger.warning('Error getting token from cache: ' + str(e))
logger.warning(f'Error getting token from cache: {str(e)}')
return token_info
@ -207,12 +206,13 @@ class RedisCacheHandler(CacheHandler):
try:
self.redis.set(self.key, json.dumps(token_info))
except RedisError as e:
logger.warning('Error saving token to cache: ' + str(e))
logger.warning(f'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:
@ -222,7 +222,7 @@ class MemcacheCacheHandler(CacheHandler):
(takes precedence over `token_info`)
"""
self.memcache = memcache
self.key = key if key else 'token_info'
self.key = key or 'token_info'
def get_cached_token(self):
from pymemcache import MemcacheError
@ -231,11 +231,11 @@ class MemcacheCacheHandler(CacheHandler):
if token_info:
return json.loads(token_info.decode())
except MemcacheError as e:
logger.warning('Error getting token from cache' + str(e))
logger.warning(f'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))
logger.warning(f'Error saving token to cache: {str(e)}')

View File

@ -189,8 +189,7 @@ class Spotify:
if isinstance(requests_session, requests.Session):
self._session = requests_session
else:
if requests_session: # Build a new session.
elif requests_session: # Build a new session.
self._build_session()
else: # Use the Requests API module as a "session".
self._session = requests.api
@ -220,7 +219,7 @@ class Spotify:
def _auth_headers(self):
if self.access_token:
return {"Authorization": "Bearer {0}".format(self.access_token)}
return {"Authorization": f"Bearer {self.access_token}"}
if not self.auth_manager:
return {}
try:
@ -332,10 +331,7 @@ class Spotify:
Parameters:
- result - a previously returned paged result
"""
if result["next"]:
return self._get(result["next"])
else:
return None
return self._get(result["next"]) if result["next"] else None
def previous(self, result):
""" returns the previous result given a paged result
@ -343,10 +339,7 @@ class Spotify:
Parameters:
- result - a previously returned paged result
"""
if result["previous"]:
return self._get(result["previous"])
else:
return None
return self._get(result["previous"]) if result["previous"] else None
def track(self, track_id, market=None):
""" returns a single track given the track's ID, URI or URL
@ -357,7 +350,7 @@ class Spotify:
"""
trid = self._get_id("track", track_id)
return self._get("tracks/" + trid, market=market)
return self._get(f"tracks/{trid}", market=market)
def tracks(self, tracks, market=None):
""" returns a list of tracks given a list of track IDs, URIs, or URLs
@ -368,7 +361,7 @@ class Spotify:
"""
tlist = [self._get_id("track", t) for t in tracks]
return self._get("tracks/?ids=" + ",".join(tlist), market=market)
return self._get(f"tracks/?ids={','.join(tlist)}", market=market)
def artist(self, artist_id):
""" returns a single artist given the artist's ID, URI or URL
@ -378,7 +371,7 @@ class Spotify:
"""
trid = self._get_id("artist", artist_id)
return self._get("artists/" + trid)
return self._get(f"artists/{trid}")
def artists(self, artists):
""" returns a list of artists given the artist IDs, URIs, or URLs
@ -388,7 +381,7 @@ class Spotify:
"""
tlist = [self._get_id("artist", a) for a in artists]
return self._get("artists/?ids=" + ",".join(tlist))
return self._get(f"artists/?ids={','.join(tlist)}")
def artist_albums(
self, artist_id, album_type=None, include_groups=None, country=None, limit=20, offset=0
@ -416,7 +409,8 @@ class Spotify:
trid = self._get_id("artist", artist_id)
return self._get(
"artists/" + trid + "/albums",
f"artists/{trid}/albums",
album_type=album_type,
include_groups=include_groups,
country=country,
limit=limit,
@ -433,7 +427,7 @@ class Spotify:
"""
trid = self._get_id("artist", artist_id)
return self._get("artists/" + trid + "/top-tracks", country=country)
return self._get(f"artists/{trid}/top-tracks", country=country)
def artist_related_artists(self, artist_id):
""" Get Spotify catalog information about artists similar to an
@ -449,7 +443,7 @@ class Spotify:
DeprecationWarning
)
trid = self._get_id("artist", artist_id)
return self._get("artists/" + trid + "/related-artists")
return self._get(f"artists/{trid}/related-artists")
def album(self, album_id, market=None):
""" returns a single album given the album's ID, URIs or URL
@ -461,9 +455,9 @@ class Spotify:
trid = self._get_id("album", album_id)
if market is not None:
return self._get("albums/" + trid + '?market=' + market)
return self._get(f"albums/{trid}?market={market}")
else:
return self._get("albums/" + trid)
return self._get(f"albums/{trid}")
def album_tracks(self, album_id, limit=50, offset=0, market=None):
""" Get Spotify catalog information about an album's tracks
@ -478,7 +472,7 @@ class Spotify:
trid = self._get_id("album", album_id)
return self._get(
"albums/" + trid + "/tracks/", limit=limit, offset=offset, market=market
f"albums/{trid}/tracks/", limit=limit, offset=offset, market=market
)
def albums(self, albums, market=None):
@ -491,9 +485,9 @@ class Spotify:
tlist = [self._get_id("album", a) for a in albums]
if market is not None:
return self._get("albums/?ids=" + ",".join(tlist) + '&market=' + market)
return self._get(f"albums/?ids={','.join(tlist)}&market={market}")
else:
return self._get("albums/?ids=" + ",".join(tlist))
return self._get(f"albums/?ids={','.join(tlist)}")
def show(self, show_id, market=None):
""" returns a single show given the show's ID, URIs or URL
@ -508,7 +502,7 @@ class Spotify:
"""
trid = self._get_id("show", show_id)
return self._get("shows/" + trid, market=market)
return self._get(f"shows/{trid}", market=market)
def shows(self, shows, market=None):
""" returns a list of shows given the show IDs, URIs, or URLs
@ -523,7 +517,7 @@ class Spotify:
"""
tlist = [self._get_id("show", s) for s in shows]
return self._get("shows/?ids=" + ",".join(tlist), market=market)
return self._get(f"shows/?ids={','.join(tlist)}", market=market)
def show_episodes(self, show_id, limit=50, offset=0, market=None):
""" Get Spotify catalog information about a show's episodes
@ -541,7 +535,7 @@ class Spotify:
trid = self._get_id("show", show_id)
return self._get(
"shows/" + trid + "/episodes/", limit=limit, offset=offset, market=market
f"shows/{trid}/episodes/", limit=limit, offset=offset, market=market
)
def episode(self, episode_id, market=None):
@ -557,7 +551,7 @@ class Spotify:
"""
trid = self._get_id("episode", episode_id)
return self._get("episodes/" + trid, market=market)
return self._get(f"episodes/{trid}", market=market)
def episodes(self, episodes, market=None):
""" returns a list of episodes given the episode IDs, URIs, or URLs
@ -572,7 +566,7 @@ class Spotify:
"""
tlist = [self._get_id("episode", e) for e in episodes]
return self._get("episodes/?ids=" + ",".join(tlist), market=market)
return self._get(f"episodes/?ids={','.join(tlist)}", market=market)
def search(self, q, limit=10, offset=0, type="track", market=None):
""" searches for an item
@ -616,7 +610,7 @@ class Spotify:
if not markets:
markets = self.country_codes
if not (isinstance(markets, list) or isinstance(markets, tuple)):
if not (isinstance(markets, (list, tuple))):
markets = []
warnings.warn(
@ -631,7 +625,7 @@ class Spotify:
Parameters:
- user - the id of the usr
"""
return self._get("users/" + user)
return self._get(f"users/{user}")
def current_user_playlists(self, limit=50, offset=0):
""" Get current user playlists without required getting his profile
@ -687,7 +681,7 @@ class Spotify:
offset=offset,
fields=fields,
market=market,
additional_types=",".join(additional_types)
additional_types=",".join(additional_types),
)
def playlist_cover_image(self, playlist_id):
@ -722,9 +716,7 @@ class Spotify:
- limit - the number of items to return
- offset - the index of the first item to return
"""
return self._get(
f"users/{user}/playlists", limit=limit, offset=offset
)
return self._get(f"users/{user}/playlists", limit=limit, offset=offset)
def user_playlist_create(self, user, name, public=True, collaborative=False, description=""):
""" Creates a playlist for a user
@ -801,9 +793,7 @@ class Spotify:
plid = self._get_id("playlist", playlist_id)
ftracks = [self._get_uri("track", tid) for tid in items]
return self._post(
f"playlists/{plid}/tracks",
payload=ftracks,
position=position,
f"playlists/{plid}/tracks", payload=ftracks, position=position
)
def playlist_replace_items(self, playlist_id, items):
@ -816,9 +806,7 @@ class Spotify:
plid = self._get_id("playlist", playlist_id)
ftracks = [self._get_uri("track", tid) for tid in items]
payload = {"uris": ftracks}
return self._put(
f"playlists/{plid}/tracks", payload=payload
)
return self._put(f"playlists/{plid}/tracks", payload=payload)
def playlist_reorder_items(
self,
@ -847,9 +835,7 @@ class Spotify:
}
if snapshot_id:
payload["snapshot_id"] = snapshot_id
return self._put(
f"playlists/{plid}/tracks", payload=payload
)
return self._put(f"playlists/{plid}/tracks", payload=payload)
def playlist_remove_all_occurrences_of_items(
self, playlist_id, items, snapshot_id=None
@ -868,9 +854,7 @@ class Spotify:
payload = {"tracks": [{"uri": track} for track in ftracks]}
if snapshot_id:
payload["snapshot_id"] = snapshot_id
return self._delete(
f"playlists/{plid}/tracks", payload=payload
)
return self._delete(f"playlists/{plid}/tracks", payload=payload)
def playlist_remove_specific_occurrences_of_items(
self, playlist_id, items, snapshot_id=None
@ -888,20 +872,17 @@ class Spotify:
"""
plid = self._get_id("playlist", playlist_id)
ftracks = []
for tr in items:
ftracks.append(
ftracks = [
{
"uri": self._get_uri("track", tr["uri"]),
"positions": tr["positions"],
}
)
for tr in items
]
payload = {"tracks": ftracks}
if snapshot_id:
payload["snapshot_id"] = snapshot_id
return self._delete(
f"playlists/{plid}/tracks", payload=payload
)
return self._delete(f"playlists/{plid}/tracks", payload=payload)
def current_user_follow_playlist(self, playlist_id, public=True):
"""
@ -928,9 +909,8 @@ class Spotify:
if they follow the playlist. Maximum: 5 ids.
"""
endpoint = "playlists/{}/followers/contains?ids={}"
return self._get(
endpoint.format(playlist_id, ",".join(user_ids))
f"playlists/{playlist_id}/followers/contains?ids={','.join(user_ids)}"
)
def me(self):
@ -962,35 +942,41 @@ class Spotify:
"""
return self._get("me/albums", limit=limit, offset=offset, market=market)
def current_user_saved_albums_add(self, albums=[]):
def current_user_saved_albums_add(self, albums=None):
""" Add one or more albums to the current user's
"Your Music" library.
Parameters:
- albums - a list of album URIs, URLs or IDs
"""
if albums is None:
albums = []
alist = [self._get_id("album", a) for a in albums]
return self._put("me/albums?ids=" + ",".join(alist))
return self._put(f"me/albums?ids={','.join(alist)}")
def current_user_saved_albums_delete(self, albums=[]):
def current_user_saved_albums_delete(self, albums=None):
""" Remove one or more albums from the current user's
"Your Music" library.
Parameters:
- albums - a list of album URIs, URLs or IDs
"""
if albums is None:
albums = []
alist = [self._get_id("album", a) for a in albums]
return self._delete("me/albums/?ids=" + ",".join(alist))
return self._delete(f"me/albums/?ids={','.join(alist)}")
def current_user_saved_albums_contains(self, albums=[]):
def current_user_saved_albums_contains(self, albums=None):
""" Check if one or more albums is already saved in
the current Spotify users Your Music library.
Parameters:
- albums - a list of album URIs, URLs or IDs
"""
if albums is None:
albums = []
alist = [self._get_id("album", a) for a in albums]
return self._get("me/albums/contains?ids=" + ",".join(alist))
return self._get(f"me/albums/contains?ids={','.join(alist)}")
def current_user_saved_tracks(self, limit=20, offset=0, market=None):
""" Gets a list of the tracks saved in the current authorized user's
@ -1010,10 +996,8 @@ class Spotify:
Parameters:
- tracks - a list of track URIs, URLs or IDs
"""
tlist = []
if tracks is not None:
tlist = [self._get_id("track", t) for t in tracks]
return self._put("me/tracks/?ids=" + ",".join(tlist))
tlist = [] if tracks is None else [self._get_id("track", t) for t in tracks]
return self._put(f"me/tracks/?ids={','.join(tlist)}")
def current_user_saved_tracks_delete(self, tracks=None):
""" Remove one or more tracks from the current user's
@ -1022,10 +1006,8 @@ class Spotify:
Parameters:
- tracks - a list of track URIs, URLs or IDs
"""
tlist = []
if tracks is not None:
tlist = [self._get_id("track", t) for t in tracks]
return self._delete("me/tracks/?ids=" + ",".join(tlist))
tlist = [] if tracks is None else [self._get_id("track", t) for t in tracks]
return self._delete(f"me/tracks/?ids={','.join(tlist)}")
def current_user_saved_tracks_contains(self, tracks=None):
""" Check if one or more tracks is already saved in
@ -1034,10 +1016,8 @@ class Spotify:
Parameters:
- tracks - a list of track URIs, URLs or IDs
"""
tlist = []
if tracks is not None:
tlist = [self._get_id("track", t) for t in tracks]
return self._get("me/tracks/contains?ids=" + ",".join(tlist))
tlist = [] if tracks is None else [self._get_id("track", t) for t in tracks]
return self._get(f"me/tracks/contains?ids={','.join(tlist)}")
def current_user_saved_episodes(self, limit=20, offset=0, market=None):
""" Gets a list of the episodes saved in the current authorized user's
@ -1061,7 +1041,7 @@ class Spotify:
elist = []
if episodes is not None:
elist = [self._get_id("episode", e) for e in episodes]
return self._put("me/episodes/?ids=" + ",".join(elist))
return self._put(f"me/episodes/?ids={','.join(elist)}")
def current_user_saved_episodes_delete(self, episodes=None):
""" Remove one or more episodes from the current user's
@ -1073,7 +1053,7 @@ class Spotify:
elist = []
if episodes is not None:
elist = [self._get_id("episode", e) for e in episodes]
return self._delete("me/episodes/?ids=" + ",".join(elist))
return self._delete(f"me/episodes/?ids={','.join(elist)}")
def current_user_saved_episodes_contains(self, episodes=None):
""" Check if one or more episodes is already saved in
@ -1085,7 +1065,7 @@ class Spotify:
elist = []
if episodes is not None:
elist = [self._get_id("episode", e) for e in episodes]
return self._get("me/episodes/contains?ids=" + ",".join(elist))
return self._get(f"me/episodes/contains?ids={','.join(elist)}")
def current_user_saved_shows(self, limit=20, offset=0, market=None):
""" Gets a list of the shows saved in the current authorized user's
@ -1099,26 +1079,30 @@ class Spotify:
"""
return self._get("me/shows", limit=limit, offset=offset, market=market)
def current_user_saved_shows_add(self, shows=[]):
def current_user_saved_shows_add(self, shows=None):
""" Add one or more albums to the current user's
"Your Music" library.
Parameters:
- shows - a list of show URIs, URLs or IDs
"""
if shows is None:
shows = []
slist = [self._get_id("show", s) for s in shows]
return self._put("me/shows?ids=" + ",".join(slist))
return self._put(f"me/shows?ids={','.join(slist)}")
def current_user_saved_shows_delete(self, shows=[]):
def current_user_saved_shows_delete(self, shows=None):
""" Remove one or more shows from the current user's
"Your Music" library.
Parameters:
- shows - a list of show URIs, URLs or IDs
"""
if shows is None:
shows = []
slist = [self._get_id("show", s) for s in shows]
return self._delete("me/shows/?ids=" + ",".join(slist))
return self._delete(f"me/shows/?ids={','.join(slist)}")
def current_user_saved_shows_contains(self, shows=[]):
def current_user_saved_shows_contains(self, shows=None):
""" Check if one or more shows is already saved in
the current Spotify users Your Music library.
@ -1126,8 +1110,10 @@ class Spotify:
- shows - a list of show URIs, URLs or IDs
"""
if shows is None:
shows = []
slist = [self._get_id("show", s) for s in shows]
return self._get("me/shows/contains?ids=" + ",".join(slist))
return self._get(f"me/shows/contains?ids={','.join(slist)}")
def current_user_followed_artists(self, limit=20, after=None):
""" Gets a list of the artists followed by the current authorized user
@ -1138,9 +1124,7 @@ class Spotify:
request
"""
return self._get(
"me/following", type="artist", limit=limit, after=after
)
return self._get(f"me/following?type=artist&limit={limit}&after={after}")
def current_user_following_artists(self, ids=None):
""" Check if the current user is following certain artists
@ -1150,9 +1134,7 @@ class Spotify:
Parameters:
- ids - a list of artist URIs, URLs or IDs
"""
idlist = []
if ids is not None:
idlist = [self._get_id("artist", i) for i in ids]
idlist = [self._get_id("artist", i) for i in ids] if ids is not None else []
return self._get(
"me/following/contains", ids=",".join(idlist), type="artist"
)
@ -1165,9 +1147,7 @@ class Spotify:
Parameters:
- ids - a list of user URIs, URLs or IDs
"""
idlist = []
if ids is not None:
idlist = [self._get_id("user", i) for i in ids]
idlist = [self._get_id("user", i) for i in ids] if ids is not None else []
return self._get(
"me/following/contains", ids=",".join(idlist), type="user"
)
@ -1221,33 +1201,41 @@ class Spotify:
before=before,
)
def user_follow_artists(self, ids=[]):
def user_follow_artists(self, ids=None):
""" Follow one or more artists
Parameters:
- ids - a list of artist IDs
"""
return self._put("me/following?type=artist&ids=" + ",".join(ids))
if ids is None:
ids = []
return self._put(f"me/following?type=artist&ids={','.join(ids)}")
def user_follow_users(self, ids=[]):
def user_follow_users(self, ids=None):
""" Follow one or more users
Parameters:
- ids - a list of user IDs
"""
return self._put("me/following?type=user&ids=" + ",".join(ids))
if ids is None:
ids = []
return self._put(f"me/following?type=user&ids={','.join(ids)}")
def user_unfollow_artists(self, ids=[]):
def user_unfollow_artists(self, ids=None):
""" Unfollow one or more artists
Parameters:
- ids - a list of artist IDs
"""
return self._delete("me/following?type=artist&ids=" + ",".join(ids))
if ids is None:
ids = []
return self._delete(f"me/following?type=artist&ids={','.join(ids)}")
def user_unfollow_users(self, ids=[]):
def user_unfollow_users(self, ids=None):
""" Unfollow one or more users
Parameters:
- ids - a list of user IDs
"""
return self._delete("me/following?type=user&ids=" + ",".join(ids))
if ids is None:
ids = []
return self._delete(f"me/following?type=user&ids={','.join(ids)}")
def featured_playlists(
self, locale=None, country=None, timestamp=None, limit=20, offset=0
@ -1316,9 +1304,7 @@ class Spotify:
by an underscore.
"""
return self._get(
"browse/categories/" + category_id,
country=country,
locale=locale,
f"browse/categories/{category_id}", country=country, locale=locale
)
def categories(self, country=None, locale=None, limit=20, offset=0):
@ -1368,7 +1354,7 @@ class Spotify:
DeprecationWarning,
)
return self._get(
"browse/categories/" + category_id + "/playlists",
f"browse/categories/{category_id}/playlists",
country=country,
limit=limit,
offset=offset,
@ -1462,9 +1448,9 @@ class Spotify:
DeprecationWarning,
)
trid = self._get_id("track", track_id)
return self._get("audio-analysis/" + trid)
return self._get(f"audio-analysis/{trid}")
def audio_features(self, tracks=[]):
def audio_features(self, tracks=None):
""" Get audio features for one or multiple tracks based upon their Spotify IDs
Parameters:
- tracks - a list of track URIs, URLs or IDs, maximum: 100 ids
@ -1475,12 +1461,15 @@ class Spotify:
DeprecationWarning,
)
if tracks is None:
tracks = []
if isinstance(tracks, str):
trackid = self._get_id("track", tracks)
results = self._get("audio-features/?ids=" + trackid)
results = self._get(f"audio-features/?ids={trackid}")
else:
tlist = [self._get_id("track", t) for t in tracks]
results = self._get("audio-features/?ids=" + ",".join(tlist))
results = self._get(f"audio-features/?ids={','.join(tlist)}")
# the response has changed, look for the new style first, and if
# it's not there, fallback on the old style
if "audio_features" in results:
@ -1620,11 +1609,7 @@ class Spotify:
if state not in ["track", "context", "off"]:
logger.warning("Invalid state")
return
self._put(
self._append_device_id(
f"me/player/repeat?state={state}", device_id
)
)
self._put(self._append_device_id(f"me/player/repeat?state={state}", device_id))
def volume(self, volume_percent, device_id=None):
""" Set playback volume.
@ -1641,8 +1626,7 @@ class Spotify:
return
self._put(
self._append_device_id(
f"me/player/volume?volume_percent={volume_percent}",
device_id,
f"me/player/volume?volume_percent={volume_percent}", device_id
)
)
@ -1658,9 +1642,7 @@ class Spotify:
return
state = str(state).lower()
self._put(
self._append_device_id(
f"me/player/shuffle?state={state}", device_id
)
self._append_device_id(f"me/player/shuffle?state={state}", device_id)
)
def queue(self):
@ -1705,10 +1687,7 @@ class Spotify:
- device_id - device id to append
"""
if device_id:
if "?" in path:
path += f"&device_id={device_id}"
else:
path += f"?device_id={device_id}"
path += f"&device_id={device_id}" if "?" in path else f"?device_id={device_id}"
return path
def _get_id(self, type, id):
@ -1736,10 +1715,7 @@ class Spotify:
raise SpotifyException(400, -1, "Unsupported URL / URI.")
def _get_uri(self, type, id):
if self._is_uri(id):
return id
else:
return "spotify:" + type + ":" + self._get_id(type, id)
return id if self._is_uri(id) else f"spotify:{type}:{self._get_id(type, id)}"
def _is_uri(self, uri):
return re.search(Spotify._regex_spotify_uri, uri) is not None
@ -1748,13 +1724,12 @@ class Spotify:
if total and limit > total:
limit = total
warnings.warn(
"limit was auto-adjusted to equal {} as it must not be higher than total".format(
total),
f"limit was auto-adjusted to equal {total} as it must not be higher than total",
UserWarning,
)
results = defaultdict(dict)
item_types = [item_type + "s" for item_type in type.split(",")]
item_types = [f"{item_type}s" for item_type in type.split(",")]
count = 0
for country in markets:

View File

@ -19,14 +19,17 @@ from urllib.parse import parse_qsl, urlparse
from spotipy.cache_handler import CacheFileHandler, CacheHandler
from spotipy.exceptions import SpotifyOauthError, SpotifyStateError
from spotipy.util import CLIENT_CREDS_ENV_VARS, get_host_port, normalize_scope
from spotipy.util import CLIENT_CREDS_ENV_VARS, get_host_port
from spotipy.scope import Scope
from typing import Iterable
import re
logger = logging.getLogger(__name__)
def _make_authorization_headers(client_id, client_secret):
auth_header = base64.b64encode(
str(client_id + ":" + client_secret).encode("ascii")
f"{client_id}:{client_secret}".encode("ascii")
)
return {"Authorization": f"Basic {auth_header.decode('ascii')}"}
@ -41,18 +44,51 @@ def _ensure_value(value, env_key):
class SpotifyAuthBase:
def __init__(self, requests_session):
if isinstance(requests_session, requests.Session):
self._session = requests_session
else:
if requests_session: # Build a new session.
elif requests_session: # Build a new session.
self._session = requests.Session()
else: # Use the Requests API module as a "session".
from requests import api
self._session = api
def _normalize_scope(self, scope):
return normalize_scope(scope)
"""
Accepts a string of scopes, or an iterable with elements of type
`Scope` or `str` and returns a space-separated string of scopes.
Returns `None` if the argument is `None`.
"""
# TODO: do we need to sort the scopes?
if isinstance(scope, str):
# allow for any separator(s) between the scopes other than a word
# character or a hyphen
scopes = re.split(pattern=r"[^\w-]+", string=scope)
return " ".join(sorted(scopes))
if isinstance(scope, Iterable):
# Assume all of the iterable's elements are of the same type.
# If the iterable is empty, then return None.
first_element = next(iter(scope), None)
if isinstance(first_element, str):
return " ".join(sorted(scope))
if isinstance(first_element, Scope):
return Scope.make_string(scope)
if first_element is None:
return ""
elif scope is None:
return None
raise TypeError(
"Unsupported type for scopes: %s. Expected either a string of scopes, or "
"an Iterable with elements of type `Scope` or `str`." % type(scope)
)
@property
def client_id(self):
@ -169,9 +205,10 @@ class SpotifyClientCredentials(SpotifyAuthBase):
self.proxies = proxies
self.requests_timeout = requests_timeout
if cache_handler:
assert issubclass(cache_handler.__class__, CacheHandler), \
"cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \
+ " != " + str(CacheHandler)
assert issubclass(
cache_handler.__class__, CacheHandler
), f"cache_handler must be a subclass of CacheHandler:\
{str(type(cache_handler))} != {str(CacheHandler)}"
self.cache_handler = cache_handler
else:
self.cache_handler = CacheFileHandler()
@ -289,12 +326,14 @@ class SpotifyOAuth(SpotifyAuthBase):
self.scope = self._normalize_scope(scope)
if cache_handler:
assert issubclass(cache_handler.__class__, CacheHandler), \
"cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \
+ " != " + str(CacheHandler)
assert issubclass(
cache_handler.__class__, CacheHandler
), f"cache_handler must be a subclass of CacheHandler:\
{str(type(cache_handler))} != {str(CacheHandler)}"
self.cache_handler = cache_handler
else:
self.cache_handler = CacheFileHandler()
self.proxies = proxies
self.requests_timeout = requests_timeout
self.show_dialog = show_dialog
@ -345,18 +384,17 @@ class SpotifyOAuth(SpotifyAuthBase):
- url - the response url
"""
_, code = self.parse_auth_response_url(url)
if code is None:
return url
else:
return code
return url if code is None else code
@staticmethod
def parse_auth_response_url(url):
query_s = urlparse(url).query
form = dict(parse_qsl(query_s))
if "error" in form:
raise SpotifyOauthError(f"Received error from auth server: {form['error']}",
error=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"])
def _make_authorization_headers(self):
@ -376,10 +414,7 @@ class SpotifyOAuth(SpotifyAuthBase):
prompt = "Enter the URL you were redirected to: "
else:
url = self.get_authorize_url()
prompt = (
"Go to the following URL: {}\n"
"Enter the URL you were redirected to: ".format(url)
)
prompt = f"Go to the following URL: {url}\nEnter the URL you were redirected to: "
response = self._get_user_input(prompt)
state, code = SpotifyOAuth.parse_auth_response_url(response)
if self.state is not None and self.state != state:
@ -565,6 +600,16 @@ class SpotifyPKCE(SpotifyAuthBase):
* scope: Optional, either a string of scopes, or an iterable with elements of type
`Scope` or `str`. E.g.,
{Scope.user_modify_playback_state, Scope.user_library_read}
* cache_path: (deprecated) Optional, will otherwise be generated
(takes precedence over `username`)
* username: (deprecated) Optional or set as environment variable
(will set `cache_path` to `.cache-{username}`)
* proxies: Optional, proxy for the requests library to route through
* 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 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.
Optional, will otherwise use `CacheFileHandler`.
@ -575,7 +620,7 @@ class SpotifyPKCE(SpotifyAuthBase):
A false value disables sessions.
It should generally be a good idea to keep sessions enabled
for performance reasons (connection pooling).
* open_browser: Optional, thether or not the web browser should be opened to
* open_browser: Optional, whether the web browser should be opened to
authorize a user
"""
@ -584,12 +629,15 @@ class SpotifyPKCE(SpotifyAuthBase):
self.redirect_uri = redirect_uri
self.state = state
self.scope = self._normalize_scope(scope)
if cache_handler:
assert issubclass(type(cache_handler), CacheHandler), \
"type(cache_handler): " + str(type(cache_handler)) + " != " + str(CacheHandler)
assert issubclass(cache_handler.__class__, CacheHandler), \
"cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \
+ " != " + str(CacheHandler)
self.cache_handler = cache_handler
else:
self.cache_handler = CacheFileHandler()
self.proxies = proxies
self.requests_timeout = requests_timeout
@ -703,10 +751,7 @@ class SpotifyPKCE(SpotifyAuthBase):
prompt = "Enter the URL you were redirected to: "
else:
url = self.get_authorize_url()
prompt = (
"Go to the following URL: {}\n"
"Enter the URL you were redirected to: ".format(url)
)
prompt = f"Go to the following URL: {url}\nEnter the URL you were redirected to: "
response = self._get_user_input(prompt)
state, code = self.parse_auth_response_url(response)
if self.state is not None and self.state != state:
@ -843,10 +888,7 @@ class SpotifyPKCE(SpotifyAuthBase):
- url - the response url
"""
_, code = self.parse_auth_response_url(url)
if code is None:
return url
else:
return code
return url if code is None else code
@staticmethod
def parse_auth_response_url(url):

View File

@ -2,8 +2,6 @@ from __future__ import annotations
from spotipy.scope import Scope
""" Shows a user's playlists. This needs to be authenticated via OAuth. """
__all__ = ["CLIENT_CREDS_ENV_VARS", "get_host_port", "normalize_scope", "Retry"]
import logging
@ -70,6 +68,7 @@ class Retry(urllib3.Retry):
"""
Custom class for printing a warning when a rate/request limit is reached.
"""
def increment(
self,
method: str | None = None,

View File

@ -68,11 +68,9 @@ class AuthTestSpotipy(unittest.TestCase):
'spotify:audiobook:67VtmjZitn25TWocsyAEyh']
@classmethod
def setUpClass(self):
self.spotify = Spotify(
auth_manager=SpotifyClientCredentials()
)
self.spotify.trace = False
def setUpClass(cls):
cls.spotify = Spotify(auth_manager=SpotifyClientCredentials())
cls.spotify.trace = False
def test_artist_urn(self):
artist = self.spotify.artist(self.radiohead_urn)
@ -204,8 +202,10 @@ class AuthTestSpotipy(unittest.TestCase):
[0]['name'] == 'Weezer' for country in results_limited))
total_limited_results = 0
for country in results_limited:
total_limited_results += len(results_limited[country]['artists']['items'])
total_limited_results = sum(
len(results_limited[country]['artists']['items'])
for country in results_limited
)
self.assertTrue(total_limited_results <= total)
def test_multiple_types_search_with_multiple_markets(self):
@ -316,7 +316,7 @@ class AuthTestSpotipy(unittest.TestCase):
spotify_no_retry = Spotify(
auth_manager=SpotifyClientCredentials(),
retries=0)
for i in range(100):
for _ in range(100):
try:
spotify_no_retry.search(q='foo')
except SpotifyException as e:
@ -414,7 +414,7 @@ class AuthTestSpotipy(unittest.TestCase):
with self.assertRaises(SpotifyException) as cm:
self.spotify.user_playlist_create(
"spotify", "Best hits of the 90s")
self.assertTrue(cm.exception.http_status == 401 or cm.exception.http_status == 403)
self.assertTrue(cm.exception.http_status in [401, 403])
def test_custom_requests_session(self):
sess = requests.Session()

View File

@ -20,8 +20,8 @@ class SpotipyScopeTest(TestCase):
normalized_scope_string_2 = self.normalize_scope(scope_string)
self.assertEqual(scope_string, "")
self.assertEqual(normalized_scope_string, None)
self.assertEqual(normalized_scope_string_2, None)
self.assertEqual(normalized_scope_string, "")
self.assertEqual(normalized_scope_string_2, "")
converted_scopes = Scope.from_string(scope_string)
self.assertEqual(converted_scopes, set())
@ -82,10 +82,10 @@ class SpotipyScopeTest(TestCase):
def test_normalize_scope(self):
normalized_scope_string = self.normalize_scope([])
self.assertEqual(normalized_scope_string, None)
self.assertEqual(normalized_scope_string, "")
normalized_scope_string_2 = self.normalize_scope(())
self.assertEqual(normalized_scope_string_2, None)
self.assertEqual(normalized_scope_string_2, "")
self.assertIsNone(self.normalize_scope(None))