Sync branches (#920)

This commit is contained in:
Stéphane Bruckert 2022-12-26 16:17:09 +01:00 committed by GitHub
parent d7e8dd1e74
commit 4918305aba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 964 additions and 366 deletions

25
.github/workflows/integration_tests.yml vendored Normal file
View File

@ -0,0 +1,25 @@
name: Integration tests
on: [push, pull_request_target]
jobs:
build:
runs-on: ubuntu-latest
env:
SPOTIPY_CLIENT_ID: ${{ secrets.SPOTIPY_CLIENT_ID }}
SPOTIPY_CLIENT_SECRET: ${{ secrets.SPOTIPY_CLIENT_SECRET }}
PYTHON_VERSION: "3.10"
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v1
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install .[test]
- name: Run non user endpoints integration tests
run: |
python -m unittest discover -v tests/integration/non_user_endpoints

View File

@ -5,26 +5,25 @@ on: [push, pull_request]
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-20.04
strategy: strategy:
matrix: matrix:
python-version: [3.5, 3.6, 3.7, 3.8, 3.9] python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1 uses: actions/setup-python@v1
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install .[test] pip install .[test]
- name: Lint with flake8 - name: Lint with flake8
run: | run: |
pip install -Iv enum34==1.1.6 # https://bitbucket.org/stoneleaf/enum34/issues/27/enum34-118-broken pip install -Iv enum34==1.1.6 # https://bitbucket.org/stoneleaf/enum34/issues/27/enum34-118-broken
pip install flake8 pip install flake8
flake8 . --count --show-source --statistics flake8 . --count --show-source --statistics
- name: Run unit tests - name: Run unit tests
run: | run: |
python -m unittest discover -v tests/unit python -m unittest discover -v tests/unit

View File

@ -50,11 +50,76 @@ While this is unreleased, please only add v3 features here. Rebasing master onto
### Added ### Added
* Added `MemoryCacheHandler`, a cache handler that simply stores the token info in memory as an instance attribute of this class. - 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 playlist_add_tracks.py to example folder
### Fixed
- Modified docstring for playlist_add_items() to accept "only URIs or URLs",
with intended deprecation for IDs in v3
- Update contributing.md
### Removed
## [2.22.0] - 2022-12-10
### Added
- Integration tests via GHA (non-user endpoints)
- Unit tests for new releases, passing limit parameter with minimum and maximum values of 1 and 50
- Unit tests for categories, omitting country code to test global releases
- Added `CODE_OF_CONDUCT.md`
### Fixed ### Fixed
* Fixed a bug in `CacheFileHandler.__init__`: The documentation says that the username will be retrieved from the environment, but it wasn't. - Incorrect `category_id` input for test_category
- Assertion value for `test_categories_limit_low` and `test_categories_limit_high`
- Pin Github Actions Runner to Ubuntu 20 for Py27
- Fixed potential error where `found` variable in `test_artist_related_artists` is undefined if for loop never evaluates to true
- Fixed false positive test `test_new_releases` which looks up the wrong property of the JSON response object and always evaluates to true
## [2.21.0] - 2022-09-26
### Added
- Added `market` parameter to `album` and `albums` to address ([#753](https://github.com/plamere/spotipy/issues/753)
- Added `show_featured_artists.py` to `/examples`.
- Expanded contribution and license sections of the documentation.
- Added `FlaskSessionCacheHandler`, a cache handler that stores the token info in a flask session.
- Added Python 3.10 in GitHub Actions
### Fixed
- Updated the documentation to specify ISO-639-1 language codes.
- Fix `AttributeError` for `text` attribute of the `Response` object
- Require redis v3 if python2.7 (fixes readthedocs)
## [2.20.0] - 2022-06-18
### Added
- Added `RedisCacheHandler`, a cache handler that stores the token info in Redis.
- Changed URI handling in `client.Spotify._get_id()` to remove qureies if provided by error.
- Added a new parameter to `RedisCacheHandler` to allow custom keys (instead of the default `token_info` key)
- Simplify check for existing token in `RedisCacheHandler`
### Changed
- Removed Python 3.5 and added Python 3.9 in Github Action
## [2.19.0] - 2021-08-12
### Added
- Added `MemoryCacheHandler`, a cache handler that simply stores the token info in memory as an instance attribute of this class.
- If a network request returns an error status code but the response body cannot be decoded into JSON, then fall back on decoding the body into a string.
- Added `DjangoSessionCacheHandler`, a cache handler that stores the token in the session framework provided by Django. Web apps using spotipy with Django can directly use this for cache handling.
### Fixed
- Fixed a bug in `CacheFileHandler.__init__`: The documentation says that the username will be retrieved from the environment, but it wasn't.
- Fixed a bug in the initializers for the auth managers that produced a spurious warning message if you provide a cache handler and you set a value for the "SPOTIPY_CLIENT_USERNAME" environment variable.
- Use generated MIT license and fix license type in `pip show`
## [2.18.0] - 2021-04-13 ## [2.18.0] - 2021-04-13
@ -63,11 +128,11 @@ While this is unreleased, please only add v3 features here. Rebasing master onto
- 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
@ -75,9 +140,9 @@ While this is unreleased, please only add v3 features here. Rebasing master onto
### Fixed ### Fixed
* Fixed the bugs in `SpotifyOAuth.refresh_access_token` and `SpotifyPKCE.refresh_access_token` which raised the incorrect exception upon receiving an error response from the server. This addresses #645. - Fixed the bugs in `SpotifyOAuth.refresh_access_token` and `SpotifyPKCE.refresh_access_token` which raised the incorrect exception upon receiving an error response from the server. This addresses #645.
* Fixed a bug in `RequestHandler.do_GET` in which the non-existent `state` attribute of `SpotifyOauthError` is accessed. This bug occurs when the user clicks "cancel" in the permissions dialog that opens in the browser. - Fixed a bug in `RequestHandler.do_GET` in which the non-existent `state` attribute of `SpotifyOauthError` is accessed. This bug occurs when the user clicks "cancel" in the permissions dialog that opens in the browser.
* Cleaned up the documentation for `SpotifyClientCredentials.__init__`, `SpotifyOAuth.__init__`, and `SpotifyPKCE.__init__`. - Cleaned up the documentation for `SpotifyClientCredentials.__init__`, `SpotifyOAuth.__init__`, and `SpotifyPKCE.__init__`.
## [2.17.1] - 2021-02-28 ## [2.17.1] - 2021-02-28
@ -148,10 +213,10 @@ While this is unreleased, please only add v3 features here. Rebasing master onto
### Added ### Added
- (experimental) Support to search multiple/all markets at once. - (experimental) Support to search multiple/all markets at once.
- 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
@ -186,176 +251,175 @@ While this is unreleased, please only add v3 features here. Rebasing master onto
### Added ### Added
- Added `SpotifyImplicitGrant` as an auth manager option. It provides - Added `SpotifyImplicitGrant` as an auth manager option. It provides
user authentication without a client secret but sacrifices the ability user authentication without a client secret but sacrifices the ability
to refresh the token without user input. (However, read the class to refresh the token without user input. (However, read the class
docstring for security advisory.) docstring for security advisory.)
- Added built-in verification of the `state` query parameter - Added built-in verification of the `state` query parameter
- Added two new attributes: error and error_description to `SpotifyOauthError` exception class to show - Added two new attributes: error and error_description to `SpotifyOauthError` exception class to show
authorization/authentication web api errors details. authorization/authentication web api errors details.
- Added `SpotifyStateError` subclass of `SpotifyOauthError` - Added `SpotifyStateError` subclass of `SpotifyOauthError`
- Allow extending `SpotifyClientCredentials` and `SpotifyOAuth` - Allow extending `SpotifyClientCredentials` and `SpotifyOAuth`
- Added the market paramter to `album_tracks` - Added the market paramter to `album_tracks`
### Deprecated ### Deprecated
- Deprecated `util.prompt_for_user_token` in favor of `spotipy.Spotify(auth_manager=SpotifyOAuth())` - Deprecated `util.prompt_for_user_token` in favor of `spotipy.Spotify(auth_manager=SpotifyOAuth())`
## [2.12.0] - 2020-04-26 ## [2.12.0] - 2020-04-26
### Added ### Added
- Added a method to update the auth token. - Added a method to update the auth token.
### Fixed ### Fixed
- Logging regression due to the addition of `logging.basicConfig()` which was unneeded. - Logging regression due to the addition of `logging.basicConfig()` which was unneeded.
## [2.11.2] - 2020-04-19 ## [2.11.2] - 2020-04-19
### Changed ### Changed
- Updated the documentation to give more details on the authorization process and reflect - Updated the documentation to give more details on the authorization process and reflect
2020 Spotify Application jargon and practices. 2020 Spotify Application jargon and practices.
- The local webserver is only started for localhost redirect_uri which specify a port, - The local webserver is only started for localhost redirect_uri which specify a port,
i.e. it is started for `http://localhost:8080` or `http://127.0.0.1:8080`, not for `http://localhost`. i.e. it is started for `http://localhost:8080` or `http://127.0.0.1:8080`, not for `http://localhost`.
### Fixed ### Fixed
- Issue where using `http://localhost` as redirect_uri would cause the authorization process to hang. - Issue where using `http://localhost` as redirect_uri would cause the authorization process to hang.
## [2.11.1] - 2020-04-11 ## [2.11.1] - 2020-04-11
### Fixed ### Fixed
- Fixed miscellaneous issues with parsing of callback URL - Fixed miscellaneous issues with parsing of callback URL
## [2.11.0] - 2020-04-11 ## [2.11.0] - 2020-04-11
### Added ### Added
- Support for shows/podcasts and episodes - Support for shows/podcasts and episodes
- Added CONTRIBUTING.md - Added CONTRIBUTING.md
### Changed ### Changed
- 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 auto-fill authentication URL - Spin up a local webserver to auto-fill authentication URL
- Use session in SpotifyAuthBase - Use session in SpotifyAuthBase
- Logging used instead of print statements - Logging used instead of print statements
### Fixed ### Fixed
- Close session when Spotipy object is unloaded - Close session when Spotipy object is unloaded
- Propagate refresh token error - Propagate refresh token error
## [2.10.0] - 2020-03-18 ## [2.10.0] - 2020-03-18
### 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
### Added ### Added
- Support `position_ms` optional parameter in `start_playback` - Support `position_ms` optional parameter in `start_playback`
- Add `requests_timeout` parameter to authentication methods - Add `requests_timeout` parameter to authentication methods
- Make cache optional in `get_access_token` - Make cache optional in `get_access_token`
## [2.8.0] - 2020-02-12 ## [2.8.0] - 2020-02-12
### Added ### Added
- Support for `playlist_cover_image` - Support for `playlist_cover_image`
- Support `after` and `before` parameter in `current_user_recently_played` - Support `after` and `before` parameter in `current_user_recently_played`
- CI for unit tests - CI for unit tests
- Automatic `token` refresh - Automatic `token` refresh
- `auth_manager` and `oauth_manager` optional parameters added to `Spotify`'s init. - `auth_manager` and `oauth_manager` optional parameters added to `Spotify`'s init.
- Optional `username` parameter to be passed to `SpotifyOAuth`, to infer a `cache_path` automatically - Optional `username` parameter to be passed to `SpotifyOAuth`, to infer a `cache_path` automatically
- Optional `as_dict` parameter to control `SpotifyOAuth`'s `get_access_token` output type. However, this is going to be deprecated in the future, and the method will always return a token string - Optional `as_dict` parameter to control `SpotifyOAuth`'s `get_access_token` output type. However, this is going to be deprecated in the future, and the method will always return a token string
- Optional `show_dialog` parameter to be passed to `SpotifyOAuth` - Optional `show_dialog` parameter to be passed to `SpotifyOAuth`
### Changed ### Changed
- Both `SpotifyClientCredentials` and `SpotifyOAuth` inherit from a common `SpotifyAuthBase` which handles common parameters and logics. - Both `SpotifyClientCredentials` and `SpotifyOAuth` inherit from a common `SpotifyAuthBase` which handles common parameters and logics.
## [2.7.1] - 2020-01-20 ## [2.7.1] - 2020-01-20
### Changed ### Changed
- PyPi release mistake without pulling last merge first - PyPi release mistake without pulling last merge first
## [2.7.0] - 2020-01-20 ## [2.7.0] - 2020-01-20
### Added ### Added
- Support for `playlist_tracks` - Support for `playlist_tracks`
- Support for `playlist_upload_cover_image` - Support for `playlist_upload_cover_image`
### Changed ### Changed
- `user_playlist_tracks` doesn't require a user anymore (accepts `None`) - `user_playlist_tracks` doesn't require a user anymore (accepts `None`)
### Deprecated ### Deprecated
- Deprecated `user_playlist` and `user_playlist_tracks` - Deprecated `user_playlist` and `user_playlist_tracks`
## [2.6.3] - 2020-01-16 ## [2.6.3] - 2020-01-16
### Fixed ### Fixed
- Fixed broken doc in 2.6.2 - Fixed broken doc in 2.6.2
## [2.6.2] - 2020-01-16 ## [2.6.2] - 2020-01-16
### Fixed ### Fixed
- Fixed broken examples in README, examples and doc - Fixed broken examples in README, examples and doc
### Changed ### Changed
- Allow session keepalive - Allow session keepalive
- Bump requests to 2.20.0 - Bump requests to 2.20.0
## [2.6.1] - 2020-01-13 ## [2.6.1] - 2020-01-13
### Fixed ### Fixed
- Fixed inconsistent behaviour with some API methods when - Fixed inconsistent behaviour with some API methods when
a full HTTP URL is passed. a full HTTP URL is passed.
- Fixed invalid calls to logging warn method - Fixed invalid calls to logging warn method
### Removed ### Removed
- `mock` no longer needed for install. Only used in `tox`. - `mock` no longer needed for install. Only used in `tox`.
## [2.6.0] - 2020-01-12 ## [2.6.0] - 2020-01-12
### Added ### Added
- Support for `playlist` to get a playlist without specifying a user - Support for `playlist` to get a playlist without specifying a user
- Support for `current_user_saved_albums_delete` - Support for `current_user_saved_albums_delete`
- Support for `current_user_saved_albums_contains` - Support for `current_user_saved_albums_contains`
- Support for `user_unfollow_artists` - Support for `user_unfollow_artists`
- Support for `user_unfollow_users` - Support for `user_unfollow_users`
- Lint with flake8 using Github action - Lint with flake8 using Github action
### Changed ### Changed
- Fix typos in doc - Fix typos in doc
- Start following [SemVer](https://semver.org) properly - Start following [SemVer](https://semver.org) properly
## [2.5.0] - 2020-01-11 ## [2.5.0] - 2020-01-11

63
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,63 @@
# Contributor Covenant Code of Conduct
## Our Pledge
Here at Spotipy, we would like to promote an environment which is open and
welcoming to all. As contributors and maintainers we want to guarantee an
experience which is free of harassment for everyone. By everyone, we mean everyone,
regardless of: age, body size, disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Here are some examples of conduct which we believe is conducive to and contributes
to a positive environment:
* Use of welcoming and inclusive language
* Giving due respect to differing viewpoints and experiences
* Being accepting of constructive criticism
* Being focused on what is best for the community
* Displaying empathy towards other members of the community
Here are some examples of conduct which we believe are unacceptable:
* Using sexualized language/imagery or giving other community members unwelcome
sexual attention
* Making insulting/derogatory comments to other community members, or making
personal/political attacks against other community members
* Trolling
* Harassing other members publicly or privately
* Doxxing other community members (leaking private information without first getting consent)
* Any other behavior which would be considered inappropriate in a professional setting
## Our Responsibilities
As project maintainers, we are responsible for clearly laying out standards for proper
conduct. We are also responsible for taking the appropriate actions if and when a
community member does not act with proper conduct. An example of appropriate action
is removing/editing/rejecting comments/commits/code/wiki edits/issues or other
contributions made by such an offender. If a community members continues to act in a
way contrary to the Code of Conduct, it is our responsibility to ban them (temporarily
or permanently).
## Scope
Community members are expected to adhere to the Code of Conduct within all project spaces,
as well as in all public spaces when representing the Spotipy community.
## Enforcement
Please report instances of abusive, harassing, or otherwise unacceptable behavior to us.
All complaints will be investigated and reviewed by the project team and will result in
an appropriate response. The project team is obligated to maintain confidentiality with
regard to the reporter of an incident.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may
face temporary or permanent repercussions as determined by other members of the projects
leadership.
## Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at 
https://www.contributor-covenant.org/version/1/4/code-of-conduct.html. For answers to
common questions about this code of conduct, see https://www.contributor-covenant.org/faq

View File

@ -5,16 +5,24 @@ If you would like to contribute to spotipy follow these steps:
### Export the needed environment variables ### Export the needed environment variables
```bash ```bash
# Linux or Mac
export SPOTIPY_CLIENT_ID=client_id_here export SPOTIPY_CLIENT_ID=client_id_here
export SPOTIPY_CLIENT_SECRET=client_secret_here export SPOTIPY_CLIENT_SECRET=client_secret_here
export SPOTIPY_CLIENT_USERNAME=client_username_here # This is actually an id not spotify display name export SPOTIPY_CLIENT_USERNAME=client_username_here # This is actually an id not spotify display name
export SPOTIPY_REDIRECT_URI=http://localhost:8080 # Make url is set in app you created to get your ID and SECRET export SPOTIPY_REDIRECT_URI=http://localhost:8080 # Make url is set in app you created to get your ID and SECRET
# Windows
$env:SPOTIPY_CLIENT_ID="client_id_here"
$env:SPOTIPY_CLIENT_SECRET="client_secret_here"
$env:SPOTIPY_CLIENT_USERNAME="client_username_here"
$env:SPOTIPY_REDIRECT_URI="http://localhost:8080"
``` ```
### Create virtual environment, install dependencies, run tests: ### Create virtual environment, install dependencies, run tests:
```bash ```bash
$ virtualenv --python=python3.7 env $ virtualenv --python=python3.7 env
$ source env/bin/activate
(env) $ pip install --user -e . (env) $ pip install --user -e .
(env) $ python -m unittest discover -v tests (env) $ python -m unittest discover -v tests
``` ```
@ -44,7 +52,13 @@ To make sure if the import lists are stored correctly:
## Unreleased ## Unreleased
// Add your changes here and then delete this line // Add new changes below
### Added
### Fixed
### Removed
- Commit changes - Commit changes
- Package to pypi: - Package to pypi:
@ -52,7 +66,7 @@ To make sure if the import lists are stored correctly:
python setup.py sdist bdist_wheel python setup.py sdist bdist_wheel
python3 setup.py sdist bdist_wheel python3 setup.py sdist bdist_wheel
twine check dist/* twine check dist/*
twine upload --repository-url https://upload.pypi.org/legacy/ --skip-existing dist/*.(whl|gz|zip)~dist/*linux*.whl twine upload dist/*
- 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

View File

@ -1,6 +1,6 @@
The MIT License (MIT) MIT License
Copyright (c) 2014 Paul Lamere Copyright (c) 2021 Paul Lamere
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions: furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in The above copyright notice and this permission notice shall be included in all
all copies or substantial portions of the Software. copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
THE SOFTWARE. SOFTWARE.

View File

@ -14,6 +14,12 @@ Spotipy's full documentation is online at [Spotipy Documentation](http://spotipy
pip install spotipy pip install spotipy
``` ```
alternatively, for Windows users
```bash
py -m pip install spotipy
```
or upgrade or upgrade
```bash ```bash
@ -43,6 +49,8 @@ for idx, track in enumerate(results['tracks']['items']):
### With user authentication ### 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.
```python ```python
import spotipy import spotipy
from spotipy.oauth2 import SpotifyOAuth from spotipy.oauth2 import SpotifyOAuth

81
TUTORIAL.md Normal file
View File

@ -0,0 +1,81 @@
# Spotipy Tutorial for Beginners
Hello and welcome to the Spotipy Tutorial for Beginners. If you have limited experience coding in Python and have never used Spotipy or the Spotify API before, you've come to the right place. This tutorial will walk you through all the steps necessary to set up Spotipy and use it to accomplish a simple task.
## Prerequisites
In order to complete this tutorial successfully, there are a few things that you should already have installed:
**1. pip package manager**
You can check to see if you have pip installed by opening up Terminal and typing the following command: pip --version
If you see a version number, pip is installed and you're ready to proceed. If not, instructions for downloading the latest version of pip can be found here: https://pip.pypa.io/en/stable/cli/pip_download/
**2. python3**
Spotipy is written in Python, so you'll need to have the lastest version of Python installed in order to use Spotipy. Check if you already have Python installed with the Terminal command: python --version
If you see a version number, Python is already installed. If not, you can download it here: https://www.python.org/downloads/
**3. experience with basic Linux commands**
This tutorial will be easiest if you have some knowledge of how to use Linux commands to create and navigate folders and files on your computer. If you're not sure how to create, edit and delete files and directories from Terminal, learn about basic Linux commands [here](https://ubuntu.com/tutorials/command-line-for-beginners#1-overview) before continuing.
Once those three setup items are taken care of, you're ready to start learning how to use Spotipy!
## Step 1. Creating a Spotify Account
Spotipy relies on the Spotify API. In order to use the Spotify API, you'll need to create a Spotify developer account.
A. Visit the [Spotify developer portal](https://developer.spotify.com/dashboard/). If you already have a Spotify account, click "Log in" and enter your username and password. Otherwise, click "Sign up" and follow the steps to create an account. After you've signed in or signed up, you should be redirected to your developer dashboard.
B. Click the "Create an App" button. Enter any name and description you'd like for your new app. Accept the terms of service and click "Create."
C. In your new app's Overview screen, click the "Edit Settings" button and scroll down to "Redirect URIs." Add "http://localhost:1234" (or any other port number of your choosing). Hit the "Save" button at the bottom of the Settings panel to return to you App Overview screen.
D. Underneath your app name and description on the lefthand side, you'll see a "Show Client Secret" link. Click that link to reveal your Client Secret, then copy both your Client Secret and your Client ID somewhere on your computer. You'll need to access them later.
## Step 2. Installation and Setup
A. Create a folder somewhere on your computer where you'd like to store the code for your Spotipy app. You can create a folder in terminal with this command: mkdir folder_name
B. In that folder, create a Python file named main.py. You can create the file directly from Terminal using a built in text editor like Vim, which comes preinstalled on Linux operating systems. To create the file with Vim, ensure that you are in your new directory, then run: vim main.py
C. Paste the following code into your main.py file:
```
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="user-library-read"))
```
D. Replace YOUR_APP_CLIENT_ID and YOUR_APP_CLIENT_SECRET with the values you copied and saved in step 1D. Replace YOUR_APP_REDIRECT_URI with the URI you set in step 1C.
## Step 3. Start Using Spotipy
After completing steps 1 and 2, your app is fully configured and ready to fetch data from the Spotify API. All that's left is to tell the API what data we're looking for, and we do that by adding some additional code to main.py. The code that follows is just an example - once you get it working, you should feel free to modify it in order to get different results.
For now, let's assume that we want to print the names of all of the albums on Spotify by Taylor Swift:
A. First, we need to find Taylor Swift's Spotify URI (Uniform Resource Indicator). Every entity (artist, album, song, etc.) has a URI that can identify it. To find Taylor's URI, navigate to [her page on Spotify](https://open.spotify.com/artist/06HL4z0CvFAxyc27GXpf02) and look at the URI in your browser. Everything there that follows the last backslash in the URL path is Taylor's URI, in this case: 06HL4z0CvFAxyc27GXpf02
B. Add the URI as a variable in main.py. Notice the prefix added the the URI:
```
taylor_uri = 'spotify:artist:06HL4z0CvFAxyc27GXpf02'
```
C. Add the following code that will get all of Taylor's album names from Spotify and iterate through them to print them all to standard output.
```
results = sp.artist_albums(taylor_uri, album_type='album')
albums = results['items']
while results['next']:
results = sp.next(results)
albums.extend(results['items'])
for album in albums:
print(album['name'])
```
D. Close main.py and return to the directory that contains main.py. You can then run your app by entering the following command: python main.py
E. You may see a window open in your browser asking you to authorize the application. Do so - you will only have to do this once.
F. Return to your terminal - you should see all of Taylor's albums printed out there.

View File

@ -11,14 +11,15 @@
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
import sys, os import spotipy
import sys
import os
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here. # documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.')) #sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.abspath('.'))
import spotipy
# -- General configuration ----------------------------------------------------- # -- General configuration -----------------------------------------------------
@ -172,21 +173,21 @@ htmlhelp_basename = 'spotipydoc'
# -- Options for LaTeX output -------------------------------------------------- # -- Options for LaTeX output --------------------------------------------------
latex_elements = { latex_elements = {
# The paper size ('letterpaper' or 'a4paper'). # The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper', # 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt'). # The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt', # 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble. # Additional stuff for the LaTeX preamble.
#'preamble': '', # 'preamble': '',
} }
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]). # (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [ latex_documents = [
('index', 'spotipy.tex', 'spotipy Documentation', ('index', 'spotipy.tex', 'spotipy Documentation',
'Paul Lamere', 'manual'), 'Paul Lamere', 'manual'),
] ]
# The name of an image file (relative to this directory) to place at the top of # The name of an image file (relative to this directory) to place at the top of
@ -229,9 +230,9 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
('index', 'spotipy', 'spotipy Documentation', ('index', 'spotipy', 'spotipy Documentation',
'Paul Lamere', 'spotipy', 'One line description of project.', 'Paul Lamere', 'spotipy', 'One line description of project.',
'Miscellaneous'), 'Miscellaneous'),
] ]
# Documents to append as an appendix to all manuals. # Documents to append as an appendix to all manuals.

View File

@ -9,7 +9,7 @@ Welcome to Spotipy!
you get full access to all of the music data provided by the Spotify platform. you get full access to all of the music data provided by the Spotify platform.
Assuming you set the ``SPOTIPY_CLIENT_ID`` and ``SPOTIPY_CLIENT_SECRET`` Assuming you set the ``SPOTIPY_CLIENT_ID`` and ``SPOTIPY_CLIENT_SECRET``
environment variables, here's a quick example of using *Spotipy* to list the environment variables (here is a `video <https://youtu.be/3RGm4jALukM>`_ explaining how to do so), here's a quick example of using *Spotipy* to list the
names of all the albums released by the artist 'Birdy':: names of all the albums released by the artist 'Birdy'::
import spotipy import spotipy
@ -132,7 +132,7 @@ class SpotifyOAuth that can be used to authenticate requests like so::
print(idx, track['artists'][0]['name'], " ", track['name']) print(idx, track['artists'][0]['name'], " ", track['name'])
or if you are reluctant to immortalize your app credentials in your source code, or if you are reluctant to immortalize your app credentials in your source code,
you can set environment variables like so (use ``SET`` instead of ``export`` you can set environment variables like so (use ``$env:"credentials"`` instead of ``export``
on Windows):: on Windows)::
export SPOTIPY_CLIENT_ID='your-spotify-client-id' export SPOTIPY_CLIENT_ID='your-spotify-client-id'
@ -143,7 +143,7 @@ Scopes
------ ------
See `Using See `Using
Scopes <https://developer.spotify.com/web-api/using-scopes/>`_ for information Scopes <https://developer.spotify.com/documentation/general/guides/authorization/scopes/>`_ for information
about scopes. about scopes.
Redirect URI Redirect URI
@ -159,6 +159,11 @@ must match the redirect URI added to your application in your Dashboard.
The redirect URI can be any valid URI (it does not need to be accessible) The redirect URI can be any valid URI (it does not need to be accessible)
such as ``http://example.com``, ``http://localhost`` or ``http://127.0.0.1:9090``. such as ``http://example.com``, ``http://localhost`` or ``http://127.0.0.1:9090``.
.. note:: If you choose an `http`-scheme URL, and it's for `localhost` or
`127.0.0.1`, **AND** it specifies a port, then spotispy will instantiate
a server on the indicated response to receive the access token from the
response at the end of the oauth flow [see the code](https://github.com/plamere/spotipy/blob/master/spotipy/oauth2.py#L483-L490).
Client Credentials Flow Client Credentials Flow
======================= =======================
@ -226,6 +231,12 @@ The custom cache handler would need to be a class that inherits from the base
cache handler ``CacheHandler``. The default cache handler ``CacheFileHandler`` is a good example. cache handler ``CacheHandler``. The default cache handler ``CacheFileHandler`` is a good example.
An instance of that new class can then be passed as a parameter when An instance of that new class can then be passed as a parameter when
creating ``SpotifyOAuth``, ``SpotifyPKCE`` or ``SpotifyImplicitGrant``. creating ``SpotifyOAuth``, ``SpotifyPKCE`` or ``SpotifyImplicitGrant``.
The following handlers are available and defined in the URL above.
- ``CacheFileHandler``
- ``MemoryCacheHandler``
- ``DjangoSessionCacheHandler``
- ``FlaskSessionCacheHandler``
- ``RedisCacheHandler``
Feel free to contribute new cache handlers to the repo. Feel free to contribute new cache handlers to the repo.
@ -282,31 +293,96 @@ Contribute
Spotipy authored by Paul Lamere (plamere) with contributions by: Spotipy authored by Paul Lamere (plamere) with contributions by:
- Daniel Beaudry // danbeaudry - Daniel Beaudry (`danbeaudry on Github <https://github.com/danbeaudry>`_)
- Faruk Emre Sahin // fsahin - Faruk Emre Sahin (`fsahin on Github <https://github.com/fsahin>`_)
- George // rogueleaderr - George (`rogueleaderr on Github <https://github.com/rogueleaderr>`_)
- Henry Greville // sethaurus - Henry Greville (`sethaurus on Github <https://github.com/sethaurus>`_)
- Hugo // hugovk - Hugo van Kemanade (`hugovk on Github <https://github.com/hugovk>`_)
- José Manuel Pérez // JMPerez - José Manuel Pérez (`JMPerez on Github <https://github.com/JMPerez>`_)
- Lucas Nunno // lnunno - Lucas Nunno (`lnunno on Github <https://github.com/lnunno>`_)
- Lynn Root // econchick - Lynn Root (`econchick on Github <https://github.com/econchick>`_)
- Matt Dennewitz // mattdennewitz - Matt Dennewitz (`mattdennewitz on Github <https://github.com/mattdennewitz>`_)
- Matthew Duck // mattduck - Matthew Duck (`mattduck on Github <https://github.com/mattduck>`_)
- Michael Thelin // thelinmichael - Michael Thelin (`thelinmichael on Github <https://github.com/thelinmichael>`_)
- Ryan Choi // ryankicks - Ryan Choi (`ryankicks on Github <https://github.com/ryankicks>`_)
- Simon Metson // drsm79 - Simon Metson (`drsm79 on Github <https://github.com/drsm79>`_)
- Steve Winton // swinton - Steve Winton (`swinton on Github <https://github.com/swinton>`_)
- Tim Balzer // timbalzer - Tim Balzer (`timbalzer on Github <https://github.com/timbalzer>`_)
- corycorycory // corycorycory - `corycorycory on Github <https://github.com/corycorycory>`_
- Nathan Coleman // nathancoleman - Nathan Coleman (`nathancoleman on Github <https://github.com/nathancoleman>`_)
- Michael Birtwell // mbirtwell - Michael Birtwell (`mbirtwell on Github <https://github.com/mbirtwell>`_)
- Harrison Hayes // Harrison97 - Harrison Hayes (`Harrison97 on Github <https://github.com/Harrison97>`_)
- Stephane Bruckert // stephanebruckert - Stephane Bruckert (`stephanebruckert on Github <https://github.com/stephanebruckert>`_)
- Ritiek Malhotra // ritiek - Ritiek Malhotra (`ritiek on Github <https://github.com/ritiek>`_)
If you are a developer with Python experience, and you would like to contribute to Spotipy, please
be sure to follow the guidelines listed below:
Export the needed Environment variables:::
export SPOTIPY_CLIENT_ID=client_id_here
export SPOTIPY_CLIENT_SECRET=client_secret_here
export SPOTIPY_CLIENT_USERNAME=client_username_here # This is actually an id not spotify display name
export SPOTIPY_REDIRECT_URI=http://localhost:8080 # Make url is set in app you created to get your ID and SECRET
Create virtual environment, install dependencies, run tests:::
$ virtualenv --python=python3.7 env
(env) $ pip install --user -e .
(env) $ python -m unittest discover -v tests
**Lint**
To automatically fix the code style:::
pip install autopep8
autopep8 --in-place --aggressive --recursive .
To verify the code style:::
pip install flake8
flake8 .
To make sure if the import lists are stored correctly:::
pip install isort
isort . -c -v
**Publishing (by maintainer)**
- Bump version in setup.py
- Bump and date changelog
- Add to changelog:
::
## Unreleased
// Add your changes here and then delete this line
- Commit changes
- Package to pypi:
::
python setup.py sdist bdist_wheel
python3 setup.py sdist bdist_wheel
twine check dist/*
twine upload --repository-url https://upload.pypi.org/legacy/ --skip-existing dist/*.(whl|gz|zip)~dist/*linux*.whl
- 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
- Build the documentation again to ensure it's on the latest version
**Changelog**
Don't forget to add a short description of your change in the `CHANGELOG <https://github.com/plamere/spotipy/blob/master/CHANGELOG.md>`_!
License License
======= =======
https://github.com/plamere/spotipy/blob/master/LICENSE.md (Taken from https://github.com/plamere/spotipy/blob/master/LICENSE.md)::
MIT License
Copyright (c) 2021 Paul Lamere
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
(the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Indices and tables Indices and tables

View File

@ -11,7 +11,7 @@ scope = 'playlist-modify-public'
def get_args(): def get_args():
parser = argparse.ArgumentParser(description='Adds track to user playlist') parser = argparse.ArgumentParser(description='Adds track to user playlist')
parser.add_argument('-t', '--tids', action='append', parser.add_argument('-u', '--uris', action='append',
required=True, help='Track ids') required=True, help='Track ids')
parser.add_argument('-p', '--playlist', required=True, parser.add_argument('-p', '--playlist', required=True,
help='Playlist to add track to') help='Playlist to add track to')
@ -22,7 +22,7 @@ def main():
args = get_args() args = get_args()
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope)) sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
sp.playlist_add_items(args.playlist, args.tids) sp.playlist_add_items(args.playlist, args.uris)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -27,7 +27,6 @@ import os
from flask import Flask, session, request, redirect from flask import Flask, session, request, redirect
from flask_session import Session from flask_session import Session
import spotipy import spotipy
import uuid
app = Flask(__name__) app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(64) app.config['SECRET_KEY'] = os.urandom(64)
@ -35,57 +34,44 @@ app.config['SESSION_TYPE'] = 'filesystem'
app.config['SESSION_FILE_DIR'] = './.flask_session/' app.config['SESSION_FILE_DIR'] = './.flask_session/'
Session(app) Session(app)
caches_folder = './.spotify_caches/'
if not os.path.exists(caches_folder):
os.makedirs(caches_folder)
def session_cache_path():
return caches_folder + session.get('uuid')
@app.route('/') @app.route('/')
def index(): def index():
if not session.get('uuid'):
# Step 1. Visitor is unknown, give random ID
session['uuid'] = str(uuid.uuid4())
cache_handler = spotipy.cache_handler.CacheFileHandler(cache_path=session_cache_path()) cache_handler = spotipy.cache_handler.FlaskSessionCacheHandler(session)
auth_manager = spotipy.oauth2.SpotifyOAuth(scope='user-read-currently-playing playlist-modify-private', auth_manager = spotipy.oauth2.SpotifyOAuth(scope='user-read-currently-playing playlist-modify-private',
cache_handler=cache_handler, cache_handler=cache_handler,
show_dialog=True) show_dialog=True)
if request.args.get("code"): if request.args.get("code"):
# Step 3. Being redirected from Spotify auth page # Step 2. Being redirected from Spotify auth page
auth_manager.get_access_token(request.args.get("code")) auth_manager.get_access_token(request.args.get("code"))
return redirect('/') return redirect('/')
if not auth_manager.validate_token(cache_handler.get_cached_token()): if not auth_manager.validate_token(cache_handler.get_cached_token()):
# Step 2. Display sign in link when no token # Step 1. Display sign in link when no token
auth_url = auth_manager.get_authorize_url() auth_url = auth_manager.get_authorize_url()
return f'<h2><a href="{auth_url}">Sign in</a></h2>' return f'<h2><a href="{auth_url}">Sign in</a></h2>'
# Step 4. Signed in, display data # Step 3. Signed in, display data
spotify = spotipy.Spotify(auth_manager=auth_manager) spotify = spotipy.Spotify(auth_manager=auth_manager)
return f'<h2>Hi {spotify.me()["display_name"]}, ' \ return f'<h2>Hi {spotify.me()["display_name"]}, ' \
f'<small><a href="/sign_out">[sign out]<a/></small></h2>' \ f'<small><a href="/sign_out">[sign out]<a/></small></h2>' \
f'<a href="/playlists">my playlists</a> | ' \ f'<a href="/playlists">my playlists</a> | ' \
f'<a href="/currently_playing">currently playing</a> | ' \ f'<a href="/currently_playing">currently playing</a> | ' \
f'<a href="/current_user">me</a>' \ f'<a href="/current_user">me</a>' \
@app.route('/sign_out') @app.route('/sign_out')
def sign_out(): def sign_out():
try: session.pop("token_info", None)
# Remove the CACHE file (.cache-test) so that a new user can authorize.
os.remove(session_cache_path())
session.clear()
except OSError as e:
print ("Error: %s - %s." % (e.filename, e.strerror))
return redirect('/') return redirect('/')
@app.route('/playlists') @app.route('/playlists')
def playlists(): def playlists():
cache_handler = spotipy.cache_handler.CacheFileHandler(cache_path=session_cache_path()) cache_handler = spotipy.cache_handler.FlaskSessionCacheHandler(session)
auth_manager = spotipy.oauth2.SpotifyOAuth(cache_handler=cache_handler) auth_manager = spotipy.oauth2.SpotifyOAuth(cache_handler=cache_handler)
if not auth_manager.validate_token(cache_handler.get_cached_token()): if not auth_manager.validate_token(cache_handler.get_cached_token()):
return redirect('/') return redirect('/')
@ -96,7 +82,7 @@ def playlists():
@app.route('/currently_playing') @app.route('/currently_playing')
def currently_playing(): def currently_playing():
cache_handler = spotipy.cache_handler.CacheFileHandler(cache_path=session_cache_path()) cache_handler = spotipy.cache_handler.FlaskSessionCacheHandler(session)
auth_manager = spotipy.oauth2.SpotifyOAuth(cache_handler=cache_handler) auth_manager = spotipy.oauth2.SpotifyOAuth(cache_handler=cache_handler)
if not auth_manager.validate_token(cache_handler.get_cached_token()): if not auth_manager.validate_token(cache_handler.get_cached_token()):
return redirect('/') return redirect('/')
@ -109,7 +95,7 @@ def currently_playing():
@app.route('/current_user') @app.route('/current_user')
def current_user(): def current_user():
cache_handler = spotipy.cache_handler.CacheFileHandler(cache_path=session_cache_path()) cache_handler = spotipy.cache_handler.FlaskSessionCacheHandler(session)
auth_manager = spotipy.oauth2.SpotifyOAuth(cache_handler=cache_handler) auth_manager = spotipy.oauth2.SpotifyOAuth(cache_handler=cache_handler)
if not auth_manager.validate_token(cache_handler.get_cached_token()): if not auth_manager.validate_token(cache_handler.get_cached_token()):
return redirect('/') return redirect('/')

View File

@ -1,4 +1,4 @@
#Shows the list of all songs sung by the artist or the band # Shows the list of all songs sung by the artist or the band
import argparse import argparse
import logging import logging
@ -34,7 +34,7 @@ def show_album_tracks(album):
results = sp.next(results) results = sp.next(results)
tracks.extend(results['items']) tracks.extend(results['items'])
for i, track in enumerate(tracks): for i, track in enumerate(tracks):
logger.info('%s. %s', i+1, track['name']) logger.info('%s. %s', i + 1, track['name'])
def show_artist_albums(artist): def show_artist_albums(artist):
@ -60,6 +60,7 @@ def show_artist(artist):
if len(artist['genres']) > 0: if len(artist['genres']) > 0:
logger.info('Genres: %s', ','.join(artist['genres'])) logger.info('Genres: %s', ','.join(artist['genres']))
def main(): def main():
args = get_args() args = get_args()
artist = get_artist(args.artist) artist = get_artist(args.artist)

View File

@ -0,0 +1,27 @@
import argparse
import spotipy
from spotipy.oauth2 import SpotifyOAuth
def get_args():
parser = argparse.ArgumentParser(description='Follows a playlist based on playlist ID')
parser.add_argument('-p', '--playlist', required=True, help='Playlist ID')
return parser.parse_args()
def main():
args = get_args()
if args.playlist is None:
# Uses the Spotify Global Top 50 playlist
spotipy.Spotify(auth_manager=SpotifyOAuth()).current_user_follow_playlist(
'37i9dQZEVXbMDoHDwVN2tF')
else:
spotipy.Spotify(auth_manager=SpotifyOAuth()).current_user_follow_playlist(args.playlist)
if __name__ == '__main__':
main()

View File

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

View File

@ -1,4 +1,5 @@
# shows artist info for a URN or URL # shows artist info for a URN or URL
# scope is not required for this function
from spotipy.oauth2 import SpotifyClientCredentials from spotipy.oauth2 import SpotifyClientCredentials
import spotipy import spotipy

View File

@ -0,0 +1,27 @@
# Shows all artists featured on an album
# usage: featured_artists.py spotify:album:[album urn]
from spotipy.oauth2 import SpotifyClientCredentials
import sys
import spotipy
from pprint import pprint
if len(sys.argv) > 1:
urn = sys.argv[1]
else:
urn = 'spotify:album:5yTx83u3qerZF7GRJu7eFk'
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials())
album = sp.album(urn)
featured_artists = set()
items = album['tracks']['items']
for item in items:
for ele in item['artists']:
if 'name' in ele:
featured_artists.add(ele['name'])
pprint(featured_artists)

View File

@ -1,4 +1,4 @@
#Shows the name of the artist/band and their image by giving a link # Shows the name of the artist/band and their image by giving a link
import sys import sys
from spotipy.oauth2 import SpotifyClientCredentials from spotipy.oauth2 import SpotifyClientCredentials

View File

@ -0,0 +1,31 @@
import argparse
import logging
import spotipy
from spotipy.oauth2 import SpotifyOAuth
logger = logging.getLogger('examples.unfollow_playlist')
logging.basicConfig(level='DEBUG')
'''
Spotify doesn't have a dedicated endpoint for deleting a playlist. However,
unfollowing a playlist has the effect of deleting it from the user's account.
When a playlist is removed from the user's account, the system unfollows it,
and then no longer shows it in playlist list.'''
def get_args():
parser = argparse.ArgumentParser(description='Unfollows a playlist')
parser.add_argument('-p', '--playlist', required=True,
help='Playlist id')
return parser.parse_args()
def main():
args = get_args()
sp = spotipy.Spotify(auth_manager=SpotifyOAuth())
sp.current_user_unfollow_playlist(args.playlist)
if __name__ == '__main__':
main()

View File

@ -25,12 +25,17 @@ setup(
author="@plamere", author="@plamere",
author_email="paul@echonest.com", author_email="paul@echonest.com",
url='https://spotipy.readthedocs.org/', url='https://spotipy.readthedocs.org/',
project_urls={
'Source': 'https://github.com/plamere/spotipy',
},
install_requires=[ install_requires=[
'requests>=2.25.0', "redis>=3.5.3",
'six>=1.15.0', "redis<4.0.0;python_version<'3.4'",
'urllib3>=1.26.0' "requests>=2.25.0",
"six>=1.15.0",
"urllib3>=1.26.0"
], ],
tests_require=test_reqs, tests_require=test_reqs,
extras_require=extra_reqs, extras_require=extra_reqs,
license='LICENSE.md', license='MIT',
packages=['spotipy']) packages=['spotipy'])

View File

@ -1,6 +1,10 @@
# -*- coding: utf-8 -*- __all__ = [
'CacheHandler',
__all__ = ['CacheHandler', 'CacheFileHandler', 'MemoryCacheHandler'] 'CacheFileHandler',
'DjangoSessionCacheHandler',
'FlaskSessionCacheHandler',
'MemoryCacheHandler',
'RedisCacheHandler']
import errno import errno
import json import json
@ -9,6 +13,8 @@ import os
from spotipy.util import CLIENT_CREDS_ENV_VARS from spotipy.util import CLIENT_CREDS_ENV_VARS
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from redis import RedisError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -107,3 +113,94 @@ class MemoryCacheHandler(CacheHandler):
def save_token_to_cache(self, token_info): def save_token_to_cache(self, token_info):
self.token_info = token_info self.token_info = token_info
class DjangoSessionCacheHandler(CacheHandler):
"""
A cache handler that stores the token info in the session framework
provided by Django.
Read more at https://docs.djangoproject.com/en/3.2/topics/http/sessions/
"""
def __init__(self, request):
"""
Parameters:
* request: HttpRequest object provided by Django for every
incoming request
"""
self.request = request
def get_cached_token(self):
token_info = None
try:
token_info = self.request.session['token_info']
except KeyError:
logger.debug("Token not found in the session")
return token_info
def save_token_to_cache(self, token_info):
try:
self.request.session['token_info'] = token_info
except Exception as e:
logger.warning("Error saving token to cache: " + str(e))
class FlaskSessionCacheHandler(CacheHandler):
"""
A cache handler that stores the token info in the session framework
provided by flask.
"""
def __init__(self, session):
self.session = session
def get_cached_token(self):
token_info = None
try:
token_info = self.session["token_info"]
except KeyError:
logger.debug("Token not found in the session")
return token_info
def save_token_to_cache(self, token_info):
try:
self.session["token_info"] = token_info
except Exception as e:
logger.warning("Error saving token to cache: " + str(e))
class RedisCacheHandler(CacheHandler):
"""
A cache handler that stores the token info in the Redis.
"""
def __init__(self, redis, key=None):
"""
Parameters:
* redis: Redis object provided by redis-py library
(https://github.com/redis/redis-py)
* key: May be supplied, will otherwise be generated
(takes precedence over `token_info`)
"""
self.redis = redis
self.key = key if key else 'token_info'
def get_cached_token(self):
token_info = None
try:
token_info = self.redis.get(self.key)
if token_info:
return json.loads(token_info)
except RedisError as e:
logger.warning('Error getting token from cache: ' + str(e))
return token_info
def save_token_to_cache(self, token_info):
try:
self.redis.set(self.key, json.dumps(token_info))
except RedisError as e:
logger.warning('Error saving token to cache: ' + str(e))

View File

@ -139,7 +139,7 @@ class Spotify(object):
See urllib3 https://urllib3.readthedocs.io/en/latest/reference/urllib3.util.html See urllib3 https://urllib3.readthedocs.io/en/latest/reference/urllib3.util.html
:param language: :param language:
The language parameter advertises what language the user prefers to see. The language parameter advertises what language the user prefers to see.
See ISO-639 language code: https://www.loc.gov/standards/iso639-2/php/code_list.php See ISO-639-1 language code: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
""" """
if access_token is not None and auth_manager is not None: if access_token is not None and auth_manager is not None:
@ -232,16 +232,22 @@ class Spotify(object):
except requests.exceptions.HTTPError as http_error: except requests.exceptions.HTTPError as http_error:
response = http_error.response response = http_error.response
try: try:
msg = response.json()["error"]["message"] json_response = response.json()
except (ValueError, KeyError): error = json_response.get("error", {})
msg = "error" msg = error.get("message")
try: reason = error.get("reason")
reason = response.json()["error"]["reason"] except ValueError:
except (ValueError, KeyError): # if the response cannot be decoded into JSON (which raises a ValueError),
# then try to decode it into text
# if we receive an empty string (which is falsy), then replace it with `None`
msg = response.text or None
reason = None reason = None
logger.error('HTTP Error for %s to %s returned %s due to %s', logger.error(
method, url, response.status_code, msg) 'HTTP Error for %s to %s with Params: %s returned %s due to %s',
method, url, args.get("params"), response.status_code, msg
)
raise SpotifyException( raise SpotifyException(
response.status_code, response.status_code,
@ -399,15 +405,19 @@ class Spotify(object):
trid = self._get_id("artist", artist_id) trid = self._get_id("artist", artist_id)
return self._get("artists/" + trid + "/related-artists") return self._get("artists/" + trid + "/related-artists")
def album(self, album_id): 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
Parameters: Parameters:
- album_id - the album ID, URI or URL - album_id - the album ID, URI or URL
- market - an ISO 3166-1 alpha-2 country code
""" """
trid = self._get_id("album", album_id) trid = self._get_id("album", album_id)
return self._get("albums/" + trid) if market is not None:
return self._get("albums/" + trid + '?market=' + market)
else:
return self._get("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
@ -425,15 +435,19 @@ class Spotify(object):
"albums/" + trid + "/tracks/", limit=limit, offset=offset, market=market "albums/" + trid + "/tracks/", limit=limit, offset=offset, market=market
) )
def albums(self, albums): def albums(self, albums, market=None):
""" returns a list of albums given the album IDs, URIs, or URLs """ returns a list of albums given the album IDs, URIs, or URLs
Parameters: Parameters:
- albums - a list of album IDs, URIs or URLs - albums - a list of album IDs, URIs or URLs
- market - an ISO 3166-1 alpha-2 country code
""" """
tlist = [self._get_id("album", a) for a in albums] tlist = [self._get_id("album", a) for a in albums]
return self._get("albums/?ids=" + ",".join(tlist)) if market is not None:
return self._get("albums/?ids=" + ",".join(tlist) + '&market=' + market)
else:
return self._get("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
@ -612,7 +626,7 @@ class Spotify(object):
""" Get full details of the tracks and episodes of a playlist. """ Get full details of the tracks and episodes of a playlist.
Parameters: Parameters:
- playlist_id - the id of the playlist - playlist_id - the playlist ID, URI or URL
- fields - which fields to return - fields - which fields to return
- limit - the maximum number of tracks to return - limit - the maximum number of tracks to return
- offset - the index of the first track to return - offset - the index of the first track to return
@ -631,10 +645,10 @@ class Spotify(object):
) )
def playlist_cover_image(self, playlist_id): def playlist_cover_image(self, playlist_id):
""" Get cover of a playlist. """ Get cover image of a playlist.
Parameters: Parameters:
- playlist_id - the id of the playlist - playlist_id - the playlist ID, URI or URL
""" """
plid = self._get_id("playlist", playlist_id) plid = self._get_id("playlist", playlist_id)
return self._get("playlists/%s/images" % (plid)) return self._get("playlists/%s/images" % (plid))
@ -693,7 +707,8 @@ class Spotify(object):
collaborative=None, collaborative=None,
description=None, description=None,
): ):
""" Changes a playlist's name and/or public/private state """ Changes a playlist's name and/or public/private state,
collaborative state, and/or description
Parameters: Parameters:
- playlist_id - the id of the playlist - playlist_id - the id of the playlist
@ -734,7 +749,7 @@ class Spotify(object):
Parameters: Parameters:
- playlist_id - the id of the playlist - playlist_id - the id of the playlist
- items - a list of track/episode URIs, URLs or IDs - items - a list of track/episode URIs or URLs
- position - the position to add the tracks - position - the position to add the tracks
""" """
plid = self._get_id("playlist", playlist_id) plid = self._get_id("playlist", playlist_id)
@ -793,7 +808,7 @@ class Spotify(object):
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
): ):
""" Removes all occurrences of the given tracks from the given playlist """ Removes all occurrences of the given tracks/episodes from the given playlist
Parameters: Parameters:
- playlist_id - the id of the playlist - playlist_id - the id of the playlist
@ -893,7 +908,7 @@ class Spotify(object):
"Your Music" library "Your Music" library
Parameters: Parameters:
- limit - the number of albums to return - limit - the number of albums to return (MAX_LIMIT=50)
- offset - the index of the first album to return - offset - the index of the first album to return
- market - an ISO 3166-1 alpha-2 country code. - market - an ISO 3166-1 alpha-2 country code.
@ -1096,7 +1111,7 @@ class Spotify(object):
) )
def current_user_following_users(self, ids=None): def current_user_following_users(self, ids=None):
""" Check if the current user is following certain artists """ Check if the current user is following certain users
Returns list of booleans respective to ids Returns list of booleans respective to ids
@ -1194,8 +1209,8 @@ class Spotify(object):
Parameters: Parameters:
- locale - The desired language, consisting of a lowercase ISO - locale - The desired language, consisting of a lowercase ISO
639 language code and an uppercase ISO 3166-1 alpha-2 country 639-1 alpha-2 language code and an uppercase ISO 3166-1 alpha-2
code, joined by an underscore. country code, joined by an underscore.
- country - An ISO 3166-1 alpha-2 country code. - country - An ISO 3166-1 alpha-2 country code.
@ -1244,7 +1259,7 @@ class Spotify(object):
- category_id - The Spotify category ID for the category. - category_id - The Spotify category ID for the category.
- country - An ISO 3166-1 alpha-2 country code. - country - An ISO 3166-1 alpha-2 country code.
- locale - The desired language, consisting of an ISO 639 - locale - The desired language, consisting of an ISO 639-1 alpha-2
language code and an ISO 3166-1 alpha-2 country code, joined language code and an ISO 3166-1 alpha-2 country code, joined
by an underscore. by an underscore.
""" """
@ -1259,7 +1274,7 @@ class Spotify(object):
Parameters: Parameters:
- country - An ISO 3166-1 alpha-2 country code. - country - An ISO 3166-1 alpha-2 country code.
- locale - The desired language, consisting of an ISO 639 - locale - The desired language, consisting of an ISO 639-1 alpha-2
language code and an ISO 3166-1 alpha-2 country code, joined language code and an ISO 3166-1 alpha-2 country code, joined
by an underscore. by an underscore.
@ -1436,7 +1451,7 @@ class Spotify(object):
): ):
""" Start or resume user's playback. """ Start or resume user's playback.
Provide a `context_uri` to start playback or a album, Provide a `context_uri` to start playback or an album,
artist, or playlist. artist, or playlist.
Provide a `uris` list to start playback of one or more Provide a `uris` list to start playback of one or more
@ -1569,13 +1584,17 @@ class Spotify(object):
) )
) )
def queue(self):
""" Gets the current user's queue """
return self._get("me/player/queue")
def add_to_queue(self, uri, device_id=None): def add_to_queue(self, uri, device_id=None):
""" Adds a song to the end of a user's queue """ Adds a song to the end of a user's queue
If device A is currently playing music and you try to add to the queue If device A is currently playing music and you try to add to the queue
and pass in the id for device B, you will get a and pass in the id for device B, you will get a
'Player command failed: Restriction violated' error 'Player command failed: Restriction violated' error
I therefore reccomend leaving device_id as None so that the active device is targeted I therefore recommend leaving device_id as None so that the active device is targeted
:param uri: song uri, id, or url :param uri: song uri, id, or url
:param device_id: :param device_id:
@ -1619,7 +1638,7 @@ class Spotify(object):
if type != fields[-2]: if type != fields[-2]:
logger.warning('Expected id of type %s but found type %s %s', logger.warning('Expected id of type %s but found type %s %s',
type, fields[-2], id) type, fields[-2], id)
return fields[-1] return fields[-1].split("?")[0]
fields = id.split("/") fields = id.split("/")
if len(fields) >= 3: if len(fields) >= 3:
itype = fields[-2] itype = fields[-2]

View File

@ -41,7 +41,7 @@ class SpotifyOauthError(Exception):
class SpotifyStateError(SpotifyOauthError): class SpotifyStateError(SpotifyOauthError):
""" The state sent and state recieved were different """ """ The state sent and state received were different """
def __init__(self, local_state=None, remote_state=None, message=None, def __init__(self, local_state=None, remote_state=None, message=None,
error=None, error_description=None, *args, **kwargs): error=None, error_description=None, *args, **kwargs):
@ -146,10 +146,7 @@ class SpotifyAuthBase(object):
@staticmethod @staticmethod
def _get_user_input(prompt): def _get_user_input(prompt):
try: return input(prompt)
return raw_input(prompt)
except NameError:
return input(prompt)
@staticmethod @staticmethod
def is_token_expired(token_info): def is_token_expired(token_info):
@ -164,6 +161,28 @@ class SpotifyAuthBase(object):
) )
return needle_scope <= haystack_scope return needle_scope <= haystack_scope
def _handle_oauth_error(self, http_error):
response = http_error.response
try:
error_payload = response.json()
error = error_payload.get('error')
error_description = error_payload.get('error_description')
except ValueError:
# if the response cannot be decoded into JSON (which raises a ValueError),
# then try to decode it into text
# if we receive an empty string (which is falsy), then replace it with `None`
error = response.text or None
error_description = None
raise SpotifyOauthError(
'error: {0}, error_description: {1}'.format(
error, error_description
),
error=error,
error_description=error_description
)
def __del__(self): def __del__(self):
"""Make sure the connection (pool) gets closed""" """Make sure the connection (pool) gets closed"""
if isinstance(self._session, requests.Session): if isinstance(self._session, requests.Session):
@ -228,7 +247,7 @@ class SpotifyClientCredentials(SpotifyAuthBase):
def get_access_token(self, check_cache=True): def get_access_token(self, check_cache=True):
""" """
If a valid access token is in memory, returns it If a valid access token is in memory, returns it
Else feches a new token and returns it Else fetches a new token and returns it
Parameters: Parameters:
- check_cache - if true, checks for a locally stored token - check_cache - if true, checks for a locally stored token
@ -258,23 +277,20 @@ class SpotifyClientCredentials(SpotifyAuthBase):
self.OAUTH_TOKEN_URL, headers, payload self.OAUTH_TOKEN_URL, headers, payload
) )
response = self._session.post( try:
self.OAUTH_TOKEN_URL, response = self._session.post(
data=payload, self.OAUTH_TOKEN_URL,
headers=headers, data=payload,
verify=True, headers=headers,
proxies=self.proxies, verify=True,
timeout=self.requests_timeout, proxies=self.proxies,
) timeout=self.requests_timeout,
if response.status_code != 200: )
error_payload = response.json() response.raise_for_status()
raise SpotifyOauthError( token_info = response.json()
'error: {0}, error_description: {1}'.format( return token_info
error_payload['error'], error_payload['error_description']), except requests.exceptions.HTTPError as http_error:
error=error_payload['error'], self._handle_oauth_error(http_error)
error_description=error_payload['error_description'])
token_info = response.json()
return token_info
def _add_custom_values_to_token_info(self, token_info): def _add_custom_values_to_token_info(self, token_info):
""" """
@ -328,7 +344,7 @@ class SpotifyOAuth(SpotifyAuthBase):
for performance reasons (connection pooling). for performance reasons (connection pooling).
* requests_timeout: Optional, tell Requests to stop waiting for a response after * requests_timeout: Optional, tell Requests to stop waiting for a response after
a given number of seconds a given number of seconds
* open_browser: Optional, whether or not the web browser should be opened to * open_browser: Optional, whether the web browser should be opened to
authorize a user authorize a user
""" """
@ -339,6 +355,7 @@ class SpotifyOAuth(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(cache_handler.__class__, CacheHandler), \ assert issubclass(cache_handler.__class__, CacheHandler), \
"cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \ "cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \
@ -346,6 +363,7 @@ class SpotifyOAuth(SpotifyAuthBase):
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
@ -443,13 +461,12 @@ class SpotifyOAuth(SpotifyAuthBase):
self._open_auth_url() self._open_auth_url()
server.handle_request() server.handle_request()
if self.state is not None and server.state != self.state: if server.error is not None:
raise SpotifyStateError(self.state, server.state)
if server.auth_code is not None:
return server.auth_code
elif server.error is not None:
raise server.error raise server.error
elif self.state is not None and server.state != self.state:
raise SpotifyStateError(self.state, server.state)
elif server.auth_code is not None:
return server.auth_code
else: else:
raise SpotifyOauthError("Server listening on localhost has not been accessed") raise SpotifyOauthError("Server listening on localhost has not been accessed")
@ -523,25 +540,22 @@ class SpotifyOAuth(SpotifyAuthBase):
self.OAUTH_TOKEN_URL, headers, payload self.OAUTH_TOKEN_URL, headers, payload
) )
response = self._session.post( try:
self.OAUTH_TOKEN_URL, response = self._session.post(
data=payload, self.OAUTH_TOKEN_URL,
headers=headers, data=payload,
verify=True, headers=headers,
proxies=self.proxies, verify=True,
timeout=self.requests_timeout, proxies=self.proxies,
) timeout=self.requests_timeout,
if response.status_code != 200: )
error_payload = response.json() response.raise_for_status()
raise SpotifyOauthError( token_info = response.json()
'error: {0}, error_description: {1}'.format( token_info = self._add_custom_values_to_token_info(token_info)
error_payload['error'], error_payload['error_description']), self.cache_handler.save_token_to_cache(token_info)
error=error_payload['error'], return token_info["access_token"]
error_description=error_payload['error_description']) except requests.exceptions.HTTPError as http_error:
token_info = response.json() self._handle_oauth_error(http_error)
token_info = self._add_custom_values_to_token_info(token_info)
self.cache_handler.save_token_to_cache(token_info)
return token_info["access_token"]
def refresh_access_token(self, refresh_token): def refresh_access_token(self, refresh_token):
payload = { payload = {
@ -556,28 +570,23 @@ class SpotifyOAuth(SpotifyAuthBase):
self.OAUTH_TOKEN_URL, headers, payload self.OAUTH_TOKEN_URL, headers, payload
) )
response = self._session.post( try:
self.OAUTH_TOKEN_URL, response = self._session.post(
data=payload, self.OAUTH_TOKEN_URL,
headers=headers, data=payload,
proxies=self.proxies, headers=headers,
timeout=self.requests_timeout, proxies=self.proxies,
) timeout=self.requests_timeout,
)
if response.status_code != 200: response.raise_for_status()
error_payload = response.json() token_info = response.json()
raise SpotifyOauthError( token_info = self._add_custom_values_to_token_info(token_info)
'error: {0}, error_description: {1}'.format( if "refresh_token" not in token_info:
error_payload['error'], error_payload['error_description']), token_info["refresh_token"] = refresh_token
error=error_payload['error'], self.cache_handler.save_token_to_cache(token_info)
error_description=error_payload['error_description']) return token_info
except requests.exceptions.HTTPError as http_error:
token_info = response.json() self._handle_oauth_error(http_error)
token_info = self._add_custom_values_to_token_info(token_info)
if "refresh_token" not in token_info:
token_info["refresh_token"] = refresh_token
self.cache_handler.save_token_to_cache(token_info)
return token_info
def _add_custom_values_to_token_info(self, token_info): def _add_custom_values_to_token_info(self, token_info):
""" """
@ -594,7 +603,7 @@ class SpotifyPKCE(SpotifyAuthBase):
This auth manager enables *user and non-user* endpoints with only This auth manager enables *user and non-user* endpoints with only
a client secret, redirect uri, and username. When the app requests a client secret, redirect uri, and username. When the app requests
an an access token for the first time, the user is prompted to an access token for the first time, the user is prompted to
authorize the new client app. After authorizing the app, the client authorize the new client app. After authorizing the app, the client
app is then given both access and refresh tokens. This is the app is then given both access and refresh tokens. This is the
preferred way of authorizing a mobile/desktop client. preferred way of authorizing a mobile/desktop client.
@ -626,6 +635,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`.
@ -645,6 +664,7 @@ 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(cache_handler.__class__, CacheHandler), \ assert issubclass(cache_handler.__class__, CacheHandler), \
"cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \ "cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \
@ -652,6 +672,7 @@ class SpotifyPKCE(SpotifyAuthBase):
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
@ -856,26 +877,22 @@ class SpotifyPKCE(SpotifyAuthBase):
self.OAUTH_TOKEN_URL, headers, payload self.OAUTH_TOKEN_URL, headers, payload
) )
response = self._session.post( try:
self.OAUTH_TOKEN_URL, response = self._session.post(
data=payload, self.OAUTH_TOKEN_URL,
headers=headers, data=payload,
verify=True, headers=headers,
proxies=self.proxies, verify=True,
timeout=self.requests_timeout, proxies=self.proxies,
) timeout=self.requests_timeout,
if response.status_code != 200: )
error_payload = response.json() response.raise_for_status()
raise SpotifyOauthError('error: {0}, error_descr: {1}'.format(error_payload['error'], token_info = response.json()
error_payload[ token_info = self._add_custom_values_to_token_info(token_info)
'error_description' self.cache_handler.save_token_to_cache(token_info)
]), return token_info["access_token"]
error=error_payload['error'], except requests.exceptions.HTTPError as http_error:
error_description=error_payload['error_description']) self._handle_oauth_error(http_error)
token_info = response.json()
token_info = self._add_custom_values_to_token_info(token_info)
self.cache_handler.save_token_to_cache(token_info)
return token_info["access_token"]
def refresh_access_token(self, refresh_token): def refresh_access_token(self, refresh_token):
payload = { payload = {
@ -891,28 +908,23 @@ class SpotifyPKCE(SpotifyAuthBase):
self.OAUTH_TOKEN_URL, headers, payload self.OAUTH_TOKEN_URL, headers, payload
) )
response = self._session.post( try:
self.OAUTH_TOKEN_URL, response = self._session.post(
data=payload, self.OAUTH_TOKEN_URL,
headers=headers, data=payload,
proxies=self.proxies, headers=headers,
timeout=self.requests_timeout, proxies=self.proxies,
) timeout=self.requests_timeout,
)
if response.status_code != 200: response.raise_for_status()
error_payload = response.json() token_info = response.json()
raise SpotifyOauthError( token_info = self._add_custom_values_to_token_info(token_info)
'error: {0}, error_description: {1}'.format( if "refresh_token" not in token_info:
error_payload['error'], error_payload['error_description']), token_info["refresh_token"] = refresh_token
error=error_payload['error'], self.cache_handler.save_token_to_cache(token_info)
error_description=error_payload['error_description']) return token_info
except requests.exceptions.HTTPError as http_error:
token_info = response.json() self._handle_oauth_error(http_error)
token_info = self._add_custom_values_to_token_info(token_info)
if "refresh_token" not in token_info:
token_info["refresh_token"] = refresh_token
self.cache_handler.save_token_to_cache(token_info)
return token_info
def parse_response_code(self, url): def parse_response_code(self, url):
""" Parse the response code in the given response url """ Parse the response code in the given response url

View File

@ -64,13 +64,13 @@ class AuthTestSpotipy(unittest.TestCase):
def test_audio_analysis(self): def test_audio_analysis(self):
result = self.spotify.audio_analysis(self.four_tracks[0]) result = self.spotify.audio_analysis(self.four_tracks[0])
assert('beats' in result) assert ('beats' in result)
def test_audio_features(self): def test_audio_features(self):
results = self.spotify.audio_features(self.four_tracks) results = self.spotify.audio_features(self.four_tracks)
self.assertTrue(len(results['audio_features']) == len(self.four_tracks)) self.assertTrue(len(results['audio_features']) == len(self.four_tracks))
for track in results['audio_features']: for track in results['audio_features']:
assert('speechiness' in track) assert ('speechiness' in track)
def test_audio_features_with_bad_track(self): def test_audio_features_with_bad_track(self):
bad_tracks = ['spotify:track:bad'] bad_tracks = ['spotify:track:bad']
@ -79,7 +79,7 @@ class AuthTestSpotipy(unittest.TestCase):
self.assertTrue(len(results['audio_features']) == len(input)) self.assertTrue(len(results['audio_features']) == len(input))
for track in results['audio_features'][:-1]: for track in results['audio_features'][:-1]:
if track is not None: if track is not None:
assert('speechiness' in track) assert ('speechiness' in track)
self.assertTrue(results['audio_features'][-1] is None) self.assertTrue(results['audio_features'][-1] is None)
def test_recommendations(self): def test_recommendations(self):
@ -159,6 +159,8 @@ class AuthTestSpotipy(unittest.TestCase):
results = self.spotify.artist_related_artists(self.weezer_urn) results = self.spotify.artist_related_artists(self.weezer_urn)
self.assertTrue('artists' in results) self.assertTrue('artists' in results)
self.assertTrue(len(results['artists']) == 20) self.assertTrue(len(results['artists']) == 20)
found = False
for artist in results['artists']: for artist in results['artists']:
if artist['name'] == 'Jimmy Eat World': if artist['name'] == 'Jimmy Eat World':
found = True found = True
@ -225,22 +227,24 @@ class AuthTestSpotipy(unittest.TestCase):
self.assertTrue('items' in results) self.assertTrue('items' in results)
self.assertTrue(len(results['items']) > 0) self.assertTrue(len(results['items']) > 0)
found = False def find_album():
for album in results['items']: for album in results['items']:
if album['name'] == 'Hurley': if album['name'] == 'Death to False Metal':
found = True return True
return False
self.assertTrue(found) self.assertTrue(find_album())
def test_search_timeout(self): def test_search_timeout(self):
auth_manager = SpotifyClientCredentials() auth_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(requests_timeout=0.01, sp = spotipy.Spotify(requests_timeout=0.01,
auth_manager=auth_manager) auth_manager=auth_manager)
# depending on the timing or bandwidth, this raises a timeout or connection error" # depending on the timing or bandwidth, this raises a timeout or connection error
self.assertRaises((requests.exceptions.Timeout, requests.exceptions.ConnectionError), self.assertRaises((requests.exceptions.Timeout, requests.exceptions.ConnectionError),
lambda: sp.search(q='my*', type='track')) lambda: sp.search(q='my*', type='track'))
@unittest.skip("flaky test, need a better method to test retries")
def test_max_retries_reached_get(self): def test_max_retries_reached_get(self):
spotify_no_retry = Spotify( spotify_no_retry = Spotify(
auth_manager=SpotifyClientCredentials(), auth_manager=SpotifyClientCredentials(),

View File

@ -74,6 +74,10 @@ class SpotipyPlaylistApiTest(unittest.TestCase):
cls.spotify.user_playlist_create(cls.username, cls.new_playlist_name) cls.spotify.user_playlist_create(cls.username, cls.new_playlist_name)
cls.new_playlist_uri = cls.new_playlist['uri'] cls.new_playlist_uri = cls.new_playlist['uri']
@classmethod
def tearDownClass(cls):
cls.spotify.current_user_unfollow_playlist(cls.new_playlist['id'])
def test_user_playlists(self): def test_user_playlists(self):
playlists = self.spotify.user_playlists(self.username, limit=5) playlists = self.spotify.user_playlists(self.username, limit=5)
self.assertTrue('items' in playlists) self.assertTrue('items' in playlists)
@ -135,14 +139,25 @@ class SpotipyPlaylistApiTest(unittest.TestCase):
self.assertEqual(pl["tracks"]["total"], 0) self.assertEqual(pl["tracks"]["total"], 0)
def test_max_retries_reached_post(self): def test_max_retries_reached_post(self):
for i in range(500): import concurrent.futures
try: max_workers = 100
self.spotify_no_retry.playlist_change_details( total_requests = 500
self.new_playlist['id'], description="test")
except SpotifyException as e: def do():
self.assertIsInstance(e, SpotifyException) self.spotify_no_retry.playlist_change_details(
self.assertEqual(e.http_status, 429) self.new_playlist['id'], description="test")
return
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_post = (executor.submit(do) for _i in range(1, total_requests))
for future in concurrent.futures.as_completed(future_to_post):
try:
future.result()
except Exception as exc:
# Test success
self.assertIsInstance(exc, SpotifyException)
self.assertEqual(exc.http_status, 429)
return
self.fail() self.fail()
def test_playlist_add_items(self): def test_playlist_add_items(self):
@ -237,9 +252,8 @@ class SpotipyLibraryApiTests(unittest.TestCase):
self.spotify.track('BadID123') self.spotify.track('BadID123')
def test_current_user_saved_tracks(self): def test_current_user_saved_tracks(self):
# TODO make this not fail if someone doesnthave saved tracks
tracks = self.spotify.current_user_saved_tracks() tracks = self.spotify.current_user_saved_tracks()
self.assertGreater(len(tracks['items']), 0) self.assertGreaterEqual(len(tracks['items']), 0)
def test_current_user_save_and_unsave_tracks(self): def test_current_user_save_and_unsave_tracks(self):
tracks = self.spotify.current_user_saved_tracks() tracks = self.spotify.current_user_saved_tracks()
@ -322,7 +336,7 @@ class SpotipyUserApiTests(unittest.TestCase):
def test_current_user_top_tracks(self): def test_current_user_top_tracks(self):
response = self.spotify.current_user_top_tracks() response = self.spotify.current_user_top_tracks()
items = response['items'] items = response['items']
self.assertGreater(len(items), 0) self.assertGreaterEqual(len(items), 0)
def test_current_user_top_artists(self): def test_current_user_top_artists(self):
response = self.spotify.current_user_top_artists() response = self.spotify.current_user_top_artists()
@ -336,13 +350,34 @@ class SpotipyBrowseApiTests(unittest.TestCase):
cls.spotify = _make_spotify() cls.spotify = _make_spotify()
def test_category(self): def test_category(self):
response = self.spotify.category('rock') rock_cat_id = '0JQ5DAqbMKFDXXwE9BDJAr'
self.assertTrue('name' in response) response = self.spotify.category(rock_cat_id)
self.assertEqual(response['name'], 'Rock')
def test_categories(self): def test_categories(self):
response = self.spotify.categories() response = self.spotify.categories()
self.assertGreater(len(response['categories']), 0) self.assertGreater(len(response['categories']), 0)
def test_categories_country(self):
response = self.spotify.categories(country='US')
self.assertGreater(len(response['categories']), 0)
def test_categories_global(self):
response = self.spotify.categories()
self.assertGreater(len(response['categories']), 0)
def test_categories_locale(self):
response = self.spotify.categories(locale='en_US')
self.assertGreater(len(response['categories']), 0)
def test_categories_limit_low(self):
response = self.spotify.categories(limit=1)
self.assertEqual(len(response['categories']['items']), 1)
def test_categories_limit_high(self):
response = self.spotify.categories(limit=50)
self.assertLessEqual(len(response['categories']['items']), 50)
def test_category_playlists(self): def test_category_playlists(self):
response = self.spotify.categories() response = self.spotify.categories()
category = 'rock' category = 'rock'
@ -352,9 +387,35 @@ class SpotipyBrowseApiTests(unittest.TestCase):
response = self.spotify.category_playlists(category_id=cat_id) response = self.spotify.category_playlists(category_id=cat_id)
self.assertGreater(len(response['playlists']["items"]), 0) self.assertGreater(len(response['playlists']["items"]), 0)
def test_category_playlists_limit_low(self):
response = self.spotify.categories()
category = 'rock'
for cat in response['categories']['items']:
cat_id = cat['id']
if cat_id == category:
response = self.spotify.category_playlists(category_id=cat_id, limit=1)
self.assertEqual(len(response['categories']['items']), 1)
def test_category_playlists_limit_high(self):
response = self.spotify.categories()
category = 'rock'
for cat in response['categories']['items']:
cat_id = cat['id']
if cat_id == category:
response = self.spotify.category_playlists(category_id=cat_id, limit=50)
self.assertLessEqual(len(response['categories']['items']), 50)
def test_new_releases(self): def test_new_releases(self):
response = self.spotify.new_releases() response = self.spotify.new_releases()
self.assertGreater(len(response['albums']), 0) self.assertGreater(len(response['albums']['items']), 0)
def test_new_releases_limit_low(self):
response = self.spotify.new_releases(limit=1)
self.assertEqual(len(response['albums']['items']), 1)
def test_new_releases_limit_high(self):
response = self.spotify.new_releases(limit=50)
self.assertLessEqual(len(response['albums']['items']), 50)
def test_featured_releases(self): def test_featured_releases(self):
response = self.spotify.featured_playlists() response = self.spotify.featured_playlists()
@ -384,7 +445,7 @@ class SpotipyFollowApiTests(unittest.TestCase):
def test_current_user_follows(self): def test_current_user_follows(self):
response = self.spotify.current_user_followed_artists() response = self.spotify.current_user_followed_artists()
artists = response['artists'] artists = response['artists']
self.assertGreater(len(artists['items']), 0) self.assertGreaterEqual(len(artists['items']), 0)
def test_user_follows_and_unfollows_artist(self): def test_user_follows_and_unfollows_artist(self):
# Initially follows 1 artist # Initially follows 1 artist
@ -437,7 +498,7 @@ class SpotipyPlayerApiTests(unittest.TestCase):
def test_devices(self): def test_devices(self):
# No devices playing by default # No devices playing by default
res = self.spotify.devices() res = self.spotify.devices()
self.assertEqual(len(res["devices"]), 0) self.assertGreaterEqual(len(res["devices"]), 0)
def test_current_user_recently_played(self): def test_current_user_recently_played(self):
# No cursor # No cursor
@ -458,22 +519,6 @@ class SpotifyPKCETests(unittest.TestCase):
auth_manager = SpotifyPKCE(scope=scope, cache_handler=cache_handler) auth_manager = SpotifyPKCE(scope=scope, cache_handler=cache_handler)
cls.spotify = Spotify(auth_manager=auth_manager) cls.spotify = Spotify(auth_manager=auth_manager)
def test_user_follows_and_unfollows_artist(self):
# Initially follows 1 artist
current_user_followed_artists = self.spotify.current_user_followed_artists()[
'artists']['total']
# Follow 2 more artists
artists = ["6DPYiyq5kWVQS4RGwxzPC7", "0NbfKEOTQCcwd6o7wSDOHI"]
self.spotify.user_follow_artists(artists)
res = self.spotify.current_user_followed_artists()
self.assertEqual(res['artists']['total'], current_user_followed_artists + len(artists))
# Unfollow these 2 artists
self.spotify.user_unfollow_artists(artists)
res = self.spotify.current_user_followed_artists()
self.assertEqual(res['artists']['total'], current_user_followed_artists)
def test_current_user(self): def test_current_user(self):
c_user = self.spotify.current_user() c_user = self.spotify.current_user()
user = self.spotify.user(c_user['id']) user = self.spotify.user(c_user['id'])

View File

@ -229,7 +229,7 @@ class TestSpotifyClientCredentials(unittest.TestCase):
def test_spotify_client_credentials_get_access_token(self): def test_spotify_client_credentials_get_access_token(self):
oauth = SpotifyClientCredentials(client_id='ID', client_secret='SECRET') oauth = SpotifyClientCredentials(client_id='ID', client_secret='SECRET')
with self.assertRaises(SpotifyOauthError) as error: with self.assertRaises(SpotifyOauthError) as error:
oauth.get_access_token() oauth.get_access_token(check_cache=False)
self.assertEqual(error.exception.error, 'invalid_client') self.assertEqual(error.exception.error, 'invalid_client')