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,11 +5,10 @@ on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
strategy:
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:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}

View File

@ -50,11 +50,76 @@ While this is unreleased, please only add v3 features here. Rebasing master onto
### 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 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
@ -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
- Added a cache handler to `SpotifyClientCredentials`
- Added the following endpoints
* `Spotify.current_user_saved_episodes`
* `Spotify.current_user_saved_episodes_add`
* `Spotify.current_user_saved_episodes_delete`
* `Spotify.current_user_saved_episodes_contains`
* `Spotify.available_markets`
- `Spotify.current_user_saved_episodes`
- `Spotify.current_user_saved_episodes_add`
- `Spotify.current_user_saved_episodes_delete`
- `Spotify.current_user_saved_episodes_contains`
- `Spotify.available_markets`
### Changed
@ -75,9 +140,9 @@ While this is unreleased, please only add v3 features here. Rebasing master onto
### 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 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__`.
- 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.
- Cleaned up the documentation for `SpotifyClientCredentials.__init__`, `SpotifyOAuth.__init__`, and `SpotifyPKCE.__init__`.
## [2.17.1] - 2021-02-28
@ -265,7 +330,6 @@ While this is unreleased, please only add v3 features here. Rebasing master onto
- device id. If None, then the active device is used.
- Add CHANGELOG and LICENSE to released package
## [2.9.0] - 2020-02-15
### Added

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
```bash
# Linux or Mac
export SPOTIPY_CLIENT_ID=client_id_here
export SPOTIPY_CLIENT_SECRET=client_secret_here
export SPOTIPY_CLIENT_USERNAME=client_username_here # This is actually an id not spotify display name
export SPOTIPY_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:
```bash
$ virtualenv --python=python3.7 env
$ source env/bin/activate
(env) $ pip install --user -e .
(env) $ python -m unittest discover -v tests
```
@ -44,7 +52,13 @@ To make sure if the import lists are stored correctly:
## Unreleased
// Add your changes here and then delete this line
// Add new changes below
### Added
### Fixed
### Removed
- Commit changes
- Package to pypi:
@ -52,7 +66,7 @@ To make sure if the import lists are stored correctly:
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
twine upload dist/*
- 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

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
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
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 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.
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -14,6 +14,12 @@ Spotipy's full documentation is online at [Spotipy Documentation](http://spotipy
pip install spotipy
```
alternatively, for Windows users
```bash
py -m pip install spotipy
```
or upgrade
```bash
@ -43,6 +49,8 @@ for idx, track in enumerate(results['tracks']['items']):
### 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
import spotipy
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
# 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,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('.'))
import spotipy
# -- General configuration -----------------------------------------------------

View File

@ -9,7 +9,7 @@ Welcome to Spotipy!
you get full access to all of the music data provided by the Spotify platform.
Assuming you set the ``SPOTIPY_CLIENT_ID`` and ``SPOTIPY_CLIENT_SECRET``
environment variables, here'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'::
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'])
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)::
export SPOTIPY_CLIENT_ID='your-spotify-client-id'
@ -143,7 +143,7 @@ Scopes
------
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.
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)
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
=======================
@ -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.
An instance of that new class can then be passed as a parameter when
creating ``SpotifyOAuth``, ``SpotifyPKCE`` or ``SpotifyImplicitGrant``.
The following handlers are available and defined in the URL above.
- ``CacheFileHandler``
- ``MemoryCacheHandler``
- ``DjangoSessionCacheHandler``
- ``FlaskSessionCacheHandler``
- ``RedisCacheHandler``
Feel free to contribute new cache handlers to the repo.
@ -282,31 +293,96 @@ Contribute
Spotipy authored by Paul Lamere (plamere) with contributions by:
- Daniel Beaudry // danbeaudry
- Faruk Emre Sahin // fsahin
- George // rogueleaderr
- Henry Greville // sethaurus
- Hugo // hugovk
- José Manuel Pérez // JMPerez
- Lucas Nunno // lnunno
- Lynn Root // econchick
- Matt Dennewitz // mattdennewitz
- Matthew Duck // mattduck
- Michael Thelin // thelinmichael
- Ryan Choi // ryankicks
- Simon Metson // drsm79
- Steve Winton // swinton
- Tim Balzer // timbalzer
- corycorycory // corycorycory
- Nathan Coleman // nathancoleman
- Michael Birtwell // mbirtwell
- Harrison Hayes // Harrison97
- Stephane Bruckert // stephanebruckert
- Ritiek Malhotra // ritiek
- Daniel Beaudry (`danbeaudry on Github <https://github.com/danbeaudry>`_)
- Faruk Emre Sahin (`fsahin on Github <https://github.com/fsahin>`_)
- George (`rogueleaderr on Github <https://github.com/rogueleaderr>`_)
- Henry Greville (`sethaurus on Github <https://github.com/sethaurus>`_)
- Hugo van Kemanade (`hugovk on Github <https://github.com/hugovk>`_)
- José Manuel Pérez (`JMPerez on Github <https://github.com/JMPerez>`_)
- Lucas Nunno (`lnunno on Github <https://github.com/lnunno>`_)
- Lynn Root (`econchick on Github <https://github.com/econchick>`_)
- Matt Dennewitz (`mattdennewitz on Github <https://github.com/mattdennewitz>`_)
- Matthew Duck (`mattduck on Github <https://github.com/mattduck>`_)
- Michael Thelin (`thelinmichael on Github <https://github.com/thelinmichael>`_)
- Ryan Choi (`ryankicks on Github <https://github.com/ryankicks>`_)
- Simon Metson (`drsm79 on Github <https://github.com/drsm79>`_)
- Steve Winton (`swinton on Github <https://github.com/swinton>`_)
- Tim Balzer (`timbalzer on Github <https://github.com/timbalzer>`_)
- `corycorycory on Github <https://github.com/corycorycory>`_
- Nathan Coleman (`nathancoleman on Github <https://github.com/nathancoleman>`_)
- Michael Birtwell (`mbirtwell on Github <https://github.com/mbirtwell>`_)
- Harrison Hayes (`Harrison97 on Github <https://github.com/Harrison97>`_)
- Stephane Bruckert (`stephanebruckert on Github <https://github.com/stephanebruckert>`_)
- 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
=======
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

View File

@ -11,7 +11,7 @@ scope = 'playlist-modify-public'
def get_args():
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')
parser.add_argument('-p', '--playlist', required=True,
help='Playlist to add track to')
@ -22,7 +22,7 @@ def main():
args = get_args()
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__':

View File

@ -27,7 +27,6 @@ import os
from flask import Flask, session, request, redirect
from flask_session import Session
import spotipy
import uuid
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(64)
@ -35,35 +34,26 @@ app.config['SESSION_TYPE'] = 'filesystem'
app.config['SESSION_FILE_DIR'] = './.flask_session/'
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('/')
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',
cache_handler=cache_handler,
show_dialog=True)
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"))
return redirect('/')
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()
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)
return f'<h2>Hi {spotify.me()["display_name"]}, ' \
f'<small><a href="/sign_out">[sign out]<a/></small></h2>' \
@ -72,20 +62,16 @@ def index():
f'<a href="/current_user">me</a>' \
@app.route('/sign_out')
def sign_out():
try:
# 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))
session.pop("token_info", None)
return redirect('/')
@app.route('/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)
if not auth_manager.validate_token(cache_handler.get_cached_token()):
return redirect('/')
@ -96,7 +82,7 @@ def playlists():
@app.route('/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)
if not auth_manager.validate_token(cache_handler.get_cached_token()):
return redirect('/')
@ -109,7 +95,7 @@ def currently_playing():
@app.route('/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)
if not auth_manager.validate_token(cache_handler.get_cached_token()):
return redirect('/')

View File

@ -60,6 +60,7 @@ def show_artist(artist):
if len(artist['genres']) > 0:
logger.info('Genres: %s', ','.join(artist['genres']))
def main():
args = get_args()
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
# scope is not required for this function
from spotipy.oauth2 import SpotifyClientCredentials
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

@ -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_email="paul@echonest.com",
url='https://spotipy.readthedocs.org/',
project_urls={
'Source': 'https://github.com/plamere/spotipy',
},
install_requires=[
'requests>=2.25.0',
'six>=1.15.0',
'urllib3>=1.26.0'
"redis>=3.5.3",
"redis<4.0.0;python_version<'3.4'",
"requests>=2.25.0",
"six>=1.15.0",
"urllib3>=1.26.0"
],
tests_require=test_reqs,
extras_require=extra_reqs,
license='LICENSE.md',
license='MIT',
packages=['spotipy'])

View File

@ -1,6 +1,10 @@
# -*- coding: utf-8 -*-
__all__ = ['CacheHandler', 'CacheFileHandler', 'MemoryCacheHandler']
__all__ = [
'CacheHandler',
'CacheFileHandler',
'DjangoSessionCacheHandler',
'FlaskSessionCacheHandler',
'MemoryCacheHandler',
'RedisCacheHandler']
import errno
import json
@ -9,6 +13,8 @@ import os
from spotipy.util import CLIENT_CREDS_ENV_VARS
from abc import ABC, abstractmethod
from redis import RedisError
logger = logging.getLogger(__name__)
@ -107,3 +113,94 @@ class MemoryCacheHandler(CacheHandler):
def save_token_to_cache(self, 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
:param language:
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:
@ -232,16 +232,22 @@ class Spotify(object):
except requests.exceptions.HTTPError as http_error:
response = http_error.response
try:
msg = response.json()["error"]["message"]
except (ValueError, KeyError):
msg = "error"
try:
reason = response.json()["error"]["reason"]
except (ValueError, KeyError):
json_response = response.json()
error = json_response.get("error", {})
msg = error.get("message")
reason = error.get("reason")
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`
msg = response.text or None
reason = None
logger.error('HTTP Error for %s to %s returned %s due to %s',
method, url, response.status_code, msg)
logger.error(
'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(
response.status_code,
@ -399,14 +405,18 @@ class Spotify(object):
trid = self._get_id("artist", artist_id)
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
Parameters:
- album_id - the album ID, URI or URL
- market - an ISO 3166-1 alpha-2 country code
"""
trid = self._get_id("album", album_id)
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):
@ -425,14 +435,18 @@ class Spotify(object):
"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
Parameters:
- 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]
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):
@ -612,7 +626,7 @@ class Spotify(object):
""" Get full details of the tracks and episodes of a playlist.
Parameters:
- playlist_id - the id of the playlist
- playlist_id - the playlist ID, URI or URL
- fields - which fields to return
- limit - the maximum number of tracks 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):
""" Get cover of a playlist.
""" Get cover image of a playlist.
Parameters:
- playlist_id - the id of the playlist
- playlist_id - the playlist ID, URI or URL
"""
plid = self._get_id("playlist", playlist_id)
return self._get("playlists/%s/images" % (plid))
@ -693,7 +707,8 @@ class Spotify(object):
collaborative=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:
- playlist_id - the id of the playlist
@ -734,7 +749,7 @@ class Spotify(object):
Parameters:
- 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
"""
plid = self._get_id("playlist", playlist_id)
@ -793,7 +808,7 @@ class Spotify(object):
def playlist_remove_all_occurrences_of_items(
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:
- playlist_id - the id of the playlist
@ -893,7 +908,7 @@ class Spotify(object):
"Your Music" library
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
- market - an ISO 3166-1 alpha-2 country code.
@ -1096,7 +1111,7 @@ class Spotify(object):
)
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
@ -1194,8 +1209,8 @@ class Spotify(object):
Parameters:
- locale - The desired language, consisting of a lowercase ISO
639 language code and an uppercase ISO 3166-1 alpha-2 country
code, joined by an underscore.
639-1 alpha-2 language code and an uppercase ISO 3166-1 alpha-2
country code, joined by an underscore.
- country - An ISO 3166-1 alpha-2 country code.
@ -1244,7 +1259,7 @@ class Spotify(object):
- category_id - The Spotify category ID for the category.
- 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
by an underscore.
"""
@ -1259,7 +1274,7 @@ class Spotify(object):
Parameters:
- 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
by an underscore.
@ -1436,7 +1451,7 @@ class Spotify(object):
):
""" 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.
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):
""" 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
and pass in the id for device B, you will get a
'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 device_id:
@ -1619,7 +1638,7 @@ class Spotify(object):
if type != fields[-2]:
logger.warning('Expected id of type %s but found type %s %s',
type, fields[-2], id)
return fields[-1]
return fields[-1].split("?")[0]
fields = id.split("/")
if len(fields) >= 3:
itype = fields[-2]

View File

@ -41,7 +41,7 @@ class SpotifyOauthError(Exception):
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,
error=None, error_description=None, *args, **kwargs):
@ -146,9 +146,6 @@ class SpotifyAuthBase(object):
@staticmethod
def _get_user_input(prompt):
try:
return raw_input(prompt)
except NameError:
return input(prompt)
@staticmethod
@ -164,6 +161,28 @@ class SpotifyAuthBase(object):
)
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):
"""Make sure the connection (pool) gets closed"""
if isinstance(self._session, requests.Session):
@ -228,7 +247,7 @@ class SpotifyClientCredentials(SpotifyAuthBase):
def get_access_token(self, check_cache=True):
"""
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:
- check_cache - if true, checks for a locally stored token
@ -258,6 +277,7 @@ class SpotifyClientCredentials(SpotifyAuthBase):
self.OAUTH_TOKEN_URL, headers, payload
)
try:
response = self._session.post(
self.OAUTH_TOKEN_URL,
data=payload,
@ -266,15 +286,11 @@ class SpotifyClientCredentials(SpotifyAuthBase):
proxies=self.proxies,
timeout=self.requests_timeout,
)
if response.status_code != 200:
error_payload = response.json()
raise SpotifyOauthError(
'error: {0}, error_description: {1}'.format(
error_payload['error'], error_payload['error_description']),
error=error_payload['error'],
error_description=error_payload['error_description'])
response.raise_for_status()
token_info = response.json()
return token_info
except requests.exceptions.HTTPError as http_error:
self._handle_oauth_error(http_error)
def _add_custom_values_to_token_info(self, token_info):
"""
@ -328,7 +344,7 @@ class SpotifyOAuth(SpotifyAuthBase):
for performance reasons (connection pooling).
* requests_timeout: Optional, tell Requests to stop waiting for a response after
a given number of seconds
* open_browser: Optional, whether or not the web browser should be opened to
* open_browser: Optional, whether the web browser should be opened to
authorize a user
"""
@ -339,6 +355,7 @@ class SpotifyOAuth(SpotifyAuthBase):
self.redirect_uri = redirect_uri
self.state = state
self.scope = self._normalize_scope(scope)
if cache_handler:
assert issubclass(cache_handler.__class__, CacheHandler), \
"cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \
@ -346,6 +363,7 @@ class SpotifyOAuth(SpotifyAuthBase):
self.cache_handler = cache_handler
else:
self.cache_handler = CacheFileHandler()
self.proxies = proxies
self.requests_timeout = requests_timeout
self.show_dialog = show_dialog
@ -443,13 +461,12 @@ class SpotifyOAuth(SpotifyAuthBase):
self._open_auth_url()
server.handle_request()
if self.state is not None and server.state != self.state:
raise SpotifyStateError(self.state, server.state)
if server.auth_code is not None:
return server.auth_code
elif server.error is not None:
if server.error is not None:
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:
raise SpotifyOauthError("Server listening on localhost has not been accessed")
@ -523,6 +540,7 @@ class SpotifyOAuth(SpotifyAuthBase):
self.OAUTH_TOKEN_URL, headers, payload
)
try:
response = self._session.post(
self.OAUTH_TOKEN_URL,
data=payload,
@ -531,17 +549,13 @@ class SpotifyOAuth(SpotifyAuthBase):
proxies=self.proxies,
timeout=self.requests_timeout,
)
if response.status_code != 200:
error_payload = response.json()
raise SpotifyOauthError(
'error: {0}, error_description: {1}'.format(
error_payload['error'], error_payload['error_description']),
error=error_payload['error'],
error_description=error_payload['error_description'])
response.raise_for_status()
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"]
except requests.exceptions.HTTPError as http_error:
self._handle_oauth_error(http_error)
def refresh_access_token(self, refresh_token):
payload = {
@ -556,6 +570,7 @@ class SpotifyOAuth(SpotifyAuthBase):
self.OAUTH_TOKEN_URL, headers, payload
)
try:
response = self._session.post(
self.OAUTH_TOKEN_URL,
data=payload,
@ -563,21 +578,15 @@ class SpotifyOAuth(SpotifyAuthBase):
proxies=self.proxies,
timeout=self.requests_timeout,
)
if response.status_code != 200:
error_payload = response.json()
raise SpotifyOauthError(
'error: {0}, error_description: {1}'.format(
error_payload['error'], error_payload['error_description']),
error=error_payload['error'],
error_description=error_payload['error_description'])
response.raise_for_status()
token_info = response.json()
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
except requests.exceptions.HTTPError as http_error:
self._handle_oauth_error(http_error)
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
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
app is then given both access and refresh tokens. This is the
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` or `str`. E.g.,
{Scope.user_modify_playback_state, Scope.user_library_read}
* cache_path: (deprecated) Optional, will otherwise be generated
(takes precedence over `username`)
* username: (deprecated) Optional or set as environment variable
(will set `cache_path` to `.cache-{username}`)
* proxies: Optional, proxy for the requests library to route through
* requests_timeout: Optional, tell Requests to stop waiting for a response after
a given number of seconds
* requests_session: A Requests session
* open_browser: Optional, whether the web browser should be opened to
authorize a user
* cache_handler: An instance of the `CacheHandler` class to handle
getting and saving cached authorization tokens.
Optional, will otherwise use `CacheFileHandler`.
@ -645,6 +664,7 @@ class SpotifyPKCE(SpotifyAuthBase):
self.redirect_uri = redirect_uri
self.state = state
self.scope = self._normalize_scope(scope)
if cache_handler:
assert issubclass(cache_handler.__class__, CacheHandler), \
"cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \
@ -652,6 +672,7 @@ class SpotifyPKCE(SpotifyAuthBase):
self.cache_handler = cache_handler
else:
self.cache_handler = CacheFileHandler()
self.proxies = proxies
self.requests_timeout = requests_timeout
@ -856,6 +877,7 @@ class SpotifyPKCE(SpotifyAuthBase):
self.OAUTH_TOKEN_URL, headers, payload
)
try:
response = self._session.post(
self.OAUTH_TOKEN_URL,
data=payload,
@ -864,18 +886,13 @@ class SpotifyPKCE(SpotifyAuthBase):
proxies=self.proxies,
timeout=self.requests_timeout,
)
if response.status_code != 200:
error_payload = response.json()
raise SpotifyOauthError('error: {0}, error_descr: {1}'.format(error_payload['error'],
error_payload[
'error_description'
]),
error=error_payload['error'],
error_description=error_payload['error_description'])
response.raise_for_status()
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"]
except requests.exceptions.HTTPError as http_error:
self._handle_oauth_error(http_error)
def refresh_access_token(self, refresh_token):
payload = {
@ -891,6 +908,7 @@ class SpotifyPKCE(SpotifyAuthBase):
self.OAUTH_TOKEN_URL, headers, payload
)
try:
response = self._session.post(
self.OAUTH_TOKEN_URL,
data=payload,
@ -898,21 +916,15 @@ class SpotifyPKCE(SpotifyAuthBase):
proxies=self.proxies,
timeout=self.requests_timeout,
)
if response.status_code != 200:
error_payload = response.json()
raise SpotifyOauthError(
'error: {0}, error_description: {1}'.format(
error_payload['error'], error_payload['error_description']),
error=error_payload['error'],
error_description=error_payload['error_description'])
response.raise_for_status()
token_info = response.json()
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
except requests.exceptions.HTTPError as http_error:
self._handle_oauth_error(http_error)
def parse_response_code(self, url):
""" Parse the response code in the given response url

View File

@ -159,6 +159,8 @@ class AuthTestSpotipy(unittest.TestCase):
results = self.spotify.artist_related_artists(self.weezer_urn)
self.assertTrue('artists' in results)
self.assertTrue(len(results['artists']) == 20)
found = False
for artist in results['artists']:
if artist['name'] == 'Jimmy Eat World':
found = True
@ -225,22 +227,24 @@ class AuthTestSpotipy(unittest.TestCase):
self.assertTrue('items' in results)
self.assertTrue(len(results['items']) > 0)
found = False
def find_album():
for album in results['items']:
if album['name'] == 'Hurley':
found = True
if album['name'] == 'Death to False Metal':
return True
return False
self.assertTrue(found)
self.assertTrue(find_album())
def test_search_timeout(self):
auth_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(requests_timeout=0.01,
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),
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):
spotify_no_retry = Spotify(
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.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):
playlists = self.spotify.user_playlists(self.username, limit=5)
self.assertTrue('items' in playlists)
@ -135,14 +139,25 @@ class SpotipyPlaylistApiTest(unittest.TestCase):
self.assertEqual(pl["tracks"]["total"], 0)
def test_max_retries_reached_post(self):
for i in range(500):
try:
import concurrent.futures
max_workers = 100
total_requests = 500
def do():
self.spotify_no_retry.playlist_change_details(
self.new_playlist['id'], description="test")
except SpotifyException as e:
self.assertIsInstance(e, SpotifyException)
self.assertEqual(e.http_status, 429)
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()
def test_playlist_add_items(self):
@ -237,9 +252,8 @@ class SpotipyLibraryApiTests(unittest.TestCase):
self.spotify.track('BadID123')
def test_current_user_saved_tracks(self):
# TODO make this not fail if someone doesnthave 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):
tracks = self.spotify.current_user_saved_tracks()
@ -322,7 +336,7 @@ class SpotipyUserApiTests(unittest.TestCase):
def test_current_user_top_tracks(self):
response = self.spotify.current_user_top_tracks()
items = response['items']
self.assertGreater(len(items), 0)
self.assertGreaterEqual(len(items), 0)
def test_current_user_top_artists(self):
response = self.spotify.current_user_top_artists()
@ -336,13 +350,34 @@ class SpotipyBrowseApiTests(unittest.TestCase):
cls.spotify = _make_spotify()
def test_category(self):
response = self.spotify.category('rock')
self.assertTrue('name' in response)
rock_cat_id = '0JQ5DAqbMKFDXXwE9BDJAr'
response = self.spotify.category(rock_cat_id)
self.assertEqual(response['name'], 'Rock')
def test_categories(self):
response = self.spotify.categories()
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):
response = self.spotify.categories()
category = 'rock'
@ -352,9 +387,35 @@ class SpotipyBrowseApiTests(unittest.TestCase):
response = self.spotify.category_playlists(category_id=cat_id)
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):
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):
response = self.spotify.featured_playlists()
@ -384,7 +445,7 @@ class SpotipyFollowApiTests(unittest.TestCase):
def test_current_user_follows(self):
response = self.spotify.current_user_followed_artists()
artists = response['artists']
self.assertGreater(len(artists['items']), 0)
self.assertGreaterEqual(len(artists['items']), 0)
def test_user_follows_and_unfollows_artist(self):
# Initially follows 1 artist
@ -437,7 +498,7 @@ class SpotipyPlayerApiTests(unittest.TestCase):
def test_devices(self):
# No devices playing by default
res = self.spotify.devices()
self.assertEqual(len(res["devices"]), 0)
self.assertGreaterEqual(len(res["devices"]), 0)
def test_current_user_recently_played(self):
# No cursor
@ -458,22 +519,6 @@ class SpotifyPKCETests(unittest.TestCase):
auth_manager = SpotifyPKCE(scope=scope, cache_handler=cache_handler)
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):
c_user = self.spotify.current_user()
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):
oauth = SpotifyClientCredentials(client_id='ID', client_secret='SECRET')
with self.assertRaises(SpotifyOauthError) as error:
oauth.get_access_token()
oauth.get_access_token(check_cache=False)
self.assertEqual(error.exception.error, 'invalid_client')