Compare commits

..

No commits in common. "master" and "2.24.0" have entirely different histories.

80 changed files with 1909 additions and 995 deletions

View File

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

View File

@ -1,23 +0,0 @@
name: Lint
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x" # Lint can be done on latest Python only
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install .[test]
- name: Check pep8 with flake8
run: |
flake8 . --count --show-source --statistics
- name: Check sorted imports with isort
run: |
isort . -c

View File

@ -10,7 +10,8 @@ on:
jobs: jobs:
build-n-publish: build-n-publish:
name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python

View File

@ -6,10 +6,10 @@ on:
jobs: jobs:
# Enforces the update of a changelog file on every pull request # Enforces the update of a changelog file on every pull request
changelog: changelog:
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dangoslen/changelog-enforcer@v3.6.1 - uses: dangoslen/changelog-enforcer@v3.6.1
with: with:
changeLogPath: 'CHANGELOG.md' changeLogPath: 'CHANGELOG.md'
skipLabels: 'skip-changelog' skipLabel: 'skip-changelog'

View File

@ -1,10 +1,11 @@
name: Unit tests name: Tests
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
build: build:
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
strategy: strategy:
matrix: matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
@ -17,7 +18,12 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install . pip install .[test]
- name: Lint with flake8
run: |
pip install -Iv enum34==1.1.6 # https://bitbucket.org/stoneleaf/enum34/issues/27/enum34-118-broken
pip install flake8
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

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "examples"]
path = examples
url = git@github.com:spotipy-dev/spotipy-examples.git

View File

@ -9,137 +9,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Add your changes below. Add your changes below.
### Added ### Added
-
### Fixed ### Fixed
-
### Removed ### Removed
-
## [2.26.0] - 2026-03-03
### Added
- Created generic methods to get user saved items
### Fixed
- Updated `/tracks` endpoints to `/items`
- Switching IDs to URIs to use `/me/library` endpoint
- Fixed playlist limit to 50 (according to API)
- Added warnings for deprecated methods
### Removed
## [2.25.2] - 2025-11-26
### Added
- Adds `additional_types` parameter to retrieve currently playing episode
- Add deprecation warnings to documentation
### Fixed
- Fixed dead link in README.md
- Corrected Spotify/Spotipy typo in documentation
- Sanitize HTML error message output for OAuth flow: https://github.com/spotipy-dev/spotipy/security/advisories/GHSA-r77h-rpp9-w2xm
## [2.25.1] - 2025-02-27
### Added
- Added examples for audiobooks, shows and episodes methods to examples directory
### Fixed
- Fixed scripts in examples directory that didn't run correctly
- Updated documentation for `Client.current_user_top_artists` to indicate maximum number of artists limit
- Set auth cache file permissions to `600`: https://github.com/spotipy-dev/spotipy/security/advisories/GHSA-pwhh-q4h6-w599
- Fixed `__del__` methods by preventing garbage collection for `requests.Session`
- Improved retry warning by using `logger` instead of `logging` and making sure that `retry_header` is an int
### Changed
- Updated get_cached_token and save_token_to_cache methods to utilize Python's Context Management Protocol
- Added except clause to get_cached_token method to handle json decode errors
- Added warnings and updated docs due to Spotify's deprecation of HTTP and "localhost" redirect URIs
- Use newer string formatters (<https://pyformat.info>)
- Marked `recommendation_genre_seeds` as deprecated
## [2.25.0] - 2025-03-01
### Added
- Added unit tests for queue functions
- Added detailed function docstrings to 'util.py', including descriptions and special sections that lists arguments, returns, and raises.
- Updated order of instructions for Python and pip package manager installation in TUTORIAL.md
- Updated TUTORIAL.md instructions to match current layout of Spotify Developer Dashboard
- Added test_artist_id, test_artist_url, and test_artists_mixed_ids to non_user_endpoints test.py
- Added rate/request limit to FAQ
- Added custom `urllib3.Retry` class for printing a warning when a rate/request limit is reached.
- Added `personalized_playlist.py`, `track_recommendations.py`, and `audio_features_analysis.py` to `/examples`.
- Discord badge in README
- Added `SpotifyBaseException` and moved all exceptions to `exceptions.py`
- Marked the following methods as deprecated:
- artist_related_artists
- recommendations
- audio_features
- audio_analysis
- featured_playlists
- category_playlists
- Added FAQ entry for inaccessible playlists
- Workflow to check for f-strings
### Changed
- Split test and lint workflows
- Updated get_cached_token and save_token_to_cache methods to utilize Python's Context Management Protocol
- Added except clause to get_cached_token method to handle json decode errors
### Fixed
- Audiobook integration tests
- Edited docstrings for certain functions in client.py for functions that are no longer in use and have been replaced.
- `current_user_unfollow_playlist()` now supports playlist IDs, URLs, and URIs rather than previously where it only supported playlist IDs.
### Removed
- `mock` no longer listed as a test dependency. Only built-in `unittest.mock` is actually used.
## [2.24.0] - 2024-05-30 ## [2.24.0] - 2024-05-30
### Added ### Added
- Added `MemcacheCacheHandler`, a cache handler that stores the token info using pymemcache. - Added `MemcacheCacheHandler`, a cache handler that stores the token info using pymemcache.
- Added support for audiobook endpoints: `get_audiobook`, `get_audiobooks`, and `get_audiobook_chapters`. - Added support for audiobook endpoints: `get_audiobook`, `get_audiobooks`, and `get_audiobook_chapters`.
- Added integration tests for audiobook endpoints. - Added integration tests for audiobook endpoints.
- Added `update` field to `current_user_follow_playlist`. - Added `update` field to `current_user_follow_playlist`.
### Changed ### Changed
- Updated get_cached_token and save_token_to_cache methods to utilize Python's Context Management Protocol
- Added except clause to get_cached_token method to handle json decode errors
- Fixed error obfuscation when Spotify class is being inherited and an error is raised in the Child's `__init__` - Fixed error obfuscation when Spotify class is being inherited and an error is raised in the Child's `__init__`
- Replaced `artist_albums(album_type=...)` with `artist_albums(include_groups=...)` due to an API change. - Replaced `artist_albums(album_type=...)` with `artist_albums(include_groups=...)` due to an API change.
- Updated `_regex_spotify_url` to ignore `/intl-<countrycode>` in Spotify links - Updated `_regex_spotify_url` to ignore `/intl-<countrycode>` in Spotify links
- Improved README, docs and examples - Improved README, docs and examples
### Fixed ### Fixed
- Readthedocs build - Readthedocs build
- Split `test_current_user_save_and_usave_tracks` unit test - Split `test_current_user_save_and_usave_tracks` unit test
### Removed ### Removed
- Drop support for EOL Python 3.7 - Drop support for EOL Python 3.7
## [2.23.0] - 2023-04-07 ## [2.23.0] - 2023-04-07
### Added ### Added
- Added optional `encoder_cls` argument to `CacheFileHandler`, which overwrite default encoder for token before writing to disk - Added optional `encoder_cls` argument to `CacheFileHandler`, which overwrite default encoder for token before writing to disk
- Integration tests for searching multiple types in multiple markets (non-user endpoints) - Integration tests for searching multiple types in multiple markets (non-user endpoints)
- Publish to PyPI action - Publish to PyPI action
### Fixed ### Fixed
- Fixed the regex for matching playlist URIs with the format spotify:user:USERNAME:playlist:PLAYLISTID. - Fixed the regex for matching playlist URIs with the format spotify:user:USERNAME:playlist:PLAYLISTID.
- `search_markets` now factors the counts of all types in the `total` rather than just the first type ([#534](https://github.com/spotipy-dev/spotipy/issues/534)) - `search_markets` now factors the counts of all types in the `total` rather than just the first type ([#534](https://github.com/spotipy-dev/spotipy/issues/534))
@ -524,7 +430,6 @@ Add your changes below.
### Changed ### Changed
- Made instructions in the CONTRIBUTING.md file more clear such that it is easier to onboard and there are no conflicts with TUTORIAL.md - Made instructions in the CONTRIBUTING.md file more clear such that it is easier to onboard and there are no conflicts with TUTORIAL.md
## [2.5.0] - 2020-01-11 ## [2.5.0] - 2020-01-11
Added follow and player endpoints Added follow and player endpoints

View File

@ -9,37 +9,16 @@ If you would like to contribute to spotipy follow these steps:
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 and can be found [here](https://www.spotify.com/us/account/overview/) export SPOTIPY_CLIENT_USERNAME=client_username_here # This is actually an id not spotify display name and can be found [here](https://www.spotify.com/us/account/overview/)
export SPOTIPY_REDIRECT_URI=http://127.0.0.1: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 # Windows
$env:SPOTIPY_CLIENT_ID="client_id_here" $env:SPOTIPY_CLIENT_ID="client_id_here"
$env:SPOTIPY_CLIENT_SECRET="client_secret_here" $env:SPOTIPY_CLIENT_SECRET="client_secret_here"
$env:SPOTIPY_CLIENT_USERNAME="client_username_here" $env:SPOTIPY_CLIENT_USERNAME="client_username_here"
$env:SPOTIPY_REDIRECT_URI="http://127.0.0.1:8080" $env:SPOTIPY_REDIRECT_URI="http://localhost:8080"
``` ```
### Branch Overview ### Create virtual environment, install dependencies, run tests:
After restarting development on version 3, we decided to restrict commits to certain branches in order to push the development forward.
To give you a flavour of what we mean, here are some examples of what PRs go where:
**v3**:
- any kind of refactoring
- better documentation
- enhancements
- code styles
**master (v2)**:
- bug fixes
- deprecations
- new endpoints (until we release v3)
- basic functionality
Just choose v3 if you are unsure which branch to work on.
### Create virtual environment, install dependencies, run tests
```bash ```bash
$ virtualenv --python=python3 env $ virtualenv --python=python3 env
@ -50,23 +29,20 @@ $ source env/bin/activate
### Lint ### Lint
pip install ".[test]" To automatically fix the code style:
To automatically fix some of the code style:
pip install autopep8
autopep8 --in-place --aggressive --recursive . autopep8 --in-place --aggressive --recursive .
To verify the code style: To verify the code style:
pip install flake8
flake8 . flake8 .
To make sure if the import lists are stored correctly: To make sure if the import lists are stored correctly:
isort . -c pip install isort
isort . -c -v
Sort them automatically with:
isort .
### Changelog ### Changelog
@ -74,9 +50,9 @@ Don't forget to add a short description of your change in the [CHANGELOG](CHANGE
### Publishing (by maintainer) ### Publishing (by maintainer)
- Bump version in setup.py - Bump version in setup.py
- Bump and date changelog - Bump and date changelog
- Add to changelog: - Add to changelog:
## Unreleased ## Unreleased
Add your changes below. Add your changes below.
@ -87,8 +63,8 @@ Don't forget to add a short description of your change in the [CHANGELOG](CHANGE
### Removed ### Removed
- Commit changes - Commit changes
- Push tag to trigger PyPI build & release workflow - Push tag to trigger PyPI build & release workflow
- Create github release <https://github.com/plamere/spotipy/releases> with the changelog content - Create github release https://github.com/plamere/spotipy/releases with the changelog content
for the version and a short name that describes the main addition for the version and a short name that describes the main addition
- Verify doc uses latest <https://readthedocs.org/projects/spotipy/> - Verify doc uses latest https://readthedocs.org/projects/spotipy/

32
FAQ.md
View File

@ -6,7 +6,7 @@ spotipy can only return fields documented on the Spotify web API https://develop
### How to use spotipy in an API? ### How to use spotipy in an API?
Check out [this example Flask app](https://github.com/spotipy-dev/spotipy-examples/tree/main/apps/flask_api) Check out [this example Flask app](examples/app.py)
### How can I store tokens in a database rather than on the filesystem? ### How can I store tokens in a database rather than on the filesystem?
@ -51,32 +51,4 @@ must be specified: `search("abba", market="DE")`.
If you cannot open a browser, set `open_browser=False` when instantiating SpotifyOAuth or SpotifyPKCE. You will be If you cannot open a browser, set `open_browser=False` when instantiating SpotifyOAuth or SpotifyPKCE. You will be
prompted to open the authorization URI manually. prompted to open the authorization URI manually.
See the [headless auth example](https://github.com/spotipy-dev/spotipy-examples/blob/main/scripts/headless.py). See the [headless auth example](examples/headless.py).
### My application is not responding
This is still speculation, but it seems that Spotify has two limits. A rate limit and a request limit.
- The rate limit prevents a script from requesting too much from the API in a short period of time.
- The request limit limits how many requests you can make in a 24 hour window.
The limits appear to be endpoint-specific, so each endpoint has its own limits.
If your application stops responding, it's likely that you've reached the request limit.
There's nothing Spotipy can do to prevent this, but you can follow Spotify's [Rate Limits](https://developer.spotify.com/documentation/web-api/concepts/rate-limits) guide to learn how rate limiting works and what you can do to avoid ever hitting a limit.
#### *Why* is the application not responding?
Spotipy (or more precisely `urllib3`) has a backoff-retry strategy built in, which is waiting until the rate limit is gone.
If you want to receive an error instead, then you can pass `retries=0` to `Spotify` like this:
```python
sp = spotipy.Spotify(
retries=0,
...
)
```
The error raised is a `spotipy.exceptions.SpotifyException`
### I get a 404 when trying to access a Spotify-owned playlist
Spotify has begun restricting access to algorithmic and Spotify-owned editorial playlists.
Only applications with an existing extended mode will still have access to these playlists.
Read more about this change here: [Introducing some changes to our Web API](https://developer.spotify.com/blog/2024-11-27-changes-to-the-web-api)

View File

@ -2,20 +2,26 @@
##### Spotipy is a lightweight Python library for the [Spotify Web API](https://developer.spotify.com/documentation/web-api). With Spotipy you get full access to all of the music data provided by the Spotify platform. ##### Spotipy is a lightweight Python library for the [Spotify Web API](https://developer.spotify.com/documentation/web-api). With Spotipy you get full access to all of the music data provided by the Spotify platform.
![Integration tests](https://github.com/spotipy-dev/spotipy/actions/workflows/integration_tests.yml/badge.svg?branch=master) [![Documentation Status](https://readthedocs.org/projects/spotipy/badge/?version=master)](https://spotipy.readthedocs.io/en/latest/?badge=master) [![Discord server](https://img.shields.io/discord/1244611850700849183?style=flat&logo=discord&logoColor=7289DA&color=7289DA)](https://discord.gg/HP6xcPsTPJ) ![Tests](https://github.com/plamere/spotipy/workflows/Tests/badge.svg?branch=master) [![Documentation Status](https://readthedocs.org/projects/spotipy/badge/?version=master)](https://spotipy.readthedocs.io/en/latest/?badge=master)
## Table of Contents ## Table of Contents
- [Features](#features) - [Features](#features)
- [Documentation](#documentation)
- [Installation](#installation) - [Installation](#installation)
- [Quick Start](#quick-start) - [Quick Start](#quick-start)
- [Reporting Issues](#reporting-issues) - [Reporting Issues](#reporting-issues)
- [Contributing](#contributing) - [Contributing](#contributing)
- [License](#license)
## Features ## Features
Spotipy supports all of the features of the Spotify Web API including access to all end points, and support for user authorization. For details on the capabilities you are encouraged to review the [Spotify Web API](https://developer.spotify.com/web-api/) documentation. Spotipy supports all of the features of the Spotify Web API including access to all end points, and support for user authorization. For details on the capabilities you are encouraged to review the [Spotify Web API](https://developer.spotify.com/web-api/) documentation.
## Documentation
Spotipy's [full documentation is online](http://spotipy.readthedocs.org/). Some function may need a [specific scope](https://developer.spotify.com/documentation/web-api/concepts/scopes). If you do not define the scope properly `ERROR 401 Unauthorized, permission missing` may occur.
## Installation ## Installation
```bash ```bash
@ -36,9 +42,9 @@ pip install spotipy --upgrade
## Quick Start ## Quick Start
A full set of examples can be found in the [online documentation](http://spotipy.readthedocs.org/) and in the [Spotipy examples directory](https://github.com/spotipy-dev/spotipy-examples). A full set of examples can be found in the [online documentation](http://spotipy.readthedocs.org/) and in the [Spotipy examples directory](https://github.com/plamere/spotipy/tree/master/examples).
To get started, [install spotipy](#installation), create a new account or log in on https://developers.spotify.com/. Go to the [dashboard](https://developer.spotify.com/dashboard), create an app and add your new ID and SECRET (ID and SECRET can be found on an app setting) to your environment: To get started, [install spotipy](#installation), create a new account or log in on https://developers.spotify.com/. Go to the [dashboard](https://developer.spotify.com/dashboard), create an app and add your new ID and SECRET (ID and SECRET can be found on an app setting) to your environment ([step-by-step video](https://www.youtube.com/watch?v=kaBVN8uP358)):
### Example without user authentication ### Example without user authentication

View File

@ -4,19 +4,25 @@ Hello and welcome to the Spotipy Tutorial for Beginners. If you have limited exp
## Prerequisites ## Prerequisites
In order to complete this tutorial successfully, there are a few things that you should already have installed: In order to complete this tutorial successfully, there are a few things that you should already have installed:
**1. python3** **1. pip package manager**
Spotipy is written in Python, so you'll need to have the latest version of Python installed in order to use Spotipy. Check if you already have Python installed with the Terminal command: python --version
If you see a version number, Python is already installed. If not, you can download it here: https://www.python.org/downloads/
**2. pip package manager**
You can check to see if you have pip installed by opening up Terminal and typing the following command: pip --version You can check to see if you have pip installed by opening up Terminal and typing the following command: pip --version
If you see a version number, pip is installed, and you're ready to proceed. If not, instructions for downloading the latest version of pip can be found here: https://pip.pypa.io/en/stable/cli/pip_download/ If you see a version number, pip is installed, and you're ready to proceed. If not, instructions for downloading the latest version of pip can be found here: https://pip.pypa.io/en/stable/cli/pip_download/
A. After ensuring that pip is installed, run the following command in Terminal to install Spotipy: pip install spotipy --upgrade
**3. Experience with Basic Linux Commands** **2. python3**
Spotipy is written in Python, so you'll need to have the latest version of Python installed in order to use Spotipy. Check if you already have Python installed with the Terminal command: python --version
If you see a version number, Python is already installed. If not, you can download it here: https://www.python.org/downloads/
**3. spotipy**
You'll need to install the packages necessary for this project. Run the following command:
```
pip install spotipy
```
**4. experience with basic Linux commands**
This tutorial will be easiest if you have some knowledge of how to use Linux commands to create and navigate folders and files on your computer. If you're not sure how to create, edit and delete files and directories from Terminal, learn about basic Linux commands [here](https://ubuntu.com/tutorials/command-line-for-beginners#1-overview) before continuing. 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.
@ -25,17 +31,19 @@ Once those three setup items are taken care of, you're ready to start learning h
## Step 1. Creating a Spotify Account ## Step 1. Creating a Spotify Account
Spotipy relies on the Spotify API. In order to use the Spotify API, you'll need to create a Spotify developer account. Spotipy relies on the Spotify API. In order to use the Spotify API, you'll need to create a Spotify developer account.
A. Visit the [Spotify developer portal](https://developer.spotify.com/dashboard/). If you already have a Spotify account, click "Log in" and enter your username and password. Otherwise, click "Sign up" and follow the steps to create an account. After you've signed in or signed up, begin by clicking on your profile name at the top right of your screen and then click “Dashboard” to go to Spotifys Developer Dashboard. A. Visit the [Spotify developer portal](https://developer.spotify.com/dashboard/). If you already have a Spotify account, click "Log in" and enter your username and password. Otherwise, click "Sign up" and follow the steps to create an account. After you've signed in or signed up, you should be redirected to your developer dashboard.
B. Check the box "Accept the Spotify Developer Terms of Service" and then click "Accept the terms". On the next page, verify your email address if you haven't already. Click the "Create an App" button. Enter any name and description you'd like for your new app. Next, add "http://127.0.0.1:1234" (or any other port number of your choosing) to the "Redirect URI" secction. Check the box "I understand and agree with Spotify's Developer Terms of Service and Design Guidelines" and then click the "Save" button. B. Click the "Create an App" button. Enter any name and description you'd like for your new app. Add "http://localhost:1234" (or any other port number of your choosing) as your "Redirect URI". Accept the terms of service and click "Create."
C. Click on "Settings". Underneath "Client ID", you'll see a "View Client Secret" link. Click the link to reveal your Client secret and copy both your Client secret and your Client ID somewhere so that you can access them later. C. In your new app's Overview screen, click the "Settings" button and then under the "Basic Information" tab click "View client secret", then copy both your Client Secret and your Client ID somewhere on your computer. You'll need to access them later.
D. Underneath your app name and description on the left-hand side, you'll see a "Show Client Secret" link. Click that link to reveal your Client Secret, then copy both your Client Secret and your Client ID somewhere on your computer. You'll need to access them later.
## Step 2. Installation and Setup ## Step 2. Installation and Setup
A. Create a folder somewhere on your computer where you'd like to store the code for your Spotipy app. You can create a folder in terminal with this command: ```mkdir folder_name``` A. Create a folder somewhere on your computer where you'd like to store the code for your Spotipy app. You can create a folder in terminal with this command: ```mkdir folder_name```
B. In your new folder, create a Python file named main.py. You can create the file directly from Terminal using a built in text editor like Vim, which comes preinstalled on Linux operating systems. To create the file with Vim, ensure that you are in your new directory, then run: vim main.py B. Install the Spotipy library. You can do this by using this command in the terminal: ```pip install spotipy```
C. In that folder, create a Python file named main.py. You can create the file directly from Terminal using a built in text editor like Vim, which comes preinstalled on Linux operating systems. To create the file with Vim, ensure that you are in your new directory, then run: vim main.py C. In that folder, create a Python file named main.py. You can create the file directly from Terminal using a built in text editor like Vim, which comes preinstalled on Linux operating systems. To create the file with Vim, ensure that you are in your new directory, then run: vim main.py
@ -49,7 +57,7 @@ sp = spotipy.Spotify(auth_manager=SpotifyOAuth(client_id="YOUR_APP_CLIENT_ID",
redirect_uri="YOUR_APP_REDIRECT_URI", redirect_uri="YOUR_APP_REDIRECT_URI",
scope="user-library-read")) scope="user-library-read"))
``` ```
D. Replace YOUR_APP_CLIENT_ID and YOUR_APP_CLIENT_SECRET with the values you copied and saved in step 1D. Replace YOUR_APP_REDIRECT_URI with the URI you set in step 1B. E. Replace YOUR_APP_CLIENT_ID and YOUR_APP_CLIENT_SECRET with the values you copied and saved in step 1D. Replace YOUR_APP_REDIRECT_URI with the URI you set in step 1C.
## Step 3. Start Using Spotipy ## Step 3. Start Using Spotipy
@ -92,7 +100,7 @@ In most cases, the recent Python version is Python 3. You may need to update Pyt
B. Encountering package error: B. Encountering package error:
If you are seeing an error "ModuleNotFoundError: No module named 'spotipy'", this means you have not installed the package. If you are seeing an error "ModuleNotFoundError: No module named 'spotipy'", this means you have not installed the package. This may occur if you followed the installation and setup (up to Step 3, Part D) and attempted to run the app with the missing package.
Run the command: Run the command:
``` ```
pip install spotipy pip install spotipy

View File

@ -10,13 +10,13 @@
# 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 os
import sys 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('.'))
sys.path.insert(0, os.path.abspath("..")) sys.path.insert(0, os.path.abspath(".."))
@ -25,7 +25,7 @@ import spotipy
# -- General configuration ----------------------------------------------------- # -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here. # If your documentation needs a minimal Sphinx version, state it here.
# needs_sphinx = '1.0' #needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions # Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
@ -41,7 +41,7 @@ templates_path = ['_templates']
source_suffix = '.rst' source_suffix = '.rst'
# The encoding of source files. # The encoding of source files.
# source_encoding = 'utf-8-sig' #source_encoding = 'utf-8-sig'
# The master toctree document. # The master toctree document.
master_doc = 'index' master_doc = 'index'
@ -61,37 +61,37 @@ release = '2.0'
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
# language = None #language = None
# There are two options for replacing |today|: either, you set today to some # There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used: # non-false value, then it is used:
# today = '' #today = ''
# Else, today_fmt is used as the format for a strftime call. # Else, today_fmt is used as the format for a strftime call.
# today_fmt = '%B %d, %Y' #today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
exclude_patterns = ['_build'] exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents. # The reST default role (used for this markup: `text`) to use for all documents.
# default_role = None #default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text. # If true, '()' will be appended to :func: etc. cross-reference text.
# add_function_parentheses = True #add_function_parentheses = True
# If true, the current module name will be prepended to all description # If true, the current module name will be prepended to all description
# unit titles (such as .. function::). # unit titles (such as .. function::).
# add_module_names = True #add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the # If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default. # output. They are ignored by default.
# show_authors = False #show_authors = False
# The name of the Pygments (syntax highlighting) style to use. # The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx' pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting. # A list of ignored prefixes for module index sorting.
# modindex_common_prefix = [] #modindex_common_prefix = []
# -- Options for HTML output --------------------------------------------------- # -- Options for HTML output ---------------------------------------------------
@ -103,26 +103,26 @@ html_theme = 'sphinx_rtd_theme'
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the # further. For a list of options available for each theme, see the
# documentation. # documentation.
# html_theme_options = {} #html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory. # Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = [] #html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to # The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation". # "<project> v<release> documentation".
# html_title = None #html_title = None
# A shorter title for the navigation bar. Default is the same as html_title. # A shorter title for the navigation bar. Default is the same as html_title.
# html_short_title = None #html_short_title = None
# The name of an image file (relative to this directory) to place at the top # The name of an image file (relative to this directory) to place at the top
# of the sidebar. # of the sidebar.
# html_logo = None #html_logo = None
# The name of an image file (within the static path) to use as favicon of the # The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large. # pixels large.
# html_favicon = None #html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here, # Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files, # relative to this directory. They are copied after the builtin static files,
@ -131,44 +131,44 @@ html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format. # using the given strftime format.
# html_last_updated_fmt = '%b %d, %Y' #html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to # If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities. # typographically correct entities.
# html_use_smartypants = True #html_use_smartypants = True
# Custom sidebar templates, maps document names to template names. # Custom sidebar templates, maps document names to template names.
# html_sidebars = {} #html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to # Additional templates that should be rendered to pages, maps page names to
# template names. # template names.
# html_additional_pages = {} #html_additional_pages = {}
# If false, no module index is generated. # If false, no module index is generated.
# html_domain_indices = True #html_domain_indices = True
# If false, no index is generated. # If false, no index is generated.
# html_use_index = True #html_use_index = True
# If true, the index is split into individual pages for each letter. # If true, the index is split into individual pages for each letter.
# html_split_index = False #html_split_index = False
# If true, links to the reST sources are added to the pages. # If true, links to the reST sources are added to the pages.
# html_show_sourcelink = True #html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
# html_show_sphinx = True #html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
# html_show_copyright = True #html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will # If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the # contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served. # base URL from which the finished HTML is served.
# html_use_opensearch = '' #html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml"). # This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None #html_file_suffix = None
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = 'spotipydoc' htmlhelp_basename = 'spotipydoc'
@ -196,23 +196,23 @@ latex_documents = [
# 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
# the title page. # the title page.
# latex_logo = None #latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts, # For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters. # not chapters.
# latex_use_parts = False #latex_use_parts = False
# If true, show page references after internal links. # If true, show page references after internal links.
# latex_show_pagerefs = False #latex_show_pagerefs = False
# If true, show URL addresses after external links. # If true, show URL addresses after external links.
# latex_show_urls = False #latex_show_urls = False
# Documents to append as an appendix to all manuals. # Documents to append as an appendix to all manuals.
# latex_appendices = [] #latex_appendices = []
# If false, no module index is generated. # If false, no module index is generated.
# latex_domain_indices = True #latex_domain_indices = True
# -- Options for manual page output -------------------------------------------- # -- Options for manual page output --------------------------------------------
@ -225,7 +225,7 @@ man_pages = [
] ]
# If true, show URL addresses after external links. # If true, show URL addresses after external links.
# man_show_urls = False #man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------ # -- Options for Texinfo output ------------------------------------------------
@ -240,10 +240,10 @@ texinfo_documents = [
] ]
# Documents to append as an appendix to all manuals. # Documents to append as an appendix to all manuals.
# texinfo_appendices = [] #texinfo_appendices = []
# If false, no module index is generated. # If false, no module index is generated.
# texinfo_domain_indices = True #texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'. # How to display URL addresses: 'footnote', 'no', or 'inline'.
# texinfo_show_urls = 'footnote' #texinfo_show_urls = 'footnote'

View File

@ -23,7 +23,7 @@ Install or upgrade *Spotipy* with::
pip install spotipy --upgrade pip install spotipy --upgrade
You can also obtain the source code from the `Spotipy GitHub repository <https://github.com/plamere/spotipy>`_. You can also obtain the source code from the `Spotify GitHub repository <https://github.com/plamere/spotipy>`_.
Getting Started Getting Started
@ -110,9 +110,9 @@ to your application at
The ``redirect_uri`` argument or ``SPOTIPY_REDIRECT_URI`` environment variable The ``redirect_uri`` argument or ``SPOTIPY_REDIRECT_URI`` environment variable
must match the redirect URI added to your application in your Dashboard. 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`` 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 .. note:: If you choose an `http`-scheme URL, and it's for `localhost` or
`127.0.0.1`, **AND** it specifies a port, then spotipy will instantiate `127.0.0.1`, **AND** it specifies a port, then spotipy will instantiate
a server on the indicated response to receive the access token from the 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). response at the end of the oauth flow [see the code](https://github.com/plamere/spotipy/blob/master/spotipy/oauth2.py#L483-L490).
@ -145,7 +145,7 @@ class SpotifyClientCredentials that can be used to authenticate requests like so
playlists = sp.user_playlists('spotify') playlists = sp.user_playlists('spotify')
while playlists: while playlists:
for i, playlist in enumerate(playlists['items']): for i, playlist in enumerate(playlists['items']):
print(f"{i + 1 + playlists['offset']:4d} {playlist['uri']} {playlist['name']}") print("%4d %s %s" % (i + 1 + playlists['offset'], playlist['uri'], playlist['name']))
if playlists['next']: if playlists['next']:
playlists = sp.next(playlists) playlists = sp.next(playlists)
else: else:
@ -254,8 +254,8 @@ artist's name::
artist = items[0] artist = items[0]
print(artist['name'], artist['images'][0]['url']) print(artist['name'], artist['images'][0]['url'])
There are many more examples of how to use *Spotipy* in the `spotipy-examples There are many more examples of how to use *Spotipy* in the `Examples
repository <https://github.com/spotipy-dev/spotipy-examples>`_ on GitHub. Directory <https://github.com/plamere/spotipy/tree/master/examples>`_ on GitHub.
API Reference API Reference
============== ==============
@ -302,6 +302,30 @@ If you think you've found a bug, let us know at
Contribute Contribute
========== ==========
Spotipy authored by Paul Lamere (plamere) with contributions by:
- 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 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: be sure to follow the guidelines listed below:
@ -309,7 +333,7 @@ Export the needed Environment variables:::
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://127.0.0.1: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
Create virtual environment, install dependencies, run tests::: Create virtual environment, install dependencies, run tests:::
$ virtualenv --python=python3.12 env $ virtualenv --python=python3.12 env
@ -378,3 +402,4 @@ Indices and tables
* :ref:`genindex` * :ref:`genindex`
* :ref:`modindex` * :ref:`modindex`
* :ref:`search` * :ref:`search`

View File

@ -1,3 +1,3 @@
Sphinx~=8.1.3 Sphinx~=7.3.7
sphinx-rtd-theme~=3.1.0 sphinx-rtd-theme~=2.0.0
redis>=3.5.3 redis>=3.5.3

@ -1 +0,0 @@
Subproject commit c610a79705ef4aa55e4d61572a012f77b6f7245d

View File

@ -0,0 +1,27 @@
import argparse
import logging
import spotipy
from spotipy.oauth2 import SpotifyOAuth
logger = logging.getLogger('examples.add_a_saved_album')
logging.basicConfig(level='DEBUG')
scope = 'user-library-modify'
def get_args():
parser = argparse.ArgumentParser(description='Creates a playlist for user')
parser.add_argument('-a', '--aids', action='append',
required=True, help='Album ids')
return parser.parse_args()
def main():
args = get_args()
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
sp.current_user_saved_albums_add(albums=args.aids)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,29 @@
import argparse
import logging
import spotipy
from spotipy.oauth2 import SpotifyOAuth
scope = 'user-library-modify'
logger = logging.getLogger('examples.add_a_saved_track')
logging.basicConfig(level='DEBUG')
def get_args():
parser = argparse.ArgumentParser(description='Add tracks to Your '
'Collection of saved tracks')
parser.add_argument('-t', '--tids', action='append',
required=True, help='Track ids')
return parser.parse_args()
def main():
args = get_args()
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
sp.current_user_saved_tracks_add(tracks=args.tids)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,29 @@
import argparse
import logging
import spotipy
from spotipy.oauth2 import SpotifyOAuth
logger = logging.getLogger('examples.add_tracks_to_playlist')
logging.basicConfig(level='DEBUG')
scope = 'playlist-modify-public'
def get_args():
parser = argparse.ArgumentParser(description='Adds track to user playlist')
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')
return parser.parse_args()
def main():
args = get_args()
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
sp.playlist_add_items(args.playlist, args.uris)
if __name__ == '__main__':
main()

113
examples/app.py Normal file
View File

@ -0,0 +1,113 @@
"""
Prerequisites
pip3 install spotipy Flask Flask-Session
// from your [app settings](https://developer.spotify.com/dashboard/applications)
export SPOTIPY_CLIENT_ID=client_id_here
export SPOTIPY_CLIENT_SECRET=client_secret_here
export SPOTIPY_REDIRECT_URI='http://127.0.0.1:8080' // must contain a port
// SPOTIPY_REDIRECT_URI must be added to your [app settings](https://developer.spotify.com/dashboard/applications)
OPTIONAL
// in development environment for debug output
export FLASK_ENV=development
// so that you can invoke the app outside the file's directory include
export FLASK_APP=/path/to/spotipy/examples/app.py
// on Windows, use `SET` instead of `export`
Run app.py
python3 app.py OR python3 -m flask run
NOTE: If receiving "port already in use" error, try other ports: 5000, 8090, 8888, etc...
(will need to be updated in your Spotify app and SPOTIPY_REDIRECT_URI variable)
"""
import os
from flask import Flask, session, request, redirect
from flask_session import Session
import spotipy
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(64)
app.config['SESSION_TYPE'] = 'filesystem'
app.config['SESSION_FILE_DIR'] = './.flask_session/'
Session(app)
@app.route('/')
def index():
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 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 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 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>' \
f'<a href="/playlists">my playlists</a> | ' \
f'<a href="/currently_playing">currently playing</a> | ' \
f'<a href="/current_user">me</a>' \
@app.route('/sign_out')
def sign_out():
session.pop("token_info", None)
return redirect('/')
@app.route('/playlists')
def playlists():
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('/')
spotify = spotipy.Spotify(auth_manager=auth_manager)
return spotify.current_user_playlists()
@app.route('/currently_playing')
def currently_playing():
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('/')
spotify = spotipy.Spotify(auth_manager=auth_manager)
track = spotify.current_user_playing_track()
if not track is None:
return track
return "No track currently playing."
@app.route('/current_user')
def current_user():
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('/')
spotify = spotipy.Spotify(auth_manager=auth_manager)
return spotify.current_user()
'''
Following lines allow application to be run more conveniently with
`python app.py` (Make sure you're using python3)
(Also includes directive to leverage pythons threading capacity.)
'''
if __name__ == '__main__':
app.run(threaded=True, port=int(os.environ.get("PORT",
os.environ.get("SPOTIPY_REDIRECT_URI", 8080).split(":")[-1])))

55
examples/artist_albums.py Normal file
View File

@ -0,0 +1,55 @@
import argparse
import logging
from spotipy.oauth2 import SpotifyClientCredentials
import spotipy
logger = logging.getLogger('examples.artist_albums')
logging.basicConfig(level='INFO')
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials())
def get_args():
parser = argparse.ArgumentParser(description='Gets albums from artist')
parser.add_argument('-a', '--artist', required=True,
help='Name of Artist')
return parser.parse_args()
def get_artist(name):
results = sp.search(q='artist:' + name, type='artist')
items = results['artists']['items']
if len(items) > 0:
return items[0]
else:
return None
def show_artist_albums(artist):
albums = []
results = sp.artist_albums(artist['id'], album_type='album')
albums.extend(results['items'])
while results['next']:
results = sp.next(results)
albums.extend(results['items'])
seen = set() # to avoid dups
albums.sort(key=lambda album: album['name'].lower())
for album in albums:
name = album['name']
if name not in seen:
logger.info('ALBUM: %s', name)
seen.add(name)
def main():
args = get_args()
artist = get_artist(args.artist)
if artist:
show_artist_albums(artist)
else:
logger.error("Can't find artist: %s", artist)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,74 @@
# Shows the list of all songs sung by the artist or the band
import argparse
import logging
from spotipy.oauth2 import SpotifyClientCredentials
import spotipy
logger = logging.getLogger('examples.artist_discography')
logging.basicConfig(level='INFO')
def get_args():
parser = argparse.ArgumentParser(description='Shows albums and tracks for '
'given artist')
parser.add_argument('-a', '--artist', required=True,
help='Name of Artist')
return parser.parse_args()
def get_artist(name):
results = sp.search(q='artist:' + name, type='artist')
items = results['artists']['items']
if len(items) > 0:
return items[0]
else:
return None
def show_album_tracks(album):
tracks = []
results = sp.album_tracks(album['id'])
tracks.extend(results['items'])
while results['next']:
results = sp.next(results)
tracks.extend(results['items'])
for i, track in enumerate(tracks):
logger.info('%s. %s', i + 1, track['name'])
def show_artist_albums(artist):
albums = []
results = sp.artist_albums(artist['id'], album_type='album')
albums.extend(results['items'])
while results['next']:
results = sp.next(results)
albums.extend(results['items'])
logger.info('Total albums: %s', len(albums))
unique = set() # skip duplicate albums
for album in albums:
name = album['name'].lower()
if name not in unique:
logger.info('ALBUM: %s', name)
unique.add(name)
show_album_tracks(album)
def show_artist(artist):
logger.info('====%s====', artist['name'])
logger.info('Popularity: %s', artist['popularity'])
if len(artist['genres']) > 0:
logger.info('Genres: %s', ','.join(artist['genres']))
def main():
args = get_args()
artist = get_artist(args.artist)
show_artist(artist)
show_artist_albums(artist)
if __name__ == '__main__':
client_credentials_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
main()

View File

@ -0,0 +1,48 @@
import argparse
import logging
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
logger = logging.getLogger('examples.artist_recommendations')
logging.basicConfig(level='INFO')
client_credentials_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
def get_args():
parser = argparse.ArgumentParser(description='Recommendations for the '
'given artist')
parser.add_argument('-a', '--artist', required=True, help='Name of Artist')
return parser.parse_args()
def get_artist(name):
results = sp.search(q='artist:' + name, type='artist')
items = results['artists']['items']
if len(items) > 0:
return items[0]
else:
return None
def show_recommendations_for_artist(artist):
results = sp.recommendations(seed_artists=[artist['id']])
for track in results['tracks']:
logger.info('Recommendation: %s - %s', track['name'],
track['artists'][0]['name'])
def main():
args = get_args()
artist = get_artist(args.artist)
if artist:
show_recommendations_for_artist(artist)
else:
logger.error("Can't find that artist", args.artist)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,22 @@
# shows audio analysis for the given track
from spotipy.oauth2 import SpotifyClientCredentials
import json
import spotipy
import time
import sys
client_credentials_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
if len(sys.argv) > 1:
tid = sys.argv[1]
else:
tid = 'spotify:track:4TTV7EcfroSLWzXRY6gLv6'
start = time.time()
analysis = sp.audio_analysis(tid)
delta = time.time() - start
print(json.dumps(analysis, indent=4))
print(f"analysis retrieved in {delta:.2f} seconds")

View File

@ -0,0 +1,34 @@
# shows acoustic features for tracks for the given artist
from spotipy.oauth2 import SpotifyClientCredentials
import json
import spotipy
import time
import sys
client_credentials_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
sp.trace = False
if len(sys.argv) > 1:
artist_name = ' '.join(sys.argv[1:])
else:
artist_name = 'weezer'
results = sp.search(q=artist_name, limit=50)
tids = []
for i, t in enumerate(results['tracks']['items']):
print(' ', i, t['name'])
tids.append(t['uri'])
start = time.time()
features = sp.audio_features(tids)
delta = time.time() - start
for feature in features:
print(json.dumps(feature, indent=4))
print()
analysis = sp._get(feature['analysis_url'])
print(json.dumps(analysis, indent=4))
print()
print(f"features retrieved in {delta:.2f} seconds")

View File

@ -0,0 +1,22 @@
# shows acoustic features for tracks for the given artist
from spotipy.oauth2 import SpotifyClientCredentials
import json
import spotipy
import time
import sys
client_credentials_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
sp.trace = True
if len(sys.argv) > 1:
tids = sys.argv[1:]
print(tids)
start = time.time()
features = sp.audio_features(tids)
delta = time.time() - start
print(json.dumps(features, indent=4))
print(f"features retrieved in {delta:.2f} seconds")

View File

@ -0,0 +1,47 @@
import argparse
import logging
import spotipy
from spotipy.oauth2 import SpotifyOAuth
logger = logging.getLogger('examples.change_playlist_details')
logging.basicConfig(level='DEBUG')
scope = 'playlist-modify-public playlist-modify-private'
def get_args():
parser = argparse.ArgumentParser(description='Modify details of playlist')
parser.add_argument('-p', '--playlist', required=True,
help='Playlist id to alter details')
parser.add_argument('-n', '--name', required=False,
help='Name of playlist')
parser.add_argument('--public', action='store_true', required=False,
help='Include param if playlist is public')
parser.add_argument('--private', action='store_false', required=False,
default=None,
help='Include param to make playlist is private')
parser.add_argument('-c', '--collaborative', action='store_true',
required=False, default=None,
help='Include param if playlist is collaborative')
parser.add_argument('-i', '--independent', action='store_false',
required=False, default=None,
help='Include param to make playlist non collaborative')
parser.add_argument('-d', '--description', default=None, required=False,
help='Description of playlist')
return parser.parse_args()
def main():
args = get_args()
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
sp.playlist_change_details(
args.playlist,
name=args.name,
public=args.public or args.private,
collaborative=args.collaborative or args.independent,
description=args.description)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,10 @@
from spotipy.oauth2 import SpotifyClientCredentials
import spotipy
from pprint import pprint
client_credentials_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
search_str = 'Muse'
result = sp.search(search_str)
pprint(result)

View File

@ -0,0 +1,19 @@
# Prints whether a track exists in your collection of saved tracks
import pprint
import sys
import spotipy
from spotipy.oauth2 import SpotifyOAuth
scope = 'user-library-read'
if len(sys.argv) > 1:
tid = sys.argv[1]
else:
print(f"Usage: {sys.argv[0]} track-id ...")
sys.exit()
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
results = sp.current_user_saved_tracks_contains(tracks=[tid])
pprint.pprint(results)

View File

@ -0,0 +1,31 @@
# Creates a playlist for a user
import argparse
import logging
import spotipy
from spotipy.oauth2 import SpotifyOAuth
logger = logging.getLogger('examples.create_playlist')
logging.basicConfig(level='DEBUG')
def get_args():
parser = argparse.ArgumentParser(description='Creates a playlist for user')
parser.add_argument('-p', '--playlist', required=True,
help='Name of Playlist')
parser.add_argument('-d', '--description', required=False, default='',
help='Description of Playlist')
return parser.parse_args()
def main():
args = get_args()
scope = "playlist-modify-public"
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
user_id = sp.me()['id']
sp.user_playlist_create(user_id, args.playlist, description=args.description)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,19 @@
# Delete a track from 'Your Collection' of saved tracks
import pprint
import sys
import spotipy
from spotipy.oauth2 import SpotifyOAuth
scope = 'user-library-modify'
if len(sys.argv) > 1:
tid = sys.argv[1]
else:
print(f"Usage: {sys.argv[0]} track-id ...")
sys.exit()
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
results = sp.current_user_saved_tracks_delete(tracks=[tid])
pprint.pprint(results)

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()

8
examples/headless.py Normal file
View File

@ -0,0 +1,8 @@
import spotipy
from spotipy.oauth2 import SpotifyOAuth
# set open_browser=False to prevent Spotipy from attempting to open the default browser
spotify = spotipy.Spotify(auth_manager=SpotifyOAuth(open_browser=False))
print(spotify.me())

View File

@ -0,0 +1,10 @@
import spotipy
import spotipy.util as util
from pprint import pprint
while True:
username = input("Type the Spotify user ID to use: ")
token = util.prompt_for_user_token(username, show_dialog=True)
sp = spotipy.Spotify(token)
pprint(sp.me())

11
examples/my_playlists.py Normal file
View File

@ -0,0 +1,11 @@
# Shows a user's playlists
import spotipy
from spotipy.oauth2 import SpotifyOAuth
scope = 'playlist-read-private'
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
results = sp.current_user_playlists(limit=50)
for i, item in enumerate(results['items']):
print("%d %s" % (i, item['name']))

View File

@ -0,0 +1,17 @@
# Shows the top artists for a user
import spotipy
from spotipy.oauth2 import SpotifyOAuth
scope = 'user-top-read'
ranges = ['short_term', 'medium_term', 'long_term']
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
for sp_range in ['short_term', 'medium_term', 'long_term']:
print("range:", sp_range)
results = sp.current_user_top_artists(time_range=sp_range, limit=50)
for i, item in enumerate(results['items']):
print(i, item['name'])
print()

16
examples/my_top_tracks.py Normal file
View File

@ -0,0 +1,16 @@
# Shows the top tracks for a user
import spotipy
from spotipy.oauth2 import SpotifyOAuth
scope = 'user-top-read'
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
ranges = ['short_term', 'medium_term', 'long_term']
for sp_range in ranges:
print("range:", sp_range)
results = sp.current_user_top_tracks(time_range=sp_range, limit=50)
for i, item in enumerate(results['items']):
print(i, item['name'], '//', item['artists'][0]['name'])
print()

21
examples/player.py Normal file
View File

@ -0,0 +1,21 @@
import spotipy
from spotipy.oauth2 import SpotifyOAuth
from pprint import pprint
from time import sleep
scope = "user-read-playback-state,user-modify-playback-state"
sp = spotipy.Spotify(client_credentials_manager=SpotifyOAuth(scope=scope))
# Shows playing devices
res = sp.devices()
pprint(res)
# Change track
sp.start_playback(uris=['spotify:track:6gdLoMygLsgktydTQ71b15'])
# Change volume
sp.volume(100)
sleep(2)
sp.volume(50)
sleep(2)
sp.volume(100)

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

@ -0,0 +1,30 @@
# get all non-local tracks of a playlist
from spotipy.oauth2 import SpotifyClientCredentials
import spotipy
# playlist id of global top 50
PlaylistExample = '37i9dQZEVXbMDoHDwVN2tF'
# create spotipy client
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials())
# load the first 100 songs
tracks = []
result = sp.playlist_items(PlaylistExample, additional_types=['track'])
tracks.extend(result['items'])
# if playlist is larger than 100 songs, continue loading it until end
while result['next']:
result = sp.next(result)
tracks.extend(result['items'])
# remove all local songs
i = 0 # just for counting how many tracks are local
for item in tracks:
if item['is_local']:
tracks.remove(item)
i += 1
# print result
print("Playlist length: " + str(len(tracks)) + "\nExcluding: " + str(i))

View File

@ -0,0 +1,21 @@
from spotipy.oauth2 import SpotifyClientCredentials
import spotipy
from pprint import pprint
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials())
pl_id = 'spotify:playlist:5RIbzhG2QqdkaP24iXLnZX'
offset = 0
while True:
response = sp.playlist_items(pl_id,
offset=offset,
fields='items.track.id,total',
additional_types=['track'])
if len(response['items']) == 0:
break
pprint(response['items'])
offset = offset + len(response['items'])
print(offset, "/", response['total'])

View File

@ -0,0 +1,10 @@
from spotipy.oauth2 import SpotifyClientCredentials
import spotipy
import json
client_credentials_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
playlist_id = 'spotify:user:spotifycharts:playlist:37i9dQZEVXbJiZcmkrIHGU'
results = sp.playlist(playlist_id)
print(json.dumps(results, indent=4))

View File

@ -0,0 +1,26 @@
# Removes tracks from a playlist
import pprint
import sys
import spotipy
from spotipy.oauth2 import SpotifyOAuth
if len(sys.argv) > 2:
playlist_id = sys.argv[1]
track_ids_and_positions = sys.argv[2:]
track_ids = []
for t_pos in sys.argv[2:]:
tid, pos = t_pos.split(',')
track_ids.append({"uri": tid, "positions": [int(pos)]})
else:
print(
f"Usage: {sys.argv[0]} playlist_id track_id,pos track_id,pos ...")
sys.exit()
scope = 'playlist-modify-public'
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
results = sp.playlist_remove_specific_occurrences_of_items(
playlist_id, track_ids)
pprint.pprint(results)

View File

@ -0,0 +1,22 @@
# Removes tracks from playlist
import pprint
import sys
import spotipy
from spotipy.oauth2 import SpotifyOAuth
if len(sys.argv) > 2:
playlist_id = sys.argv[2]
track_ids = sys.argv[3:]
else:
print(f"Usage: {sys.argv[0]} playlist_id track_id ...")
sys.exit()
scope = 'playlist-modify-public'
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
results = sp.playlist_remove_all_occurrences_of_items(
playlist_id, track_ids)
pprint.pprint(results)

View File

@ -0,0 +1,21 @@
# Replaces all tracks in a playlist
import pprint
import sys
import spotipy
from spotipy.oauth2 import SpotifyOAuth
if len(sys.argv) > 3:
playlist_id = sys.argv[1]
track_ids = sys.argv[2:]
else:
print(f"Usage: {sys.argv[0]} playlist_id track_id ...")
sys.exit()
scope = 'playlist-modify-public'
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
results = sp.playlist_replace_items(playlist_id, track_ids)
pprint.pprint(results)

15
examples/search.py Normal file
View File

@ -0,0 +1,15 @@
# shows artist info for a URN or URL
from spotipy.oauth2 import SpotifyClientCredentials
import spotipy
import sys
import pprint
if len(sys.argv) > 1:
search_str = sys.argv[1]
else:
search_str = 'Radiohead'
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials())
result = sp.search(search_str)
pprint.pprint(result)

15
examples/show_album.py Normal file
View File

@ -0,0 +1,15 @@
# shows album info for a URN or URL
from spotipy.oauth2 import SpotifyClientCredentials
import spotipy
import sys
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)
pprint(album)

16
examples/show_artist.py Normal file
View File

@ -0,0 +1,16 @@
# shows artist info for a URN or URL
from spotipy.oauth2 import SpotifyClientCredentials
import spotipy
import sys
from pprint import pprint
if len(sys.argv) > 1:
urn = sys.argv[1]
else:
urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu'
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials())
artist = sp.artist(urn)
pprint(artist)

View File

@ -0,0 +1,17 @@
# shows artist info for a URN or URL
# scope is not required for this function
from spotipy.oauth2 import SpotifyClientCredentials
import spotipy
import sys
if len(sys.argv) > 1:
urn = sys.argv[1]
else:
urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu'
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials())
response = sp.artist_top_tracks(urn)
for track in response['tracks']:
print(track['name'])

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,19 @@
# shows artist info for a URN or URL
import spotipy
from spotipy.oauth2 import SpotifyOAuth
sp = spotipy.Spotify(auth_manager=SpotifyOAuth())
response = sp.featured_playlists()
print(response['message'])
while response:
playlists = response['playlists']
for i, item in enumerate(playlists['items']):
print(playlists['offset'] + i, item['name'])
if playlists['next']:
response = sp.next(playlists)
else:
response = None

View File

@ -0,0 +1,22 @@
# Shows a user's saved tracks (need to be authenticated via oauth)
import spotipy
from spotipy.oauth2 import SpotifyOAuth
scope = 'user-library-read'
def show_tracks(results):
for item in results['items']:
track = item['track']
print("%32.32s %s" % (track['artists'][0]['name'], track['name']))
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
results = sp.current_user_saved_tracks()
show_tracks(results)
while results['next']:
results = sp.next(results)
show_tracks(results)

View File

@ -0,0 +1,18 @@
# shows artist info for a URN or URL
import spotipy
from spotipy.oauth2 import SpotifyOAuth
sp = spotipy.Spotify(auth_manager=SpotifyOAuth())
response = sp.new_releases()
while response:
albums = response['albums']
for i, item in enumerate(albums['items']):
print(albums['offset'] + i, item['name'])
if albums['next']:
response = sp.next(albums)
else:
response = None

24
examples/show_related.py Normal file
View File

@ -0,0 +1,24 @@
# shows related artists for the given seed artist
from spotipy.oauth2 import SpotifyClientCredentials
import spotipy
import sys
if len(sys.argv) > 1:
artist_name = sys.argv[1]
else:
artist_name = 'weezer'
client_credentials_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
result = sp.search(q='artist:' + artist_name, type='artist')
try:
name = result['artists']['items'][0]['name']
uri = result['artists']['items'][0]['uri']
related = sp.artist_related_artists(uri)
print('Related artists for', name)
for artist in related['artists']:
print(' ', artist['name'])
except BaseException:
print("usage show_related.py [artist-name]")

View File

@ -0,0 +1,16 @@
# shows track info for a URN or URL
from spotipy.oauth2 import SpotifyClientCredentials
import spotipy
import sys
from pprint import pprint
if len(sys.argv) > 1:
urn = sys.argv[1]
else:
urn = 'spotify:track:0Svkvt5I79wficMFgaqEQJ'
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials())
track = sp.track(urn)
pprint(track)

23
examples/show_tracks.py Normal file
View File

@ -0,0 +1,23 @@
'''
usage: show_tracks.py path_of_ids
given a list of track IDs show the artist and track name
'''
from spotipy.oauth2 import SpotifyClientCredentials
import sys
import spotipy
if __name__ == '__main__':
max_tracks_per_call = 50
if len(sys.argv) > 1:
file = open(sys.argv[1])
else:
file = sys.stdin
tids = file.read().split()
client_credentials_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
for start in range(0, len(tids), max_tracks_per_call):
results = sp.tracks(tids[start: start + max_tracks_per_call])
for track in results['tracks']:
print(track['name'] + ' - ' + track['artists'][0]['name'])

17
examples/show_user.py Normal file
View File

@ -0,0 +1,17 @@
# Shows artist info for a URN or URL
from spotipy.oauth2 import SpotifyClientCredentials
import spotipy
import sys
import pprint
if len(sys.argv) > 1:
username = sys.argv[1]
else:
username = 'plamere'
client_credentials_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
sp.trace = True
user = sp.user(username)
pprint.pprint(user)

View File

@ -0,0 +1,16 @@
from spotipy.oauth2 import SpotifyClientCredentials
import spotipy
birdy_uri = 'spotify:artist:2WX2uTcsvV5OnS0inACecP'
client_credentials_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
results = sp.artist_albums(birdy_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'])

View File

@ -0,0 +1,14 @@
from spotipy.oauth2 import SpotifyClientCredentials
import spotipy
lz_uri = 'spotify:artist:36QJpDe2go2KgaRleHCDTp'
client_credentials_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
results = sp.artist_top_tracks(lz_uri)
for track in results['tracks'][:10]:
print('track : ' + track['name'])
print('audio : ' + track['preview_url'])
print('cover art: ' + track['album']['images'][0]['url'])

12
examples/simple_me.py Normal file
View File

@ -0,0 +1,12 @@
import spotipy
from pprint import pprint
def main():
spotify = spotipy.Spotify(auth_manager=spotipy.SpotifyOAuth())
me = spotify.me()
pprint(me)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,9 @@
from spotipy.oauth2 import SpotifyClientCredentials
import spotipy
client_credentials_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
results = sp.search(q='weezer', limit=20)
for i, t in enumerate(results['tracks']['items']):
print(' ', i, t['name'])

View File

@ -0,0 +1,18 @@
# Shows the name of the artist/band and their image by giving a link
import sys
from spotipy.oauth2 import SpotifyClientCredentials
import spotipy
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials())
if len(sys.argv) > 1:
name = ' '.join(sys.argv[1:])
else:
name = 'Radiohead'
results = sp.search(q='artist:' + name, type='artist')
items = results['artists']['items']
if len(items) > 0:
artist = items[0]
print(artist['name'], artist['images'][0]['url'])

11
examples/test.py Normal file
View File

@ -0,0 +1,11 @@
import spotipy
from spotipy.oauth2 import SpotifyOAuth
scope = "user-library-read"
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
results = sp.current_user_saved_tracks()
for idx, item in enumerate(results['items']):
track = item['track']
print(idx, track['artists'][0]['name'], " ", track['name'])

69
examples/title_chain.py Normal file
View File

@ -0,0 +1,69 @@
from spotipy.oauth2 import SpotifyClientCredentials
import spotipy
import random
'''
generates a list of songs where the first word in each subsequent song
matches the last word of the previous song.
usage: python title_chain.py [song name]
'''
client_credentials_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
skiplist = {'dm', 'remix'}
max_offset = 500
seen = set()
def find_songs_that_start_with_word(word):
max_titles = 20
max_offset = 200
offset = 0
out = []
while offset < max_offset and len(out) < max_titles:
results = sp.search(q=word, type='track', limit=50, offset=offset)
if len(results['tracks']['items']) == 0:
break
for item in results['tracks']['items']:
name = item['name'].lower()
if name in seen:
continue
seen.add(name)
if '(' in name:
continue
if '-' in name:
continue
if '/' in name:
continue
words = name.split()
if len(words) > 1 and words[0] == word \
and words[-1] not in skiplist:
# print " ", name, len(out)
out.append(item)
offset += 50
# print "found", len(out), "matches"
return out
def make_chain(word):
which = 1
while True:
songs = find_songs_that_start_with_word(word)
if len(songs) > 0:
song = random.choice(songs)
print(which, song['name'] + " by " + song['artists'][0]['name'])
which += 1
word = song['name'].lower().split()[-1]
else:
break
if __name__ == '__main__':
import sys
title = ' '.join(sys.argv[1:])
make_chain(sys.argv[1].lower())

16
examples/tracks.py Normal file
View File

@ -0,0 +1,16 @@
# shows tracks for the given artist
# usage: python tracks.py [artist name]
from spotipy.oauth2 import SpotifyClientCredentials
import spotipy
import sys
client_credentials_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
if len(sys.argv) > 1:
artist_name = ' '.join(sys.argv[1:])
results = sp.search(q=artist_name, limit=20)
for i, t in enumerate(results['tracks']['items']):
print(' ', i, t['name'])

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

@ -0,0 +1,19 @@
# Shows a user's playlists (need to be authenticated via oauth)
import sys
import spotipy
from spotipy.oauth2 import SpotifyOAuth
if len(sys.argv) > 1:
username = sys.argv[1]
else:
print("Whoops, need a username!")
print("usage: python user_playlists.py [username]")
sys.exit()
sp = spotipy.Spotify(auth_manager=SpotifyOAuth())
playlists = sp.user_playlists(username)
for playlist in playlists['items']:
print(playlist['name'])

View File

@ -0,0 +1,33 @@
# Shows a user's playlists (need to be authenticated via oauth)
import spotipy
from spotipy.oauth2 import SpotifyOAuth
def show_tracks(results):
for i, item in enumerate(results['items']):
track = item['track']
print(
" %d %32.32s %s" %
(i, track['artists'][0]['name'], track['name']))
if __name__ == '__main__':
scope = 'playlist-read-private'
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
playlists = sp.current_user_playlists()
user_id = sp.me()['id']
for playlist in playlists['items']:
if playlist['owner']['id'] == user_id:
print()
print(playlist['name'])
print(' total tracks', playlist['tracks']['total'])
tracks = sp.playlist_items(playlist['id'], fields="items,next", additional_types=('tracks', ))
show_tracks(tracks)
while tracks['next']:
tracks = sp.next(tracks)
show_tracks(tracks)

View File

@ -0,0 +1,31 @@
# Gets all the public playlists for the given
# user. Uses Client Credentials flow
#
import sys
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
client_credentials_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
user = 'spotify'
if len(sys.argv) > 1:
user = sys.argv[1]
playlists = sp.user_playlists(user)
while playlists:
for i, playlist in enumerate(playlists['items']):
print(
"%4d %s %s" %
(i +
1 +
playlists['offset'],
playlist['uri'],
playlist['name']))
if playlists['next']:
playlists = sp.next(playlists)
else:
playlists = None

View File

@ -0,0 +1,12 @@
# Deletes user saved album
import spotipy
from spotipy.oauth2 import SpotifyOAuth
scope = 'user-library-modify'
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
uris = input("input a list of album URIs, URLs or IDs: ")
uris = list(map(str, uris.split()))
deleted = sp.current_user_saved_albums_delete(uris)
print("Deletion successful.")

View File

@ -3,25 +3,22 @@ from setuptools import setup
with open("README.md") as f: with open("README.md") as f:
long_description = f.read() long_description = f.read()
test_reqs = [
'mock==2.0.0'
]
memcache_cache_reqs = [ memcache_cache_reqs = [
'pymemcache>=3.5.2' 'pymemcache>=3.5.2'
] ]
extra_reqs = { extra_reqs = {
'memcache': [ 'test': test_reqs,
'pymemcache>=3.5.2' 'memcache': memcache_cache_reqs
],
'test': [
'autopep8>=2.3.2',
'flake8>=7.3.0',
'flake8-use-fstring>=1.4',
'isort>=7.0.0'
]
} }
setup( setup(
name='spotipy', name='spotipy',
version='2.26.0', version='2.24.0',
description='A light weight Python library for the Spotify Web API', description='A light weight Python library for the Spotify Web API',
long_description=long_description, long_description=long_description,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
@ -37,6 +34,7 @@ setup(
"requests>=2.25.0", "requests>=2.25.0",
"urllib3>=1.26.0" "urllib3>=1.26.0"
], ],
tests_require=test_reqs,
extras_require=extra_reqs, extras_require=extra_reqs,
license='MIT', license='MIT',
packages=['spotipy']) packages=['spotipy'])

View File

@ -11,11 +11,10 @@ import errno
import json import json
import logging import logging
import os import os
from spotipy.util import CLIENT_CREDS_ENV_VARS
from redis import RedisError from redis import RedisError
from spotipy.util import CLIENT_CREDS_ENV_VARS
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -41,6 +40,7 @@ class CacheHandler():
Save a token_info dictionary object to the cache and return None. Save a token_info dictionary object to the cache and return None.
""" """
raise NotImplementedError() raise NotImplementedError()
return None
class CacheFileHandler(CacheHandler): class CacheFileHandler(CacheHandler):
@ -76,30 +76,27 @@ class CacheFileHandler(CacheHandler):
token_info = None token_info = None
try: try:
with open(self.cache_path, encoding='utf-8') as f: f = open(self.cache_path)
token_info_string = f.read() token_info_string = f.read()
f.close()
token_info = json.loads(token_info_string) token_info = json.loads(token_info_string)
except OSError as error: except OSError as error:
if error.errno == errno.ENOENT: if error.errno == errno.ENOENT:
logger.debug(f"cache does not exist at: {self.cache_path}") logger.debug("cache does not exist at: %s", self.cache_path)
else: else:
logger.warning(f"Couldn't read cache at: {self.cache_path}") logger.warning("Couldn't read cache at: %s", self.cache_path)
except json.JSONDecodeError:
logger.warning(f"Couldn't decode JSON from cache at: {self.cache_path}")
return token_info return token_info
def save_token_to_cache(self, token_info): def save_token_to_cache(self, token_info):
try: try:
with open(self.cache_path, "w", encoding='utf-8') as f: f = open(self.cache_path, "w")
f.write(json.dumps(token_info, cls=self.encoder_cls)) f.write(json.dumps(token_info, cls=self.encoder_cls))
# https://github.com/spotipy-dev/spotipy/security/advisories/GHSA-pwhh-q4h6-w599 f.close()
os.chmod(self.cache_path, 0o600)
except OSError: except OSError:
logger.warning(f"Couldn't write token to cache at: {self.cache_path}") logger.warning('Couldn\'t write token to cache at: %s',
except FileNotFoundError: self.cache_path)
logger.warning(f"Couldn't set permissions to cache file at: {self.cache_path}")
class MemoryCacheHandler(CacheHandler): class MemoryCacheHandler(CacheHandler):
@ -152,7 +149,7 @@ class DjangoSessionCacheHandler(CacheHandler):
try: try:
self.request.session['token_info'] = token_info self.request.session['token_info'] = token_info
except Exception as e: except Exception as e:
logger.warning(f"Error saving token to cache: {e}") logger.warning("Error saving token to cache: " + str(e))
class FlaskSessionCacheHandler(CacheHandler): class FlaskSessionCacheHandler(CacheHandler):
@ -177,7 +174,7 @@ class FlaskSessionCacheHandler(CacheHandler):
try: try:
self.session["token_info"] = token_info self.session["token_info"] = token_info
except Exception as e: except Exception as e:
logger.warning(f"Error saving token to cache: {e}") logger.warning("Error saving token to cache: " + str(e))
class RedisCacheHandler(CacheHandler): class RedisCacheHandler(CacheHandler):
@ -203,7 +200,7 @@ class RedisCacheHandler(CacheHandler):
if token_info: if token_info:
return json.loads(token_info) return json.loads(token_info)
except RedisError as e: except RedisError as e:
logger.warning(f"Error getting token from cache: {e}") logger.warning('Error getting token from cache: ' + str(e))
return token_info return token_info
@ -211,13 +208,12 @@ class RedisCacheHandler(CacheHandler):
try: try:
self.redis.set(self.key, json.dumps(token_info)) self.redis.set(self.key, json.dumps(token_info))
except RedisError as e: except RedisError as e:
logger.warning(f"Error saving token to cache: {e}") logger.warning('Error saving token to cache: ' + str(e))
class MemcacheCacheHandler(CacheHandler): class MemcacheCacheHandler(CacheHandler):
"""A Cache handler that stores the token info in Memcache using the pymemcache client """A Cache handler that stores the token info in Memcache using the pymemcache client
""" """
def __init__(self, memcache, key=None) -> None: def __init__(self, memcache, key=None) -> None:
""" """
Parameters: Parameters:
@ -236,11 +232,11 @@ class MemcacheCacheHandler(CacheHandler):
if token_info: if token_info:
return json.loads(token_info.decode()) return json.loads(token_info.decode())
except MemcacheError as e: except MemcacheError as e:
logger.warning(f"Error getting token to cache: {e}") logger.warning('Error getting token from cache' + str(e))
def save_token_to_cache(self, token_info): def save_token_to_cache(self, token_info):
from pymemcache import MemcacheError from pymemcache import MemcacheError
try: try:
self.memcache.set(self.key, json.dumps(token_info)) self.memcache.set(self.key, json.dumps(token_info))
except MemcacheError as e: except MemcacheError as e:
logger.warning(f"Error saving token to cache: {e}") logger.warning('Error saving token to cache' + str(e))

View File

@ -6,12 +6,13 @@ import json
import logging import logging
import re import re
import warnings import warnings
from collections import defaultdict
import requests import requests
import urllib3
from spotipy.exceptions import SpotifyException from spotipy.exceptions import SpotifyException
from spotipy.util import REQUESTS_SESSION, Retry
from collections import defaultdict
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -211,12 +212,15 @@ class Spotify:
def __del__(self): def __del__(self):
"""Make sure the connection (pool) gets closed""" """Make sure the connection (pool) gets closed"""
if getattr(self, "_session", None) and isinstance(self._session, REQUESTS_SESSION): try:
if isinstance(self._session, requests.Session):
self._session.close() self._session.close()
except AttributeError:
pass
def _build_session(self): def _build_session(self):
self._session = requests.Session() self._session = requests.Session()
retry = Retry( retry = urllib3.Retry(
total=self.retries, total=self.retries,
connect=None, connect=None,
read=False, read=False,
@ -259,8 +263,8 @@ class Spotify:
if self.language is not None: if self.language is not None:
headers["Accept-Language"] = self.language headers["Accept-Language"] = self.language
logger.debug(f"Sending {method} to {url} with Params: " logger.debug('Sending %s to %s with Params: %s Headers: %s and Body: %r ',
f"{args.get('params')} Headers: {headers} and Body: {args.get('data')!r}") method, url, args.get("params"), headers, args.get('data'))
try: try:
response = self._session.request( response = self._session.request(
@ -285,8 +289,10 @@ class Spotify:
msg = response.text or None msg = response.text or None
reason = None reason = None
logger.error(f"HTTP Error for {method} to {url} with Params: " logger.error(
f"{args.get('params')} returned {response.status_code} due to {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,
@ -311,7 +317,7 @@ class Spotify:
except ValueError: except ValueError:
results = None results = None
logger.debug(f'RESULTS: {results}') logger.debug('RESULTS: %s', results)
return results return results
def _get(self, url, args=None, payload=None, **kwargs): def _get(self, url, args=None, payload=None, **kwargs):
@ -400,14 +406,10 @@ class Spotify:
return self._get("artists/?ids=" + ",".join(tlist)) return self._get("artists/?ids=" + ",".join(tlist))
def artist_albums( def artist_albums(
self, artist_id, album_type=None, include_groups=None, country=None, limit=10, offset=0 self, artist_id, album_type=None, include_groups=None, country=None, limit=20, offset=0
): ):
""" Get Spotify catalog information about an artist's albums """ Get Spotify catalog information about an artist's albums
.. deprecated::
This method is deprecated and may be removed in a future version. Use
`artist_albums(..., include_groups='...')` instead.
Parameters: Parameters:
- artist_id - the artist ID, URI or URL - artist_id - the artist ID, URI or URL
- include_groups - the types of items to return. One or more of 'album', 'single', - include_groups - the types of items to return. One or more of 'album', 'single',
@ -445,11 +447,6 @@ class Spotify:
- country - limit the response to one particular country. - country - limit the response to one particular country.
""" """
warnings.warn(
"You're using `artist_top_tracks(...)`, "
"which is marked as deprecated by Spotify.",
DeprecationWarning,
)
trid = self._get_id("artist", artist_id) trid = self._get_id("artist", artist_id)
return self._get("artists/" + trid + "/top-tracks", country=country) return self._get("artists/" + trid + "/top-tracks", country=country)
@ -458,17 +455,9 @@ class Spotify:
identified artist. Similarity is based on analysis of the identified artist. Similarity is based on analysis of the
Spotify community's listening history. Spotify community's listening history.
.. deprecated::
This endpoint has been removed by Spotify and is no longer available.
Parameters: Parameters:
- artist_id - the artist ID, URI or URL - artist_id - the artist ID, URI or URL
""" """
warnings.warn(
"You're using `artist_related_artists(...)`, "
"which is marked as deprecated by Spotify.",
DeprecationWarning
)
trid = self._get_id("artist", artist_id) trid = self._get_id("artist", artist_id)
return self._get("artists/" + trid + "/related-artists") return self._get("artists/" + trid + "/related-artists")
@ -652,11 +641,6 @@ class Spotify:
Parameters: Parameters:
- user - the id of the usr - user - the id of the usr
""" """
warnings.warn(
"You're using `user(...)`, "
"which is marked as deprecated by Spotify.",
DeprecationWarning,
)
return self._get("users/" + user) return self._get("users/" + user)
def current_user_playlists(self, limit=50, offset=0): def current_user_playlists(self, limit=50, offset=0):
@ -690,17 +674,13 @@ class Spotify:
self, self,
playlist_id, playlist_id,
fields=None, fields=None,
limit=50, limit=100,
offset=0, offset=0,
market=None, market=None,
additional_types=("track",) additional_types=("track",)
): ):
""" Get full details of the tracks of a playlist. """ Get full details of the tracks of a playlist.
.. deprecated::
This method is deprecated and may be removed in a future version. Use
`playlist_items(playlist_id, ..., additional_types=('track',))` instead.
Parameters: Parameters:
- playlist_id - the playlist ID, URI or URL - playlist_id - the playlist ID, URI or URL
- fields - which fields to return - fields - which fields to return
@ -722,7 +702,7 @@ class Spotify:
self, self,
playlist_id, playlist_id,
fields=None, fields=None,
limit=50, limit=100,
offset=0, offset=0,
market=None, market=None,
additional_types=("track", "episode") additional_types=("track", "episode")
@ -740,7 +720,7 @@ class Spotify:
""" """
plid = self._get_id("playlist", playlist_id) plid = self._get_id("playlist", playlist_id)
return self._get( return self._get(
f"playlists/{plid}/items", f"playlists/{plid}/tracks",
limit=limit, limit=limit,
offset=offset, offset=offset,
fields=fields, fields=fields,
@ -773,22 +753,18 @@ class Spotify:
) )
def user_playlist(self, user, playlist_id=None, fields=None, market=None): def user_playlist(self, user, playlist_id=None, fields=None, market=None):
""" Gets a single playlist of a user warnings.warn(
"You should use `playlist(playlist_id)` instead",
DeprecationWarning,
)
.. deprecated:: """ Gets a single playlist of a user
This method is deprecated and may be removed in a future version. Use
`playlist(playlist_id)` instead.
Parameters: Parameters:
- user - the id of the user - user - the id of the user
- playlist_id - the id of the playlist - playlist_id - the id of the playlist
- fields - which fields to return - fields - which fields to return
""" """
warnings.warn(
"You should use `playlist(playlist_id)` instead",
DeprecationWarning,
)
if playlist_id is None: if playlist_id is None:
return self._get(f"users/{user}/starred") return self._get(f"users/{user}/starred")
return self.playlist(playlist_id, fields=fields, market=market) return self.playlist(playlist_id, fields=fields, market=market)
@ -802,11 +778,12 @@ class Spotify:
offset=0, offset=0,
market=None, market=None,
): ):
""" Get full details of the tracks of a playlist owned by a user. warnings.warn(
"You should use `playlist_tracks(playlist_id)` instead",
DeprecationWarning,
)
.. deprecated:: """ Get full details of the tracks of a playlist owned by a user.
This method is deprecated and may be removed in a future version. Use
`playlist_tracks(playlist_id)` instead.
Parameters: Parameters:
- user - the id of the user - user - the id of the user
@ -816,10 +793,6 @@ class Spotify:
- offset - the index of the first track to return - offset - the index of the first track to return
- market - an ISO 3166-1 alpha-2 country code. - market - an ISO 3166-1 alpha-2 country code.
""" """
warnings.warn(
"You should use `playlist_tracks(playlist_id)` instead",
DeprecationWarning,
)
return self.playlist_tracks( return self.playlist_tracks(
playlist_id, playlist_id,
limit=limit, limit=limit,
@ -836,12 +809,6 @@ class Spotify:
- limit - the number of items to return - limit - the number of items to return
- offset - the index of the first item to return - offset - the index of the first item to return
""" """
warnings.warn(
"You're using `user_playlists(...)`, "
"which is marked as deprecated by Spotify. Use "
"current_user_playlists(...) instead.",
DeprecationWarning,
)
return self._get( return self._get(
f"users/{user}/playlists", limit=limit, offset=offset f"users/{user}/playlists", limit=limit, offset=offset
) )
@ -856,12 +823,6 @@ class Spotify:
- collaborative - is the created playlist collaborative - collaborative - is the created playlist collaborative
- description - the description of the playlist - description - the description of the playlist
""" """
warnings.warn(
"You're using `user_playlist_create(...)`, "
"which is marked as deprecated by Spotify. Use "
"current_user_playlist_create(...) instead.",
DeprecationWarning,
)
data = { data = {
"name": name, "name": name,
"public": public, "public": public,
@ -871,24 +832,6 @@ class Spotify:
return self._post(f"users/{user}/playlists", payload=data) return self._post(f"users/{user}/playlists", payload=data)
def current_user_playlist_create(self, name, public=True, collaborative=False, description=""):
""" Creates a playlist for the current user
Parameters:
- name - the name of the playlist
- public - is the created playlist public
- collaborative - is the created playlist collaborative
- description - the description of the playlist
"""
data = {
"name": name,
"public": public,
"collaborative": collaborative,
"description": description
}
return self._post("me/playlists", payload=data)
def user_playlist_change_details( def user_playlist_change_details(
self, self,
user, user,
@ -898,13 +841,11 @@ class Spotify:
collaborative=None, collaborative=None,
description=None, description=None,
): ):
""" This function is no longer in use, please use the recommended function in the warning! warnings.warn(
"You should use `playlist_change_details(playlist_id, ...)` instead",
Changes a playlist's name and/or public/private state DeprecationWarning,
)
.. deprecated:: """ Changes a playlist's name and/or public/private state
This method is deprecated and may be removed in a future version. Use
`playlist_change_details(playlist_id, ...)` instead.
Parameters: Parameters:
- user - the id of the user - user - the id of the user
@ -914,22 +855,12 @@ class Spotify:
- collaborative - optional is the playlist collaborative - collaborative - optional is the playlist collaborative
- description - optional description of the playlist - description - optional description of the playlist
""" """
warnings.warn(
"You should use `playlist_change_details(playlist_id, ...)` instead",
DeprecationWarning,
)
return self.playlist_change_details(playlist_id, name, public, return self.playlist_change_details(playlist_id, name, public,
collaborative, description) collaborative, description)
def user_playlist_unfollow(self, user, playlist_id): def user_playlist_unfollow(self, user, playlist_id):
""" This function is no longer in use, please use the recommended function in the warning! """ Unfollows (deletes) a playlist for a user
Unfollows (deletes) a playlist for a user
.. deprecated::
This method is deprecated and may be removed in a future version. Use
`current_user_unfollow_playlist(playlist_id)` instead.
Parameters: Parameters:
- user - the id of the user - user - the id of the user
@ -944,13 +875,11 @@ class Spotify:
def user_playlist_add_tracks( def user_playlist_add_tracks(
self, user, playlist_id, tracks, position=None self, user, playlist_id, tracks, position=None
): ):
""" This function is no longer in use, please use the recommended function in the warning! warnings.warn(
"You should use `playlist_add_items(playlist_id, tracks)` instead",
Adds tracks to a playlist DeprecationWarning,
)
.. deprecated:: """ Adds tracks to a playlist
This method is deprecated and may be removed in a future version. Use
`playlist_add_items(playlist_id, tracks)` instead.
Parameters: Parameters:
- user - the id of the user - user - the id of the user
@ -958,24 +887,17 @@ class Spotify:
- tracks - a list of track URIs, URLs or IDs - tracks - a list of track URIs, URLs or IDs
- position - the position to add the tracks - position - the position to add the tracks
""" """
warnings.warn(
"You should use `playlist_add_items(playlist_id, tracks)` instead",
DeprecationWarning,
)
tracks = [self._get_uri("track", tid) for tid in tracks] tracks = [self._get_uri("track", tid) for tid in tracks]
return self.playlist_add_items(playlist_id, tracks, position) return self.playlist_add_items(playlist_id, tracks, position)
def user_playlist_add_episodes( def user_playlist_add_episodes(
self, user, playlist_id, episodes, position=None self, user, playlist_id, episodes, position=None
): ):
""" This function is no longer in use, please use the recommended function in the warning! warnings.warn(
"You should use `playlist_add_items(playlist_id, episodes)` instead",
Adds episodes to a playlist DeprecationWarning,
)
.. deprecated:: """ Adds episodes to a playlist
This method is deprecated and may be removed in a future version. Use
`playlist_add_items(playlist_id, episodes)` instead.
Parameters: Parameters:
- user - the id of the user - user - the id of the user
@ -983,22 +905,11 @@ class Spotify:
- episodes - a list of track URIs, URLs or IDs - episodes - a list of track URIs, URLs or IDs
- position - the position to add the episodes - position - the position to add the episodes
""" """
warnings.warn(
"You should use `playlist_add_items(playlist_id, episodes)` instead",
DeprecationWarning,
)
episodes = [self._get_uri("episode", tid) for tid in episodes] episodes = [self._get_uri("episode", tid) for tid in episodes]
return self.playlist_add_items(playlist_id, episodes, position) return self.playlist_add_items(playlist_id, episodes, position)
def user_playlist_replace_tracks(self, user, playlist_id, tracks): def user_playlist_replace_tracks(self, user, playlist_id, tracks):
""" This function is no longer in use, please use the recommended function in the warning! """ Replace all tracks in a playlist for a user
Replace all tracks in a playlist for a user
.. deprecated::
This method is deprecated and may be removed in a future version. Use
`playlist_replace_items(playlist_id, tracks)` instead.
Parameters: Parameters:
- user - the id of the user - user - the id of the user
@ -1020,13 +931,7 @@ class Spotify:
range_length=1, range_length=1,
snapshot_id=None, snapshot_id=None,
): ):
""" This function is no longer in use, please use the recommended function in the warning! """ Reorder tracks in a playlist from a user
Reorder tracks in a playlist from a user
.. deprecated::
This method is deprecated and may be removed in a future version. Use
`playlist_reorder_items(playlist_id, ...)` instead.
Parameters: Parameters:
- user - the id of the user - user - the id of the user
@ -1049,19 +954,14 @@ class Spotify:
def user_playlist_remove_all_occurrences_of_tracks( def user_playlist_remove_all_occurrences_of_tracks(
self, user, playlist_id, tracks, snapshot_id=None self, user, playlist_id, tracks, snapshot_id=None
): ):
""" This function is no longer in use, please use the recommended function in the warning! """ Removes all occurrences of the given tracks from the given playlist
Removes all occurrences of the given tracks from the given playlist
.. deprecated::
This method is deprecated and may be removed in a future version. Use
`playlist_remove_all_occurrences_of_items(playlist_id, tracks)` instead.
Parameters: Parameters:
- user - the id of the user - user - the id of the user
- playlist_id - the id of the playlist - playlist_id - the id of the playlist
- tracks - the list of track ids to remove from the playlist - tracks - the list of track ids to remove from the playlist
- snapshot_id - optional id of the playlist snapshot - snapshot_id - optional id of the playlist snapshot
""" """
warnings.warn( warnings.warn(
"You should use `playlist_remove_all_occurrences_of_items" "You should use `playlist_remove_all_occurrences_of_items"
@ -1075,12 +975,7 @@ class Spotify:
def user_playlist_remove_specific_occurrences_of_tracks( def user_playlist_remove_specific_occurrences_of_tracks(
self, user, playlist_id, tracks, snapshot_id=None self, user, playlist_id, tracks, snapshot_id=None
): ):
""" This function is no longer in use, please use the recommended function in the warning! """ Removes all occurrences of the given tracks from the given playlist
Removes specific occurrences of the given tracks from the given playlist
.. deprecated::
This endpoint has been removed by Spotify and is no longer available.
Parameters: Parameters:
- user - the id of the user - user - the id of the user
@ -1093,8 +988,8 @@ class Spotify:
- snapshot_id - optional id of the playlist snapshot - snapshot_id - optional id of the playlist snapshot
""" """
warnings.warn( warnings.warn(
"You're using `user_playlist_remove_specific_occurrences_of_tracks(...)`, " "You should use `playlist_remove_specific_occurrences_of_items"
"which is marked as deprecated by Spotify.", "(playlist_id, tracks)` instead",
DeprecationWarning, DeprecationWarning,
) )
plid = self._get_id("playlist", playlist_id) plid = self._get_id("playlist", playlist_id)
@ -1114,17 +1009,13 @@ class Spotify:
) )
def user_playlist_follow_playlist(self, playlist_owner_id, playlist_id): def user_playlist_follow_playlist(self, playlist_owner_id, playlist_id):
""" This function is no longer in use, please use the recommended function in the warning! """
Add the current authenticated user as a follower of a playlist. Add the current authenticated user as a follower of a playlist.
.. deprecated::
This method is deprecated and may be removed in a future version. Use
`current_user_follow_playlist(playlist_id)` instead.
Parameters: Parameters:
- playlist_owner_id - the user id of the playlist owner - playlist_owner_id - the user id of the playlist owner
- playlist_id - the id of the playlist - playlist_id - the id of the playlist
""" """
warnings.warn( warnings.warn(
"You should use `current_user_follow_playlist(playlist_id)` instead", "You should use `current_user_follow_playlist(playlist_id)` instead",
@ -1135,19 +1026,15 @@ class Spotify:
def user_playlist_is_following( def user_playlist_is_following(
self, playlist_owner_id, playlist_id, user_ids self, playlist_owner_id, playlist_id, user_ids
): ):
""" This function is no longer in use, please use the recommended function in the warning! """
Check to see if the given users are following the given playlist Check to see if the given users are following the given playlist
.. deprecated::
This method is deprecated and may be removed in a future version. Use
`playlist_is_following(playlist_id, user_ids)` instead.
Parameters: Parameters:
- playlist_owner_id - the user id of the playlist owner - playlist_owner_id - the user id of the playlist owner
- playlist_id - the id of the playlist - playlist_id - the id of the playlist
- user_ids - the ids of the users that you want to check to see - user_ids - the ids of the users that you want to check to see
if they follow the playlist. Maximum: 5 ids. if they follow the playlist. Maximum: 5 ids.
""" """
warnings.warn( warnings.warn(
"You should use `playlist_is_following(playlist_id, user_ids)` instead", "You should use `playlist_is_following(playlist_id, user_ids)` instead",
@ -1192,10 +1079,10 @@ class Spotify:
user user
Parameters: Parameters:
- playlist_id - the id of the playlist - name - the name of the playlist
""" """
return self._delete( return self._delete(
f"playlists/{self._get_id('playlist', playlist_id)}/followers" f"playlists/{playlist_id}/followers"
) )
def playlist_add_items( def playlist_add_items(
@ -1211,7 +1098,7 @@ class Spotify:
plid = self._get_id("playlist", playlist_id) plid = self._get_id("playlist", playlist_id)
ftracks = [self._get_uri("track", tid) for tid in items] ftracks = [self._get_uri("track", tid) for tid in items]
return self._post( return self._post(
f"playlists/{plid}/items", f"playlists/{plid}/tracks",
payload=ftracks, payload=ftracks,
position=position, position=position,
) )
@ -1227,7 +1114,7 @@ class Spotify:
ftracks = [self._get_uri("track", tid) for tid in items] ftracks = [self._get_uri("track", tid) for tid in items]
payload = {"uris": ftracks} payload = {"uris": ftracks}
return self._put( return self._put(
f"playlists/{plid}/items", payload=payload f"playlists/{plid}/tracks", payload=payload
) )
def playlist_reorder_items( def playlist_reorder_items(
@ -1258,7 +1145,7 @@ class Spotify:
if snapshot_id: if snapshot_id:
payload["snapshot_id"] = snapshot_id payload["snapshot_id"] = snapshot_id
return self._put( return self._put(
f"playlists/{plid}/items", payload=payload f"playlists/{plid}/tracks", payload=payload
) )
def playlist_remove_all_occurrences_of_items( def playlist_remove_all_occurrences_of_items(
@ -1275,11 +1162,11 @@ class Spotify:
plid = self._get_id("playlist", playlist_id) plid = self._get_id("playlist", playlist_id)
ftracks = [self._get_uri("track", tid) for tid in items] ftracks = [self._get_uri("track", tid) for tid in items]
payload = {"items": [{"uri": track} for track in ftracks]} payload = {"tracks": [{"uri": track} for track in ftracks]}
if snapshot_id: if snapshot_id:
payload["snapshot_id"] = snapshot_id payload["snapshot_id"] = snapshot_id
return self._delete( return self._delete(
f"playlists/{plid}/items", payload=payload f"playlists/{plid}/tracks", payload=payload
) )
def playlist_remove_specific_occurrences_of_items( def playlist_remove_specific_occurrences_of_items(
@ -1306,14 +1193,14 @@ class Spotify:
"positions": tr["positions"], "positions": tr["positions"],
} }
) )
payload = {"items": ftracks} payload = {"tracks": ftracks}
if snapshot_id: if snapshot_id:
payload["snapshot_id"] = snapshot_id payload["snapshot_id"] = snapshot_id
return self._delete( return self._delete(
f"playlists/{plid}/items", payload=payload f"playlists/{plid}/tracks", payload=payload
) )
def current_user_follow_playlist(self, playlist_id): def current_user_follow_playlist(self, playlist_id, public=True):
""" """
Add the current authenticated user as a follower of a playlist. Add the current authenticated user as a follower of a playlist.
@ -1321,7 +1208,10 @@ class Spotify:
- playlist_id - the id of the playlist - playlist_id - the id of the playlist
""" """
return self._put("me/library", uris=self._get_uri("playlist", playlist_id)) return self._put(
f"playlists/{playlist_id}/followers",
payload={"public": public}
)
def playlist_is_following( def playlist_is_following(
self, playlist_id, user_ids self, playlist_id, user_ids
@ -1335,27 +1225,11 @@ class Spotify:
if they follow the playlist. Maximum: 5 ids. if they follow the playlist. Maximum: 5 ids.
""" """
warnings.warn( endpoint = "playlists/{}/followers/contains?ids={}"
"You're using `playlist_is_following(..., user_ids=...)`, " return self._get(
"which is marked as deprecated by Spotify. Use ", endpoint.format(playlist_id, ",".join(user_ids))
"current_user_follow_playlist(...) instead.",
DeprecationWarning,
) )
endpoint = f"playlists/{playlist_id}/followers/contains?ids={','.join(user_ids)}"
return self._get(endpoint)
def current_user_saved_items(self, uris):
"""
Check if the current user is following the given artists, users, or playlists
Parameters:
- uris - a list of URIs to check for following status. Maximum: 40 ids.
"""
valid_uris = [uri for uri in uris if self._is_uri(uri)]
return self._get("me/library/contains", uris=",".join(valid_uris))
def me(self): def me(self):
""" Get detailed profile information about the current user. """ Get detailed profile information about the current user.
An alias for the 'current_user' method. An alias for the 'current_user' method.
@ -1368,20 +1242,10 @@ class Spotify:
""" """
return self.me() return self.me()
def current_user_playing_track(self, market=None, additional_types=("track",)): def current_user_playing_track(self):
""" Get information about the current users currently playing track. """ Get information about the current users currently playing track.
Parameters:
- market - An ISO 3166-1 alpha-2 country code or the
string from_token.
- additional_types - list of item types to return.
valid types are: track and episode
""" """
return self._get( return self._get("me/player/currently-playing")
"me/player/currently-playing",
market=market,
additional_types=",".join(additional_types)
)
def current_user_saved_albums(self, limit=20, offset=0, market=None): def current_user_saved_albums(self, limit=20, offset=0, market=None):
""" Gets a list of the albums saved in the current authorized user's """ Gets a list of the albums saved in the current authorized user's
@ -1402,8 +1266,8 @@ class Spotify:
- albums - a list of album URIs, URLs or IDs - albums - a list of album URIs, URLs or IDs
""" """
alist = [self._get_uri("album", a) for a in albums] alist = [self._get_id("album", a) for a in albums]
return self._put("me/library", uris=",".join(alist)) return self._put("me/albums?ids=" + ",".join(alist))
def current_user_saved_albums_delete(self, albums=[]): def current_user_saved_albums_delete(self, albums=[]):
""" Remove one or more albums from the current user's """ Remove one or more albums from the current user's
@ -1412,8 +1276,8 @@ class Spotify:
Parameters: Parameters:
- albums - a list of album URIs, URLs or IDs - albums - a list of album URIs, URLs or IDs
""" """
alist = [self._get_uri("album", a) for a in albums] alist = [self._get_id("album", a) for a in albums]
return self._delete("me/library", uris=",".join(alist)) return self._delete("me/albums/?ids=" + ",".join(alist))
def current_user_saved_albums_contains(self, albums=[]): def current_user_saved_albums_contains(self, albums=[]):
""" Check if one or more albums is already saved in """ Check if one or more albums is already saved in
@ -1422,8 +1286,8 @@ class Spotify:
Parameters: Parameters:
- albums - a list of album URIs, URLs or IDs - albums - a list of album URIs, URLs or IDs
""" """
alist = [self._get_uri("album", a) for a in albums] alist = [self._get_id("album", a) for a in albums]
return self._get("me/library/contains", uris=",".join(alist)) return self._get("me/albums/contains?ids=" + ",".join(alist))
def current_user_saved_tracks(self, limit=20, offset=0, market=None): def current_user_saved_tracks(self, limit=20, offset=0, market=None):
""" Gets a list of the tracks saved in the current authorized user's """ Gets a list of the tracks saved in the current authorized user's
@ -1446,8 +1310,8 @@ class Spotify:
""" """
tlist = [] tlist = []
if tracks is not None: if tracks is not None:
tlist = [self._get_uri("track", t) for t in tracks] tlist = [self._get_id("track", t) for t in tracks]
return self._put("me/library", uris=",".join(tlist)) return self._put("me/tracks/?ids=" + ",".join(tlist))
def current_user_saved_tracks_delete(self, tracks=None): def current_user_saved_tracks_delete(self, tracks=None):
""" Remove one or more tracks from the current user's """ Remove one or more tracks from the current user's
@ -1458,8 +1322,8 @@ class Spotify:
""" """
tlist = [] tlist = []
if tracks is not None: if tracks is not None:
tlist = [self._get_uri("track", t) for t in tracks] tlist = [self._get_id("track", t) for t in tracks]
return self._delete("me/library", uris=",".join(tlist)) return self._delete("me/tracks/?ids=" + ",".join(tlist))
def current_user_saved_tracks_contains(self, tracks=None): def current_user_saved_tracks_contains(self, tracks=None):
""" Check if one or more tracks is already saved in """ Check if one or more tracks is already saved in
@ -1470,8 +1334,8 @@ class Spotify:
""" """
tlist = [] tlist = []
if tracks is not None: if tracks is not None:
tlist = [self._get_uri("track", t) for t in tracks] tlist = [self._get_id("track", t) for t in tracks]
return self._get("me/library/contains", uris=",".join(tlist)) return self._get("me/tracks/contains?ids=" + ",".join(tlist))
def current_user_saved_episodes(self, limit=20, offset=0, market=None): def current_user_saved_episodes(self, limit=20, offset=0, market=None):
""" Gets a list of the episodes saved in the current authorized user's """ Gets a list of the episodes saved in the current authorized user's
@ -1494,8 +1358,8 @@ class Spotify:
""" """
elist = [] elist = []
if episodes is not None: if episodes is not None:
elist = [self._get_uri("episode", e) for e in episodes] elist = [self._get_id("episode", e) for e in episodes]
return self._put("me/library", uris=",".join(elist)) return self._put("me/episodes/?ids=" + ",".join(elist))
def current_user_saved_episodes_delete(self, episodes=None): def current_user_saved_episodes_delete(self, episodes=None):
""" Remove one or more episodes from the current user's """ Remove one or more episodes from the current user's
@ -1506,8 +1370,8 @@ class Spotify:
""" """
elist = [] elist = []
if episodes is not None: if episodes is not None:
elist = [self._get_uri("episode", e) for e in episodes] elist = [self._get_id("episode", e) for e in episodes]
return self._delete("me/library", uris=",".join(elist)) return self._delete("me/episodes/?ids=" + ",".join(elist))
def current_user_saved_episodes_contains(self, episodes=None): def current_user_saved_episodes_contains(self, episodes=None):
""" Check if one or more episodes is already saved in """ Check if one or more episodes is already saved in
@ -1539,8 +1403,8 @@ class Spotify:
Parameters: Parameters:
- shows - a list of show URIs, URLs or IDs - shows - a list of show URIs, URLs or IDs
""" """
slist = [self._get_uri("show", s) for s in shows] slist = [self._get_id("show", s) for s in shows]
return self._put("me/library", uris=",".join(slist)) return self._put("me/shows?ids=" + ",".join(slist))
def current_user_saved_shows_delete(self, shows=[]): def current_user_saved_shows_delete(self, shows=[]):
""" Remove one or more shows from the current user's """ Remove one or more shows from the current user's
@ -1549,8 +1413,8 @@ class Spotify:
Parameters: Parameters:
- shows - a list of show URIs, URLs or IDs - shows - a list of show URIs, URLs or IDs
""" """
slist = [self._get_uri("show", s) for s in shows] slist = [self._get_id("show", s) for s in shows]
return self._delete("me/library", uris=",".join(slist)) return self._delete("me/shows/?ids=" + ",".join(slist))
def current_user_saved_shows_contains(self, shows=[]): def current_user_saved_shows_contains(self, shows=[]):
""" Check if one or more shows is already saved in """ Check if one or more shows is already saved in
@ -1559,8 +1423,8 @@ class Spotify:
Parameters: Parameters:
- shows - a list of show URIs, URLs or IDs - shows - a list of show URIs, URLs or IDs
""" """
slist = [self._get_uri("show", s) for s in shows] slist = [self._get_id("show", s) for s in shows]
return self._get("me/library/contains", uris=",".join(slist)) return self._get("me/shows/contains?ids=" + ",".join(slist))
def current_user_followed_artists(self, limit=20, after=None): def current_user_followed_artists(self, limit=20, after=None):
""" Gets a list of the artists followed by the current authorized user """ Gets a list of the artists followed by the current authorized user
@ -1583,11 +1447,11 @@ class Spotify:
Parameters: Parameters:
- ids - a list of artist URIs, URLs or IDs - ids - a list of artist URIs, URLs or IDs
""" """
ulist = [] idlist = []
if ids is not None: if ids is not None:
ulist = [self._get_uri("artist", i) for i in ids] idlist = [self._get_id("artist", i) for i in ids]
return self._get( return self._get(
"me/library/contains", uris=",".join(ulist) "me/following/contains", ids=",".join(idlist), type="artist"
) )
def current_user_following_users(self, ids=None): def current_user_following_users(self, ids=None):
@ -1598,11 +1462,11 @@ class Spotify:
Parameters: Parameters:
- ids - a list of user URIs, URLs or IDs - ids - a list of user URIs, URLs or IDs
""" """
ulist = [] idlist = []
if ids is not None: if ids is not None:
ulist = [self._get_uri("user", i) for i in ids] idlist = [self._get_id("user", i) for i in ids]
return self._get( return self._get(
"me/library/contains", uris=",".join(ulist) "me/following/contains", ids=",".join(idlist), type="user"
) )
def current_user_top_artists( def current_user_top_artists(
@ -1611,7 +1475,7 @@ class Spotify:
""" Get the current user's top artists """ Get the current user's top artists
Parameters: Parameters:
- limit - the number of entities to return (max 50) - limit - the number of entities to return
- offset - the index of the first entity to return - offset - the index of the first entity to return
- time_range - Over what time frame are the affinities computed - time_range - Over what time frame are the affinities computed
Valid-values: short_term, medium_term, long_term Valid-values: short_term, medium_term, long_term
@ -1659,41 +1523,34 @@ class Spotify:
Parameters: Parameters:
- ids - a list of artist IDs - ids - a list of artist IDs
""" """
alist = [self._get_uri("artist", a) for a in ids] return self._put("me/following?type=artist&ids=" + ",".join(ids))
return self._put("me/library", uris=",".join(alist))
def user_follow_users(self, ids=[]): def user_follow_users(self, ids=[]):
""" Follow one or more users """ Follow one or more users
Parameters: Parameters:
- ids - a list of user IDs - ids - a list of user IDs
""" """
ulist = [self._get_uri("user", a) for a in ids] return self._put("me/following?type=user&ids=" + ",".join(ids))
return self._put("me/library", uris=",".join(ulist))
def user_unfollow_artists(self, ids=[]): def user_unfollow_artists(self, ids=[]):
""" Unfollow one or more artists """ Unfollow one or more artists
Parameters: Parameters:
- ids - a list of artist IDs - ids - a list of artist IDs
""" """
alist = [self._get_uri("artist", a) for a in ids] return self._delete("me/following?type=artist&ids=" + ",".join(ids))
return self._delete("me/library", uris=",".join(alist))
def user_unfollow_users(self, ids=[]): def user_unfollow_users(self, ids=[]):
""" Unfollow one or more users """ Unfollow one or more users
Parameters: Parameters:
- ids - a list of user IDs - ids - a list of user IDs
""" """
ulist = [self._get_uri("user", a) for a in ids] return self._delete("me/following?type=user&ids=" + ",".join(ids))
return self._delete("me/library", uris=",".join(ulist))
def featured_playlists( def featured_playlists(
self, locale=None, country=None, timestamp=None, limit=20, offset=0 self, locale=None, country=None, timestamp=None, limit=20, offset=0
): ):
""" Get a list of Spotify featured playlists """ Get a list of Spotify featured playlists
.. deprecated::
This endpoint has been removed by Spotify and is no longer available.
Parameters: Parameters:
- locale - The desired language, consisting of a lowercase ISO - locale - The desired language, consisting of a lowercase ISO
639-1 alpha-2 language code and an uppercase ISO 3166-1 alpha-2 639-1 alpha-2 language code and an uppercase ISO 3166-1 alpha-2
@ -1713,11 +1570,6 @@ class Spotify:
(the first object). Use with limit to get the next set of (the first object). Use with limit to get the next set of
items. items.
""" """
warnings.warn(
"You're using `featured_playlists(...)`, "
"which is marked as deprecated by Spotify.",
DeprecationWarning,
)
return self._get( return self._get(
"browse/featured-playlists", "browse/featured-playlists",
locale=locale, locale=locale,
@ -1740,11 +1592,6 @@ class Spotify:
(the first object). Use with limit to get the next set of (the first object). Use with limit to get the next set of
items. items.
""" """
warnings.warn(
"You're using `new_release(...)`, "
"which is marked as deprecated by Spotify.",
DeprecationWarning,
)
return self._get( return self._get(
"browse/new-releases", country=country, limit=limit, offset=offset "browse/new-releases", country=country, limit=limit, offset=offset
) )
@ -1760,11 +1607,6 @@ class Spotify:
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.
""" """
warnings.warn(
"You're using `category(...)`, "
"which is marked as deprecated by Spotify.",
DeprecationWarning,
)
return self._get( return self._get(
"browse/categories/" + category_id, "browse/categories/" + category_id,
country=country, country=country,
@ -1787,11 +1629,6 @@ class Spotify:
(the first object). Use with limit to get the next set of (the first object). Use with limit to get the next set of
items. items.
""" """
warnings.warn(
"You're using `categories(...)`, "
"which is marked as deprecated by Spotify.",
DeprecationWarning,
)
return self._get( return self._get(
"browse/categories", "browse/categories",
country=country, country=country,
@ -1805,9 +1642,6 @@ class Spotify:
): ):
""" Get a list of playlists for a specific Spotify category """ Get a list of playlists for a specific Spotify category
.. deprecated::
This endpoint has been removed by Spotify and is no longer available.
Parameters: Parameters:
- category_id - The Spotify category ID for the category. - category_id - The Spotify category ID for the category.
@ -1820,11 +1654,6 @@ class Spotify:
(the first object). Use with limit to get the next set of (the first object). Use with limit to get the next set of
items. items.
""" """
warnings.warn(
"You're using `category_playlists(...)`, "
"which is marked as deprecated by Spotify.",
DeprecationWarning,
)
return self._get( return self._get(
"browse/categories/" + category_id + "/playlists", "browse/categories/" + category_id + "/playlists",
country=country, country=country,
@ -1845,9 +1674,6 @@ class Spotify:
(at least one of `seed_artists`, `seed_tracks` and `seed_genres` (at least one of `seed_artists`, `seed_tracks` and `seed_genres`
are needed) are needed)
.. deprecated::
This endpoint has been removed by Spotify and is no longer available.
Parameters: Parameters:
- seed_artists - a list of artist IDs, URIs or URLs - seed_artists - a list of artist IDs, URIs or URLs
- seed_tracks - a list of track IDs, URIs or URLs - seed_tracks - a list of track IDs, URIs or URLs
@ -1865,12 +1691,6 @@ class Spotify:
attributes listed in the documentation, these values attributes listed in the documentation, these values
provide filters and targeting on results. provide filters and targeting on results.
""" """
warnings.warn(
"You're using `recommendations(...)`, "
"which is marked as deprecated by Spotify.",
DeprecationWarning,
)
params = dict(limit=limit) params = dict(limit=limit)
if seed_artists: if seed_artists:
params["seed_artists"] = ",".join( params["seed_artists"] = ",".join(
@ -1909,49 +1729,22 @@ class Spotify:
def recommendation_genre_seeds(self): def recommendation_genre_seeds(self):
""" Get a list of genres available for the recommendations function. """ Get a list of genres available for the recommendations function.
.. deprecated::
This endpoint has been removed by Spotify and is no longer available.
""" """
warnings.warn(
"You're using `recommendation_genre_seeds(...)`, "
"which is marked as deprecated by Spotify.",
DeprecationWarning,
)
return self._get("recommendations/available-genre-seeds") return self._get("recommendations/available-genre-seeds")
def audio_analysis(self, track_id): def audio_analysis(self, track_id):
""" Get audio analysis for a track based upon its Spotify ID """ Get audio analysis for a track based upon its Spotify ID
.. deprecated::
This endpoint has been removed by Spotify and is no longer available.
Parameters: Parameters:
- track_id - a track URI, URL or ID - track_id - a track URI, URL or ID
""" """
warnings.warn(
"You're using `audio_analysis(...)`, "
"which is marked as deprecated by Spotify.",
DeprecationWarning,
)
trid = self._get_id("track", track_id) trid = self._get_id("track", track_id)
return self._get("audio-analysis/" + trid) return self._get("audio-analysis/" + trid)
def audio_features(self, tracks=[]): def audio_features(self, tracks=[]):
""" Get audio features for one or multiple tracks based upon their Spotify IDs """ Get audio features for one or multiple tracks based upon their Spotify IDs
.. deprecated::
This endpoint has been removed by Spotify and is no longer available.
Parameters: Parameters:
- tracks - a list of track URIs, URLs or IDs, maximum: 100 ids - tracks - a list of track URIs, URLs or IDs, maximum: 100 ids
""" """
warnings.warn(
"You're using `audio_features(...)`, "
"which is marked as deprecated by Spotify.",
DeprecationWarning,
)
if isinstance(tracks, str): if isinstance(tracks, str):
trackid = self._get_id("track", tracks) trackid = self._get_id("track", tracks)
results = self._get("audio-features/?ids=" + trackid) results = self._get("audio-features/?ids=" + trackid)
@ -2224,9 +2017,11 @@ class Spotify:
def _search_multiple_markets(self, q, limit, offset, type, markets, total): def _search_multiple_markets(self, q, limit, offset, type, markets, total):
if total and limit > total: if total and limit > total:
limit = total limit = total
warnings.warn(f"limit was auto-adjusted to equal {total} " warnings.warn(
f"as it must not be higher than total", "limit was auto-adjusted to equal {} as it must not be higher than total".format(
UserWarning) total),
UserWarning,
)
results = defaultdict(dict) results = defaultdict(dict)
item_types = [item_type + "s" for item_type in type.split(",")] item_types = [item_type + "s" for item_type in type.split(",")]

View File

@ -1,8 +1,4 @@
class SpotifyBaseException(Exception): class SpotifyException(Exception):
pass
class SpotifyException(SpotifyBaseException):
def __init__(self, http_status, code, msg, reason=None, headers=None): def __init__(self, http_status, code, msg, reason=None, headers=None):
self.http_status = http_status self.http_status = http_status
@ -16,29 +12,5 @@ class SpotifyException(SpotifyBaseException):
self.headers = headers self.headers = headers
def __str__(self): def __str__(self):
return (f"http status: {self.http_status}, " return 'http status: {}, code:{} - {}, reason: {}'.format(
f"code: {self.code} - {self.msg}, " self.http_status, self.code, self.msg, self.reason)
f"reason: {self.reason}")
class SpotifyOauthError(SpotifyBaseException):
""" Error during Auth Code or Implicit Grant flow """
def __init__(self, message, error=None, error_description=None, *args, **kwargs):
self.error = error
self.error_description = error_description
self.__dict__.update(kwargs)
super().__init__(message, *args, **kwargs)
class SpotifyStateError(SpotifyOauthError):
""" 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):
if not message:
message = ("Expected " + local_state + " but received "
+ remote_state)
super(SpotifyOauthError, self).__init__(message, error,
error_description, *args,
**kwargs)

View File

@ -8,26 +8,46 @@ __all__ = [
] ]
import base64 import base64
import html
import logging import logging
import os import os
import time import time
import urllib.parse as urllibparse
import warnings import warnings
import webbrowser import webbrowser
import requests
import urllib.parse as urllibparse
from http.server import BaseHTTPRequestHandler, HTTPServer from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qsl, urlparse from urllib.parse import parse_qsl, urlparse
import requests
from spotipy.cache_handler import CacheFileHandler, CacheHandler from spotipy.cache_handler import CacheFileHandler, CacheHandler
from spotipy.exceptions import SpotifyOauthError, SpotifyStateError from spotipy.util import CLIENT_CREDS_ENV_VARS, get_host_port, normalize_scope
from spotipy.util import (CLIENT_CREDS_ENV_VARS, REQUESTS_SESSION,
get_host_port, normalize_scope)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SpotifyOauthError(Exception):
""" Error during Auth Code or Implicit Grant flow """
def __init__(self, message, error=None, error_description=None, *args, **kwargs):
self.error = error
self.error_description = error_description
self.__dict__.update(kwargs)
super().__init__(message, *args, **kwargs)
class SpotifyStateError(SpotifyOauthError):
""" 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):
if not message:
message = ("Expected " + local_state + " but received "
+ remote_state)
super(SpotifyOauthError, self).__init__(message, error,
error_description, *args,
**kwargs)
def _make_authorization_headers(client_id, client_secret): def _make_authorization_headers(client_id, client_secret):
auth_header = base64.b64encode( auth_header = base64.b64encode(
str(client_id + ":" + client_secret).encode("ascii") str(client_id + ":" + client_secret).encode("ascii")
@ -124,7 +144,7 @@ class SpotifyAuthBase:
def __del__(self): def __del__(self):
"""Make sure the connection (pool) gets closed""" """Make sure the connection (pool) gets closed"""
if getattr(self, "_session", None) and isinstance(self._session, REQUESTS_SESSION): if isinstance(self._session, requests.Session):
self._session.close() self._session.close()
@ -187,7 +207,7 @@ class SpotifyClientCredentials(SpotifyAuthBase):
Else fetches a new token and returns it Else fetches a new token and returns it
Parameters: Parameters:
- as_dict: (deprecated) a boolean indicating if returning the access token - as_dict - a boolean indicating if returning the access token
as a token_info dictionary, otherwise it will be returned as a token_info dictionary, otherwise it will be returned
as a string. as a string.
""" """
@ -219,8 +239,10 @@ class SpotifyClientCredentials(SpotifyAuthBase):
self.client_id, self.client_secret self.client_id, self.client_secret
) )
logger.debug(f"Sending POST request to {self.OAUTH_TOKEN_URL} with Headers: " logger.debug(
f"{headers} and Body: {payload}") "sending POST request to %s with Headers: %s and Body: %r",
self.OAUTH_TOKEN_URL, headers, payload
)
try: try:
response = self._session.post( response = self._session.post(
@ -401,9 +423,9 @@ class SpotifyOAuth(SpotifyAuthBase):
auth_url = self.get_authorize_url() auth_url = self.get_authorize_url()
try: try:
webbrowser.open(auth_url) webbrowser.open(auth_url)
logger.info(f"Opened {auth_url} in your browser") logger.info("Opened %s in your browser", auth_url)
except webbrowser.Error: except webbrowser.Error:
logger.error(f"Please navigate here: {auth_url}") logger.error("Please navigate here: %s", auth_url)
def _get_auth_response_interactive(self, open_browser=False): def _get_auth_response_interactive(self, open_browser=False):
if open_browser: if open_browser:
@ -412,8 +434,8 @@ class SpotifyOAuth(SpotifyAuthBase):
else: else:
url = self.get_authorize_url() url = self.get_authorize_url()
prompt = ( prompt = (
f"Go to the following URL: {url}\n" "Go to the following URL: {}\n"
"Enter the URL you were redirected to: " "Enter the URL you were redirected to: ".format(url)
) )
response = self._get_user_input(prompt) response = self._get_user_input(prompt)
state, code = SpotifyOAuth.parse_auth_response_url(response) state, code = SpotifyOAuth.parse_auth_response_url(response)
@ -445,17 +467,6 @@ class SpotifyOAuth(SpotifyAuthBase):
redirect_info = urlparse(self.redirect_uri) redirect_info = urlparse(self.redirect_uri)
redirect_host, redirect_port = get_host_port(redirect_info.netloc) redirect_host, redirect_port = get_host_port(redirect_info.netloc)
if redirect_host == 'localhost':
logger.warning(
"Using 'localhost' as a redirect URI is being deprecated. "
"Use a loopback IP address such as 127.0.0.1 "
"to ensure your app remains functional.")
if redirect_info.scheme == "http" and redirect_host not in ("127.0.0.1", "localhost"):
logger.warning(
"Redirect URIs using HTTP are being deprecated. "
"To ensure your app remains functional, use HTTPS instead.")
if open_browser is None: if open_browser is None:
open_browser = self.open_browser open_browser = self.open_browser
@ -468,11 +479,12 @@ class SpotifyOAuth(SpotifyAuthBase):
if redirect_port: if redirect_port:
return self._get_auth_response_local_server(redirect_port) return self._get_auth_response_local_server(redirect_port)
else: else:
logger.warning(f'Using `{redirect_host}` as redirect URI without a port. ' logger.warning('Using `%s` as redirect URI without a port. '
f'Specify a port (e.g. `{redirect_host}:8080`) to allow ' 'Specify a port (e.g. `%s:8080`) to allow '
'automatic retrieval of authentication code ' 'automatic retrieval of authentication code '
'instead of having to copy and paste ' 'instead of having to copy and paste '
'the URL your browser is redirected to.') 'the URL your browser is redirected to.',
redirect_host, redirect_host)
return self._get_auth_response_interactive(open_browser=open_browser) return self._get_auth_response_interactive(open_browser=open_browser)
@ -485,8 +497,8 @@ class SpotifyOAuth(SpotifyAuthBase):
""" Gets the access token for the app given the code """ Gets the access token for the app given the code
Parameters: Parameters:
- code: the response code - code - the response code
- as_dict: (deprecated) a boolean indicating if returning the access token - as_dict - a boolean indicating if returning the access token
as a token_info dictionary, otherwise it will be returned as a token_info dictionary, otherwise it will be returned
as a string. as a string.
""" """
@ -520,8 +532,10 @@ class SpotifyOAuth(SpotifyAuthBase):
headers = self._make_authorization_headers() headers = self._make_authorization_headers()
logger.debug(f"Sending POST request to {self.OAUTH_TOKEN_URL} with Headers: " logger.debug(
f"{headers} and Body: {payload}") "sending POST request to %s with Headers: %s and Body: %r",
self.OAUTH_TOKEN_URL, headers, payload
)
try: try:
response = self._session.post( response = self._session.post(
@ -548,8 +562,10 @@ class SpotifyOAuth(SpotifyAuthBase):
headers = self._make_authorization_headers() headers = self._make_authorization_headers()
logger.debug(f"Sending POST request to {self.OAUTH_TOKEN_URL} with Headers: " logger.debug(
f"{headers} and Body: {payload}") "sending POST request to %s with Headers: %s and Body: %r",
self.OAUTH_TOKEN_URL, headers, payload
)
try: try:
response = self._session.post( response = self._session.post(
@ -579,11 +595,6 @@ class SpotifyOAuth(SpotifyAuthBase):
return token_info return token_info
def get_cached_token(self): def get_cached_token(self):
""" Gets the cached token for the app
.. deprecated::
This method is deprecated and may be removed in a future version.
"""
warnings.warn("Calling get_cached_token directly on the SpotifyOAuth object will be " + warnings.warn("Calling get_cached_token directly on the SpotifyOAuth object will be " +
"deprecated. Instead, please specify a CacheFileHandler instance as " + "deprecated. Instead, please specify a CacheFileHandler instance as " +
"the cache_handler in SpotifyOAuth and use the CacheFileHandler's " + "the cache_handler in SpotifyOAuth and use the CacheFileHandler's " +
@ -744,9 +755,9 @@ class SpotifyPKCE(SpotifyAuthBase):
auth_url = self.get_authorize_url(state) auth_url = self.get_authorize_url(state)
try: try:
webbrowser.open(auth_url) webbrowser.open(auth_url)
logger.info(f"Opened {auth_url} in your browser") logger.info("Opened %s in your browser", auth_url)
except webbrowser.Error: except webbrowser.Error:
logger.error(f"Please navigate here: {auth_url}") logger.error("Please navigate here: %s", auth_url)
def _get_auth_response(self, open_browser=None): def _get_auth_response(self, open_browser=None):
logger.info('User authentication requires interaction with your ' logger.info('User authentication requires interaction with your '
@ -761,17 +772,6 @@ class SpotifyPKCE(SpotifyAuthBase):
if open_browser is None: if open_browser is None:
open_browser = self.open_browser open_browser = self.open_browser
if redirect_host == 'localhost':
logger.warning(
"Using 'localhost' as a redirect URI is being deprecated. "
"Use a loopback IP address such as 127.0.0.1 "
"to ensure your app remains functional.")
if redirect_info.scheme == "http" and redirect_host not in ("127.0.0.1", "localhost"):
logger.warning(
"Redirect URIs using HTTP are being deprecated. "
"To ensure your app remains functional, use HTTPS instead.")
if ( if (
open_browser open_browser
and redirect_host in ("127.0.0.1", "localhost") and redirect_host in ("127.0.0.1", "localhost")
@ -781,11 +781,12 @@ class SpotifyPKCE(SpotifyAuthBase):
if redirect_port: if redirect_port:
return self._get_auth_response_local_server(redirect_port) return self._get_auth_response_local_server(redirect_port)
else: else:
logger.warning(f'Using `{redirect_host}` as redirect URI without a port. ' logger.warning('Using `%s` as redirect URI without a port. '
f'Specify a port (e.g. `{redirect_host}:8080`) to allow ' 'Specify a port (e.g. `%s:8080`) to allow '
'automatic retrieval of authentication code ' 'automatic retrieval of authentication code '
'instead of having to copy and paste ' 'instead of having to copy and paste '
'the URL your browser is redirected to.') 'the URL your browser is redirected to.',
redirect_host, redirect_host)
return self._get_auth_response_interactive(open_browser=open_browser) return self._get_auth_response_interactive(open_browser=open_browser)
def _get_auth_response_local_server(self, redirect_port): def _get_auth_response_local_server(self, redirect_port):
@ -809,8 +810,10 @@ class SpotifyPKCE(SpotifyAuthBase):
prompt = "Enter the URL you were redirected to: " prompt = "Enter the URL you were redirected to: "
else: else:
url = self.get_authorize_url() url = self.get_authorize_url()
prompt = (f"Go to the following URL: {url}\n" prompt = (
f"Enter the URL you were redirected to: ") "Go to the following URL: {}\n"
"Enter the URL you were redirected to: ".format(url)
)
response = self._get_user_input(prompt) response = self._get_user_input(prompt)
state, code = self.parse_auth_response_url(response) state, code = self.parse_auth_response_url(response)
if self.state is not None and self.state != state: if self.state is not None and self.state != state:
@ -886,8 +889,10 @@ class SpotifyPKCE(SpotifyAuthBase):
headers = {"Content-Type": "application/x-www-form-urlencoded"} headers = {"Content-Type": "application/x-www-form-urlencoded"}
logger.debug(f"Sending POST request to {self.OAUTH_TOKEN_URL} with Headers: " logger.debug(
f"{headers} and Body: {payload}") "sending POST request to %s with Headers: %s and Body: %r",
self.OAUTH_TOKEN_URL, headers, payload
)
try: try:
response = self._session.post( response = self._session.post(
@ -915,8 +920,10 @@ class SpotifyPKCE(SpotifyAuthBase):
headers = {"Content-Type": "application/x-www-form-urlencoded"} headers = {"Content-Type": "application/x-www-form-urlencoded"}
logger.debug(f"Sending POST request to {self.OAUTH_TOKEN_URL} with Headers: " logger.debug(
f"{headers} and Body: {payload}") "sending POST request to %s with Headers: %s and Body: %r",
self.OAUTH_TOKEN_URL, headers, payload
)
try: try:
response = self._session.post( response = self._session.post(
@ -1038,7 +1045,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
(will set `cache_path` to `.cache-{username}`) (will set `cache_path` to `.cache-{username}`)
* show_dialog: Interpreted as boolean * show_dialog: Interpreted as boolean
""" """
logger.warning("Spotify is deprecating the Implicit " logger.warning("The OAuth standard no longer recommends the Implicit "
"Grant Flow for client-side code. Use the SpotifyPKCE " "Grant Flow for client-side code. Use the SpotifyPKCE "
"auth manager instead of SpotifyImplicitGrant. For " "auth manager instead of SpotifyImplicitGrant. For "
"more details and a guide to switching, see " "more details and a guide to switching, see "
@ -1167,9 +1174,9 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
auth_url = self.get_authorize_url(state) auth_url = self.get_authorize_url(state)
try: try:
webbrowser.open(auth_url) webbrowser.open(auth_url)
logger.info(f"Opened {auth_url} in your browser") logger.info("Opened %s in your browser", auth_url)
except webbrowser.Error: except webbrowser.Error:
logger.error(f"Please navigate here: {auth_url}") logger.error("Please navigate here: %s", auth_url)
def get_auth_response(self, state=None): def get_auth_response(self, state=None):
""" Gets a new auth **token** with user interaction """ """ Gets a new auth **token** with user interaction """
@ -1210,11 +1217,6 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
return token_info return token_info
def get_cached_token(self): def get_cached_token(self):
""" Gets the cached token for the app
.. deprecated::
This method is deprecated and may be removed in a future version.
"""
warnings.warn("Calling get_cached_token directly on the SpotifyImplicitGrant " + warnings.warn("Calling get_cached_token directly on the SpotifyImplicitGrant " +
"object will be deprecated. Instead, please specify a " + "object will be deprecated. Instead, please specify a " +
"CacheFileHandler instance as the cache_handler in SpotifyOAuth " + "CacheFileHandler instance as the cache_handler in SpotifyOAuth " +
@ -1253,26 +1255,24 @@ class RequestHandler(BaseHTTPRequestHandler):
if self.server.auth_code: if self.server.auth_code:
status = "successful" status = "successful"
elif self.server.error: elif self.server.error:
status = f"failed ({html.escape(str(self.server.error))})" status = f"failed ({self.server.error})"
else: else:
self._write("<html><body><h1>Invalid request</h1></body></html>") self._write("<html><body><h1>Invalid request</h1></body></html>")
return return
self._write(f"""<html> self._write("""<html>
<script> <script>
window.close() window.close()
</script> </script>
<body> <body>
<h1>Authentication status: {status}</h1> <h1>Authentication status: {}</h1>
This window can be closed. This window can be closed.
<script> <script>
window.close() window.close()
</script> </script>
<button class="closeButton" style="cursor: pointer" onclick="window.close();"> <button class="closeButton" style="cursor: pointer" onclick="window.close();">Close Window</button>
Close Window
</button>
</body> </body>
</html>""") </html>""".format(status))
def _write(self, text): def _write(self, text):
return self.wfile.write(text.encode("utf-8")) return self.wfile.write(text.encode("utf-8"))

View File

@ -1,20 +1,14 @@
from __future__ import annotations """ Shows a user's playlists (need to be authenticated via oauth) """
""" Shows a user's playlists. This needs to be authenticated via OAuth. """
__all__ = ["CLIENT_CREDS_ENV_VARS", "prompt_for_user_token"] __all__ = ["CLIENT_CREDS_ENV_VARS", "prompt_for_user_token"]
import logging import logging
import os import os
import warnings import warnings
from types import TracebackType
import requests
import urllib3
import spotipy import spotipy
logger = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
CLIENT_CREDS_ENV_VARS = { CLIENT_CREDS_ENV_VARS = {
"client_id": "SPOTIPY_CLIENT_ID", "client_id": "SPOTIPY_CLIENT_ID",
@ -23,9 +17,6 @@ CLIENT_CREDS_ENV_VARS = {
"redirect_uri": "SPOTIPY_REDIRECT_URI", "redirect_uri": "SPOTIPY_REDIRECT_URI",
} }
# workaround for garbage collection
REQUESTS_SESSION = requests.Session
def prompt_for_user_token( def prompt_for_user_token(
username=None, username=None,
@ -37,22 +28,6 @@ def prompt_for_user_token(
oauth_manager=None, oauth_manager=None,
show_dialog=False show_dialog=False
): ):
""" Prompt the user to login if necessary and returns a user token
suitable for use with the spotipy.Spotify constructor.
.. deprecated::
This method is deprecated and may be removed in a future version.
Parameters:
- username - the Spotify username. (optional)
- scope - the desired scope of the request. (optional)
- client_id - the client ID of your app. (required)
- client_secret - the client secret of your app. (required)
- redirect_uri - the redirect URI of your app. (required)
- cache_path - path to location to save tokens. (required)
- oauth_manager - OAuth manager object. (optional)
- show_dialog - If True, a login prompt always shows or defaults to False. (optional)
"""
warnings.warn( warnings.warn(
"'prompt_for_user_token' is deprecated." "'prompt_for_user_token' is deprecated."
"Use the following instead: " "Use the following instead: "
@ -60,7 +35,22 @@ def prompt_for_user_token(
" spotipy.Spotify(auth_manager=auth_manager)", " spotipy.Spotify(auth_manager=auth_manager)",
DeprecationWarning DeprecationWarning
) )
""" prompts the user to login if necessary and returns
the user token suitable for use with the spotipy.Spotify
constructor
Parameters:
- username - the Spotify username (optional)
- scope - the desired scope of the request (optional)
- client_id - the client id of your app (required)
- client_secret - the client secret of your app (required)
- redirect_uri - the redirect URI of your app (required)
- cache_path - path to location to save tokens (optional)
- oauth_manager - Oauth manager object (optional)
- show_dialog - If true, a login prompt always shows (optional, defaults to False)
"""
if not oauth_manager: if not oauth_manager:
if not client_id: if not client_id:
client_id = os.getenv("SPOTIPY_CLIENT_ID") client_id = os.getenv("SPOTIPY_CLIENT_ID")
@ -72,7 +62,7 @@ def prompt_for_user_token(
redirect_uri = os.getenv("SPOTIPY_REDIRECT_URI") redirect_uri = os.getenv("SPOTIPY_REDIRECT_URI")
if not client_id: if not client_id:
logger.warning( LOGGER.warning(
""" """
You need to set your Spotify API credentials. You need to set your Spotify API credentials.
You can do this by setting environment variables like so: You can do this by setting environment variables like so:
@ -117,12 +107,6 @@ def prompt_for_user_token(
def get_host_port(netloc): def get_host_port(netloc):
""" Split the network location string into host and port and returns a tuple
where the host is a string and the the port is an integer.
Parameters:
- netloc - a string representing the network location.
"""
if ":" in netloc: if ":" in netloc:
host, port = netloc.split(":", 1) host, port = netloc.split(":", 1)
port = int(port) port = int(port)
@ -134,14 +118,6 @@ def get_host_port(netloc):
def normalize_scope(scope): def normalize_scope(scope):
"""Normalize the scope to verify that it is a list or tuple. A string
input will split the string by commas to create a list of scopes.
A list or tuple input is used directly.
Parameters:
- scope - a string representing scopes separated by commas,
or a list/tuple of scopes.
"""
if scope: if scope:
if isinstance(scope, str): if isinstance(scope, str):
scopes = scope.split(',') scopes = scope.split(',')
@ -150,36 +126,8 @@ def normalize_scope(scope):
else: else:
raise Exception( raise Exception(
"Unsupported scope value, please either provide a list of scopes, " "Unsupported scope value, please either provide a list of scopes, "
"or a string of scopes separated by commas." "or a string of scopes separated by commas"
) )
return " ".join(sorted(scopes)) return " ".join(sorted(scopes))
else: else:
return None return None
class Retry(urllib3.Retry):
"""
Custom class for printing a warning when a rate/request limit is reached.
"""
def increment(
self,
method: str | None = None,
url: str | None = None,
response: urllib3.BaseHTTPResponse | None = None,
error: Exception | None = None,
_pool: urllib3.connectionpool.ConnectionPool | None = None,
_stacktrace: TracebackType | None = None,
) -> urllib3.Retry:
if response:
retry_header = response.headers.get("Retry-After")
if self.is_retry(method, response.status, bool(retry_header)):
retry_header = retry_header or 0
logger.warning("Your application has reached a rate/request limit. "
f"Retry will occur after: {retry_header} s")
return super().increment(method,
url,
response=response,
error=error,
_pool=_pool,
_stacktrace=_stacktrace)

View File

@ -1,5 +1,4 @@
import base64 import base64
import requests import requests

View File

@ -1,9 +1,11 @@
import unittest from spotipy import (
Spotify,
import requests SpotifyClientCredentials,
SpotifyException
)
import spotipy import spotipy
from spotipy import Spotify, SpotifyClientCredentials, SpotifyException import unittest
import requests
class AuthTestSpotipy(unittest.TestCase): class AuthTestSpotipy(unittest.TestCase):
@ -35,19 +37,12 @@ class AuthTestSpotipy(unittest.TestCase):
creep_urn = 'spotify:track:6b2oQwSGFkzsMtQruIWm2p' creep_urn = 'spotify:track:6b2oQwSGFkzsMtQruIWm2p'
creep_id = '6b2oQwSGFkzsMtQruIWm2p' creep_id = '6b2oQwSGFkzsMtQruIWm2p'
creep_url = 'http://open.spotify.com/track/6b2oQwSGFkzsMtQruIWm2p' creep_url = 'http://open.spotify.com/track/6b2oQwSGFkzsMtQruIWm2p'
el_scorcho_urn = 'spotify:track:0Svkvt5I79wficMFgaqEQJ' el_scorcho_urn = 'spotify:track:0Svkvt5I79wficMFgaqEQJ'
el_scorcho_bad_urn = 'spotify:track:0Svkvt5I79wficMFgaqEQK' el_scorcho_bad_urn = 'spotify:track:0Svkvt5I79wficMFgaqEQK'
pinkerton_urn = 'spotify:album:04xe676vyiTeYNXw15o9jT' pinkerton_urn = 'spotify:album:04xe676vyiTeYNXw15o9jT'
weezer_urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu' weezer_urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu'
pablo_honey_urn = 'spotify:album:6AZv3m27uyRxi8KyJSfUxL' pablo_honey_urn = 'spotify:album:6AZv3m27uyRxi8KyJSfUxL'
radiohead_urn = 'spotify:artist:4Z8W4fKeB5YxbusRsdQVPb' radiohead_urn = 'spotify:artist:4Z8W4fKeB5YxbusRsdQVPb'
radiohead_id = "4Z8W4fKeB5YxbusRsdQVPb"
radiohead_url = "https://open.spotify.com/artist/4Z8W4fKeB5YxbusRsdQVPb"
qotsa_url = "https://open.spotify.com/artist/4pejUc4iciQfgdX6OKulQn"
angeles_haydn_urn = 'spotify:album:1vAbqAeuJVWNAe7UR00bdM' angeles_haydn_urn = 'spotify:album:1vAbqAeuJVWNAe7UR00bdM'
heavyweight_urn = 'spotify:show:5c26B28vZMN8PG0Nppmn5G' heavyweight_urn = 'spotify:show:5c26B28vZMN8PG0Nppmn5G'
heavyweight_id = '5c26B28vZMN8PG0Nppmn5G' heavyweight_id = '5c26B28vZMN8PG0Nppmn5G'
@ -58,12 +53,15 @@ class AuthTestSpotipy(unittest.TestCase):
heavyweight_ep1_url = 'https://open.spotify.com/episode/68kq3bNz6hEuq8NtdfwERG' heavyweight_ep1_url = 'https://open.spotify.com/episode/68kq3bNz6hEuq8NtdfwERG'
reply_all_ep1_urn = 'spotify:episode:1KHjbpnmNpFmNTczQmTZlR' reply_all_ep1_urn = 'spotify:episode:1KHjbpnmNpFmNTczQmTZlR'
dune_urn = 'spotify:audiobook:7iHfbu1YPACw6oZPAFJtqe' american_gods_urn = 'spotify:audiobook:1IcM9Untg6d3ktuwObYGcN'
dune_id = '7iHfbu1YPACw6oZPAFJtqe' american_gods_id = '1IcM9Untg6d3ktuwObYGcN'
dune_url = 'https://open.spotify.com/audiobook/7iHfbu1YPACw6oZPAFJtqe' american_gods_url = 'https://open.spotify.com/audiobook/1IcM9Untg6d3ktuwObYGcN'
two_books = [
'spotify:audiobook:7iHfbu1YPACw6oZPAFJtqe', four_books = [
'spotify:audiobook:67VtmjZitn25TWocsyAEyh'] 'spotify:audiobook:1IcM9Untg6d3ktuwObYGcN',
'spotify:audiobook:37sRC6carIX2Vf3Vv716T7',
'spotify:audiobook:1Gep4UJ95xQawA55OgRI8n',
'spotify:audiobook:4Sm381mcf5gBsi9yfhqgVB']
@classmethod @classmethod
def setUpClass(self): def setUpClass(self):
@ -71,28 +69,43 @@ class AuthTestSpotipy(unittest.TestCase):
client_credentials_manager=SpotifyClientCredentials()) client_credentials_manager=SpotifyClientCredentials())
self.spotify.trace = False self.spotify.trace = False
def test_audio_analysis(self):
result = self.spotify.audio_analysis(self.four_tracks[0])
assert ('beats' in result)
def test_audio_features(self):
results = self.spotify.audio_features(self.four_tracks)
self.assertTrue(len(results) == len(self.four_tracks))
for track in results:
assert ('speechiness' in track)
def test_audio_features_with_bad_track(self):
bad_tracks = ['spotify:track:bad']
input = self.four_tracks + bad_tracks
results = self.spotify.audio_features(input)
self.assertTrue(len(results) == len(input))
for track in results[:-1]:
if track is not None:
assert ('speechiness' in track)
self.assertTrue(results[-1] is None)
def test_recommendations(self):
results = self.spotify.recommendations(
seed_tracks=self.four_tracks,
min_danceability=0,
max_loudness=0,
target_popularity=50)
self.assertTrue(len(results['tracks']) == 20)
def test_artist_urn(self): def test_artist_urn(self):
artist = self.spotify.artist(self.radiohead_urn) artist = self.spotify.artist(self.radiohead_urn)
self.assertTrue(artist['name'] == 'Radiohead') self.assertTrue(artist['name'] == 'Radiohead')
def test_artist_url(self):
artist = self.spotify.artist(self.radiohead_url)
self.assertTrue(artist['name'] == 'Radiohead')
def test_artist_id(self):
artist = self.spotify.artist(self.radiohead_id)
self.assertTrue(artist['name'] == 'Radiohead')
def test_artists(self): def test_artists(self):
results = self.spotify.artists([self.weezer_urn, self.radiohead_urn]) results = self.spotify.artists([self.weezer_urn, self.radiohead_urn])
self.assertTrue('artists' in results) self.assertTrue('artists' in results)
self.assertTrue(len(results['artists']) == 2) self.assertTrue(len(results['artists']) == 2)
def test_artists_mixed_ids(self):
results = self.spotify.artists([self.weezer_urn, self.radiohead_id, self.qotsa_url])
self.assertTrue('artists' in results)
self.assertTrue(len(results['artists']) == 3)
def test_album_urn(self): def test_album_urn(self):
album = self.spotify.album(self.pinkerton_urn) album = self.spotify.album(self.pinkerton_urn)
self.assertTrue(album['name'] == 'Pinkerton') self.assertTrue(album['name'] == 'Pinkerton')
@ -149,6 +162,17 @@ class AuthTestSpotipy(unittest.TestCase):
self.assertTrue('tracks' in results) self.assertTrue('tracks' in results)
self.assertTrue(len(results['tracks']) == 10) self.assertTrue(len(results['tracks']) == 10)
def test_artist_related_artists(self):
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
self.assertTrue(found)
def test_artist_search(self): def test_artist_search(self):
results = self.spotify.search(q='weezer', type='artist') results = self.spotify.search(q='weezer', type='artist')
self.assertTrue('artists' in results) self.assertTrue('artists' in results)
@ -293,7 +317,7 @@ class AuthTestSpotipy(unittest.TestCase):
def find_album(): def find_album():
for album in results['items']: for album in results['items']:
if 'Weezer' in album['name']: # Weezer has many albums containing Weezer if album['name'] == 'Death to False Metal':
return True return True
return False return False
@ -441,25 +465,28 @@ class AuthTestSpotipy(unittest.TestCase):
self.assertIn("GB", markets) self.assertIn("GB", markets)
def test_get_audiobook(self): def test_get_audiobook(self):
audiobook = self.spotify.get_audiobook(self.dune_urn, market="US") audiobook = self.spotify.get_audiobook(self.american_gods_urn, market="US")
print(audiobook)
self.assertTrue(audiobook['name'] == self.assertTrue(audiobook['name'] ==
'Dune: Book One in the Dune Chronicles') 'American Gods: The Tenth Anniversary Edition: A Novel')
def test_get_audiobook_bad_urn(self): def test_get_audiobook_bad_urn(self):
with self.assertRaises(SpotifyException): with self.assertRaises(SpotifyException):
self.spotify.get_audiobook("bogus_urn", market="US") self.spotify.get_audiobook("bogus_urn", market="US")
def test_get_audiobooks(self): def test_get_audiobooks(self):
results = self.spotify.get_audiobooks(self.two_books, market="US") results = self.spotify.get_audiobooks(self.four_books, market="US")
self.assertTrue('audiobooks' in results) self.assertTrue('audiobooks' in results)
self.assertTrue(len(results['audiobooks']) == 2) self.assertTrue(len(results['audiobooks']) == 4)
self.assertTrue(results['audiobooks'][0]['name'] self.assertTrue(results['audiobooks'][0]['name'] ==
== 'Dune: Book One in the Dune Chronicles') 'American Gods: The Tenth Anniversary Edition: A Novel')
self.assertTrue(results['audiobooks'][1]['name'] == 'The Helper') self.assertTrue(results['audiobooks'][1]['name'] == 'The Da Vinci Code: A Novel')
self.assertTrue(results['audiobooks'][2]['name'] == 'Outlander')
self.assertTrue(results['audiobooks'][3]['name'] == 'Pachinko: A Novel')
def test_get_audiobook_chapters(self): def test_get_audiobook_chapters(self):
results = self.spotify.get_audiobook_chapters( results = self.spotify.get_audiobook_chapters(
self.dune_urn, market="US", limit=10, offset=5) self.american_gods_urn, market="US", limit=10, offset=5)
self.assertTrue('items' in results) self.assertTrue('items' in results)
self.assertTrue(len(results['items']) == 10) self.assertTrue(len(results['items']) == 10)
self.assertTrue(results['items'][0]['chapter_number'] == 5) self.assertTrue(results['items'][0]['chapter_number'] == 5)

View File

@ -1,9 +1,14 @@
import os import os
import unittest
from spotipy import CLIENT_CREDS_ENV_VARS as CCEV from spotipy import (
from spotipy import (Spotify, SpotifyException, SpotifyImplicitGrant, CLIENT_CREDS_ENV_VARS as CCEV,
SpotifyPKCE, prompt_for_user_token) prompt_for_user_token,
Spotify,
SpotifyException,
SpotifyImplicitGrant,
SpotifyPKCE
)
import unittest
from tests import helpers from tests import helpers
@ -388,6 +393,33 @@ class SpotipyBrowseApiTests(unittest.TestCase):
response = self.spotify.categories(limit=50) response = self.spotify.categories(limit=50)
self.assertLessEqual(len(response['categories']['items']), 50) self.assertLessEqual(len(response['categories']['items']), 50)
def test_category_playlists(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)
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']['items']), 0) self.assertGreater(len(response['albums']['items']), 0)
@ -400,6 +432,10 @@ class SpotipyBrowseApiTests(unittest.TestCase):
response = self.spotify.new_releases(limit=50) response = self.spotify.new_releases(limit=50)
self.assertLessEqual(len(response['albums']['items']), 50) self.assertLessEqual(len(response['albums']['items']), 50)
def test_featured_releases(self):
response = self.spotify.featured_playlists()
self.assertGreater(len(response['playlists']), 0)
class SpotipyFollowApiTests(unittest.TestCase): class SpotipyFollowApiTests(unittest.TestCase):
@classmethod @classmethod
@ -522,44 +558,3 @@ class SpotifyPKCETests(unittest.TestCase):
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'])
self.assertEqual(c_user['display_name'], user['display_name']) self.assertEqual(c_user['display_name'], user['display_name'])
class SpotifyQueueApiTests(unittest.TestCase):
@classmethod
def setUp(self):
self.spotify = Spotify(auth="test_token")
def test_get_queue(self, mock_get):
# Mock the response from _get
mock_get.return_value = {'songs': ['song1', 'song2']}
# Call the queue function
response = self.spotify.queue()
# Check if the correct endpoint is called
mock_get.assert_called_with("me/player/queue")
# Check if the response is as expected
self.assertEqual(response, {'songs': ['song1', 'song2']})
def test_add_to_queue(self, mock_post):
test_uri = 'spotify:track:123'
# Call the add_to_queue function
self.spotify.add_to_queue(test_uri)
# Check if the correct endpoint is called
endpoint = f"me/player/queue?uri={test_uri}"
mock_post.assert_called_with(endpoint)
def test_add_to_queue_with_device_id(self, mock_post):
test_uri = 'spotify:track:123'
device_id = 'device123'
# Call the add_to_queue function with a device_id
self.spotify.add_to_queue(test_uri, device_id=device_id)
# Check if the correct endpoint is called
endpoint = f"me/player/queue?uri={test_uri}&device_id={device_id}"
mock_post.assert_called_with(endpoint)

View File

@ -1,13 +1,14 @@
import io import io
import json import json
import unittest import unittest
import unittest.mock as mock import unittest.mock as mock
import urllib.parse as urllibparse import urllib.parse as urllibparse
from spotipy import SpotifyImplicitGrant, SpotifyOAuth, SpotifyPKCE from spotipy import SpotifyOAuth, SpotifyImplicitGrant, SpotifyPKCE
from spotipy.cache_handler import MemoryCacheHandler from spotipy.cache_handler import MemoryCacheHandler
from spotipy.oauth2 import (SpotifyClientCredentials, SpotifyOauthError, from spotipy.oauth2 import SpotifyClientCredentials, SpotifyOauthError
SpotifyStateError) from spotipy.oauth2 import SpotifyStateError
patch = mock.patch patch = mock.patch
DEFAULT = mock.DEFAULT DEFAULT = mock.DEFAULT
@ -52,21 +53,18 @@ class OAuthCacheTest(unittest.TestCase):
@patch('spotipy.cache_handler.open', create=True) @patch('spotipy.cache_handler.open', create=True)
def test_gets_from_cache_path(self, opener, def test_gets_from_cache_path(self, opener,
is_token_expired, refresh_access_token): is_token_expired, refresh_access_token):
"""Test that the token is retrieved from the cache path."""
scope = "playlist-modify-private" scope = "playlist-modify-private"
path = ".cache-username" path = ".cache-username"
tok = _make_fake_token(1, 1, scope) tok = _make_fake_token(1, 1, scope)
token_file = _token_file(json.dumps(tok, ensure_ascii=False))
opener.return_value = token_file opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
opener.return_value.__enter__ = mock.Mock(return_value=token_file)
opener.return_value.__exit__ = mock.Mock(return_value=False)
is_token_expired.return_value = False is_token_expired.return_value = False
spot = _make_oauth(scope, path) spot = _make_oauth(scope, path)
cached_tok = spot.validate_token(spot.cache_handler.get_cached_token()) cached_tok = spot.validate_token(spot.cache_handler.get_cached_token())
cached_tok_legacy = spot.get_cached_token() cached_tok_legacy = spot.get_cached_token()
opener.assert_called_with(path, encoding='utf-8') opener.assert_called_with(path)
self.assertIsNotNone(cached_tok) self.assertIsNotNone(cached_tok)
self.assertIsNotNone(cached_tok_legacy) self.assertIsNotNone(cached_tok_legacy)
self.assertEqual(refresh_access_token.call_count, 0) self.assertEqual(refresh_access_token.call_count, 0)
@ -76,15 +74,13 @@ class OAuthCacheTest(unittest.TestCase):
@patch('spotipy.cache_handler.open', create=True) @patch('spotipy.cache_handler.open', create=True)
def test_expired_token_refreshes(self, opener, def test_expired_token_refreshes(self, opener,
is_token_expired, refresh_access_token): is_token_expired, refresh_access_token):
"""Test that an expired token is refreshed."""
scope = "playlist-modify-private" scope = "playlist-modify-private"
path = ".cache-username" path = ".cache-username"
expired_tok = _make_fake_token(0, None, scope) expired_tok = _make_fake_token(0, None, scope)
fresh_tok = _make_fake_token(1, 1, scope) fresh_tok = _make_fake_token(1, 1, scope)
token_file = _token_file(json.dumps(expired_tok, ensure_ascii=False)) token_file = _token_file(json.dumps(expired_tok, ensure_ascii=False))
opener.return_value.__enter__ = mock.Mock(return_value=token_file) opener.return_value = token_file
opener.return_value.__exit__ = mock.Mock(return_value=False)
refresh_access_token.return_value = fresh_tok refresh_access_token.return_value = fresh_tok
spot = _make_oauth(scope, path) spot = _make_oauth(scope, path)
@ -92,7 +88,7 @@ class OAuthCacheTest(unittest.TestCase):
is_token_expired.assert_called_with(expired_tok) is_token_expired.assert_called_with(expired_tok)
refresh_access_token.assert_called_with(expired_tok['refresh_token']) refresh_access_token.assert_called_with(expired_tok['refresh_token'])
opener.assert_any_call(path, encoding='utf-8') opener.assert_any_call(path)
@patch.multiple(SpotifyOAuth, @patch.multiple(SpotifyOAuth,
is_token_expired=DEFAULT, refresh_access_token=DEFAULT) is_token_expired=DEFAULT, refresh_access_token=DEFAULT)
@ -104,35 +100,29 @@ class OAuthCacheTest(unittest.TestCase):
path = ".cache-username" path = ".cache-username"
tok = _make_fake_token(1, 1, token_scope) tok = _make_fake_token(1, 1, token_scope)
token_file = _token_file(json.dumps(tok, ensure_ascii=False)) opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
opener.return_value = token_file
opener.return_value.__enter__ = mock.Mock(return_value=token_file)
opener.return_value.__exit__ = mock.Mock(return_value=False)
is_token_expired.return_value = False is_token_expired.return_value = False
spot = _make_oauth(requested_scope, path) spot = _make_oauth(requested_scope, path)
cached_tok = spot.validate_token(spot.cache_handler.get_cached_token()) cached_tok = spot.validate_token(spot.cache_handler.get_cached_token())
opener.assert_called_with(path, encoding='utf-8') opener.assert_called_with(path)
self.assertIsNone(cached_tok) self.assertIsNone(cached_tok)
self.assertEqual(refresh_access_token.call_count, 0) self.assertEqual(refresh_access_token.call_count, 0)
@patch('spotipy.cache_handler.open', create=True) @patch('spotipy.cache_handler.open', create=True)
def test_saves_to_cache_path(self, opener): def test_saves_to_cache_path(self, opener):
"""Test that the token is saved to the cache path."""
scope = "playlist-modify-private" scope = "playlist-modify-private"
path = ".cache-username" path = ".cache-username"
tok = _make_fake_token(1, 1, scope) tok = _make_fake_token(1, 1, scope)
fi = _fake_file() fi = _fake_file()
opener.return_value = fi opener.return_value = fi
opener.return_value.__enter__ = mock.Mock(return_value=fi)
opener.return_value.__exit__ = mock.Mock(return_value=False)
spot = SpotifyOAuth("CLID", "CLISEC", "REDIR", "STATE", scope, path) spot = SpotifyOAuth("CLID", "CLISEC", "REDIR", "STATE", scope, path)
spot.cache_handler.save_token_to_cache(tok) spot.cache_handler.save_token_to_cache(tok)
opener.assert_called_with(path, 'w', encoding='utf-8') opener.assert_called_with(path, 'w')
self.assertTrue(fi.write.called) self.assertTrue(fi.write.called)
@patch('spotipy.cache_handler.open', create=True) @patch('spotipy.cache_handler.open', create=True)
@ -143,13 +133,11 @@ class OAuthCacheTest(unittest.TestCase):
fi = _fake_file() fi = _fake_file()
opener.return_value = fi opener.return_value = fi
opener.return_value.__enter__ = mock.Mock(return_value=fi)
opener.return_value.__exit__ = mock.Mock(return_value=False)
spot = SpotifyOAuth("CLID", "CLISEC", "REDIR", "STATE", scope, path) spot = SpotifyOAuth("CLID", "CLISEC", "REDIR", "STATE", scope, path)
spot._save_token_info(tok) spot._save_token_info(tok)
opener.assert_called_with(path, 'w', encoding='utf-8') opener.assert_called_with(path, 'w')
self.assertTrue(fi.write.called) self.assertTrue(fi.write.called)
def test_cache_handler(self): def test_cache_handler(self):
@ -265,38 +253,32 @@ class ImplicitGrantCacheTest(unittest.TestCase):
path = ".cache-username" path = ".cache-username"
tok = _make_fake_token(1, 1, scope) tok = _make_fake_token(1, 1, scope)
token_file = _token_file(json.dumps(tok, ensure_ascii=False)) opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
opener.return_value = token_file
opener.return_value.__enter__ = mock.Mock(return_value=token_file)
opener.return_value.__exit__ = mock.Mock(return_value=False)
is_token_expired.return_value = False is_token_expired.return_value = False
spot = _make_implicitgrantauth(scope, path) spot = _make_implicitgrantauth(scope, path)
cached_tok = spot.cache_handler.get_cached_token() cached_tok = spot.cache_handler.get_cached_token()
cached_tok_legacy = spot.get_cached_token() cached_tok_legacy = spot.get_cached_token()
opener.assert_called_with(path, encoding='utf-8') opener.assert_called_with(path)
self.assertIsNotNone(cached_tok) self.assertIsNotNone(cached_tok)
self.assertIsNotNone(cached_tok_legacy) self.assertIsNotNone(cached_tok_legacy)
@patch.object(SpotifyImplicitGrant, "is_token_expired", DEFAULT) @patch.object(SpotifyImplicitGrant, "is_token_expired", DEFAULT)
@patch('spotipy.cache_handler.open', create=True) @patch('spotipy.cache_handler.open', create=True)
def test_expired_token_returns_none(self, opener, is_token_expired): def test_expired_token_returns_none(self, opener, is_token_expired):
"""Test that an expired token returns None."""
scope = "playlist-modify-private" scope = "playlist-modify-private"
path = ".cache-username" path = ".cache-username"
expired_tok = _make_fake_token(0, None, scope) expired_tok = _make_fake_token(0, None, scope)
token_file = _token_file(json.dumps(expired_tok, ensure_ascii=False)) token_file = _token_file(json.dumps(expired_tok, ensure_ascii=False))
opener.return_value = token_file opener.return_value = token_file
opener.return_value.__enter__ = mock.Mock(return_value=token_file)
opener.return_value.__exit__ = mock.Mock(return_value=False)
spot = _make_implicitgrantauth(scope, path) spot = _make_implicitgrantauth(scope, path)
cached_tok = spot.validate_token(spot.cache_handler.get_cached_token()) cached_tok = spot.validate_token(spot.cache_handler.get_cached_token())
is_token_expired.assert_called_with(expired_tok) is_token_expired.assert_called_with(expired_tok)
opener.assert_any_call(path, encoding='utf-8') opener.assert_any_call(path)
self.assertIsNone(cached_tok) self.assertIsNone(cached_tok)
@patch.object(SpotifyImplicitGrant, "is_token_expired", DEFAULT) @patch.object(SpotifyImplicitGrant, "is_token_expired", DEFAULT)
@ -307,16 +289,13 @@ class ImplicitGrantCacheTest(unittest.TestCase):
path = ".cache-username" path = ".cache-username"
tok = _make_fake_token(1, 1, token_scope) tok = _make_fake_token(1, 1, token_scope)
token_file = _token_file(json.dumps(tok, ensure_ascii=False)) opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
opener.return_value = token_file
opener.return_value.__enter__ = mock.Mock(return_value=token_file)
opener.return_value.__exit__ = mock.Mock(return_value=False)
is_token_expired.return_value = False is_token_expired.return_value = False
spot = _make_implicitgrantauth(requested_scope, path) spot = _make_implicitgrantauth(requested_scope, path)
cached_tok = spot.validate_token(spot.cache_handler.get_cached_token()) cached_tok = spot.validate_token(spot.cache_handler.get_cached_token())
opener.assert_called_with(path, encoding='utf-8') opener.assert_called_with(path)
self.assertIsNone(cached_tok) self.assertIsNone(cached_tok)
@patch('spotipy.cache_handler.open', create=True) @patch('spotipy.cache_handler.open', create=True)
@ -328,12 +307,10 @@ class ImplicitGrantCacheTest(unittest.TestCase):
fi = _fake_file() fi = _fake_file()
opener.return_value = fi opener.return_value = fi
opener.return_value.__enter__ = mock.Mock(return_value=fi)
opener.return_value.__exit__ = mock.Mock(return_value=False)
spot = SpotifyImplicitGrant("CLID", "REDIR", "STATE", scope, path) spot = SpotifyImplicitGrant("CLID", "REDIR", "STATE", scope, path)
spot.cache_handler.save_token_to_cache(tok) spot.cache_handler.save_token_to_cache(tok)
opener.assert_called_with(path, 'w', encoding='utf-8') opener.assert_called_with(path, 'w')
self.assertTrue(fi.write.called) self.assertTrue(fi.write.called)
@patch('spotipy.cache_handler.open', create=True) @patch('spotipy.cache_handler.open', create=True)
@ -344,13 +321,11 @@ class ImplicitGrantCacheTest(unittest.TestCase):
fi = _fake_file() fi = _fake_file()
opener.return_value = fi opener.return_value = fi
opener.return_value.__enter__ = mock.Mock(return_value=fi)
opener.return_value.__exit__ = mock.Mock(return_value=False)
spot = SpotifyImplicitGrant("CLID", "REDIR", "STATE", scope, path) spot = SpotifyImplicitGrant("CLID", "REDIR", "STATE", scope, path)
spot._save_token_info(tok) spot._save_token_info(tok)
opener.assert_called_with(path, 'w', encoding='utf-8') opener.assert_called_with(path, 'w')
self.assertTrue(fi.write.called) self.assertTrue(fi.write.called)
@ -415,17 +390,14 @@ class SpotifyPKCECacheTest(unittest.TestCase):
path = ".cache-username" path = ".cache-username"
tok = _make_fake_token(1, 1, scope) tok = _make_fake_token(1, 1, scope)
token_file = _token_file(json.dumps(tok, ensure_ascii=False)) opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
opener.return_value = token_file
opener.return_value.__enter__ = mock.Mock(return_value=token_file)
opener.return_value.__exit__ = mock.Mock(return_value=False)
is_token_expired.return_value = False is_token_expired.return_value = False
spot = _make_pkceauth(scope, path) spot = _make_pkceauth(scope, path)
cached_tok = spot.cache_handler.get_cached_token() cached_tok = spot.cache_handler.get_cached_token()
cached_tok_legacy = spot.get_cached_token() cached_tok_legacy = spot.get_cached_token()
opener.assert_called_with(path, encoding='utf-8') opener.assert_called_with(path)
self.assertIsNotNone(cached_tok) self.assertIsNotNone(cached_tok)
self.assertIsNotNone(cached_tok_legacy) self.assertIsNotNone(cached_tok_legacy)
self.assertEqual(refresh_access_token.call_count, 0) self.assertEqual(refresh_access_token.call_count, 0)
@ -441,8 +413,7 @@ class SpotifyPKCECacheTest(unittest.TestCase):
fresh_tok = _make_fake_token(1, 1, scope) fresh_tok = _make_fake_token(1, 1, scope)
token_file = _token_file(json.dumps(expired_tok, ensure_ascii=False)) token_file = _token_file(json.dumps(expired_tok, ensure_ascii=False))
opener.return_value.__enter__ = mock.Mock(return_value=token_file) opener.return_value = token_file
opener.return_value.__exit__ = mock.Mock(return_value=False)
refresh_access_token.return_value = fresh_tok refresh_access_token.return_value = fresh_tok
spot = _make_pkceauth(scope, path) spot = _make_pkceauth(scope, path)
@ -450,7 +421,7 @@ class SpotifyPKCECacheTest(unittest.TestCase):
is_token_expired.assert_called_with(expired_tok) is_token_expired.assert_called_with(expired_tok)
refresh_access_token.assert_called_with(expired_tok['refresh_token']) refresh_access_token.assert_called_with(expired_tok['refresh_token'])
opener.assert_any_call(path, encoding='utf-8') opener.assert_any_call(path)
@patch.multiple(SpotifyPKCE, @patch.multiple(SpotifyPKCE,
is_token_expired=DEFAULT, refresh_access_token=DEFAULT) is_token_expired=DEFAULT, refresh_access_token=DEFAULT)
@ -462,16 +433,13 @@ class SpotifyPKCECacheTest(unittest.TestCase):
path = ".cache-username" path = ".cache-username"
tok = _make_fake_token(1, 1, token_scope) tok = _make_fake_token(1, 1, token_scope)
token_file = _token_file(json.dumps(tok, ensure_ascii=False)) opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
opener.return_value = token_file
opener.return_value.__enter__ = mock.Mock(return_value=token_file)
opener.return_value.__exit__ = mock.Mock(return_value=False)
is_token_expired.return_value = False is_token_expired.return_value = False
spot = _make_pkceauth(requested_scope, path) spot = _make_pkceauth(requested_scope, path)
cached_tok = spot.validate_token(spot.cache_handler.get_cached_token()) cached_tok = spot.validate_token(spot.cache_handler.get_cached_token())
opener.assert_called_with(path, encoding='utf-8') opener.assert_called_with(path)
self.assertIsNone(cached_tok) self.assertIsNone(cached_tok)
self.assertEqual(refresh_access_token.call_count, 0) self.assertEqual(refresh_access_token.call_count, 0)
@ -483,12 +451,11 @@ class SpotifyPKCECacheTest(unittest.TestCase):
fi = _fake_file() fi = _fake_file()
opener.return_value = fi opener.return_value = fi
opener.return_value.__enter__ = mock.Mock(return_value=fi)
opener.return_value.__exit__ = mock.Mock(return_value=False)
spot = SpotifyPKCE("CLID", "REDIR", "STATE", scope, path) spot = SpotifyPKCE("CLID", "REDIR", "STATE", scope, path)
spot.cache_handler.save_token_to_cache(tok) spot.cache_handler.save_token_to_cache(tok)
opener.assert_called_with(path, 'w', encoding='utf-8') opener.assert_called_with(path, 'w')
self.assertTrue(fi.write.called) self.assertTrue(fi.write.called)
@patch('spotipy.cache_handler.open', create=True) @patch('spotipy.cache_handler.open', create=True)
@ -499,13 +466,11 @@ class SpotifyPKCECacheTest(unittest.TestCase):
fi = _fake_file() fi = _fake_file()
opener.return_value = fi opener.return_value = fi
opener.return_value.__enter__ = mock.Mock(return_value=fi)
opener.return_value.__exit__ = mock.Mock(return_value=False)
spot = SpotifyPKCE("CLID", "REDIR", "STATE", scope, path) spot = SpotifyPKCE("CLID", "REDIR", "STATE", scope, path)
spot._save_token_info(tok) spot._save_token_info(tok)
opener.assert_called_with(path, 'w', encoding='utf-8') opener.assert_called_with(path, 'w')
self.assertTrue(fi.write.called) self.assertTrue(fi.write.called)
@ -522,8 +487,8 @@ class TestSpotifyPKCE(unittest.TestCase):
self.assertTrue(auth.code_challenge) self.assertTrue(auth.code_challenge)
def test_code_verifier_and_code_challenge_are_correct(self): def test_code_verifier_and_code_challenge_are_correct(self):
import base64
import hashlib import hashlib
import base64
auth = SpotifyPKCE("CLID", "REDIR") auth = SpotifyPKCE("CLID", "REDIR")
auth.get_pkce_handshake_parameters() auth.get_pkce_handshake_parameters()
self.assertEqual(auth.code_challenge, self.assertEqual(auth.code_challenge,

View File

@ -3,14 +3,11 @@ envlist = py3{8,9,10,11,12}
[testenv] [testenv]
deps= deps=
requests requests
commands=python -m unittest discover -v tests/unit commands=python -m unittest discover -v tests
[flake8] [flake8]
max-line-length = 99 max-line-length = 99
exclude= exclude=
.git, .git,
.venv,
build,
dist, dist,
docs, docs,
examples, examples
spotipy.egg-info