mirror of
https://github.com/spotipy-dev/spotipy.git
synced 2026-06-19 01:03:53 +00:00
parent
3c75886229
commit
5d0b8edff4
97
CHANGELOG.md
97
CHANGELOG.md
@ -12,46 +12,49 @@ Rebasing master onto v3 doesn't require a changelog update.
|
|||||||
|
|
||||||
### Added
|
### 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
|
### Changed
|
||||||
|
|
||||||
* Made `CacheHandler` an abstract base class
|
- 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.
|
- 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 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 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)
|
- [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.
|
- 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.
|
- 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 the `set_auth` and `auth_manager` properties with standard attributes.
|
||||||
|
- Replaced string concatenations and `str.format()` with f-strings
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
* Removed the following deprecated methods from `Spotify`:
|
- Removed the following deprecated methods from `Spotify`:
|
||||||
* `playlist_tracks`
|
- `playlist_tracks`
|
||||||
* `user_playlist`
|
- `user_playlist`
|
||||||
* `user_playlist_tracks`
|
- `user_playlist_tracks`
|
||||||
* `user_playlist_change_details`
|
- `user_playlist_change_details`
|
||||||
* `user_playlist_unfollow`
|
- `user_playlist_unfollow`
|
||||||
* `user_playlist_add_tracks`
|
- `user_playlist_add_tracks`
|
||||||
* `user_playlist_replace_tracks`
|
- `user_playlist_replace_tracks`
|
||||||
* `user_playlist_reorder_tracks`
|
- `user_playlist_reorder_tracks`
|
||||||
* `user_playlist_remove_all_occurrences_of_tracks`
|
- `user_playlist_remove_all_occurrences_of_tracks`
|
||||||
* `user_playlist_remove_specific_occurrences_of_tracks`
|
- `user_playlist_remove_specific_occurrences_of_tracks`
|
||||||
* `user_playlist_follow_playlist`
|
- `user_playlist_follow_playlist`
|
||||||
* `user_playlist_is_following`
|
- `user_playlist_is_following`
|
||||||
|
|
||||||
* Removed the deprecated `as_dict` parameter from the `get_access_token` method of `SpotifyOAuth` and `SpotifyPKCE`.
|
- 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 the deprecated `get_cached_token` and `_save_token_info` methods of `SpotifyOAuth` and `SpotifyPKCE`.
|
||||||
* Removed `SpotifyImplicitGrant`.
|
- Removed `SpotifyImplicitGrant`.
|
||||||
* Removed `prompt_for_user_token`.
|
- Removed `prompt_for_user_token`.
|
||||||
|
|
||||||
## Unreleased [2.x.x]
|
## Unreleased [2.x.x]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added examples for audiobooks, shows and episodes methods to examples directory
|
- Added examples for audiobooks, shows and episodes methods to examples directory
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed scripts in examples directory that didn't run correctly
|
- 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
|
- 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
|
## [2.25.0] - 2025-03-01
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added unit tests for queue functions
|
- Added unit tests for queue functions
|
||||||
- Added detailed function docstrings to 'util.py', including descriptions and special sections that lists arguments, returns, and raises.
|
- 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 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
|
- Added FAQ entry for inaccessible playlists
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Audiobook integration tests
|
- Audiobook integration tests
|
||||||
- Edited docstrings for certain functions in client.py for functions that are no longer in use and have been replaced.
|
- 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.
|
- `current_user_unfollow_playlist()` now supports playlist IDs, URLs, and URIs rather than previously where it only supported playlist IDs.
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
- `mock` no longer listed as a test dependency. Only built-in `unittest.mock` is actually used.
|
- `mock` no longer listed as a test dependency. Only built-in `unittest.mock` is actually used.
|
||||||
|
|
||||||
## [2.24.0] - 2024-05-30
|
## [2.24.0] - 2024-05-30
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added `MemcacheCacheHandler`, a cache handler that stores the token info using pymemcache.
|
- 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.
|
- Added integration tests for audiobook endpoints.
|
||||||
- Added `update` field to `current_user_follow_playlist`.
|
- Added `update` field to `current_user_follow_playlist`.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- 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__`
|
||||||
- Replaced `artist_albums(album_type=...)` with `artist_albums(include_groups=...)` due to an API change.
|
- 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
|
- Updated `_regex_spotify_url` to ignore `/intl-<countrycode>` in Spotify links
|
||||||
- Improved README, docs and examples
|
- Improved README, docs and examples
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Readthedocs build
|
- Readthedocs build
|
||||||
- Split `test_current_user_save_and_usave_tracks` unit test
|
- Split `test_current_user_save_and_usave_tracks` unit test
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
- Drop support for EOL Python 3.7
|
- Drop support for EOL Python 3.7
|
||||||
|
|
||||||
## [2.23.0] - 2023-04-07
|
## [2.23.0] - 2023-04-07
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added optional `encoder_cls` argument to `CacheFileHandler`, which overwrite default encoder for token before writing to disk
|
- 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)
|
- Integration tests for searching multiple types in multiple markets (non-user endpoints)
|
||||||
- Publish to PyPI action
|
- Publish to PyPI action
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed the regex for matching playlist URIs with the format spotify:user:USERNAME:playlist:PLAYLISTID.
|
- 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))
|
- `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))
|
||||||
|
|
||||||
@ -124,7 +136,7 @@ Rebasing master onto v3 doesn't require a changelog update.
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Add alternative module installation instruction to README
|
- Add alternative module installation instruction to README
|
||||||
- Added Comment to README - Getting Started for user to add URI to app in Spotify Developer Dashboard.
|
- 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
|
- Added playlist_add_tracks.py to example folder
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@ -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.
|
- 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`
|
- 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
|
## [2.18.0] - 2021-04-13
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@ -208,11 +216,11 @@ Rebasing master onto v3 doesn't require a changelog update.
|
|||||||
- Enabled using both short and long IDs for playlist_change_details
|
- Enabled using both short and long IDs for playlist_change_details
|
||||||
- Added a cache handler to `SpotifyClientCredentials`
|
- Added a cache handler to `SpotifyClientCredentials`
|
||||||
- Added the following endpoints
|
- Added the following endpoints
|
||||||
- `Spotify.current_user_saved_episodes`
|
- `Spotify.current_user_saved_episodes`
|
||||||
- `Spotify.current_user_saved_episodes_add`
|
- `Spotify.current_user_saved_episodes_add`
|
||||||
- `Spotify.current_user_saved_episodes_delete`
|
- `Spotify.current_user_saved_episodes_delete`
|
||||||
- `Spotify.current_user_saved_episodes_contains`
|
- `Spotify.current_user_saved_episodes_contains`
|
||||||
- `Spotify.available_markets`
|
- `Spotify.available_markets`
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@ -297,7 +305,7 @@ Rebasing master onto v3 doesn't require a changelog update.
|
|||||||
- Support to test whether the current user is following certain
|
- Support to test whether the current user is following certain
|
||||||
users or artists
|
users or artists
|
||||||
- Proper replacements for all deprecated playlist endpoints
|
- 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.
|
- 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
|
- Reason for 403 error in SpotifyException
|
||||||
- Support for the PKCE Auth Flow
|
- Support for the PKCE Auth Flow
|
||||||
@ -387,10 +395,10 @@ Rebasing master onto v3 doesn't require a changelog update.
|
|||||||
|
|
||||||
- Client retry logic has changed as it now uses urllib3's `Retry` in conjunction with requests `Session`
|
- Client retry logic has changed as it now uses urllib3's `Retry` in conjunction with requests `Session`
|
||||||
- The session is customizable as it allows for:
|
- The session is customizable as it allows for:
|
||||||
- status_forcelist
|
- status_forcelist
|
||||||
- retries
|
- retries
|
||||||
- status_retries
|
- status_retries
|
||||||
- backoff_factor
|
- backoff_factor
|
||||||
- Spin up a local webserver to autofill authentication URL
|
- Spin up a local webserver to autofill authentication URL
|
||||||
- Use session in SpotifyAuthBase
|
- Use session in SpotifyAuthBase
|
||||||
- Logging used instead of print statements
|
- Logging used instead of print statements
|
||||||
@ -405,9 +413,9 @@ Rebasing master onto v3 doesn't require a changelog update.
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Support for `add_to_queue`
|
- Support for `add_to_queue`
|
||||||
- **Parameters:**
|
- **Parameters:**
|
||||||
- track uri, id, or url
|
- track uri, id, or url
|
||||||
- device id. If None, then the active device is used.
|
- device id. If None, then the active device is used.
|
||||||
- Add CHANGELOG and LICENSE to released package
|
- Add CHANGELOG and LICENSE to released package
|
||||||
|
|
||||||
## [2.9.0] - 2020-02-15
|
## [2.9.0] - 2020-02-15
|
||||||
@ -504,6 +512,7 @@ Rebasing master onto v3 doesn't require a changelog update.
|
|||||||
### Changed
|
### 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
|
- 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
|
## [2.5.0] - 2020-01-11
|
||||||
|
|
||||||
Added follow and player endpoints
|
Added follow and player endpoints
|
||||||
|
|||||||
@ -18,7 +18,7 @@ $env:SPOTIPY_CLIENT_USERNAME="client_username_here"
|
|||||||
$env:SPOTIPY_REDIRECT_URI="http://localhost:8080"
|
$env:SPOTIPY_REDIRECT_URI="http://localhost:8080"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create virtual environment, install dependencies, run tests:
|
### Create virtual environment, install dependencies, run tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ virtualenv --python=python3 env
|
$ 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)
|
### Publishing (by maintainer)
|
||||||
|
|
||||||
- Bump version in setup.py
|
- Bump version in setup.py
|
||||||
- Bump and date changelog
|
- Bump and date changelog
|
||||||
- Add to changelog:
|
- Add to changelog:
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
Add your changes below.
|
Add your changes below.
|
||||||
@ -63,9 +63,8 @@ Don't forget to add a short description of your change in the [CHANGELOG](CHANGE
|
|||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
- Commit changes
|
- Commit changes
|
||||||
- Push tag to trigger PyPI build & release workflow
|
- Push tag to trigger PyPI build & release workflow
|
||||||
- Create github release https://github.com/plamere/spotipy/releases with the changelog content
|
- 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
|
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/>
|
||||||
|
|
||||||
|
|||||||
12
README.md
12
README.md
@ -1,6 +1,6 @@
|
|||||||
# Spotipy
|
# 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
|
||||||
|
|
||||||
 [](https://spotipy.readthedocs.io/en/latest/?badge=master) [](https://discord.gg/HP6xcPsTPJ)
|
 [](https://spotipy.readthedocs.io/en/latest/?badge=master) [](https://discord.gg/HP6xcPsTPJ)
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ Spotipy supports all of the features of the Spotify Web API including access to
|
|||||||
pip install spotipy
|
pip install spotipy
|
||||||
```
|
```
|
||||||
|
|
||||||
alternatively, for Windows users
|
alternatively, for Windows users
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
py -m pip install spotipy
|
py -m pip install spotipy
|
||||||
@ -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).
|
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
|
### Example without user authentication
|
||||||
|
|
||||||
@ -53,7 +53,9 @@ results = sp.search(q='weezer', limit=20)
|
|||||||
for idx, track in enumerate(results['tracks']['items']):
|
for idx, track in enumerate(results['tracks']['items']):
|
||||||
print(idx, track['name'])
|
print(idx, track['name'])
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected result:
|
Expected result:
|
||||||
|
|
||||||
```
|
```
|
||||||
0 Island In The Sun
|
0 Island In The Sun
|
||||||
1 Say It Ain't So
|
1 Say It Ain't So
|
||||||
@ -65,7 +67,6 @@ Expected result:
|
|||||||
19 Feels Like Summer
|
19 Feels Like Summer
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Example 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.
|
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']
|
track = item['track']
|
||||||
print(idx, track['artists'][0]['name'], " – ", track['name'])
|
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:
|
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
|
0 Post Malone – Sunflower - Spider-Man: Into the Spider-Verse
|
||||||
1 Taylor Swift – Red
|
1 Taylor Swift – Red
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Reporting Issues
|
## Reporting Issues
|
||||||
|
|
||||||
For common questions please check our [FAQ](FAQ.md).
|
For common questions please check our [FAQ](FAQ.md).
|
||||||
|
|||||||
23
TUTORIAL.md
23
TUTORIAL.md
@ -1,18 +1,20 @@
|
|||||||
# Spotipy Tutorial for Beginners
|
# 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.
|
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
|
## Prerequisites
|
||||||
|
|
||||||
In order to complete this tutorial successfully, there are a few things that you should already have installed:
|
In order to complete this tutorial successfully, there are a few things that you should already have installed:
|
||||||
|
|
||||||
**1. 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
|
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**
|
**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
|
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
|
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!
|
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
|
## 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.
|
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 Spotify’s Developer Dashboard.
|
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.
|
||||||
|
|
||||||
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.
|
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
|
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:
|
D. Paste the following code into your main.py file:
|
||||||
|
|
||||||
```
|
```
|
||||||
import spotipy
|
import spotipy
|
||||||
from spotipy.oauth2 import SpotifyOAuth
|
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",
|
redirect_uri="YOUR_APP_REDIRECT_URI",
|
||||||
scope="user-library-read"))
|
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.
|
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
|
## 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
|
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:
|
B. Add the URI as a variable in main.py. Notice the prefix added the URI:
|
||||||
|
|
||||||
```
|
```
|
||||||
taylor_uri = 'spotify:artist:06HL4z0CvFAxyc27GXpf02'
|
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.
|
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')
|
results = sp.artist_albums(taylor_uri, album_type='album')
|
||||||
albums = results['items']
|
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.
|
F. Return to your terminal - you should see all of Taylor's albums printed out there.
|
||||||
|
|
||||||
## Troubleshooting Tips
|
## Troubleshooting Tips
|
||||||
|
|
||||||
A. Command not found running the application "zsh: command not found: python"
|
A. Command not found running the application "zsh: command not found: python"
|
||||||
|
|
||||||
Check which Python version that you have by running the command:
|
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:
|
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:
|
B. Encountering package error:
|
||||||
|
|
||||||
If you are seeing an error "ModuleNotFoundError: No module named 'spotipy'", this means you have not installed the package.
|
If you are seeing an error "ModuleNotFoundError: No module named 'spotipy'", this means you have not installed the package.
|
||||||
Run the command:
|
Run the command:
|
||||||
|
|
||||||
```
|
```
|
||||||
pip install spotipy
|
pip install spotipy
|
||||||
```
|
```
|
||||||
|
|
||||||
After the package is installed, run the app again.
|
After the package is installed, run the app again.
|
||||||
|
|||||||
@ -246,4 +246,4 @@ texinfo_documents = [
|
|||||||
# texinfo_domain_indices = True
|
# texinfo_domain_indices = True
|
||||||
|
|
||||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||||
# texinfo_show_urls = 'footnote'
|
# texinfo_show_urls = 'footnote'
|
||||||
@ -377,4 +377,4 @@ Indices and tables
|
|||||||
|
|
||||||
* :ref:`genindex`
|
* :ref:`genindex`
|
||||||
* :ref:`modindex`
|
* :ref:`modindex`
|
||||||
* :ref:`search`
|
* :ref:`search`
|
||||||
@ -1,3 +1,3 @@
|
|||||||
Sphinx~=7.4.7
|
Sphinx~=7.4.7
|
||||||
sphinx-rtd-theme~=2.0.0
|
sphinx-rtd-theme~=2.0.0
|
||||||
redis>=3.5.3
|
redis>=3.5.3
|
||||||
@ -88,9 +88,7 @@ def currently_playing():
|
|||||||
return redirect('/')
|
return redirect('/')
|
||||||
spotify = spotipy.Spotify(auth_manager=auth_manager)
|
spotify = spotipy.Spotify(auth_manager=auth_manager)
|
||||||
track = spotify.current_user_playing_track()
|
track = spotify.current_user_playing_track()
|
||||||
if not track is None:
|
return track if track is not None else "No track currently playing."
|
||||||
return track
|
|
||||||
return "No track currently playing."
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/current_user')
|
@app.route('/current_user')
|
||||||
|
|||||||
@ -18,12 +18,9 @@ def get_args():
|
|||||||
|
|
||||||
|
|
||||||
def get_artist(name):
|
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']
|
items = results['artists']['items']
|
||||||
if len(items) > 0:
|
return items[0] if len(items) > 0 else None
|
||||||
return items[0]
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def show_artist_albums(artist):
|
def show_artist_albums(artist):
|
||||||
|
|||||||
@ -18,12 +18,9 @@ def get_args():
|
|||||||
|
|
||||||
|
|
||||||
def get_artist(name):
|
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']
|
items = results['artists']['items']
|
||||||
if len(items) > 0:
|
return items[0] if len(items) > 0 else None
|
||||||
return items[0]
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def show_album_tracks(album):
|
def show_album_tracks(album):
|
||||||
|
|||||||
@ -20,12 +20,9 @@ def get_args():
|
|||||||
|
|
||||||
|
|
||||||
def get_artist(name):
|
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']
|
items = results['artists']['items']
|
||||||
if len(items) > 0:
|
return items[0] if len(items) > 0 else None
|
||||||
return items[0]
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def show_recommendations_for_artist(artist):
|
def show_recommendations_for_artist(artist):
|
||||||
|
|||||||
12
examples/playlist_add_items.py
Normal file
12
examples/playlist_add_items.py
Normal 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'])
|
||||||
@ -25,6 +25,5 @@ for item in tracks:
|
|||||||
tracks.remove(item)
|
tracks.remove(item)
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
|
|
||||||
# print result
|
# print result
|
||||||
print("Playlist length: " + str(len(tracks)) + "\nExcluding: " + str(i))
|
print(f"Playlist length: {len(tracks)}\nExcluding: {i}")
|
||||||
|
|||||||
@ -14,8 +14,7 @@ if len(sys.argv) > 2:
|
|||||||
tid, pos = t_pos.split(',')
|
tid, pos = t_pos.split(',')
|
||||||
track_ids.append({"uri": tid, "positions": [int(pos)]})
|
track_ids.append({"uri": tid, "positions": [int(pos)]})
|
||||||
else:
|
else:
|
||||||
print(
|
print(f"Usage: {sys.argv[0]} playlist_id track_id,pos track_id,pos ...")
|
||||||
f"Usage: {sys.argv[0]} playlist_id track_id,pos track_id,pos ...")
|
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
scope = 'playlist-modify-public'
|
scope = 'playlist-modify-public'
|
||||||
|
|||||||
@ -5,11 +5,7 @@ import spotipy
|
|||||||
import sys
|
import sys
|
||||||
import pprint
|
import pprint
|
||||||
|
|
||||||
if len(sys.argv) > 1:
|
search_str = sys.argv[1] if len(sys.argv) > 1 else 'Radiohead'
|
||||||
search_str = sys.argv[1]
|
|
||||||
else:
|
|
||||||
search_str = 'Radiohead'
|
|
||||||
|
|
||||||
sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
|
sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
|
||||||
result = sp.search(search_str)
|
result = sp.search(search_str)
|
||||||
pprint.pprint(result)
|
pprint.pprint(result)
|
||||||
|
|||||||
@ -13,7 +13,4 @@ while response:
|
|||||||
for i, item in enumerate(playlists['items']):
|
for i, item in enumerate(playlists['items']):
|
||||||
print(playlists['offset'] + i, item['name'])
|
print(playlists['offset'] + i, item['name'])
|
||||||
|
|
||||||
if playlists['next']:
|
response = sp.next(playlists) if playlists['next'] else None
|
||||||
response = sp.next(playlists)
|
|
||||||
else:
|
|
||||||
response = None
|
|
||||||
|
|||||||
@ -12,7 +12,4 @@ while response:
|
|||||||
for i, item in enumerate(albums['items']):
|
for i, item in enumerate(albums['items']):
|
||||||
print(albums['offset'] + i, item['name'])
|
print(albums['offset'] + i, item['name'])
|
||||||
|
|
||||||
if albums['next']:
|
response = sp.next(albums) if albums['next'] else None
|
||||||
response = sp.next(albums)
|
|
||||||
else:
|
|
||||||
response = None
|
|
||||||
|
|||||||
@ -4,14 +4,10 @@ from spotipy.oauth2 import SpotifyClientCredentials
|
|||||||
import spotipy
|
import spotipy
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
if len(sys.argv) > 1:
|
artist_name = sys.argv[1] if len(sys.argv) > 1 else 'weezer'
|
||||||
artist_name = sys.argv[1]
|
|
||||||
else:
|
|
||||||
artist_name = 'weezer'
|
|
||||||
|
|
||||||
auth_manager = SpotifyClientCredentials()
|
auth_manager = SpotifyClientCredentials()
|
||||||
sp = spotipy.Spotify(auth_manager=auth_manager)
|
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:
|
try:
|
||||||
name = result['artists']['items'][0]['name']
|
name = result['artists']['items'][0]['name']
|
||||||
uri = result['artists']['items'][0]['uri']
|
uri = result['artists']['items'][0]['uri']
|
||||||
|
|||||||
@ -5,11 +5,7 @@ import spotipy
|
|||||||
import sys
|
import sys
|
||||||
import pprint
|
import pprint
|
||||||
|
|
||||||
if len(sys.argv) > 1:
|
username = sys.argv[1] if len(sys.argv) > 1 else 'plamere'
|
||||||
username = sys.argv[1]
|
|
||||||
else:
|
|
||||||
username = 'plamere'
|
|
||||||
|
|
||||||
auth_manager = SpotifyClientCredentials()
|
auth_manager = SpotifyClientCredentials()
|
||||||
sp = spotipy.Spotify(auth_manager=auth_manager)
|
sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||||
sp.trace = True
|
sp.trace = True
|
||||||
|
|||||||
@ -6,12 +6,8 @@ import spotipy
|
|||||||
|
|
||||||
sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
|
sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
|
||||||
|
|
||||||
if len(sys.argv) > 1:
|
name = ' '.join(sys.argv[1:]) if len(sys.argv) > 1 else 'Radiohead'
|
||||||
name = ' '.join(sys.argv[1:])
|
results = sp.search(q=f'artist:{name}', type='artist')
|
||||||
else:
|
|
||||||
name = 'Radiohead'
|
|
||||||
|
|
||||||
results = sp.search(q='artist:' + name, type='artist')
|
|
||||||
items = results['artists']['items']
|
items = results['artists']['items']
|
||||||
if len(items) > 0:
|
if len(items) > 0:
|
||||||
artist = items[0]
|
artist = items[0]
|
||||||
|
|||||||
@ -39,7 +39,5 @@ recommendations = sp.recommendations(
|
|||||||
# Display the recommendations
|
# Display the recommendations
|
||||||
for i, track in enumerate(recommendations['tracks']):
|
for i, track in enumerate(recommendations['tracks']):
|
||||||
print(
|
print(
|
||||||
"{}. {} by {}"
|
f"{i+1}. {track['name']} by {', '.join([artist['name'] for artist in track['artists']])}"
|
||||||
.format(i+1, track['name'], ', '
|
|
||||||
.join([artist['name'] for artist in track['artists']]))
|
|
||||||
)
|
)
|
||||||
|
|||||||
@ -9,11 +9,7 @@ from spotipy.oauth2 import SpotifyClientCredentials
|
|||||||
auth_manager = SpotifyClientCredentials()
|
auth_manager = SpotifyClientCredentials()
|
||||||
sp = spotipy.Spotify(auth_manager=auth_manager)
|
sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||||
|
|
||||||
user = 'spotify'
|
user = sys.argv[1] if len(sys.argv) > 1 else 'spotify'
|
||||||
|
|
||||||
if len(sys.argv) > 1:
|
|
||||||
user = sys.argv[1]
|
|
||||||
|
|
||||||
playlists = sp.user_playlists(user)
|
playlists = sp.user_playlists(user)
|
||||||
|
|
||||||
while playlists:
|
while playlists:
|
||||||
@ -25,7 +21,4 @@ while playlists:
|
|||||||
playlists['offset'],
|
playlists['offset'],
|
||||||
playlist['uri'],
|
playlist['uri'],
|
||||||
playlist['name']))
|
playlist['name']))
|
||||||
if playlists['next']:
|
playlists = sp.next(playlists) if playlists['next'] else None
|
||||||
playlists = sp.next(playlists)
|
|
||||||
else:
|
|
||||||
playlists = None
|
|
||||||
|
|||||||
@ -68,7 +68,7 @@ class CacheFileHandler(CacheHandler):
|
|||||||
cache_path = ".cache"
|
cache_path = ".cache"
|
||||||
username = (username or os.getenv(CLIENT_CREDS_ENV_VARS["client_username"]))
|
username = (username or os.getenv(CLIENT_CREDS_ENV_VARS["client_username"]))
|
||||||
if username:
|
if username:
|
||||||
cache_path += "-" + str(username)
|
cache_path += f"-{username}"
|
||||||
self.cache_path = cache_path
|
self.cache_path = cache_path
|
||||||
|
|
||||||
def get_cached_token(self):
|
def get_cached_token(self):
|
||||||
@ -82,9 +82,9 @@ class CacheFileHandler(CacheHandler):
|
|||||||
|
|
||||||
except OSError as error:
|
except OSError as error:
|
||||||
if error.errno == errno.ENOENT:
|
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:
|
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
|
return token_info
|
||||||
|
|
||||||
@ -94,8 +94,7 @@ class CacheFileHandler(CacheHandler):
|
|||||||
f.write(json.dumps(token_info, cls=self.encoder_cls))
|
f.write(json.dumps(token_info, cls=self.encoder_cls))
|
||||||
f.close()
|
f.close()
|
||||||
except OSError:
|
except OSError:
|
||||||
logger.warning('Couldn\'t write token to cache at: %s',
|
logger.warning(f'Couldn\'t write token to cache at: {self.cache_path}')
|
||||||
self.cache_path)
|
|
||||||
|
|
||||||
|
|
||||||
class MemoryCacheHandler(CacheHandler):
|
class MemoryCacheHandler(CacheHandler):
|
||||||
@ -148,7 +147,7 @@ class DjangoSessionCacheHandler(CacheHandler):
|
|||||||
try:
|
try:
|
||||||
self.request.session['token_info'] = token_info
|
self.request.session['token_info'] = token_info
|
||||||
except Exception as e:
|
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):
|
class FlaskSessionCacheHandler(CacheHandler):
|
||||||
@ -173,7 +172,7 @@ class FlaskSessionCacheHandler(CacheHandler):
|
|||||||
try:
|
try:
|
||||||
self.session["token_info"] = token_info
|
self.session["token_info"] = token_info
|
||||||
except Exception as e:
|
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):
|
class RedisCacheHandler(CacheHandler):
|
||||||
@ -199,7 +198,7 @@ class RedisCacheHandler(CacheHandler):
|
|||||||
if token_info:
|
if token_info:
|
||||||
return json.loads(token_info)
|
return json.loads(token_info)
|
||||||
except RedisError as e:
|
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
|
return token_info
|
||||||
|
|
||||||
@ -207,12 +206,13 @@ class RedisCacheHandler(CacheHandler):
|
|||||||
try:
|
try:
|
||||||
self.redis.set(self.key, json.dumps(token_info))
|
self.redis.set(self.key, json.dumps(token_info))
|
||||||
except RedisError as e:
|
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):
|
class MemcacheCacheHandler(CacheHandler):
|
||||||
"""A Cache handler that stores the token info in Memcache using the pymemcache client
|
"""A Cache handler that stores the token info in Memcache using the pymemcache client
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, memcache, key=None) -> None:
|
def __init__(self, memcache, key=None) -> None:
|
||||||
"""
|
"""
|
||||||
Parameters:
|
Parameters:
|
||||||
@ -222,7 +222,7 @@ class MemcacheCacheHandler(CacheHandler):
|
|||||||
(takes precedence over `token_info`)
|
(takes precedence over `token_info`)
|
||||||
"""
|
"""
|
||||||
self.memcache = memcache
|
self.memcache = memcache
|
||||||
self.key = key if key else 'token_info'
|
self.key = key or 'token_info'
|
||||||
|
|
||||||
def get_cached_token(self):
|
def get_cached_token(self):
|
||||||
from pymemcache import MemcacheError
|
from pymemcache import MemcacheError
|
||||||
@ -231,11 +231,11 @@ class MemcacheCacheHandler(CacheHandler):
|
|||||||
if token_info:
|
if token_info:
|
||||||
return json.loads(token_info.decode())
|
return json.loads(token_info.decode())
|
||||||
except MemcacheError as e:
|
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):
|
def save_token_to_cache(self, token_info):
|
||||||
from pymemcache import MemcacheError
|
from pymemcache import MemcacheError
|
||||||
try:
|
try:
|
||||||
self.memcache.set(self.key, json.dumps(token_info))
|
self.memcache.set(self.key, json.dumps(token_info))
|
||||||
except MemcacheError as e:
|
except MemcacheError as e:
|
||||||
logger.warning('Error saving token to cache' + str(e))
|
logger.warning(f'Error saving token to cache: {str(e)}')
|
||||||
|
|||||||
@ -189,11 +189,10 @@ class Spotify:
|
|||||||
|
|
||||||
if isinstance(requests_session, requests.Session):
|
if isinstance(requests_session, requests.Session):
|
||||||
self._session = requests_session
|
self._session = requests_session
|
||||||
else:
|
elif requests_session: # Build a new session.
|
||||||
if requests_session: # Build a new session.
|
self._build_session()
|
||||||
self._build_session()
|
else: # Use the Requests API module as a "session".
|
||||||
else: # Use the Requests API module as a "session".
|
self._session = requests.api
|
||||||
self._session = requests.api
|
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
"""Make sure the connection (pool) gets closed"""
|
"""Make sure the connection (pool) gets closed"""
|
||||||
@ -220,7 +219,7 @@ class Spotify:
|
|||||||
|
|
||||||
def _auth_headers(self):
|
def _auth_headers(self):
|
||||||
if self.access_token:
|
if self.access_token:
|
||||||
return {"Authorization": "Bearer {0}".format(self.access_token)}
|
return {"Authorization": f"Bearer {self.access_token}"}
|
||||||
if not self.auth_manager:
|
if not self.auth_manager:
|
||||||
return {}
|
return {}
|
||||||
try:
|
try:
|
||||||
@ -332,10 +331,7 @@ class Spotify:
|
|||||||
Parameters:
|
Parameters:
|
||||||
- result - a previously returned paged result
|
- result - a previously returned paged result
|
||||||
"""
|
"""
|
||||||
if result["next"]:
|
return self._get(result["next"]) if result["next"] else None
|
||||||
return self._get(result["next"])
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def previous(self, result):
|
def previous(self, result):
|
||||||
""" returns the previous result given a paged result
|
""" returns the previous result given a paged result
|
||||||
@ -343,10 +339,7 @@ class Spotify:
|
|||||||
Parameters:
|
Parameters:
|
||||||
- result - a previously returned paged result
|
- result - a previously returned paged result
|
||||||
"""
|
"""
|
||||||
if result["previous"]:
|
return self._get(result["previous"]) if result["previous"] else None
|
||||||
return self._get(result["previous"])
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def track(self, track_id, market=None):
|
def track(self, track_id, market=None):
|
||||||
""" returns a single track given the track's ID, URI or URL
|
""" 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)
|
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):
|
def tracks(self, tracks, market=None):
|
||||||
""" returns a list of tracks given a list of track IDs, URIs, or URLs
|
""" 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]
|
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):
|
def artist(self, artist_id):
|
||||||
""" returns a single artist given the artist's ID, URI or URL
|
""" 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)
|
trid = self._get_id("artist", artist_id)
|
||||||
return self._get("artists/" + trid)
|
return self._get(f"artists/{trid}")
|
||||||
|
|
||||||
def artists(self, artists):
|
def artists(self, artists):
|
||||||
""" returns a list of artists given the artist IDs, URIs, or URLs
|
""" 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]
|
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(
|
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
|
||||||
@ -416,7 +409,8 @@ class Spotify:
|
|||||||
|
|
||||||
trid = self._get_id("artist", artist_id)
|
trid = self._get_id("artist", artist_id)
|
||||||
return self._get(
|
return self._get(
|
||||||
"artists/" + trid + "/albums",
|
f"artists/{trid}/albums",
|
||||||
|
album_type=album_type,
|
||||||
include_groups=include_groups,
|
include_groups=include_groups,
|
||||||
country=country,
|
country=country,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
@ -433,7 +427,7 @@ class Spotify:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
trid = self._get_id("artist", artist_id)
|
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):
|
def artist_related_artists(self, artist_id):
|
||||||
""" Get Spotify catalog information about artists similar to an
|
""" Get Spotify catalog information about artists similar to an
|
||||||
@ -449,7 +443,7 @@ class Spotify:
|
|||||||
DeprecationWarning
|
DeprecationWarning
|
||||||
)
|
)
|
||||||
trid = self._get_id("artist", artist_id)
|
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):
|
def album(self, album_id, market=None):
|
||||||
""" returns a single album given the album's ID, URIs or URL
|
""" 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)
|
trid = self._get_id("album", album_id)
|
||||||
if market is not None:
|
if market is not None:
|
||||||
return self._get("albums/" + trid + '?market=' + market)
|
return self._get(f"albums/{trid}?market={market}")
|
||||||
else:
|
else:
|
||||||
return self._get("albums/" + trid)
|
return self._get(f"albums/{trid}")
|
||||||
|
|
||||||
def album_tracks(self, album_id, limit=50, offset=0, market=None):
|
def album_tracks(self, album_id, limit=50, offset=0, market=None):
|
||||||
""" Get Spotify catalog information about an album's tracks
|
""" Get Spotify catalog information about an album's tracks
|
||||||
@ -478,7 +472,7 @@ class Spotify:
|
|||||||
|
|
||||||
trid = self._get_id("album", album_id)
|
trid = self._get_id("album", album_id)
|
||||||
return self._get(
|
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):
|
def albums(self, albums, market=None):
|
||||||
@ -491,9 +485,9 @@ class Spotify:
|
|||||||
|
|
||||||
tlist = [self._get_id("album", a) for a in albums]
|
tlist = [self._get_id("album", a) for a in albums]
|
||||||
if market is not None:
|
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:
|
else:
|
||||||
return self._get("albums/?ids=" + ",".join(tlist))
|
return self._get(f"albums/?ids={','.join(tlist)}")
|
||||||
|
|
||||||
def show(self, show_id, market=None):
|
def show(self, show_id, market=None):
|
||||||
""" returns a single show given the show's ID, URIs or URL
|
""" 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)
|
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):
|
def shows(self, shows, market=None):
|
||||||
""" returns a list of shows given the show IDs, URIs, or URLs
|
""" 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]
|
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):
|
def show_episodes(self, show_id, limit=50, offset=0, market=None):
|
||||||
""" Get Spotify catalog information about a show's episodes
|
""" Get Spotify catalog information about a show's episodes
|
||||||
@ -541,7 +535,7 @@ class Spotify:
|
|||||||
|
|
||||||
trid = self._get_id("show", show_id)
|
trid = self._get_id("show", show_id)
|
||||||
return self._get(
|
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):
|
def episode(self, episode_id, market=None):
|
||||||
@ -557,7 +551,7 @@ class Spotify:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
trid = self._get_id("episode", episode_id)
|
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):
|
def episodes(self, episodes, market=None):
|
||||||
""" returns a list of episodes given the episode IDs, URIs, or URLs
|
""" 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]
|
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):
|
def search(self, q, limit=10, offset=0, type="track", market=None):
|
||||||
""" searches for an item
|
""" searches for an item
|
||||||
@ -616,7 +610,7 @@ class Spotify:
|
|||||||
if not markets:
|
if not markets:
|
||||||
markets = self.country_codes
|
markets = self.country_codes
|
||||||
|
|
||||||
if not (isinstance(markets, list) or isinstance(markets, tuple)):
|
if not (isinstance(markets, (list, tuple))):
|
||||||
markets = []
|
markets = []
|
||||||
|
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
@ -631,7 +625,7 @@ class Spotify:
|
|||||||
Parameters:
|
Parameters:
|
||||||
- user - the id of the usr
|
- 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):
|
def current_user_playlists(self, limit=50, offset=0):
|
||||||
""" Get current user playlists without required getting his profile
|
""" Get current user playlists without required getting his profile
|
||||||
@ -687,7 +681,7 @@ class Spotify:
|
|||||||
offset=offset,
|
offset=offset,
|
||||||
fields=fields,
|
fields=fields,
|
||||||
market=market,
|
market=market,
|
||||||
additional_types=",".join(additional_types)
|
additional_types=",".join(additional_types),
|
||||||
)
|
)
|
||||||
|
|
||||||
def playlist_cover_image(self, playlist_id):
|
def playlist_cover_image(self, playlist_id):
|
||||||
@ -722,9 +716,7 @@ class Spotify:
|
|||||||
- limit - the number of items to return
|
- limit - the number of items to return
|
||||||
- offset - the index of the first item to return
|
- offset - the index of the first item to return
|
||||||
"""
|
"""
|
||||||
return self._get(
|
return self._get(f"users/{user}/playlists", limit=limit, offset=offset)
|
||||||
f"users/{user}/playlists", limit=limit, offset=offset
|
|
||||||
)
|
|
||||||
|
|
||||||
def user_playlist_create(self, user, name, public=True, collaborative=False, description=""):
|
def user_playlist_create(self, user, name, public=True, collaborative=False, description=""):
|
||||||
""" Creates a playlist for a user
|
""" Creates a playlist for a user
|
||||||
@ -801,9 +793,7 @@ class Spotify:
|
|||||||
plid = self._get_id("playlist", playlist_id)
|
plid = self._get_id("playlist", playlist_id)
|
||||||
ftracks = [self._get_uri("track", tid) for tid in items]
|
ftracks = [self._get_uri("track", tid) for tid in items]
|
||||||
return self._post(
|
return self._post(
|
||||||
f"playlists/{plid}/tracks",
|
f"playlists/{plid}/tracks", payload=ftracks, position=position
|
||||||
payload=ftracks,
|
|
||||||
position=position,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def playlist_replace_items(self, playlist_id, items):
|
def playlist_replace_items(self, playlist_id, items):
|
||||||
@ -816,9 +806,7 @@ class Spotify:
|
|||||||
plid = self._get_id("playlist", playlist_id)
|
plid = self._get_id("playlist", playlist_id)
|
||||||
ftracks = [self._get_uri("track", tid) for tid in items]
|
ftracks = [self._get_uri("track", tid) for tid in items]
|
||||||
payload = {"uris": ftracks}
|
payload = {"uris": ftracks}
|
||||||
return self._put(
|
return self._put(f"playlists/{plid}/tracks", payload=payload)
|
||||||
f"playlists/{plid}/tracks", payload=payload
|
|
||||||
)
|
|
||||||
|
|
||||||
def playlist_reorder_items(
|
def playlist_reorder_items(
|
||||||
self,
|
self,
|
||||||
@ -847,9 +835,7 @@ class Spotify:
|
|||||||
}
|
}
|
||||||
if snapshot_id:
|
if snapshot_id:
|
||||||
payload["snapshot_id"] = snapshot_id
|
payload["snapshot_id"] = snapshot_id
|
||||||
return self._put(
|
return self._put(f"playlists/{plid}/tracks", payload=payload)
|
||||||
f"playlists/{plid}/tracks", payload=payload
|
|
||||||
)
|
|
||||||
|
|
||||||
def playlist_remove_all_occurrences_of_items(
|
def playlist_remove_all_occurrences_of_items(
|
||||||
self, playlist_id, items, snapshot_id=None
|
self, playlist_id, items, snapshot_id=None
|
||||||
@ -868,9 +854,7 @@ class Spotify:
|
|||||||
payload = {"tracks": [{"uri": track} for track in ftracks]}
|
payload = {"tracks": [{"uri": track} for track in ftracks]}
|
||||||
if snapshot_id:
|
if snapshot_id:
|
||||||
payload["snapshot_id"] = snapshot_id
|
payload["snapshot_id"] = snapshot_id
|
||||||
return self._delete(
|
return self._delete(f"playlists/{plid}/tracks", payload=payload)
|
||||||
f"playlists/{plid}/tracks", payload=payload
|
|
||||||
)
|
|
||||||
|
|
||||||
def playlist_remove_specific_occurrences_of_items(
|
def playlist_remove_specific_occurrences_of_items(
|
||||||
self, playlist_id, items, snapshot_id=None
|
self, playlist_id, items, snapshot_id=None
|
||||||
@ -888,20 +872,17 @@ class Spotify:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
plid = self._get_id("playlist", playlist_id)
|
plid = self._get_id("playlist", playlist_id)
|
||||||
ftracks = []
|
ftracks = [
|
||||||
for tr in items:
|
{
|
||||||
ftracks.append(
|
"uri": self._get_uri("track", tr["uri"]),
|
||||||
{
|
"positions": tr["positions"],
|
||||||
"uri": self._get_uri("track", tr["uri"]),
|
}
|
||||||
"positions": tr["positions"],
|
for tr in items
|
||||||
}
|
]
|
||||||
)
|
|
||||||
payload = {"tracks": ftracks}
|
payload = {"tracks": ftracks}
|
||||||
if snapshot_id:
|
if snapshot_id:
|
||||||
payload["snapshot_id"] = snapshot_id
|
payload["snapshot_id"] = snapshot_id
|
||||||
return self._delete(
|
return self._delete(f"playlists/{plid}/tracks", payload=payload)
|
||||||
f"playlists/{plid}/tracks", payload=payload
|
|
||||||
)
|
|
||||||
|
|
||||||
def current_user_follow_playlist(self, playlist_id, public=True):
|
def current_user_follow_playlist(self, playlist_id, public=True):
|
||||||
"""
|
"""
|
||||||
@ -928,9 +909,8 @@ class Spotify:
|
|||||||
if they follow the playlist. Maximum: 5 ids.
|
if they follow the playlist. Maximum: 5 ids.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
endpoint = "playlists/{}/followers/contains?ids={}"
|
|
||||||
return self._get(
|
return self._get(
|
||||||
endpoint.format(playlist_id, ",".join(user_ids))
|
f"playlists/{playlist_id}/followers/contains?ids={','.join(user_ids)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def me(self):
|
def me(self):
|
||||||
@ -962,35 +942,41 @@ class Spotify:
|
|||||||
"""
|
"""
|
||||||
return self._get("me/albums", limit=limit, offset=offset, market=market)
|
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
|
""" Add one or more albums to the current user's
|
||||||
"Your Music" library.
|
"Your Music" library.
|
||||||
Parameters:
|
Parameters:
|
||||||
- albums - a list of album URIs, URLs or IDs
|
- albums - a list of album URIs, URLs or IDs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if albums is None:
|
||||||
|
albums = []
|
||||||
alist = [self._get_id("album", a) for a in 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
|
""" Remove one or more albums from the current user's
|
||||||
"Your Music" library.
|
"Your Music" library.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
- albums - a list of album URIs, URLs or IDs
|
- albums - a list of album URIs, URLs or IDs
|
||||||
"""
|
"""
|
||||||
|
if albums is None:
|
||||||
|
albums = []
|
||||||
alist = [self._get_id("album", a) for a in 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
|
""" Check if one or more albums is already saved in
|
||||||
the current Spotify user’s “Your Music” library.
|
the current Spotify user’s “Your Music” library.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
- albums - a list of album URIs, URLs or IDs
|
- albums - a list of album URIs, URLs or IDs
|
||||||
"""
|
"""
|
||||||
|
if albums is None:
|
||||||
|
albums = []
|
||||||
alist = [self._get_id("album", a) for a in 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):
|
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
|
""" Gets a list of the tracks saved in the current authorized user's
|
||||||
@ -1010,10 +996,8 @@ class Spotify:
|
|||||||
Parameters:
|
Parameters:
|
||||||
- tracks - a list of track URIs, URLs or IDs
|
- tracks - a list of track URIs, URLs or IDs
|
||||||
"""
|
"""
|
||||||
tlist = []
|
tlist = [] if tracks is None else [self._get_id("track", t) for t in tracks]
|
||||||
if tracks is not None:
|
return self._put(f"me/tracks/?ids={','.join(tlist)}")
|
||||||
tlist = [self._get_id("track", t) for t in tracks]
|
|
||||||
return self._put("me/tracks/?ids=" + ",".join(tlist))
|
|
||||||
|
|
||||||
def current_user_saved_tracks_delete(self, tracks=None):
|
def current_user_saved_tracks_delete(self, tracks=None):
|
||||||
""" Remove one or more tracks from the current user's
|
""" Remove one or more tracks from the current user's
|
||||||
@ -1022,10 +1006,8 @@ class Spotify:
|
|||||||
Parameters:
|
Parameters:
|
||||||
- tracks - a list of track URIs, URLs or IDs
|
- tracks - a list of track URIs, URLs or IDs
|
||||||
"""
|
"""
|
||||||
tlist = []
|
tlist = [] if tracks is None else [self._get_id("track", t) for t in tracks]
|
||||||
if tracks is not None:
|
return self._delete(f"me/tracks/?ids={','.join(tlist)}")
|
||||||
tlist = [self._get_id("track", t) for t in tracks]
|
|
||||||
return self._delete("me/tracks/?ids=" + ",".join(tlist))
|
|
||||||
|
|
||||||
def current_user_saved_tracks_contains(self, tracks=None):
|
def current_user_saved_tracks_contains(self, tracks=None):
|
||||||
""" Check if one or more tracks is already saved in
|
""" Check if one or more tracks is already saved in
|
||||||
@ -1034,10 +1016,8 @@ class Spotify:
|
|||||||
Parameters:
|
Parameters:
|
||||||
- tracks - a list of track URIs, URLs or IDs
|
- tracks - a list of track URIs, URLs or IDs
|
||||||
"""
|
"""
|
||||||
tlist = []
|
tlist = [] if tracks is None else [self._get_id("track", t) for t in tracks]
|
||||||
if tracks is not None:
|
return self._get(f"me/tracks/contains?ids={','.join(tlist)}")
|
||||||
tlist = [self._get_id("track", t) for t in tracks]
|
|
||||||
return self._get("me/tracks/contains?ids=" + ",".join(tlist))
|
|
||||||
|
|
||||||
def current_user_saved_episodes(self, limit=20, offset=0, market=None):
|
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
|
""" Gets a list of the episodes saved in the current authorized user's
|
||||||
@ -1061,7 +1041,7 @@ class Spotify:
|
|||||||
elist = []
|
elist = []
|
||||||
if episodes is not None:
|
if episodes is not None:
|
||||||
elist = [self._get_id("episode", e) for e in episodes]
|
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):
|
def current_user_saved_episodes_delete(self, episodes=None):
|
||||||
""" Remove one or more episodes from the current user's
|
""" Remove one or more episodes from the current user's
|
||||||
@ -1073,7 +1053,7 @@ class Spotify:
|
|||||||
elist = []
|
elist = []
|
||||||
if episodes is not None:
|
if episodes is not None:
|
||||||
elist = [self._get_id("episode", e) for e in episodes]
|
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):
|
def current_user_saved_episodes_contains(self, episodes=None):
|
||||||
""" Check if one or more episodes is already saved in
|
""" Check if one or more episodes is already saved in
|
||||||
@ -1085,7 +1065,7 @@ class Spotify:
|
|||||||
elist = []
|
elist = []
|
||||||
if episodes is not None:
|
if episodes is not None:
|
||||||
elist = [self._get_id("episode", e) for e in episodes]
|
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):
|
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
|
""" 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)
|
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
|
""" Add one or more albums to the current user's
|
||||||
"Your Music" library.
|
"Your Music" library.
|
||||||
Parameters:
|
Parameters:
|
||||||
- shows - a list of show URIs, URLs or IDs
|
- shows - a list of show URIs, URLs or IDs
|
||||||
"""
|
"""
|
||||||
|
if shows is None:
|
||||||
|
shows = []
|
||||||
slist = [self._get_id("show", s) for s in 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
|
""" Remove one or more shows from the current user's
|
||||||
"Your Music" library.
|
"Your Music" library.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
- shows - a list of show URIs, URLs or IDs
|
- shows - a list of show URIs, URLs or IDs
|
||||||
"""
|
"""
|
||||||
|
if shows is None:
|
||||||
|
shows = []
|
||||||
slist = [self._get_id("show", s) for s in 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
|
""" Check if one or more shows is already saved in
|
||||||
the current Spotify user’s “Your Music” library.
|
the current Spotify user’s “Your Music” library.
|
||||||
|
|
||||||
@ -1126,8 +1110,10 @@ class Spotify:
|
|||||||
- shows - a list of show URIs, URLs or IDs
|
- shows - a list of show URIs, URLs or IDs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if shows is None:
|
||||||
|
shows = []
|
||||||
slist = [self._get_id("show", s) for s in 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):
|
def current_user_followed_artists(self, limit=20, after=None):
|
||||||
""" Gets a list of the artists followed by the current authorized user
|
""" Gets a list of the artists followed by the current authorized user
|
||||||
@ -1138,9 +1124,7 @@ class Spotify:
|
|||||||
request
|
request
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self._get(
|
return self._get(f"me/following?type=artist&limit={limit}&after={after}")
|
||||||
"me/following", type="artist", limit=limit, after=after
|
|
||||||
)
|
|
||||||
|
|
||||||
def current_user_following_artists(self, ids=None):
|
def current_user_following_artists(self, ids=None):
|
||||||
""" Check if the current user is following certain artists
|
""" Check if the current user is following certain artists
|
||||||
@ -1150,9 +1134,7 @@ class Spotify:
|
|||||||
Parameters:
|
Parameters:
|
||||||
- ids - a list of artist URIs, URLs or IDs
|
- ids - a list of artist URIs, URLs or IDs
|
||||||
"""
|
"""
|
||||||
idlist = []
|
idlist = [self._get_id("artist", i) for i in ids] if ids is not None else []
|
||||||
if ids is not None:
|
|
||||||
idlist = [self._get_id("artist", i) for i in ids]
|
|
||||||
return self._get(
|
return self._get(
|
||||||
"me/following/contains", ids=",".join(idlist), type="artist"
|
"me/following/contains", ids=",".join(idlist), type="artist"
|
||||||
)
|
)
|
||||||
@ -1165,9 +1147,7 @@ class Spotify:
|
|||||||
Parameters:
|
Parameters:
|
||||||
- ids - a list of user URIs, URLs or IDs
|
- ids - a list of user URIs, URLs or IDs
|
||||||
"""
|
"""
|
||||||
idlist = []
|
idlist = [self._get_id("user", i) for i in ids] if ids is not None else []
|
||||||
if ids is not None:
|
|
||||||
idlist = [self._get_id("user", i) for i in ids]
|
|
||||||
return self._get(
|
return self._get(
|
||||||
"me/following/contains", ids=",".join(idlist), type="user"
|
"me/following/contains", ids=",".join(idlist), type="user"
|
||||||
)
|
)
|
||||||
@ -1221,33 +1201,41 @@ class Spotify:
|
|||||||
before=before,
|
before=before,
|
||||||
)
|
)
|
||||||
|
|
||||||
def user_follow_artists(self, ids=[]):
|
def user_follow_artists(self, ids=None):
|
||||||
""" Follow one or more artists
|
""" Follow one or more artists
|
||||||
Parameters:
|
Parameters:
|
||||||
- ids - a list of artist IDs
|
- 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
|
""" Follow one or more users
|
||||||
Parameters:
|
Parameters:
|
||||||
- ids - a list of user IDs
|
- 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
|
""" Unfollow one or more artists
|
||||||
Parameters:
|
Parameters:
|
||||||
- ids - a list of artist IDs
|
- 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
|
""" Unfollow one or more users
|
||||||
Parameters:
|
Parameters:
|
||||||
- ids - a list of user IDs
|
- 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(
|
def featured_playlists(
|
||||||
self, locale=None, country=None, timestamp=None, limit=20, offset=0
|
self, locale=None, country=None, timestamp=None, limit=20, offset=0
|
||||||
@ -1316,9 +1304,7 @@ class Spotify:
|
|||||||
by an underscore.
|
by an underscore.
|
||||||
"""
|
"""
|
||||||
return self._get(
|
return self._get(
|
||||||
"browse/categories/" + category_id,
|
f"browse/categories/{category_id}", country=country, locale=locale
|
||||||
country=country,
|
|
||||||
locale=locale,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def categories(self, country=None, locale=None, limit=20, offset=0):
|
def categories(self, country=None, locale=None, limit=20, offset=0):
|
||||||
@ -1368,7 +1354,7 @@ class Spotify:
|
|||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
return self._get(
|
return self._get(
|
||||||
"browse/categories/" + category_id + "/playlists",
|
f"browse/categories/{category_id}/playlists",
|
||||||
country=country,
|
country=country,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
offset=offset,
|
offset=offset,
|
||||||
@ -1462,9 +1448,9 @@ class Spotify:
|
|||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
trid = self._get_id("track", track_id)
|
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
|
""" Get audio features for one or multiple tracks based upon their Spotify IDs
|
||||||
Parameters:
|
Parameters:
|
||||||
- tracks - a list of track URIs, URLs or IDs, maximum: 100 ids
|
- tracks - a list of track URIs, URLs or IDs, maximum: 100 ids
|
||||||
@ -1475,12 +1461,15 @@ class Spotify:
|
|||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if tracks is None:
|
||||||
|
tracks = []
|
||||||
|
|
||||||
if isinstance(tracks, str):
|
if isinstance(tracks, str):
|
||||||
trackid = self._get_id("track", tracks)
|
trackid = self._get_id("track", tracks)
|
||||||
results = self._get("audio-features/?ids=" + trackid)
|
results = self._get(f"audio-features/?ids={trackid}")
|
||||||
else:
|
else:
|
||||||
tlist = [self._get_id("track", t) for t in tracks]
|
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
|
# the response has changed, look for the new style first, and if
|
||||||
# it's not there, fallback on the old style
|
# it's not there, fallback on the old style
|
||||||
if "audio_features" in results:
|
if "audio_features" in results:
|
||||||
@ -1620,11 +1609,7 @@ class Spotify:
|
|||||||
if state not in ["track", "context", "off"]:
|
if state not in ["track", "context", "off"]:
|
||||||
logger.warning("Invalid state")
|
logger.warning("Invalid state")
|
||||||
return
|
return
|
||||||
self._put(
|
self._put(self._append_device_id(f"me/player/repeat?state={state}", device_id))
|
||||||
self._append_device_id(
|
|
||||||
f"me/player/repeat?state={state}", device_id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def volume(self, volume_percent, device_id=None):
|
def volume(self, volume_percent, device_id=None):
|
||||||
""" Set playback volume.
|
""" Set playback volume.
|
||||||
@ -1641,8 +1626,7 @@ class Spotify:
|
|||||||
return
|
return
|
||||||
self._put(
|
self._put(
|
||||||
self._append_device_id(
|
self._append_device_id(
|
||||||
f"me/player/volume?volume_percent={volume_percent}",
|
f"me/player/volume?volume_percent={volume_percent}", device_id
|
||||||
device_id,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1658,9 +1642,7 @@ class Spotify:
|
|||||||
return
|
return
|
||||||
state = str(state).lower()
|
state = str(state).lower()
|
||||||
self._put(
|
self._put(
|
||||||
self._append_device_id(
|
self._append_device_id(f"me/player/shuffle?state={state}", device_id)
|
||||||
f"me/player/shuffle?state={state}", device_id
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def queue(self):
|
def queue(self):
|
||||||
@ -1705,10 +1687,7 @@ class Spotify:
|
|||||||
- device_id - device id to append
|
- device_id - device id to append
|
||||||
"""
|
"""
|
||||||
if device_id:
|
if device_id:
|
||||||
if "?" in path:
|
path += f"&device_id={device_id}" if "?" in path else f"?device_id={device_id}"
|
||||||
path += f"&device_id={device_id}"
|
|
||||||
else:
|
|
||||||
path += f"?device_id={device_id}"
|
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def _get_id(self, type, id):
|
def _get_id(self, type, id):
|
||||||
@ -1736,10 +1715,7 @@ class Spotify:
|
|||||||
raise SpotifyException(400, -1, "Unsupported URL / URI.")
|
raise SpotifyException(400, -1, "Unsupported URL / URI.")
|
||||||
|
|
||||||
def _get_uri(self, type, id):
|
def _get_uri(self, type, id):
|
||||||
if self._is_uri(id):
|
return id if self._is_uri(id) else f"spotify:{type}:{self._get_id(type, id)}"
|
||||||
return id
|
|
||||||
else:
|
|
||||||
return "spotify:" + type + ":" + self._get_id(type, id)
|
|
||||||
|
|
||||||
def _is_uri(self, uri):
|
def _is_uri(self, uri):
|
||||||
return re.search(Spotify._regex_spotify_uri, uri) is not None
|
return re.search(Spotify._regex_spotify_uri, uri) is not None
|
||||||
@ -1748,13 +1724,12 @@ class Spotify:
|
|||||||
if total and limit > total:
|
if total and limit > total:
|
||||||
limit = total
|
limit = total
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"limit was auto-adjusted to equal {} as it must not be higher than total".format(
|
f"limit was auto-adjusted to equal {total} as it must not be higher than total",
|
||||||
total),
|
|
||||||
UserWarning,
|
UserWarning,
|
||||||
)
|
)
|
||||||
|
|
||||||
results = defaultdict(dict)
|
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
|
count = 0
|
||||||
|
|
||||||
for country in markets:
|
for country in markets:
|
||||||
|
|||||||
@ -19,14 +19,17 @@ from urllib.parse import parse_qsl, urlparse
|
|||||||
|
|
||||||
from spotipy.cache_handler import CacheFileHandler, CacheHandler
|
from spotipy.cache_handler import CacheFileHandler, CacheHandler
|
||||||
from spotipy.exceptions import SpotifyOauthError, SpotifyStateError
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _make_authorization_headers(client_id, client_secret):
|
def _make_authorization_headers(client_id, client_secret):
|
||||||
auth_header = base64.b64encode(
|
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')}"}
|
return {"Authorization": f"Basic {auth_header.decode('ascii')}"}
|
||||||
|
|
||||||
@ -41,18 +44,51 @@ def _ensure_value(value, env_key):
|
|||||||
|
|
||||||
|
|
||||||
class SpotifyAuthBase:
|
class SpotifyAuthBase:
|
||||||
|
|
||||||
def __init__(self, requests_session):
|
def __init__(self, requests_session):
|
||||||
if isinstance(requests_session, requests.Session):
|
if isinstance(requests_session, requests.Session):
|
||||||
self._session = requests_session
|
self._session = requests_session
|
||||||
else:
|
elif requests_session: # Build a new session.
|
||||||
if requests_session: # Build a new session.
|
self._session = requests.Session()
|
||||||
self._session = requests.Session()
|
else: # Use the Requests API module as a "session".
|
||||||
else: # Use the Requests API module as a "session".
|
from requests import api
|
||||||
from requests import api
|
self._session = api
|
||||||
self._session = api
|
|
||||||
|
|
||||||
def _normalize_scope(self, scope):
|
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
|
@property
|
||||||
def client_id(self):
|
def client_id(self):
|
||||||
@ -169,9 +205,10 @@ class SpotifyClientCredentials(SpotifyAuthBase):
|
|||||||
self.proxies = proxies
|
self.proxies = proxies
|
||||||
self.requests_timeout = requests_timeout
|
self.requests_timeout = requests_timeout
|
||||||
if cache_handler:
|
if cache_handler:
|
||||||
assert issubclass(cache_handler.__class__, CacheHandler), \
|
assert issubclass(
|
||||||
"cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \
|
cache_handler.__class__, CacheHandler
|
||||||
+ " != " + str(CacheHandler)
|
), f"cache_handler must be a subclass of CacheHandler:\
|
||||||
|
{str(type(cache_handler))} != {str(CacheHandler)}"
|
||||||
self.cache_handler = cache_handler
|
self.cache_handler = cache_handler
|
||||||
else:
|
else:
|
||||||
self.cache_handler = CacheFileHandler()
|
self.cache_handler = CacheFileHandler()
|
||||||
@ -289,12 +326,14 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
self.scope = self._normalize_scope(scope)
|
self.scope = self._normalize_scope(scope)
|
||||||
|
|
||||||
if cache_handler:
|
if cache_handler:
|
||||||
assert issubclass(cache_handler.__class__, CacheHandler), \
|
assert issubclass(
|
||||||
"cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \
|
cache_handler.__class__, CacheHandler
|
||||||
+ " != " + str(CacheHandler)
|
), f"cache_handler must be a subclass of CacheHandler:\
|
||||||
|
{str(type(cache_handler))} != {str(CacheHandler)}"
|
||||||
self.cache_handler = cache_handler
|
self.cache_handler = cache_handler
|
||||||
else:
|
else:
|
||||||
self.cache_handler = CacheFileHandler()
|
self.cache_handler = CacheFileHandler()
|
||||||
|
|
||||||
self.proxies = proxies
|
self.proxies = proxies
|
||||||
self.requests_timeout = requests_timeout
|
self.requests_timeout = requests_timeout
|
||||||
self.show_dialog = show_dialog
|
self.show_dialog = show_dialog
|
||||||
@ -345,18 +384,17 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
- url - the response url
|
- url - the response url
|
||||||
"""
|
"""
|
||||||
_, code = self.parse_auth_response_url(url)
|
_, code = self.parse_auth_response_url(url)
|
||||||
if code is None:
|
return url if code is None else code
|
||||||
return url
|
|
||||||
else:
|
|
||||||
return code
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_auth_response_url(url):
|
def parse_auth_response_url(url):
|
||||||
query_s = urlparse(url).query
|
query_s = urlparse(url).query
|
||||||
form = dict(parse_qsl(query_s))
|
form = dict(parse_qsl(query_s))
|
||||||
if "error" in form:
|
if "error" in form:
|
||||||
raise SpotifyOauthError(f"Received error from auth server: {form['error']}",
|
raise SpotifyOauthError(
|
||||||
error=form["error"])
|
f"Received error from auth server: {form['error']}",
|
||||||
|
error=form["error"]
|
||||||
|
)
|
||||||
return tuple(form.get(param) for param in ["state", "code"])
|
return tuple(form.get(param) for param in ["state", "code"])
|
||||||
|
|
||||||
def _make_authorization_headers(self):
|
def _make_authorization_headers(self):
|
||||||
@ -376,10 +414,7 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
prompt = "Enter the URL you were redirected to: "
|
prompt = "Enter the URL you were redirected to: "
|
||||||
else:
|
else:
|
||||||
url = self.get_authorize_url()
|
url = self.get_authorize_url()
|
||||||
prompt = (
|
prompt = f"Go to the following URL: {url}\nEnter the URL you were redirected to: "
|
||||||
"Go to the following URL: {}\n"
|
|
||||||
"Enter the URL you were redirected to: ".format(url)
|
|
||||||
)
|
|
||||||
response = self._get_user_input(prompt)
|
response = self._get_user_input(prompt)
|
||||||
state, code = SpotifyOAuth.parse_auth_response_url(response)
|
state, code = SpotifyOAuth.parse_auth_response_url(response)
|
||||||
if self.state is not None and self.state != state:
|
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: Optional, either a string of scopes, or an iterable with elements of type
|
||||||
`Scope` or `str`. E.g.,
|
`Scope` or `str`. E.g.,
|
||||||
{Scope.user_modify_playback_state, Scope.user_library_read}
|
{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
|
* cache_handler: An instance of the `CacheHandler` class to handle
|
||||||
getting and saving cached authorization tokens.
|
getting and saving cached authorization tokens.
|
||||||
Optional, will otherwise use `CacheFileHandler`.
|
Optional, will otherwise use `CacheFileHandler`.
|
||||||
@ -575,7 +620,7 @@ class SpotifyPKCE(SpotifyAuthBase):
|
|||||||
A false value disables sessions.
|
A false value disables sessions.
|
||||||
It should generally be a good idea to keep sessions enabled
|
It should generally be a good idea to keep sessions enabled
|
||||||
for performance reasons (connection pooling).
|
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
|
authorize a user
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -584,12 +629,15 @@ class SpotifyPKCE(SpotifyAuthBase):
|
|||||||
self.redirect_uri = redirect_uri
|
self.redirect_uri = redirect_uri
|
||||||
self.state = state
|
self.state = state
|
||||||
self.scope = self._normalize_scope(scope)
|
self.scope = self._normalize_scope(scope)
|
||||||
|
|
||||||
if cache_handler:
|
if cache_handler:
|
||||||
assert issubclass(type(cache_handler), CacheHandler), \
|
assert issubclass(cache_handler.__class__, CacheHandler), \
|
||||||
"type(cache_handler): " + str(type(cache_handler)) + " != " + str(CacheHandler)
|
"cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \
|
||||||
|
+ " != " + str(CacheHandler)
|
||||||
self.cache_handler = cache_handler
|
self.cache_handler = cache_handler
|
||||||
else:
|
else:
|
||||||
self.cache_handler = CacheFileHandler()
|
self.cache_handler = CacheFileHandler()
|
||||||
|
|
||||||
self.proxies = proxies
|
self.proxies = proxies
|
||||||
self.requests_timeout = requests_timeout
|
self.requests_timeout = requests_timeout
|
||||||
|
|
||||||
@ -703,10 +751,7 @@ class SpotifyPKCE(SpotifyAuthBase):
|
|||||||
prompt = "Enter the URL you were redirected to: "
|
prompt = "Enter the URL you were redirected to: "
|
||||||
else:
|
else:
|
||||||
url = self.get_authorize_url()
|
url = self.get_authorize_url()
|
||||||
prompt = (
|
prompt = f"Go to the following URL: {url}\nEnter the URL you were redirected to: "
|
||||||
"Go to the following URL: {}\n"
|
|
||||||
"Enter the URL you were redirected to: ".format(url)
|
|
||||||
)
|
|
||||||
response = self._get_user_input(prompt)
|
response = self._get_user_input(prompt)
|
||||||
state, code = self.parse_auth_response_url(response)
|
state, code = self.parse_auth_response_url(response)
|
||||||
if self.state is not None and self.state != state:
|
if self.state is not None and self.state != state:
|
||||||
@ -843,10 +888,7 @@ class SpotifyPKCE(SpotifyAuthBase):
|
|||||||
- url - the response url
|
- url - the response url
|
||||||
"""
|
"""
|
||||||
_, code = self.parse_auth_response_url(url)
|
_, code = self.parse_auth_response_url(url)
|
||||||
if code is None:
|
return url if code is None else code
|
||||||
return url
|
|
||||||
else:
|
|
||||||
return code
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_auth_response_url(url):
|
def parse_auth_response_url(url):
|
||||||
|
|||||||
@ -2,8 +2,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from spotipy.scope import Scope
|
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"]
|
__all__ = ["CLIENT_CREDS_ENV_VARS", "get_host_port", "normalize_scope", "Retry"]
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@ -70,6 +68,7 @@ class Retry(urllib3.Retry):
|
|||||||
"""
|
"""
|
||||||
Custom class for printing a warning when a rate/request limit is reached.
|
Custom class for printing a warning when a rate/request limit is reached.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def increment(
|
def increment(
|
||||||
self,
|
self,
|
||||||
method: str | None = None,
|
method: str | None = None,
|
||||||
|
|||||||
@ -68,11 +68,9 @@ class AuthTestSpotipy(unittest.TestCase):
|
|||||||
'spotify:audiobook:67VtmjZitn25TWocsyAEyh']
|
'spotify:audiobook:67VtmjZitn25TWocsyAEyh']
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(self):
|
def setUpClass(cls):
|
||||||
self.spotify = Spotify(
|
cls.spotify = Spotify(auth_manager=SpotifyClientCredentials())
|
||||||
auth_manager=SpotifyClientCredentials()
|
cls.spotify.trace = False
|
||||||
)
|
|
||||||
self.spotify.trace = False
|
|
||||||
|
|
||||||
def test_artist_urn(self):
|
def test_artist_urn(self):
|
||||||
artist = self.spotify.artist(self.radiohead_urn)
|
artist = self.spotify.artist(self.radiohead_urn)
|
||||||
@ -204,8 +202,10 @@ class AuthTestSpotipy(unittest.TestCase):
|
|||||||
[0]['name'] == 'Weezer' for country in results_limited))
|
[0]['name'] == 'Weezer' for country in results_limited))
|
||||||
|
|
||||||
total_limited_results = 0
|
total_limited_results = 0
|
||||||
for country in results_limited:
|
total_limited_results = sum(
|
||||||
total_limited_results += len(results_limited[country]['artists']['items'])
|
len(results_limited[country]['artists']['items'])
|
||||||
|
for country in results_limited
|
||||||
|
)
|
||||||
self.assertTrue(total_limited_results <= total)
|
self.assertTrue(total_limited_results <= total)
|
||||||
|
|
||||||
def test_multiple_types_search_with_multiple_markets(self):
|
def test_multiple_types_search_with_multiple_markets(self):
|
||||||
@ -316,7 +316,7 @@ class AuthTestSpotipy(unittest.TestCase):
|
|||||||
spotify_no_retry = Spotify(
|
spotify_no_retry = Spotify(
|
||||||
auth_manager=SpotifyClientCredentials(),
|
auth_manager=SpotifyClientCredentials(),
|
||||||
retries=0)
|
retries=0)
|
||||||
for i in range(100):
|
for _ in range(100):
|
||||||
try:
|
try:
|
||||||
spotify_no_retry.search(q='foo')
|
spotify_no_retry.search(q='foo')
|
||||||
except SpotifyException as e:
|
except SpotifyException as e:
|
||||||
@ -414,7 +414,7 @@ class AuthTestSpotipy(unittest.TestCase):
|
|||||||
with self.assertRaises(SpotifyException) as cm:
|
with self.assertRaises(SpotifyException) as cm:
|
||||||
self.spotify.user_playlist_create(
|
self.spotify.user_playlist_create(
|
||||||
"spotify", "Best hits of the 90s")
|
"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):
|
def test_custom_requests_session(self):
|
||||||
sess = requests.Session()
|
sess = requests.Session()
|
||||||
|
|||||||
@ -20,8 +20,8 @@ class SpotipyScopeTest(TestCase):
|
|||||||
normalized_scope_string_2 = self.normalize_scope(scope_string)
|
normalized_scope_string_2 = self.normalize_scope(scope_string)
|
||||||
|
|
||||||
self.assertEqual(scope_string, "")
|
self.assertEqual(scope_string, "")
|
||||||
self.assertEqual(normalized_scope_string, None)
|
self.assertEqual(normalized_scope_string, "")
|
||||||
self.assertEqual(normalized_scope_string_2, None)
|
self.assertEqual(normalized_scope_string_2, "")
|
||||||
|
|
||||||
converted_scopes = Scope.from_string(scope_string)
|
converted_scopes = Scope.from_string(scope_string)
|
||||||
self.assertEqual(converted_scopes, set())
|
self.assertEqual(converted_scopes, set())
|
||||||
@ -82,10 +82,10 @@ class SpotipyScopeTest(TestCase):
|
|||||||
def test_normalize_scope(self):
|
def test_normalize_scope(self):
|
||||||
|
|
||||||
normalized_scope_string = self.normalize_scope([])
|
normalized_scope_string = self.normalize_scope([])
|
||||||
self.assertEqual(normalized_scope_string, None)
|
self.assertEqual(normalized_scope_string, "")
|
||||||
|
|
||||||
normalized_scope_string_2 = self.normalize_scope(())
|
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))
|
self.assertIsNone(self.normalize_scope(None))
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user