mirror of
https://github.com/spotipy-dev/spotipy.git
synced 2026-06-19 01:03:53 +00:00
Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
351d4223d0 | ||
|
|
6787aabe0f | ||
|
|
fa7049ea1d | ||
|
|
c52a29f6d2 | ||
|
|
179d3e486c | ||
|
|
9119b6a070 | ||
|
|
b5be7fba6a | ||
|
|
48dab6eb6b | ||
|
|
880b92d724 | ||
|
|
a91d9feb51 | ||
|
|
5a8b55f5e8 | ||
|
|
9dfb7177b8 | ||
|
|
6bc91ecf98 | ||
|
|
1a8d9da033 | ||
|
|
5b018cf6af | ||
|
|
743059989d | ||
|
|
4f5759dbfb | ||
|
|
3ec8a2312c | ||
|
|
4f01f7187d | ||
|
|
103d6873fa | ||
|
|
1ca453f6ef | ||
|
|
668158f055 | ||
|
|
a6f6841e62 | ||
|
|
d9ec669d5b | ||
|
|
c738376b80 | ||
|
|
1d920ff5df | ||
|
|
81788935cb | ||
|
|
1dbbbf65ec | ||
|
|
d319c6e09f | ||
|
|
e37b1a8a55 | ||
|
|
f5ebcc6fc6 | ||
|
|
84f8b0a64b | ||
|
|
2243e135df | ||
|
|
2bc8d35fc6 | ||
|
|
ca207e4f2d | ||
|
|
ba01a6aee5 | ||
|
|
645ed6da56 | ||
|
|
14ab13d292 | ||
|
|
db3fb9a5ee | ||
|
|
d9da5af53c | ||
|
|
c6209436d1 | ||
|
|
185140a812 | ||
|
|
51c5bd8d7d | ||
|
|
1e05bdba67 | ||
|
|
66ad716595 | ||
|
|
5e09c78ccf | ||
|
|
ef282e2423 | ||
|
|
d7640404a5 | ||
|
|
c90ce4a875 | ||
|
|
c7856d0120 | ||
|
|
d92951b356 | ||
|
|
8f003147f7 | ||
|
|
c5a0943016 | ||
|
|
f94a89a995 | ||
|
|
cb36133ca9 | ||
|
|
bb36290b8e | ||
|
|
9a8b8a7f02 | ||
|
|
eee7ae7d0d | ||
|
|
8a40e038b9 | ||
|
|
c0343b8b80 | ||
|
|
5fa2343162 | ||
|
|
22e3b7406d | ||
|
|
a810edf5da | ||
|
|
126da62dd0 | ||
|
|
df27fe93bf | ||
|
|
62a27a20e0 | ||
|
|
913ae57275 | ||
|
|
1ce8c4f06b | ||
|
|
2d1cb99be7 | ||
|
|
cc24b4c22f | ||
|
|
677841b417 | ||
|
|
e153dabe7c | ||
|
|
939b7557a5 | ||
|
|
b109ca722c | ||
|
|
85c9d74dc1 | ||
|
|
958ff6ad2b | ||
|
|
74330dae6d | ||
|
|
070b54f494 | ||
|
|
3b5708f5a0 | ||
|
|
52f2b923ba | ||
|
|
f4c2b90a29 | ||
|
|
0a8e7f635e | ||
|
|
2e54f2c138 | ||
|
|
028539e08a | ||
|
|
6d8d9d1f9b | ||
|
|
17893a6115 | ||
|
|
160a57a1d6 | ||
|
|
23bf3c9d88 | ||
|
|
97c9917a02 | ||
|
|
d9a5f008ff | ||
|
|
e3629cdacb | ||
|
|
a14a28e10c | ||
|
|
1416d47cba | ||
|
|
f647ca75c2 | ||
|
|
2063df0fa5 | ||
|
|
d31969108d | ||
|
|
3b6d14404e | ||
|
|
6cc817af85 | ||
|
|
45f78ce7a6 | ||
|
|
612b30efa3 | ||
|
|
796c03338f | ||
|
|
3bac7250ec | ||
|
|
b7baaabf74 | ||
|
|
fe438c0432 | ||
|
|
f2d23e2219 | ||
|
|
ca332a538e | ||
|
|
572195617b | ||
|
|
b3f308d289 |
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "pip" # See documentation for possible values
|
||||||
|
directory: "/" # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
19
.github/workflows/integration_tests.yml
vendored
19
.github/workflows/integration_tests.yml
vendored
@ -1,25 +1,26 @@
|
|||||||
name: Integration tests
|
name: Integration tests
|
||||||
|
|
||||||
on: [push, pull_request_target]
|
on: [push, pull_request]
|
||||||
|
|
||||||
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 }}
|
||||||
PYTHON_VERSION: "3.10"
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.PYTHON_VERSION }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install .[test]
|
pip install .
|
||||||
- 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
|
||||||
|
|||||||
23
.github/workflows/lint.yml
vendored
Normal file
23
.github/workflows/lint.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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
|
||||||
38
.github/workflows/publish.yml
vendored
Normal file
38
.github/workflows/publish.yml
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
name: Publish to PyPI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- '**'
|
||||||
|
tags:
|
||||||
|
- '*.*.*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-n-publish:
|
||||||
|
name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
- name: Install pypa/build
|
||||||
|
run: >-
|
||||||
|
python -m
|
||||||
|
pip install
|
||||||
|
build
|
||||||
|
--user
|
||||||
|
- name: Build a binary wheel and a source tarball
|
||||||
|
run: >-
|
||||||
|
python -m
|
||||||
|
build
|
||||||
|
--sdist
|
||||||
|
--wheel
|
||||||
|
--outdir dist/
|
||||||
|
.
|
||||||
|
- name: Publish distribution 📦 to PyPI
|
||||||
|
if: startsWith(github.ref, 'refs/tags')
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
with:
|
||||||
|
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||||
8
.github/workflows/pull_request.yml
vendored
8
.github/workflows/pull_request.yml
vendored
@ -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-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v4
|
||||||
- uses: dangoslen/changelog-enforcer@v1.1.1
|
- uses: dangoslen/changelog-enforcer@v3.6.1
|
||||||
with:
|
with:
|
||||||
changeLogPath: 'CHANGELOG.md'
|
changeLogPath: 'CHANGELOG.md'
|
||||||
skipLabel: 'skip-changelog'
|
skipLabels: 'skip-changelog'
|
||||||
|
|||||||
29
.github/workflows/pythonapp.yml
vendored
29
.github/workflows/pythonapp.yml
vendored
@ -1,29 +0,0 @@
|
|||||||
name: Tests
|
|
||||||
|
|
||||||
on: [push, pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
python-version: ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10"]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
|
||||||
uses: actions/setup-python@v1
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
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
|
|
||||||
run: |
|
|
||||||
python -m unittest discover -v tests/unit
|
|
||||||
23
.github/workflows/unit_tests.yml
vendored
Normal file
23
.github/workflows/unit_tests.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
name: Unit tests
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install .
|
||||||
|
- name: Run unit tests
|
||||||
|
run: |
|
||||||
|
python -m unittest discover -v tests/unit
|
||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[submodule "examples"]
|
||||||
|
path = examples
|
||||||
|
url = git@github.com:spotipy-dev/spotipy-examples.git
|
||||||
16
.readthedocs.yaml
Normal file
16
.readthedocs.yaml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Read the Docs configuration file for Sphinx projects
|
||||||
|
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
build:
|
||||||
|
os: ubuntu-22.04
|
||||||
|
tools:
|
||||||
|
python: "3.12"
|
||||||
|
|
||||||
|
sphinx:
|
||||||
|
configuration: docs/conf.py
|
||||||
|
|
||||||
|
python:
|
||||||
|
install:
|
||||||
|
- requirements: docs/requirements.txt
|
||||||
208
CHANGELOG.md
208
CHANGELOG.md
@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
Add your changes below.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
@ -13,12 +14,141 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### 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
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `MemcacheCacheHandler`, a cache handler that stores the token info using pymemcache.
|
||||||
|
- Added support for audiobook endpoints: `get_audiobook`, `get_audiobooks`, and `get_audiobook_chapters`.
|
||||||
|
- Added integration tests for audiobook endpoints.
|
||||||
|
- Added `update` field to `current_user_follow_playlist`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- 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__`
|
||||||
|
- Replaced `artist_albums(album_type=...)` with `artist_albums(include_groups=...)` due to an API change.
|
||||||
|
- Updated `_regex_spotify_url` to ignore `/intl-<countrycode>` in Spotify links
|
||||||
|
- Improved README, docs and examples
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Readthedocs build
|
||||||
|
- Split `test_current_user_save_and_usave_tracks` unit test
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Drop support for EOL Python 3.7
|
||||||
|
|
||||||
|
## [2.23.0] - 2023-04-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added optional `encoder_cls` argument to `CacheFileHandler`, which overwrite default encoder for token before writing to disk
|
||||||
|
- Integration tests for searching multiple types in multiple markets (non-user endpoints)
|
||||||
|
- Publish to PyPI action
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the regex for matching playlist URIs with the format spotify:user:USERNAME:playlist:PLAYLISTID.
|
||||||
|
- `search_markets` now factors the counts of all types in the `total` rather than just the first type ([#534](https://github.com/spotipy-dev/spotipy/issues/534))
|
||||||
|
|
||||||
## [2.22.1] - 2023-01-23
|
## [2.22.1] - 2023-01-23
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Add alternative module installation instruction to README
|
- Add alternative module installation instruction to README
|
||||||
- Added Comment to README - Getting Started for user to add URI to app in Spotify Developer Dashboard.
|
- Added Comment to README - Getting Started for user to add URI to app in Spotify Developer Dashboard.
|
||||||
- Added playlist_add_tracks.py to example folder
|
- Added playlist_add_tracks.py to example folder
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@ -44,7 +174,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
- Incorrect `category_id` input for test_category
|
- Incorrect `category_id` input for test_category
|
||||||
- Assertion value for `test_categories_limit_low` and `test_categories_limit_high`
|
- Assertion value for `test_categories_limit_low` and `test_categories_limit_high`
|
||||||
- Pin Github Actions Runner to Ubuntu 20 for Py27
|
- Pin GitHub Actions Runner to Ubuntu 20 for Py27
|
||||||
- Fixed potential error where `found` variable in `test_artist_related_artists` is undefined if for loop never evaluates to true
|
- Fixed potential error where `found` variable in `test_artist_related_artists` is undefined if for loop never evaluates to true
|
||||||
- Fixed false positive test `test_new_releases` which looks up the wrong property of the JSON response object and always evaluates to true
|
- Fixed false positive test `test_new_releases` which looks up the wrong property of the JSON response object and always evaluates to true
|
||||||
|
|
||||||
@ -69,13 +199,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added `RedisCacheHandler`, a cache handler that stores the token info in Redis.
|
- Added `RedisCacheHandler`, a cache handler that stores the token info in Redis.
|
||||||
- Changed URI handling in `client.Spotify._get_id()` to remove qureies if provided by error.
|
- Changed URI handling in `client.Spotify._get_id()` to remove queries if provided by error.
|
||||||
- Added a new parameter to `RedisCacheHandler` to allow custom keys (instead of the default `token_info` key)
|
- Added a new parameter to `RedisCacheHandler` to allow custom keys (instead of the default `token_info` key)
|
||||||
- Simplify check for existing token in `RedisCacheHandler`
|
- Simplify check for existing token in `RedisCacheHandler`
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Removed Python 3.5 and added Python 3.9 in Github Action
|
- Removed Python 3.5 and added Python 3.9 in GitHub Action
|
||||||
|
|
||||||
## [2.19.0] - 2021-08-12
|
## [2.19.0] - 2021-08-12
|
||||||
|
|
||||||
@ -88,7 +218,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed a bug in `CacheFileHandler.__init__`: The documentation says that the username will be retrieved from the environment, but it wasn't.
|
- Fixed a bug in `CacheFileHandler.__init__`: The documentation says that the username will be retrieved from the environment, but it wasn't.
|
||||||
- Fixed a bug in the initializers for the auth managers that produced a spurious warning message if you provide a cache handler and you set a value for the "SPOTIPY_CLIENT_USERNAME" environment variable.
|
- Fixed a bug in the initializers for the auth managers that produced a spurious warning message if you provide a cache handler, and you set a value for the "SPOTIPY_CLIENT_USERNAME" environment variable.
|
||||||
- Use generated MIT license and fix license type in `pip show`
|
- Use generated MIT license and fix license type in `pip show`
|
||||||
|
|
||||||
## [2.18.0] - 2021-04-13
|
## [2.18.0] - 2021-04-13
|
||||||
@ -98,11 +228,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Enabled using both short and long IDs for playlist_change_details
|
- Enabled using both short and long IDs for playlist_change_details
|
||||||
- Added a cache handler to `SpotifyClientCredentials`
|
- Added a cache handler to `SpotifyClientCredentials`
|
||||||
- Added the following endpoints
|
- Added the following endpoints
|
||||||
- `Spotify.current_user_saved_episodes`
|
- `Spotify.current_user_saved_episodes`
|
||||||
- `Spotify.current_user_saved_episodes_add`
|
- `Spotify.current_user_saved_episodes_add`
|
||||||
- `Spotify.current_user_saved_episodes_delete`
|
- `Spotify.current_user_saved_episodes_delete`
|
||||||
- `Spotify.current_user_saved_episodes_contains`
|
- `Spotify.current_user_saved_episodes_contains`
|
||||||
- `Spotify.available_markets`
|
- `Spotify.available_markets`
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@ -129,7 +259,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- The docs for the `auth` parameter of `Spotify.init` use the term "access token" instead of "authorization token"
|
- The docs for the `auth` parameter of `Spotify.init` use the term "access token" instead of "authorization token"
|
||||||
- Changed docs for `search` to mention that you can provide multiple types to search for
|
- Changed docs for `search` to mention that you can provide multiple types to search for
|
||||||
- The query parameters of requests are now logged
|
- The query parameters of requests are now logged
|
||||||
- Deprecate specifing `cache_path` or `username` directly to `SpotifyOAuth`, `SpotifyPKCE`, and `SpotifyImplicitGrant` constructors, instead directing users to use the `CacheFileHandler` cache handler
|
- Deprecate specifying `cache_path` or `username` directly to `SpotifyOAuth`, `SpotifyPKCE`, and `SpotifyImplicitGrant` constructors, instead directing users to use the `CacheFileHandler` cache handler
|
||||||
- Removed requirement for examples/app.py to specify port multiple times (only SPOTIPY_REDIRECT_URI needs to contain the port)
|
- Removed requirement for examples/app.py to specify port multiple times (only SPOTIPY_REDIRECT_URI needs to contain the port)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@ -168,7 +298,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `SpotifyPKCE.parse_auth_response_url`, mirroring that method in
|
- `SpotifyPKCE.parse_auth_response_url`, mirroring that method in
|
||||||
`SpotifyOAuth`
|
`SpotifyOAuth`
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@ -177,7 +307,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Using `SpotifyPKCE.get_authorization_url` will now generate a code
|
- Using `SpotifyPKCE.get_authorization_url` will now generate a code
|
||||||
challenge if needed
|
challenge if needed
|
||||||
|
|
||||||
## [2.14.0] - 2020-08-29
|
## [2.14.0] - 2020-08-29
|
||||||
|
|
||||||
@ -185,9 +315,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
- (experimental) Support to search multiple/all markets at once.
|
- (experimental) Support to search multiple/all markets at once.
|
||||||
- Support to test whether the current user is following certain
|
- Support to test whether the current user is following certain
|
||||||
users or artists
|
users or artists
|
||||||
- Proper replacements for all deprecated playlist endpoints
|
- Proper replacements for all deprecated playlist endpoints
|
||||||
(See https://developer.spotify.com/community/news/2018/06/12/changes-to-playlist-uris/ and below)
|
(See https://developer.spotify.com/community/news/2018/06/12/changes-to-playlist-uris/ and below)
|
||||||
- Allow for OAuth 2.0 authorization by instructing the user to open the URL in a browser instead of opening the browser.
|
- Allow for OAuth 2.0 authorization by instructing the user to open the URL in a browser instead of opening the browser.
|
||||||
- Reason for 403 error in SpotifyException
|
- Reason for 403 error in SpotifyException
|
||||||
- Support for the PKCE Auth Flow
|
- Support for the PKCE Auth Flow
|
||||||
@ -205,11 +335,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- `user_playlist_replace_tracks` in favor of `playlist_replace_items`
|
- `user_playlist_replace_tracks` in favor of `playlist_replace_items`
|
||||||
- `user_playlist_reorder_tracks` in favor of `playlist_reorder_items`
|
- `user_playlist_reorder_tracks` in favor of `playlist_reorder_items`
|
||||||
- `user_playlist_remove_all_occurrences_of_tracks` in favor of
|
- `user_playlist_remove_all_occurrences_of_tracks` in favor of
|
||||||
`playlist_remove_all_occurrences_of_items`
|
`playlist_remove_all_occurrences_of_items`
|
||||||
- `user_playlist_remove_specific_occurrences_of_tracks` in favor of
|
- `user_playlist_remove_specific_occurrences_of_tracks` in favor of
|
||||||
`playlist_remove_specific_occurrences_of_items`
|
`playlist_remove_specific_occurrences_of_items`
|
||||||
- `user_playlist_follow_playlist` in favor of
|
- `user_playlist_follow_playlist` in favor of
|
||||||
`current_user_follow_playlist`
|
`current_user_follow_playlist`
|
||||||
- `user_playlist_is_following` in favor of `playlist_is_following`
|
- `user_playlist_is_following` in favor of `playlist_is_following`
|
||||||
- `playlist_tracks` in favor of `playlist_items`
|
- `playlist_tracks` in favor of `playlist_items`
|
||||||
|
|
||||||
@ -222,15 +352,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added `SpotifyImplicitGrant` as an auth manager option. It provides
|
- Added `SpotifyImplicitGrant` as an auth manager option. It provides
|
||||||
user authentication without a client secret but sacrifices the ability
|
user authentication without a client secret but sacrifices the ability
|
||||||
to refresh the token without user input. (However, read the class
|
to refresh the token without user input. (However, read the class
|
||||||
docstring for security advisory.)
|
docstring for security advisory.)
|
||||||
- Added built-in verification of the `state` query parameter
|
- Added built-in verification of the `state` query parameter
|
||||||
- Added two new attributes: error and error_description to `SpotifyOauthError` exception class to show
|
- Added two new attributes: error and error_description to `SpotifyOauthError` exception class to show
|
||||||
authorization/authentication web api errors details.
|
authorization/authentication web api errors details.
|
||||||
- Added `SpotifyStateError` subclass of `SpotifyOauthError`
|
- Added `SpotifyStateError` subclass of `SpotifyOauthError`
|
||||||
- Allow extending `SpotifyClientCredentials` and `SpotifyOAuth`
|
- Allow extending `SpotifyClientCredentials` and `SpotifyOAuth`
|
||||||
- Added the market paramter to `album_tracks`
|
- Added the market parameter to `album_tracks`
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
@ -251,10 +381,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Updated the documentation to give more details on the authorization process and reflect
|
- Updated the documentation to give more details on the authorization process and reflect
|
||||||
2020 Spotify Application jargon and practices.
|
2020 Spotify Application jargon and practices.
|
||||||
|
|
||||||
- The local webserver is only started for localhost redirect_uri which specify a port,
|
- The local webserver is only started for localhost redirect_uri which specify a port,
|
||||||
i.e. it is started for `http://localhost:8080` or `http://127.0.0.1:8080`, not for `http://localhost`.
|
i.e. it is started for `http://localhost:8080` or `http://127.0.0.1:8080`, not for `http://localhost`.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
@ -277,11 +407,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
- Client retry logic has changed as it now uses urllib3's `Retry` in conjunction with requests `Session`
|
- Client retry logic has changed as it now uses urllib3's `Retry` in conjunction with requests `Session`
|
||||||
- The session is customizable as it allows for:
|
- The session is customizable as it allows for:
|
||||||
- status_forcelist
|
- status_forcelist
|
||||||
- retries
|
- retries
|
||||||
- status_retries
|
- status_retries
|
||||||
- backoff_factor
|
- backoff_factor
|
||||||
- Spin up a local webserver to auto-fill authentication URL
|
- Spin up a local webserver to autofill authentication URL
|
||||||
- Use session in SpotifyAuthBase
|
- Use session in SpotifyAuthBase
|
||||||
- Logging used instead of print statements
|
- Logging used instead of print statements
|
||||||
|
|
||||||
@ -295,9 +425,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Support for `add_to_queue`
|
- Support for `add_to_queue`
|
||||||
- **Parameters:**
|
- **Parameters:**
|
||||||
- track uri, id, or url
|
- track uri, id, or url
|
||||||
- device id. If None, then the active device is used.
|
- device id. If None, then the active device is used.
|
||||||
- Add CHANGELOG and LICENSE to released package
|
- Add CHANGELOG and LICENSE to released package
|
||||||
|
|
||||||
## [2.9.0] - 2020-02-15
|
## [2.9.0] - 2020-02-15
|
||||||
@ -368,7 +498,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed inconsistent behaviour with some API methods when
|
- Fixed inconsistent behaviour with some API methods when
|
||||||
a full HTTP URL is passed.
|
a full HTTP URL is passed.
|
||||||
- Fixed invalid calls to logging warn method
|
- Fixed invalid calls to logging warn method
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
@ -384,13 +514,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Support for `current_user_saved_albums_contains`
|
- Support for `current_user_saved_albums_contains`
|
||||||
- Support for `user_unfollow_artists`
|
- Support for `user_unfollow_artists`
|
||||||
- Support for `user_unfollow_users`
|
- Support for `user_unfollow_users`
|
||||||
- Lint with flake8 using Github action
|
- Lint with flake8 using GitHub action
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Fix typos in doc
|
- Fix typos in doc
|
||||||
- Start following [SemVer](https://semver.org) properly
|
- Start following [SemVer](https://semver.org) properly
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Made instructions in the CONTRIBUTING.md file more clear such that it is easier to onboard and there are no conflicts with TUTORIAL.md
|
||||||
|
|
||||||
## [2.5.0] - 2020-01-11
|
## [2.5.0] - 2020-01-11
|
||||||
|
|
||||||
Added follow and player endpoints
|
Added follow and player endpoints
|
||||||
@ -433,7 +567,7 @@ Fixed bug in auto retry logic
|
|||||||
|
|
||||||
## [2.3.3] - 2015-04-01
|
## [2.3.3] - 2015-04-01
|
||||||
|
|
||||||
Aadded client credential flow
|
Added client credential flow
|
||||||
|
|
||||||
## [2.3.2] - 2015-03-31
|
## [2.3.2] - 2015-03-31
|
||||||
|
|
||||||
@ -477,7 +611,7 @@ Support for "Your Music" tracks (add, delete, get), with examples
|
|||||||
|
|
||||||
## [1.45.0] - 2014-07-07
|
## [1.45.0] - 2014-07-07
|
||||||
|
|
||||||
Support for related artists endpoint. Don't use cache auth codes when scope changes
|
Support for related artists' endpoint. Don't use cache auth codes when scope changes
|
||||||
|
|
||||||
## [1.44.0] - 2014-07-03
|
## [1.44.0] - 2014-07-03
|
||||||
|
|
||||||
|
|||||||
@ -8,51 +8,78 @@ If you would like to contribute to spotipy follow these steps:
|
|||||||
# Linux or Mac
|
# Linux or Mac
|
||||||
export SPOTIPY_CLIENT_ID=client_id_here
|
export SPOTIPY_CLIENT_ID=client_id_here
|
||||||
export SPOTIPY_CLIENT_SECRET=client_secret_here
|
export SPOTIPY_CLIENT_SECRET=client_secret_here
|
||||||
export SPOTIPY_CLIENT_USERNAME=client_username_here # This is actually an id not spotify display name
|
export SPOTIPY_CLIENT_USERNAME=client_username_here # This is actually an id not spotify display name and can be found [here](https://www.spotify.com/us/account/overview/)
|
||||||
export SPOTIPY_REDIRECT_URI=http://localhost:8080 # Make url is set in app you created to get your ID and SECRET
|
export SPOTIPY_REDIRECT_URI=http://127.0.0.1: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://localhost:8080"
|
$env:SPOTIPY_REDIRECT_URI="http://127.0.0.1:8080"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create virtual environment, install dependencies, run tests:
|
### Branch Overview
|
||||||
|
|
||||||
|
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.7 env
|
$ virtualenv --python=python3 env
|
||||||
$ source env/bin/activate
|
$ source env/bin/activate
|
||||||
(env) $ pip install --user -e .
|
(env) $ pip install -e .
|
||||||
(env) $ python -m unittest discover -v tests
|
(env) $ python -m unittest discover -v tests
|
||||||
```
|
```
|
||||||
|
|
||||||
### Lint
|
### Lint
|
||||||
|
|
||||||
To automatically fix the code style:
|
pip install ".[test]"
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
pip install isort
|
isort . -c
|
||||||
isort . -c -v
|
|
||||||
|
Sort them automatically with:
|
||||||
|
|
||||||
|
isort .
|
||||||
|
|
||||||
|
### Changelog
|
||||||
|
|
||||||
|
Don't forget to add a short description of your change in the [CHANGELOG](CHANGELOG.md)
|
||||||
|
|
||||||
### 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 new changes below
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
@ -60,18 +87,8 @@ To make sure if the import lists are stored correctly:
|
|||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
- Commit changes
|
- Commit changes
|
||||||
- Package to pypi:
|
- Push tag to trigger PyPI build & release workflow
|
||||||
|
- Create github release <https://github.com/plamere/spotipy/releases> with the changelog content
|
||||||
python setup.py sdist bdist_wheel
|
|
||||||
python3 setup.py sdist bdist_wheel
|
|
||||||
twine check dist/*
|
|
||||||
twine upload dist/*
|
|
||||||
|
|
||||||
- Create github release https://github.com/plamere/spotipy/releases with the changelog content
|
|
||||||
for the version and a short name that describes the main addition
|
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/>
|
||||||
|
|
||||||
### Changelog
|
|
||||||
|
|
||||||
Don't forget to add a short description of your change in the [CHANGELOG](CHANGELOG.md)
|
|
||||||
|
|||||||
34
FAQ.md
34
FAQ.md
@ -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](examples/app.py)
|
Check out [this example Flask app](https://github.com/spotipy-dev/spotipy-examples/tree/main/apps/flask_api)
|
||||||
|
|
||||||
### 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?
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ Error:
|
|||||||
Solution:
|
Solution:
|
||||||
|
|
||||||
- You are likely missing a scope when requesting the endpoint, check
|
- You are likely missing a scope when requesting the endpoint, check
|
||||||
https://developer.spotify.com/web-api/using-scopes/
|
https://developer.spotify.com/documentation/web-api/concepts/scopes/
|
||||||
|
|
||||||
### Search doesn't find some tracks
|
### Search doesn't find some tracks
|
||||||
|
|
||||||
@ -51,4 +51,32 @@ 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](examples/headless.py).
|
See the [headless auth example](https://github.com/spotipy-dev/spotipy-examples/blob/main/scripts/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)
|
||||||
|
|||||||
49
README.md
49
README.md
@ -1,12 +1,20 @@
|
|||||||
# Spotipy
|
# Spotipy
|
||||||
|
|
||||||
##### A light weight Python library for the Spotify Web API
|
##### Spotipy is a lightweight Python library for the [Spotify Web API](https://developer.spotify.com/documentation/web-api). With Spotipy you get full access to all of the music data provided by the Spotify platform.
|
||||||
|
|
||||||
 [](https://spotipy.readthedocs.io/en/latest/?badge=latest)
|
 [](https://spotipy.readthedocs.io/en/latest/?badge=master) [](https://discord.gg/HP6xcPsTPJ)
|
||||||
|
|
||||||
## Documentation
|
## Table of Contents
|
||||||
|
|
||||||
Spotipy's full documentation is online at [Spotipy Documentation](http://spotipy.readthedocs.org/).
|
- [Features](#features)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Quick Start](#quick-start)
|
||||||
|
- [Reporting Issues](#reporting-issues)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -28,12 +36,11 @@ 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/plamere/spotipy/tree/master/examples).
|
A full set of examples can be found in the [online documentation](http://spotipy.readthedocs.org/) and in the [Spotipy examples directory](https://github.com/spotipy-dev/spotipy-examples).
|
||||||
|
|
||||||
To get started, install spotipy and create an app on https://developers.spotify.com/.
|
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:
|
||||||
Add your new ID and SECRET to your environment:
|
|
||||||
|
|
||||||
### Without user authentication
|
### Example without user authentication
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import spotipy
|
import spotipy
|
||||||
@ -46,8 +53,20 @@ results = sp.search(q='weezer', limit=20)
|
|||||||
for idx, track in enumerate(results['tracks']['items']):
|
for idx, track in enumerate(results['tracks']['items']):
|
||||||
print(idx, track['name'])
|
print(idx, track['name'])
|
||||||
```
|
```
|
||||||
|
Expected result:
|
||||||
|
```
|
||||||
|
0 Island In The Sun
|
||||||
|
1 Say It Ain't So
|
||||||
|
2 Buddy Holly
|
||||||
|
.
|
||||||
|
.
|
||||||
|
.
|
||||||
|
18 Troublemaker
|
||||||
|
19 Feels Like Summer
|
||||||
|
```
|
||||||
|
|
||||||
### With user authentication
|
|
||||||
|
### Example with user authentication
|
||||||
|
|
||||||
A redirect URI must be added to your application at [My Dashboard](https://developer.spotify.com/dashboard/applications) to access user authenticated features.
|
A redirect URI must be added to your application at [My Dashboard](https://developer.spotify.com/dashboard/applications) to access user authenticated features.
|
||||||
|
|
||||||
@ -65,6 +84,12 @@ for idx, item in enumerate(results['items']):
|
|||||||
track = item['track']
|
track = item['track']
|
||||||
print(idx, track['artists'][0]['name'], " – ", track['name'])
|
print(idx, track['artists'][0]['name'], " – ", track['name'])
|
||||||
```
|
```
|
||||||
|
Expected result will be the list of music that you liked. For example if you liked Red and Sunflower, the result will be:
|
||||||
|
```
|
||||||
|
0 Post Malone – Sunflower - Spider-Man: Into the Spider-Verse
|
||||||
|
1 Taylor Swift – Red
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Reporting Issues
|
## Reporting Issues
|
||||||
|
|
||||||
@ -77,3 +102,9 @@ Don’t forget to add the *Spotipy* tag, and any other relevant tags as well, be
|
|||||||
If you have suggestions, bugs or other issues specific to this library,
|
If you have suggestions, bugs or other issues specific to this library,
|
||||||
file them [here](https://github.com/plamere/spotipy/issues).
|
file them [here](https://github.com/plamere/spotipy/issues).
|
||||||
Or just send a pull request.
|
Or just send a pull request.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
If you are a developer with Python experience, and you would like to contribute to Spotipy, please be sure to follow the guidelines listed on documentation page
|
||||||
|
|
||||||
|
> #### [Visit the guideline](https://spotipy.readthedocs.io/en/#contribute)
|
||||||
|
|||||||
61
TUTORIAL.md
61
TUTORIAL.md
@ -4,18 +4,19 @@ 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. pip package manager**
|
**1. python3**
|
||||||
|
|
||||||
You can check to see if you have pip installed by opening up Terminal and typing the following command: pip --version
|
Spotipy is written in Python, so you'll need to have the latest version of Python installed in order to use Spotipy. Check if you already have Python installed with the Terminal command: python --version
|
||||||
If you see a version number, pip is installed and you're ready to proceed. If not, instructions for downloading the latest version of pip can be found here: https://pip.pypa.io/en/stable/cli/pip_download/
|
|
||||||
|
|
||||||
|
|
||||||
**2. python3**
|
|
||||||
|
|
||||||
Spotipy is written in Python, so you'll need to have the lastest version of Python installed in order to use Spotipy. Check if you already have Python installed with the Terminal command: python --version
|
|
||||||
If you see a version number, Python is already installed. If not, you can download it here: https://www.python.org/downloads/
|
If you see a version number, Python is already installed. If not, you can download it here: https://www.python.org/downloads/
|
||||||
|
|
||||||
**3. experience with basic Linux commands**
|
**2. pip package manager**
|
||||||
|
|
||||||
|
You can check to see if you have pip installed by opening up Terminal and typing the following command: pip --version
|
||||||
|
If you see a version number, pip is installed, and you're ready to proceed. If not, instructions for downloading the latest version of pip can be found here: https://pip.pypa.io/en/stable/cli/pip_download/
|
||||||
|
|
||||||
|
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**
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
@ -24,21 +25,21 @@ 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, you should be redirected to your developer dashboard.
|
A. Visit the [Spotify developer portal](https://developer.spotify.com/dashboard/). If you already have a Spotify account, click "Log in" and enter your username and password. Otherwise, click "Sign up" and follow the steps to create an account. After you've signed in or signed up, begin by clicking on your profile name at the top right of your screen and then click “Dashboard” to go to Spotify’s Developer Dashboard.
|
||||||
|
|
||||||
B. Click the "Create an App" button. Enter any name and description you'd like for your new app. Accept the terms of service and click "Create."
|
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.
|
||||||
|
|
||||||
C. In your new app's Overview screen, click the "Edit Settings" button and scroll down to "Redirect URIs." Add "http://localhost:1234" (or any other port number of your choosing). Hit the "Save" button at the bottom of the Settings panel to return to you App Overview screen.
|
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.
|
||||||
|
|
||||||
D. Underneath your app name and description on the lefthand side, you'll see a "Show Client Secret" link. Click that link to reveal your Client Secret, then copy both your Client Secret and your Client ID somewhere on your computer. You'll need to access them later.
|
|
||||||
|
|
||||||
## Step 2. Installation and Setup
|
## 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 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
|
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
|
||||||
|
|
||||||
C. Paste the following code into your main.py file:
|
C. In that folder, create a Python file named main.py. You can create the file directly from Terminal using a built in text editor like Vim, which comes preinstalled on Linux operating systems. To create the file with Vim, ensure that you are in your new directory, then run: vim main.py
|
||||||
|
|
||||||
|
D. Paste the following code into your main.py file:
|
||||||
```
|
```
|
||||||
import spotipy
|
import spotipy
|
||||||
from spotipy.oauth2 import SpotifyOAuth
|
from spotipy.oauth2 import SpotifyOAuth
|
||||||
@ -48,17 +49,17 @@ 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 1C.
|
D. Replace YOUR_APP_CLIENT_ID and YOUR_APP_CLIENT_SECRET with the values you copied and saved in step 1D. Replace YOUR_APP_REDIRECT_URI with the URI you set in step 1B.
|
||||||
|
|
||||||
## Step 3. Start Using Spotipy
|
## Step 3. Start Using Spotipy
|
||||||
|
|
||||||
After completing steps 1 and 2, your app is fully configured and ready to fetch data from the Spotify API. All that's left is to tell the API what data we're looking for, and we do that by adding some additional code to main.py. The code that follows is just an example - once you get it working, you should feel free to modify it in order to get different results.
|
After completing steps 1 and 2, your app is fully configured and ready to fetch data from the Spotify API. All that's left is to tell the API what data we're looking for, and we do that by adding some additional code to main.py. The code that follows is just an example - once you get it working, you should feel free to modify it in order to get different results.
|
||||||
|
|
||||||
For now, let's assume that we want to print the names of all of the albums on Spotify by Taylor Swift:
|
For now, let's assume that we want to print the names of all the albums on Spotify by Taylor Swift:
|
||||||
|
|
||||||
A. First, we need to find Taylor Swift's Spotify URI (Uniform Resource Indicator). Every entity (artist, album, song, etc.) has a URI that can identify it. To find Taylor's URI, navigate to [her page on Spotify](https://open.spotify.com/artist/06HL4z0CvFAxyc27GXpf02) and look at the URI in your browser. Everything there that follows the last backslash in the URL path is Taylor's URI, in this case: 06HL4z0CvFAxyc27GXpf02
|
A. First, we need to find Taylor Swift's Spotify URI (Uniform Resource Indicator). Every entity (artist, album, song, etc.) has a URI that can identify it. To find Taylor's URI, navigate to [her page on Spotify](https://open.spotify.com/artist/06HL4z0CvFAxyc27GXpf02) and look at the URI in your browser. Everything there that follows the last backslash in the URL path is Taylor's URI, in this case: 06HL4z0CvFAxyc27GXpf02
|
||||||
|
|
||||||
B. Add the URI as a variable in main.py. Notice the prefix added the the URI:
|
B. Add the URI as a variable in main.py. Notice the prefix added the URI:
|
||||||
```
|
```
|
||||||
taylor_uri = 'spotify:artist:06HL4z0CvFAxyc27GXpf02'
|
taylor_uri = 'spotify:artist:06HL4z0CvFAxyc27GXpf02'
|
||||||
```
|
```
|
||||||
@ -78,4 +79,22 @@ D. Close main.py and return to the directory that contains main.py. You can then
|
|||||||
|
|
||||||
E. You may see a window open in your browser asking you to authorize the application. Do so - you will only have to do this once.
|
E. You may see a window open in your browser asking you to authorize the application. Do so - you will only have to do this once.
|
||||||
|
|
||||||
F. Return to your terminal - you should see all of Taylor's albums printed out there.
|
F. Return to your terminal - you should see all of Taylor's albums printed out there.
|
||||||
|
|
||||||
|
## Troubleshooting Tips
|
||||||
|
A. Command not found running the application "zsh: command not found: python"
|
||||||
|
|
||||||
|
Check which Python version that you have by running the command:
|
||||||
|
```python --version ``` or ```python3 --version```.
|
||||||
|
|
||||||
|
In most cases, the recent Python version is Python 3. You may need to update Python. Once you have updated Python to the most recent version, run the command:
|
||||||
|
``` python3 main.py```
|
||||||
|
|
||||||
|
B. Encountering package error:
|
||||||
|
|
||||||
|
If you are seeing an error "ModuleNotFoundError: No module named 'spotipy'", this means you have not installed the package.
|
||||||
|
Run the command:
|
||||||
|
```
|
||||||
|
pip install spotipy
|
||||||
|
```
|
||||||
|
After the package is installed, run the app again.
|
||||||
|
|||||||
92
docs/conf.py
92
docs/conf.py
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# spotipy documentation build configuration file, created by
|
# spotipy documentation build configuration file, created by
|
||||||
# sphinx-quickstart on Thu Aug 21 11:04:39 2014.
|
# sphinx-quickstart on Thu Aug 21 11:04:39 2014.
|
||||||
@ -11,24 +10,29 @@
|
|||||||
# 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 spotipy
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
# 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(".."))
|
||||||
|
|
||||||
|
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.
|
||||||
extensions = ['sphinx.ext.autodoc']
|
extensions = [
|
||||||
|
'sphinx.ext.autodoc',
|
||||||
|
'sphinx_rtd_theme'
|
||||||
|
]
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
templates_path = ['_templates']
|
templates_path = ['_templates']
|
||||||
@ -37,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'
|
||||||
@ -57,68 +61,68 @@ 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 ---------------------------------------------------
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
# a list of builtin themes.
|
# a list of builtin themes.
|
||||||
html_theme = 'default'
|
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,
|
||||||
@ -127,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'
|
||||||
@ -192,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 --------------------------------------------
|
||||||
@ -221,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 ------------------------------------------------
|
||||||
@ -236,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'
|
||||||
|
|||||||
410
docs/index.rst
410
docs/index.rst
@ -5,11 +5,201 @@ Welcome to Spotipy!
|
|||||||
===================================
|
===================================
|
||||||
|
|
||||||
*Spotipy* is a lightweight Python library for the `Spotify Web API
|
*Spotipy* is a lightweight Python library for the `Spotify Web API
|
||||||
<https://developer.spotify.com/web-api/>`_. With *Spotipy*
|
<https://developer.spotify.com/documentation/web-api/>`_. With *Spotipy*
|
||||||
you get full access to all of the music data provided by the Spotify platform.
|
you get full access to all of the music data provided by the Spotify platform.
|
||||||
|
|
||||||
Assuming you set the ``SPOTIPY_CLIENT_ID`` and ``SPOTIPY_CLIENT_SECRET``
|
Features
|
||||||
environment variables (here is a `video <https://youtu.be/3RGm4jALukM>`_ explaining how to do so). For a longer tutorial with examples included, refer to this `video playlist <https://www.youtube.com/watch?v=tmt5SdvTqUI&list=PLqgOPibB_QnzzcaOFYmY2cQjs35y0is9N&index=1>`_. Below is a quick example of using *Spotipy* to list the
|
========
|
||||||
|
|
||||||
|
*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/documentation/web-api/>`_ documentation.
|
||||||
|
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
Install or upgrade *Spotipy* with::
|
||||||
|
|
||||||
|
pip install spotipy --upgrade
|
||||||
|
|
||||||
|
You can also obtain the source code from the `Spotipy GitHub repository <https://github.com/plamere/spotipy>`_.
|
||||||
|
|
||||||
|
|
||||||
|
Getting Started
|
||||||
|
===============
|
||||||
|
|
||||||
|
All methods require user authorization. You will need to register your app at
|
||||||
|
`My Dashboard <https://developer.spotify.com/dashboard/applications>`_
|
||||||
|
to get the credentials necessary to make authorized calls
|
||||||
|
(a *client id* and *client secret*).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
*Spotipy* supports two authorization flows:
|
||||||
|
|
||||||
|
- **Authorization Code flow** This method is suitable for long-running applications
|
||||||
|
which the user logs into once. It provides an access token that can be refreshed.
|
||||||
|
|
||||||
|
.. note:: Requires you to add a redirect URI to your application at
|
||||||
|
`My Dashboard <https://developer.spotify.com/dashboard/applications>`_.
|
||||||
|
See `Redirect URI`_ for more details.
|
||||||
|
|
||||||
|
- **Client Credentials flow** This method makes it possible
|
||||||
|
to authenticate your requests to the Spotify Web API and to obtain
|
||||||
|
a higher rate limit than you would with the Authorization Code flow.
|
||||||
|
|
||||||
|
|
||||||
|
For guidance on setting your app credentials watch this `video tutorial <https://youtu.be/kaBVN8uP358>`_ or follow the
|
||||||
|
`Spotipy Tutorial for Beginners <https://github.com/spotipy-dev/spotipy/blob/2.22.1/TUTORIAL.md>`_.
|
||||||
|
|
||||||
|
For a longer tutorial with examples included, refer to this `video playlist <https://www.youtube.com/watch?v=tmt5SdvTqUI&list=PLqgOPibB_QnzzcaOFYmY2cQjs35y0is9N&index=1>`_.
|
||||||
|
|
||||||
|
|
||||||
|
Authorization Code Flow
|
||||||
|
=======================
|
||||||
|
|
||||||
|
This flow is suitable for long-running applications in which the user grants
|
||||||
|
permission only once. It provides an access token that can be refreshed.
|
||||||
|
Since the token exchange involves sending your secret key, perform this on a
|
||||||
|
secure location, like a backend service, and not from a client such as a
|
||||||
|
browser or from a mobile app.
|
||||||
|
|
||||||
|
Quick start
|
||||||
|
-----------
|
||||||
|
|
||||||
|
To support the **Client Authorization Code Flow** *Spotipy* provides a
|
||||||
|
class SpotifyOAuth that can be used to authenticate requests like so::
|
||||||
|
|
||||||
|
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'])
|
||||||
|
|
||||||
|
or if you are reluctant to immortalize your app credentials in your source code,
|
||||||
|
you can set environment variables like so (use ``$env:"credentials"`` instead of ``export``
|
||||||
|
on Windows)::
|
||||||
|
|
||||||
|
export SPOTIPY_CLIENT_ID='your-spotify-client-id'
|
||||||
|
export SPOTIPY_CLIENT_SECRET='your-spotify-client-secret'
|
||||||
|
export SPOTIPY_REDIRECT_URI='your-app-redirect-url'
|
||||||
|
|
||||||
|
|
||||||
|
Scopes
|
||||||
|
------
|
||||||
|
|
||||||
|
See `Using
|
||||||
|
Scopes <https://developer.spotify.com/documentation/web-api/concepts/scopes/>`_ for information
|
||||||
|
about scopes.
|
||||||
|
|
||||||
|
Redirect URI
|
||||||
|
------------
|
||||||
|
|
||||||
|
The **Authorization Code Flow** needs you to add a **redirect URI**
|
||||||
|
to your application at
|
||||||
|
`My Dashboard <https://developer.spotify.com/dashboard/applications>`_
|
||||||
|
(navigate to your application and then *[Edit Settings]*).
|
||||||
|
|
||||||
|
The ``redirect_uri`` argument or ``SPOTIPY_REDIRECT_URI`` environment variable
|
||||||
|
must match the redirect URI added to your application in your Dashboard.
|
||||||
|
The redirect URI can be any valid URI (it does not need to be accessible)
|
||||||
|
such as ``http://example.com`` or ``http://127.0.0.1:9090``.
|
||||||
|
|
||||||
|
.. note:: If you choose an `http`-scheme URL, and it's for
|
||||||
|
`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
|
||||||
|
response at the end of the oauth flow [see the code](https://github.com/plamere/spotipy/blob/master/spotipy/oauth2.py#L483-L490).
|
||||||
|
|
||||||
|
|
||||||
|
Client Credentials Flow
|
||||||
|
=======================
|
||||||
|
|
||||||
|
The Client Credentials flow is used in server-to-server authentication. Only
|
||||||
|
endpoints that do not access user information can be accessed. The advantage here
|
||||||
|
in comparison with requests to the Web API made without an access token,
|
||||||
|
is that a higher rate limit is applied.
|
||||||
|
|
||||||
|
As opposed to the Authorization Code Flow, you will not need to set ``SPOTIPY_REDIRECT_URI``,
|
||||||
|
which means you will never be redirected to the sign in page in your browser::
|
||||||
|
|
||||||
|
export SPOTIPY_CLIENT_ID='your-spotify-client-id'
|
||||||
|
export SPOTIPY_CLIENT_SECRET='your-spotify-client-secret'
|
||||||
|
|
||||||
|
To support the **Client Credentials Flow** *Spotipy* provides a
|
||||||
|
class SpotifyClientCredentials that can be used to authenticate requests like so::
|
||||||
|
|
||||||
|
|
||||||
|
import spotipy
|
||||||
|
from spotipy.oauth2 import SpotifyClientCredentials
|
||||||
|
|
||||||
|
auth_manager = SpotifyClientCredentials()
|
||||||
|
sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||||
|
|
||||||
|
playlists = sp.user_playlists('spotify')
|
||||||
|
while playlists:
|
||||||
|
for i, playlist in enumerate(playlists['items']):
|
||||||
|
print(f"{i + 1 + playlists['offset']:4d} {playlist['uri']} {playlist['name']}")
|
||||||
|
if playlists['next']:
|
||||||
|
playlists = sp.next(playlists)
|
||||||
|
else:
|
||||||
|
playlists = None
|
||||||
|
|
||||||
|
|
||||||
|
IDs URIs and URLs
|
||||||
|
=================
|
||||||
|
|
||||||
|
*Spotipy* supports a number of different ID types:
|
||||||
|
|
||||||
|
- **Spotify URI** - The resource identifier that you can enter, for example, in
|
||||||
|
the Spotify Desktop client's search box to locate an artist, album, or
|
||||||
|
track. Example: ``spotify:track:6rqhFgbbKwnb9MLmUQDhG6``
|
||||||
|
- **Spotify URL** - An HTML link that opens a track, album, app, playlist or other
|
||||||
|
Spotify resource in a Spotify client. Example:
|
||||||
|
``http://open.spotify.com/track/6rqhFgbbKwnb9MLmUQDhG6``
|
||||||
|
- **Spotify ID** - A base-62 number that you can find at the end of the Spotify
|
||||||
|
URI (see above) for an artist, track, album, etc. Example:
|
||||||
|
``6rqhFgbbKwnb9MLmUQDhG6``
|
||||||
|
|
||||||
|
In general, any *Spotipy* method that needs an artist, album, track or playlist ID
|
||||||
|
will accept ids in any of the above form
|
||||||
|
|
||||||
|
|
||||||
|
Customized token caching
|
||||||
|
========================
|
||||||
|
|
||||||
|
Tokens are refreshed automatically and stored by default in the project main folder.
|
||||||
|
As this might not suit everyone's needs, spotipy provides a way to create customized
|
||||||
|
cache handlers.
|
||||||
|
|
||||||
|
https://github.com/plamere/spotipy/blob/master/spotipy/cache_handler.py
|
||||||
|
|
||||||
|
The custom cache handler would need to be a class that inherits from the base
|
||||||
|
cache handler ``CacheHandler``. The default cache handler ``CacheFileHandler`` is a good example.
|
||||||
|
An instance of that new class can then be passed as a parameter when
|
||||||
|
creating ``SpotifyOAuth``, ``SpotifyPKCE`` or ``SpotifyImplicitGrant``.
|
||||||
|
The following handlers are available and defined in the URL above.
|
||||||
|
|
||||||
|
- ``CacheFileHandler``
|
||||||
|
- ``MemoryCacheHandler``
|
||||||
|
- ``DjangoSessionCacheHandler``
|
||||||
|
- ``FlaskSessionCacheHandler``
|
||||||
|
- ``RedisCacheHandler``
|
||||||
|
- ``MemcacheCacheHandler``: install with dependency using ``pip install "spotipy[pymemcache]"``
|
||||||
|
|
||||||
|
Feel free to contribute new cache handlers to the repo.
|
||||||
|
|
||||||
|
|
||||||
|
Examples
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Here is an example of using *Spotipy* to list the
|
||||||
names of all the albums released by the artist 'Birdy'::
|
names of all the albums released by the artist 'Birdy'::
|
||||||
|
|
||||||
import spotipy
|
import spotipy
|
||||||
@ -64,187 +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
|
||||||
Features
|
repository <https://github.com/spotipy-dev/spotipy-examples>`_ on GitHub.
|
||||||
========
|
|
||||||
|
|
||||||
*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.
|
|
||||||
|
|
||||||
Installation
|
|
||||||
============
|
|
||||||
|
|
||||||
Install or upgrade *Spotipy* with::
|
|
||||||
|
|
||||||
pip install spotipy --upgrade
|
|
||||||
|
|
||||||
Or you can get the source from github at https://github.com/plamere/spotipy
|
|
||||||
|
|
||||||
Getting Started
|
|
||||||
===============
|
|
||||||
|
|
||||||
All methods require user authorization. You will need to register your app at
|
|
||||||
`My Dashboard <https://developer.spotify.com/dashboard/applications>`_
|
|
||||||
to get the credentials necessary to make authorized calls
|
|
||||||
(a *client id* and *client secret*).
|
|
||||||
|
|
||||||
*Spotipy* supports two authorization flows:
|
|
||||||
|
|
||||||
- The **Authorization Code flow** This method is suitable for long-running applications
|
|
||||||
which the user logs into once. It provides an access token that can be refreshed.
|
|
||||||
|
|
||||||
.. note:: Requires you to add a redirect URI to your application at
|
|
||||||
`My Dashboard <https://developer.spotify.com/dashboard/applications>`_.
|
|
||||||
See `Redirect URI`_ for more details.
|
|
||||||
|
|
||||||
- The **Client Credentials flow** The method makes it possible
|
|
||||||
to authenticate your requests to the Spotify Web API and to obtain
|
|
||||||
a higher rate limit than you would with the Authorization Code flow.
|
|
||||||
|
|
||||||
|
|
||||||
Authorization Code Flow
|
|
||||||
=======================
|
|
||||||
|
|
||||||
This flow is suitable for long-running applications in which the user grants
|
|
||||||
permission only once. It provides an access token that can be refreshed.
|
|
||||||
Since the token exchange involves sending your secret key, perform this on a
|
|
||||||
secure location, like a backend service, and not from a client such as a
|
|
||||||
browser or from a mobile app.
|
|
||||||
|
|
||||||
Quick start
|
|
||||||
-----------
|
|
||||||
|
|
||||||
To support the **Client Authorization Code Flow** *Spotipy* provides a
|
|
||||||
class SpotifyOAuth that can be used to authenticate requests like so::
|
|
||||||
|
|
||||||
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'])
|
|
||||||
|
|
||||||
or if you are reluctant to immortalize your app credentials in your source code,
|
|
||||||
you can set environment variables like so (use ``$env:"credentials"`` instead of ``export``
|
|
||||||
on Windows)::
|
|
||||||
|
|
||||||
export SPOTIPY_CLIENT_ID='your-spotify-client-id'
|
|
||||||
export SPOTIPY_CLIENT_SECRET='your-spotify-client-secret'
|
|
||||||
export SPOTIPY_REDIRECT_URI='your-app-redirect-url'
|
|
||||||
|
|
||||||
Scopes
|
|
||||||
------
|
|
||||||
|
|
||||||
See `Using
|
|
||||||
Scopes <https://developer.spotify.com/documentation/general/guides/authorization/scopes/>`_ for information
|
|
||||||
about scopes.
|
|
||||||
|
|
||||||
Redirect URI
|
|
||||||
------------
|
|
||||||
|
|
||||||
The **Authorization Code Flow** needs you to add a **redirect URI**
|
|
||||||
to your application at
|
|
||||||
`My Dashboard <https://developer.spotify.com/dashboard/applications>`_
|
|
||||||
(navigate to your application and then *[Edit Settings]*).
|
|
||||||
|
|
||||||
The ``redirect_uri`` argument or ``SPOTIPY_REDIRECT_URI`` environment variable
|
|
||||||
must match the redirect URI added to your application in your Dashboard.
|
|
||||||
The redirect URI can be any valid URI (it does not need to be accessible)
|
|
||||||
such as ``http://example.com``, ``http://localhost`` or ``http://127.0.0.1:9090``.
|
|
||||||
|
|
||||||
.. note:: If you choose an `http`-scheme URL, and it's for `localhost` or
|
|
||||||
`127.0.0.1`, **AND** it specifies a port, then spotispy will instantiate
|
|
||||||
a server on the indicated response to receive the access token from the
|
|
||||||
response at the end of the oauth flow [see the code](https://github.com/plamere/spotipy/blob/master/spotipy/oauth2.py#L483-L490).
|
|
||||||
|
|
||||||
|
|
||||||
Client Credentials Flow
|
|
||||||
=======================
|
|
||||||
|
|
||||||
The Client Credentials flow is used in server-to-server authentication. Only
|
|
||||||
endpoints that do not access user information can be accessed. The advantage here
|
|
||||||
in comparison with requests to the Web API made without an access token,
|
|
||||||
is that a higher rate limit is applied.
|
|
||||||
|
|
||||||
As opposed to the Authorization Code Flow, you will not need to set ``SPOTIPY_REDIRECT_URI``,
|
|
||||||
which means you will never be redirected to the sign in page in your browser::
|
|
||||||
|
|
||||||
export SPOTIPY_CLIENT_ID='your-spotify-client-id'
|
|
||||||
export SPOTIPY_CLIENT_SECRET='your-spotify-client-secret'
|
|
||||||
|
|
||||||
To support the **Client Credentials Flow** *Spotipy* provides a
|
|
||||||
class SpotifyClientCredentials that can be used to authenticate requests like so::
|
|
||||||
|
|
||||||
|
|
||||||
import spotipy
|
|
||||||
from spotipy.oauth2 import SpotifyClientCredentials
|
|
||||||
|
|
||||||
auth_manager = SpotifyClientCredentials()
|
|
||||||
sp = spotipy.Spotify(auth_manager=auth_manager)
|
|
||||||
|
|
||||||
playlists = sp.user_playlists('spotify')
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
IDs URIs and URLs
|
|
||||||
=================
|
|
||||||
|
|
||||||
*Spotipy* supports a number of different ID types:
|
|
||||||
|
|
||||||
- **Spotify URI** - The resource identifier that you can enter, for example, in
|
|
||||||
the Spotify Desktop client's search box to locate an artist, album, or
|
|
||||||
track. Example: ``spotify:track:6rqhFgbbKwnb9MLmUQDhG6``
|
|
||||||
- **Spotify URL** - An HTML link that opens a track, album, app, playlist or other
|
|
||||||
Spotify resource in a Spotify client. Example:
|
|
||||||
``http://open.spotify.com/track/6rqhFgbbKwnb9MLmUQDhG6``
|
|
||||||
- **Spotify ID** - A base-62 number that you can find at the end of the Spotify
|
|
||||||
URI (see above) for an artist, track, album, etc. Example:
|
|
||||||
``6rqhFgbbKwnb9MLmUQDhG6``
|
|
||||||
|
|
||||||
In general, any *Spotipy* method that needs an artist, album, track or playlist ID
|
|
||||||
will accept ids in any of the above form
|
|
||||||
|
|
||||||
|
|
||||||
Customized token caching
|
|
||||||
========================
|
|
||||||
|
|
||||||
Tokens are refreshed automatically and stored by default in the project main folder.
|
|
||||||
As this might not suit everyone's needs, spotipy provides a way to create customized
|
|
||||||
cache handlers.
|
|
||||||
|
|
||||||
https://github.com/plamere/spotipy/blob/master/spotipy/cache_handler.py
|
|
||||||
|
|
||||||
The custom cache handler would need to be a class that inherits from the base
|
|
||||||
cache handler ``CacheHandler``. The default cache handler ``CacheFileHandler`` is a good example.
|
|
||||||
An instance of that new class can then be passed as a parameter when
|
|
||||||
creating ``SpotifyOAuth``, ``SpotifyPKCE`` or ``SpotifyImplicitGrant``.
|
|
||||||
The following handlers are available and defined in the URL above.
|
|
||||||
- ``CacheFileHandler``
|
|
||||||
- ``MemoryCacheHandler``
|
|
||||||
- ``DjangoSessionCacheHandler``
|
|
||||||
- ``FlaskSessionCacheHandler``
|
|
||||||
- ``RedisCacheHandler``
|
|
||||||
|
|
||||||
Feel free to contribute new cache handlers to the repo.
|
|
||||||
|
|
||||||
Examples
|
|
||||||
=======================
|
|
||||||
|
|
||||||
There are many more examples of how to use *Spotipy* in the `Examples
|
|
||||||
Directory <https://github.com/plamere/spotipy/tree/master/examples>`_ on Github
|
|
||||||
|
|
||||||
API Reference
|
API Reference
|
||||||
==============
|
==============
|
||||||
@ -285,36 +296,12 @@ You can ask questions about Spotipy on Stack Overflow. Don’t forget to add t
|
|||||||
http://stackoverflow.com/questions/ask
|
http://stackoverflow.com/questions/ask
|
||||||
|
|
||||||
If you think you've found a bug, let us know at
|
If you think you've found a bug, let us know at
|
||||||
`Spotify Issues <https://github.com/plamere/spotipy/issues>`_
|
`Spotipy Issues <https://github.com/plamere/spotipy/issues>`_
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
@ -322,10 +309,10 @@ 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://localhost:8080 # Make url is set in app you created to get your ID and SECRET
|
export SPOTIPY_REDIRECT_URI=http://127.0.0.1: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.7 env
|
$ virtualenv --python=python3.12 env
|
||||||
(env) $ pip install --user -e .
|
(env) $ pip install --user -e .
|
||||||
(env) $ python -m unittest discover -v tests
|
(env) $ python -m unittest discover -v tests
|
||||||
|
|
||||||
@ -391,4 +378,3 @@ Indices and tables
|
|||||||
* :ref:`genindex`
|
* :ref:`genindex`
|
||||||
* :ref:`modindex`
|
* :ref:`modindex`
|
||||||
* :ref:`search`
|
* :ref:`search`
|
||||||
|
|
||||||
|
|||||||
3
docs/requirements.txt
Normal file
3
docs/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Sphinx~=8.1.3
|
||||||
|
sphinx-rtd-theme~=3.1.0
|
||||||
|
redis>=3.5.3
|
||||||
1
examples
Submodule
1
examples
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit c610a79705ef4aa55e4d61572a012f77b6f7245d
|
||||||
@ -1,27 +0,0 @@
|
|||||||
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()
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
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()
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
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
113
examples/app.py
@ -1,113 +0,0 @@
|
|||||||
"""
|
|
||||||
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 of 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])))
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
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()
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
# 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()
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
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()
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
# shows audio analysis for the given track
|
|
||||||
|
|
||||||
from __future__ import print_function # (at top of module)
|
|
||||||
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("analysis retrieved in %.2f seconds" % (delta,))
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
|
|
||||||
# shows acoustic features for tracks for the given artist
|
|
||||||
|
|
||||||
from __future__ import print_function # (at top of module)
|
|
||||||
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("features retrieved in %.2f seconds" % (delta,))
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
# shows acoustic features for tracks for the given artist
|
|
||||||
|
|
||||||
from __future__ import print_function # (at top of module)
|
|
||||||
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("features retrieved in %.2f seconds" % (delta,))
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
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()
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
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)
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
# 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("Usage: %s track-id ..." % (sys.argv[0],))
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
|
|
||||||
results = sp.current_user_saved_tracks_contains(tracks=[tid])
|
|
||||||
pprint.pprint(results)
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
# 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)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
# 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("Usage: %s track-id ..." % (sys.argv[0],))
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
|
|
||||||
results = sp.current_user_saved_tracks_delete(tracks=[tid])
|
|
||||||
pprint.pprint(results)
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
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()
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
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())
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
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())
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
# 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']))
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
# 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()
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
# 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()
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
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)
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
# 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'])
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
# 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))
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
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'])
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
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))
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
# 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(
|
|
||||||
"Usage: %s playlist_id track_id,pos track_id,pos ..." %
|
|
||||||
(sys.argv[0],))
|
|
||||||
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)
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
# 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("Usage: %s playlist_id track_id ..." % (sys.argv[0]))
|
|
||||||
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)
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
# 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("Usage: %s playlist_id track_id ..." % (sys.argv[0],))
|
|
||||||
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)
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
|
|
||||||
# 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)
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
# 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'])
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
# 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
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
# 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
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
|
|
||||||
# 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]")
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
'''
|
|
||||||
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'])
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
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']))
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
|
|
||||||
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'])
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import spotipy
|
|
||||||
from pprint import pprint
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
spotify = spotipy.Spotify(auth_manager=spotipy.SpotifyOAuth())
|
|
||||||
me = spotify.me()
|
|
||||||
pprint(me)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
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'])
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
# 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'])
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
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'])
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
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 = set(['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())
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
# 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'])
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
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()
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
# 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'])
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
# 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'])
|
|
||||||
|
|
||||||
results = sp.playlist(playlist['id'], fields="tracks,next")
|
|
||||||
tracks = results['tracks']
|
|
||||||
show_tracks(tracks)
|
|
||||||
|
|
||||||
while tracks['next']:
|
|
||||||
tracks = sp.next(tracks)
|
|
||||||
show_tracks(tracks)
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
# 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
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
# 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.")
|
|
||||||
29
setup.py
29
setup.py
@ -1,24 +1,27 @@
|
|||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
with open("README.md", "r") as f:
|
with open("README.md") as f:
|
||||||
long_description = f.read()
|
long_description = f.read()
|
||||||
|
|
||||||
test_reqs = [
|
memcache_cache_reqs = [
|
||||||
'mock==2.0.0'
|
'pymemcache>=3.5.2'
|
||||||
]
|
|
||||||
|
|
||||||
doc_reqs = [
|
|
||||||
'Sphinx>=1.5.2'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
extra_reqs = {
|
extra_reqs = {
|
||||||
'doc': doc_reqs,
|
'memcache': [
|
||||||
'test': test_reqs
|
'pymemcache>=3.5.2'
|
||||||
|
],
|
||||||
|
'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.22.1',
|
version='2.26.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",
|
||||||
@ -28,14 +31,12 @@ setup(
|
|||||||
project_urls={
|
project_urls={
|
||||||
'Source': 'https://github.com/plamere/spotipy',
|
'Source': 'https://github.com/plamere/spotipy',
|
||||||
},
|
},
|
||||||
|
python_requires='>3.8',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"redis>=3.5.3",
|
"redis>=3.5.3", # TODO: Move to extras_require in v3
|
||||||
"redis<4.0.0;python_version<'3.4'",
|
|
||||||
"requests>=2.25.0",
|
"requests>=2.25.0",
|
||||||
"six>=1.15.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'])
|
||||||
|
|||||||
@ -4,16 +4,18 @@ __all__ = [
|
|||||||
'DjangoSessionCacheHandler',
|
'DjangoSessionCacheHandler',
|
||||||
'FlaskSessionCacheHandler',
|
'FlaskSessionCacheHandler',
|
||||||
'MemoryCacheHandler',
|
'MemoryCacheHandler',
|
||||||
'RedisCacheHandler']
|
'RedisCacheHandler',
|
||||||
|
'MemcacheCacheHandler']
|
||||||
|
|
||||||
import errno
|
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__)
|
||||||
|
|
||||||
|
|
||||||
@ -39,7 +41,6 @@ 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):
|
||||||
@ -50,15 +51,18 @@ class CacheFileHandler(CacheHandler):
|
|||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
cache_path=None,
|
cache_path=None,
|
||||||
username=None):
|
username=None,
|
||||||
|
encoder_cls=None):
|
||||||
"""
|
"""
|
||||||
Parameters:
|
Parameters:
|
||||||
* cache_path: May be supplied, will otherwise be generated
|
* cache_path: May be supplied, will otherwise be generated
|
||||||
(takes precedence over `username`)
|
(takes precedence over `username`)
|
||||||
* username: May be supplied or set as environment variable
|
* username: May be supplied or set as environment variable
|
||||||
(will set `cache_path` to `.cache-{username}`)
|
(will set `cache_path` to `.cache-{username}`)
|
||||||
|
* encoder_cls: May be supplied as a means of overwriting the
|
||||||
|
default serializer used for writing tokens to disk
|
||||||
"""
|
"""
|
||||||
|
self.encoder_cls = encoder_cls
|
||||||
if cache_path:
|
if cache_path:
|
||||||
self.cache_path = cache_path
|
self.cache_path = cache_path
|
||||||
else:
|
else:
|
||||||
@ -72,27 +76,30 @@ class CacheFileHandler(CacheHandler):
|
|||||||
token_info = None
|
token_info = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
f = open(self.cache_path)
|
with open(self.cache_path, encoding='utf-8') as f:
|
||||||
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 IOError as error:
|
except OSError as error:
|
||||||
if error.errno == errno.ENOENT:
|
if error.errno == errno.ENOENT:
|
||||||
logger.debug("cache does not exist at: %s", self.cache_path)
|
logger.debug(f"cache does not exist at: {self.cache_path}")
|
||||||
else:
|
else:
|
||||||
logger.warning("Couldn't read cache at: %s", self.cache_path)
|
logger.warning(f"Couldn't read cache at: {self.cache_path}")
|
||||||
|
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:
|
||||||
f = open(self.cache_path, "w")
|
with open(self.cache_path, "w", encoding='utf-8') as f:
|
||||||
f.write(json.dumps(token_info))
|
f.write(json.dumps(token_info, cls=self.encoder_cls))
|
||||||
f.close()
|
# https://github.com/spotipy-dev/spotipy/security/advisories/GHSA-pwhh-q4h6-w599
|
||||||
except IOError:
|
os.chmod(self.cache_path, 0o600)
|
||||||
logger.warning('Couldn\'t write token to cache at: %s',
|
except OSError:
|
||||||
self.cache_path)
|
logger.warning(f"Couldn't write token to cache at: {self.cache_path}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.warning(f"Couldn't set permissions to cache file at: {self.cache_path}")
|
||||||
|
|
||||||
|
|
||||||
class MemoryCacheHandler(CacheHandler):
|
class MemoryCacheHandler(CacheHandler):
|
||||||
@ -145,7 +152,7 @@ class DjangoSessionCacheHandler(CacheHandler):
|
|||||||
try:
|
try:
|
||||||
self.request.session['token_info'] = token_info
|
self.request.session['token_info'] = token_info
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Error saving token to cache: " + str(e))
|
logger.warning(f"Error saving token to cache: {e}")
|
||||||
|
|
||||||
|
|
||||||
class FlaskSessionCacheHandler(CacheHandler):
|
class FlaskSessionCacheHandler(CacheHandler):
|
||||||
@ -170,7 +177,7 @@ class FlaskSessionCacheHandler(CacheHandler):
|
|||||||
try:
|
try:
|
||||||
self.session["token_info"] = token_info
|
self.session["token_info"] = token_info
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Error saving token to cache: " + str(e))
|
logger.warning(f"Error saving token to cache: {e}")
|
||||||
|
|
||||||
|
|
||||||
class RedisCacheHandler(CacheHandler):
|
class RedisCacheHandler(CacheHandler):
|
||||||
@ -196,7 +203,7 @@ class RedisCacheHandler(CacheHandler):
|
|||||||
if token_info:
|
if token_info:
|
||||||
return json.loads(token_info)
|
return json.loads(token_info)
|
||||||
except RedisError as e:
|
except RedisError as e:
|
||||||
logger.warning('Error getting token from cache: ' + str(e))
|
logger.warning(f"Error getting token from cache: {e}")
|
||||||
|
|
||||||
return token_info
|
return token_info
|
||||||
|
|
||||||
@ -204,4 +211,36 @@ class RedisCacheHandler(CacheHandler):
|
|||||||
try:
|
try:
|
||||||
self.redis.set(self.key, json.dumps(token_info))
|
self.redis.set(self.key, json.dumps(token_info))
|
||||||
except RedisError as e:
|
except RedisError as e:
|
||||||
logger.warning('Error saving token to cache: ' + str(e))
|
logger.warning(f"Error saving token to cache: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
class MemcacheCacheHandler(CacheHandler):
|
||||||
|
"""A Cache handler that stores the token info in Memcache using the pymemcache client
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, memcache, key=None) -> None:
|
||||||
|
"""
|
||||||
|
Parameters:
|
||||||
|
* memcache: memcache client object provided by pymemcache
|
||||||
|
(https://pymemcache.readthedocs.io/en/latest/getting_started.html)
|
||||||
|
* key: May be supplied, will otherwise be generated
|
||||||
|
(takes precedence over `token_info`)
|
||||||
|
"""
|
||||||
|
self.memcache = memcache
|
||||||
|
self.key = key if key else 'token_info'
|
||||||
|
|
||||||
|
def get_cached_token(self):
|
||||||
|
from pymemcache import MemcacheError
|
||||||
|
try:
|
||||||
|
token_info = self.memcache.get(self.key)
|
||||||
|
if token_info:
|
||||||
|
return json.loads(token_info.decode())
|
||||||
|
except MemcacheError as e:
|
||||||
|
logger.warning(f"Error getting token to cache: {e}")
|
||||||
|
|
||||||
|
def save_token_to_cache(self, token_info):
|
||||||
|
from pymemcache import MemcacheError
|
||||||
|
try:
|
||||||
|
self.memcache.set(self.key, json.dumps(token_info))
|
||||||
|
except MemcacheError as e:
|
||||||
|
logger.warning(f"Error saving token to cache: {e}")
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,8 @@
|
|||||||
class SpotifyException(Exception):
|
class SpotifyBaseException(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
|
||||||
@ -12,5 +16,29 @@ class SpotifyException(Exception):
|
|||||||
self.headers = headers
|
self.headers = headers
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return 'http status: {0}, code:{1} - {2}, reason: {3}'.format(
|
return (f"http status: {self.http_status}, "
|
||||||
self.http_status, self.code, self.msg, self.reason)
|
f"code: {self.code} - {self.msg}, "
|
||||||
|
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)
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"SpotifyClientCredentials",
|
"SpotifyClientCredentials",
|
||||||
"SpotifyOAuth",
|
"SpotifyOAuth",
|
||||||
@ -10,68 +8,43 @@ __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
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
from urllib.parse import parse_qsl, urlparse
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
# Workaround to support both python 2 & 3
|
|
||||||
import six
|
|
||||||
import six.moves.urllib.parse as urllibparse
|
|
||||||
from six.moves.BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
|
||||||
from six.moves.urllib_parse import parse_qsl, urlparse
|
|
||||||
|
|
||||||
from spotipy.cache_handler import CacheFileHandler, CacheHandler
|
from spotipy.cache_handler import CacheFileHandler, CacheHandler
|
||||||
from spotipy.util import CLIENT_CREDS_ENV_VARS, get_host_port, normalize_scope
|
from spotipy.exceptions import SpotifyOauthError, SpotifyStateError
|
||||||
|
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(SpotifyOauthError, self).__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 recieved "
|
|
||||||
+ 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(
|
||||||
six.text_type(client_id + ":" + client_secret).encode("ascii")
|
str(client_id + ":" + client_secret).encode("ascii")
|
||||||
)
|
)
|
||||||
return {"Authorization": "Basic %s" % auth_header.decode("ascii")}
|
return {"Authorization": f"Basic {auth_header.decode('ascii')}"}
|
||||||
|
|
||||||
|
|
||||||
def _ensure_value(value, env_key):
|
def _ensure_value(value, env_key):
|
||||||
env_val = CLIENT_CREDS_ENV_VARS[env_key]
|
env_val = CLIENT_CREDS_ENV_VARS[env_key]
|
||||||
_val = value or os.getenv(env_val)
|
_val = value or os.getenv(env_val)
|
||||||
if _val is None:
|
if _val is None:
|
||||||
msg = "No %s. Pass it or set a %s environment variable." % (
|
msg = f"No {env_key}. Pass it or set a {env_val} environment variable."
|
||||||
env_key,
|
|
||||||
env_val,
|
|
||||||
)
|
|
||||||
raise SpotifyOauthError(msg)
|
raise SpotifyOauthError(msg)
|
||||||
return _val
|
return _val
|
||||||
|
|
||||||
|
|
||||||
class SpotifyAuthBase(object):
|
class SpotifyAuthBase:
|
||||||
def __init__(self, requests_session):
|
def __init__(self, requests_session):
|
||||||
if isinstance(requests_session, requests.Session):
|
if isinstance(requests_session, requests.Session):
|
||||||
self._session = requests_session
|
self._session = requests_session
|
||||||
@ -144,16 +117,14 @@ class SpotifyAuthBase(object):
|
|||||||
error_description = None
|
error_description = None
|
||||||
|
|
||||||
raise SpotifyOauthError(
|
raise SpotifyOauthError(
|
||||||
'error: {0}, error_description: {1}'.format(
|
f'error: {error}, error_description: {error_description}',
|
||||||
error, error_description
|
|
||||||
),
|
|
||||||
error=error,
|
error=error,
|
||||||
error_description=error_description
|
error_description=error_description
|
||||||
)
|
)
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
"""Make sure the connection (pool) gets closed"""
|
"""Make sure the connection (pool) gets closed"""
|
||||||
if isinstance(self._session, requests.Session):
|
if getattr(self, "_session", None) and isinstance(self._session, REQUESTS_SESSION):
|
||||||
self._session.close()
|
self._session.close()
|
||||||
|
|
||||||
|
|
||||||
@ -196,7 +167,7 @@ class SpotifyClientCredentials(SpotifyAuthBase):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super(SpotifyClientCredentials, self).__init__(requests_session)
|
super().__init__(requests_session)
|
||||||
|
|
||||||
self.client_id = client_id
|
self.client_id = client_id
|
||||||
self.client_secret = client_secret
|
self.client_secret = client_secret
|
||||||
@ -216,7 +187,7 @@ class SpotifyClientCredentials(SpotifyAuthBase):
|
|||||||
Else fetches a new token and returns it
|
Else fetches a new token and returns it
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
- as_dict - a boolean indicating if returning the access token
|
- as_dict: (deprecated) 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.
|
||||||
"""
|
"""
|
||||||
@ -248,10 +219,8 @@ class SpotifyClientCredentials(SpotifyAuthBase):
|
|||||||
self.client_id, self.client_secret
|
self.client_id, self.client_secret
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(f"Sending POST request to {self.OAUTH_TOKEN_URL} with Headers: "
|
||||||
"sending POST request to %s with Headers: %s and Body: %r",
|
f"{headers} and Body: {payload}")
|
||||||
self.OAUTH_TOKEN_URL, headers, payload
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self._session.post(
|
response = self._session.post(
|
||||||
@ -319,7 +288,7 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
* requests_session: A Requests session
|
* requests_session: A Requests session
|
||||||
* requests_timeout: Optional, tell Requests to stop waiting for a response after
|
* requests_timeout: Optional, tell Requests to stop waiting for a response after
|
||||||
a given number of seconds
|
a given number of seconds
|
||||||
* open_browser: Optional, whether or not the web browser should be opened to
|
* open_browser: Optional, whether the web browser should be opened to
|
||||||
authorize a user
|
authorize a user
|
||||||
* cache_handler: An instance of the `CacheHandler` class to handle
|
* cache_handler: An instance of the `CacheHandler` class to handle
|
||||||
getting and saving cached authorization tokens.
|
getting and saving cached authorization tokens.
|
||||||
@ -327,7 +296,7 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
(takes precedence over `cache_path` and `username`)
|
(takes precedence over `cache_path` and `username`)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super(SpotifyOAuth, self).__init__(requests_session)
|
super().__init__(requests_session)
|
||||||
|
|
||||||
self.client_id = client_id
|
self.client_id = client_id
|
||||||
self.client_secret = client_secret
|
self.client_secret = client_secret
|
||||||
@ -402,7 +371,7 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
|
|
||||||
urlparams = urllibparse.urlencode(payload)
|
urlparams = urllibparse.urlencode(payload)
|
||||||
|
|
||||||
return "%s?%s" % (self.OAUTH_AUTHORIZE_URL, urlparams)
|
return f"{self.OAUTH_AUTHORIZE_URL}?{urlparams}"
|
||||||
|
|
||||||
def parse_response_code(self, url):
|
def parse_response_code(self, url):
|
||||||
""" Parse the response code in the given response url
|
""" Parse the response code in the given response url
|
||||||
@ -421,8 +390,7 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
query_s = urlparse(url).query
|
query_s = urlparse(url).query
|
||||||
form = dict(parse_qsl(query_s))
|
form = dict(parse_qsl(query_s))
|
||||||
if "error" in form:
|
if "error" in form:
|
||||||
raise SpotifyOauthError("Received error from auth server: "
|
raise SpotifyOauthError(f"Received error from auth server: {form['error']}",
|
||||||
"{}".format(form["error"]),
|
|
||||||
error=form["error"])
|
error=form["error"])
|
||||||
return tuple(form.get(param) for param in ["state", "code"])
|
return tuple(form.get(param) for param in ["state", "code"])
|
||||||
|
|
||||||
@ -433,9 +401,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("Opened %s in your browser", auth_url)
|
logger.info(f"Opened {auth_url} in your browser")
|
||||||
except webbrowser.Error:
|
except webbrowser.Error:
|
||||||
logger.error("Please navigate here: %s", auth_url)
|
logger.error(f"Please navigate here: {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:
|
||||||
@ -444,8 +412,8 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
else:
|
else:
|
||||||
url = self.get_authorize_url()
|
url = self.get_authorize_url()
|
||||||
prompt = (
|
prompt = (
|
||||||
"Go to the following URL: {}\n"
|
f"Go to the following URL: {url}\n"
|
||||||
"Enter the URL you were redirected to: ".format(url)
|
"Enter the URL you were redirected to: "
|
||||||
)
|
)
|
||||||
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)
|
||||||
@ -477,6 +445,17 @@ 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
|
||||||
|
|
||||||
@ -489,12 +468,11 @@ 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('Using `%s` as redirect URI without a port. '
|
logger.warning(f'Using `{redirect_host}` as redirect URI without a port. '
|
||||||
'Specify a port (e.g. `%s:8080`) to allow '
|
f'Specify a port (e.g. `{redirect_host}: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)
|
||||||
|
|
||||||
@ -507,8 +485,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 - a boolean indicating if returning the access token
|
- as_dict: (deprecated) 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.
|
||||||
"""
|
"""
|
||||||
@ -542,10 +520,8 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
|
|
||||||
headers = self._make_authorization_headers()
|
headers = self._make_authorization_headers()
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(f"Sending POST request to {self.OAUTH_TOKEN_URL} with Headers: "
|
||||||
"sending POST request to %s with Headers: %s and Body: %r",
|
f"{headers} and Body: {payload}")
|
||||||
self.OAUTH_TOKEN_URL, headers, payload
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self._session.post(
|
response = self._session.post(
|
||||||
@ -572,10 +548,8 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
|
|
||||||
headers = self._make_authorization_headers()
|
headers = self._make_authorization_headers()
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(f"Sending POST request to {self.OAUTH_TOKEN_URL} with Headers: "
|
||||||
"sending POST request to %s with Headers: %s and Body: %r",
|
f"{headers} and Body: {payload}")
|
||||||
self.OAUTH_TOKEN_URL, headers, payload
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self._session.post(
|
response = self._session.post(
|
||||||
@ -605,6 +579,11 @@ 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 " +
|
||||||
@ -629,7 +608,7 @@ class SpotifyPKCE(SpotifyAuthBase):
|
|||||||
""" Implements PKCE Authorization Flow for client apps
|
""" Implements PKCE Authorization Flow for client apps
|
||||||
|
|
||||||
This auth manager enables *user and non-user* endpoints with only
|
This auth manager enables *user and non-user* endpoints with only
|
||||||
a client secret, redirect uri, and username. When the app requests
|
a client ID, redirect URI, and username. When the app requests
|
||||||
an access token for the first time, the user is prompted to
|
an access token for the first time, the user is prompted to
|
||||||
authorize the new client app. After authorizing the app, the client
|
authorize the new client app. After authorizing the app, the client
|
||||||
app is then given both access and refresh tokens. This is the
|
app is then given both access and refresh tokens. This is the
|
||||||
@ -669,7 +648,7 @@ class SpotifyPKCE(SpotifyAuthBase):
|
|||||||
* requests_timeout: Optional, tell Requests to stop waiting for a response after
|
* requests_timeout: Optional, tell Requests to stop waiting for a response after
|
||||||
a given number of seconds
|
a given number of seconds
|
||||||
* requests_session: A Requests session
|
* requests_session: A Requests session
|
||||||
* open_browser: Optional, whether or not the web browser should be opened to
|
* open_browser: Optional, whether the web browser should be opened to
|
||||||
authorize a user
|
authorize a user
|
||||||
* cache_handler: An instance of the `CacheHandler` class to handle
|
* cache_handler: An instance of the `CacheHandler` class to handle
|
||||||
getting and saving cached authorization tokens.
|
getting and saving cached authorization tokens.
|
||||||
@ -677,7 +656,7 @@ class SpotifyPKCE(SpotifyAuthBase):
|
|||||||
(takes precedence over `cache_path` and `username`)
|
(takes precedence over `cache_path` and `username`)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super(SpotifyPKCE, self).__init__(requests_session)
|
super().__init__(requests_session)
|
||||||
self.client_id = client_id
|
self.client_id = client_id
|
||||||
self.redirect_uri = redirect_uri
|
self.redirect_uri = redirect_uri
|
||||||
self.state = state
|
self.state = state
|
||||||
@ -727,15 +706,8 @@ class SpotifyPKCE(SpotifyAuthBase):
|
|||||||
length = random.randint(33, 96)
|
length = random.randint(33, 96)
|
||||||
|
|
||||||
# The seeded length generates between a 44 and 128 base64 characters encoded string
|
# The seeded length generates between a 44 and 128 base64 characters encoded string
|
||||||
try:
|
import secrets
|
||||||
import secrets
|
return secrets.token_urlsafe(length)
|
||||||
verifier = secrets.token_urlsafe(length)
|
|
||||||
except ImportError: # For python 3.5 support
|
|
||||||
import base64
|
|
||||||
import os
|
|
||||||
rand_bytes = os.urandom(length)
|
|
||||||
verifier = base64.urlsafe_b64encode(rand_bytes).decode('utf-8').replace('=', '')
|
|
||||||
return verifier
|
|
||||||
|
|
||||||
def _get_code_challenge(self):
|
def _get_code_challenge(self):
|
||||||
""" Spotify PCKE code challenge - See step 1 of the reference guide below
|
""" Spotify PCKE code challenge - See step 1 of the reference guide below
|
||||||
@ -766,15 +738,15 @@ class SpotifyPKCE(SpotifyAuthBase):
|
|||||||
if state is not None:
|
if state is not None:
|
||||||
payload["state"] = state
|
payload["state"] = state
|
||||||
urlparams = urllibparse.urlencode(payload)
|
urlparams = urllibparse.urlencode(payload)
|
||||||
return "%s?%s" % (self.OAUTH_AUTHORIZE_URL, urlparams)
|
return f"{self.OAUTH_AUTHORIZE_URL}?{urlparams}"
|
||||||
|
|
||||||
def _open_auth_url(self, state=None):
|
def _open_auth_url(self, state=None):
|
||||||
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("Opened %s in your browser", auth_url)
|
logger.info(f"Opened {auth_url} in your browser")
|
||||||
except webbrowser.Error:
|
except webbrowser.Error:
|
||||||
logger.error("Please navigate here: %s", auth_url)
|
logger.error(f"Please navigate here: {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 '
|
||||||
@ -789,6 +761,17 @@ 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")
|
||||||
@ -798,12 +781,11 @@ 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('Using `%s` as redirect URI without a port. '
|
logger.warning(f'Using `{redirect_host}` as redirect URI without a port. '
|
||||||
'Specify a port (e.g. `%s:8080`) to allow '
|
f'Specify a port (e.g. `{redirect_host}: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):
|
||||||
@ -817,7 +799,7 @@ class SpotifyPKCE(SpotifyAuthBase):
|
|||||||
if server.auth_code is not None:
|
if server.auth_code is not None:
|
||||||
return server.auth_code
|
return server.auth_code
|
||||||
elif server.error is not None:
|
elif server.error is not None:
|
||||||
raise SpotifyOauthError("Received error from OAuth server: {}".format(server.error))
|
raise SpotifyOauthError(f"Received error from OAuth server: {server.error}")
|
||||||
else:
|
else:
|
||||||
raise SpotifyOauthError("Server listening on localhost has not been accessed")
|
raise SpotifyOauthError("Server listening on localhost has not been accessed")
|
||||||
|
|
||||||
@ -827,10 +809,8 @@ class SpotifyPKCE(SpotifyAuthBase):
|
|||||||
prompt = "Enter the URL you were redirected to: "
|
prompt = "Enter the URL you were redirected to: "
|
||||||
else:
|
else:
|
||||||
url = self.get_authorize_url()
|
url = self.get_authorize_url()
|
||||||
prompt = (
|
prompt = (f"Go to the following URL: {url}\n"
|
||||||
"Go to the following URL: {}\n"
|
f"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 = 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:
|
||||||
@ -906,10 +886,8 @@ class SpotifyPKCE(SpotifyAuthBase):
|
|||||||
|
|
||||||
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(f"Sending POST request to {self.OAUTH_TOKEN_URL} with Headers: "
|
||||||
"sending POST request to %s with Headers: %s and Body: %r",
|
f"{headers} and Body: {payload}")
|
||||||
self.OAUTH_TOKEN_URL, headers, payload
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self._session.post(
|
response = self._session.post(
|
||||||
@ -937,10 +915,8 @@ class SpotifyPKCE(SpotifyAuthBase):
|
|||||||
|
|
||||||
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(f"Sending POST request to {self.OAUTH_TOKEN_URL} with Headers: "
|
||||||
"sending POST request to %s with Headers: %s and Body: %r",
|
f"{headers} and Body: {payload}")
|
||||||
self.OAUTH_TOKEN_URL, headers, payload
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self._session.post(
|
response = self._session.post(
|
||||||
@ -1012,7 +988,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
|
|||||||
Authentication Code flow. Use the SpotifyPKCE auth manager instead
|
Authentication Code flow. Use the SpotifyPKCE auth manager instead
|
||||||
of SpotifyImplicitGrant.
|
of SpotifyImplicitGrant.
|
||||||
|
|
||||||
SpotifyPKCE contains all of the functionality of
|
SpotifyPKCE contains all the functionality of
|
||||||
SpotifyImplicitGrant, plus automatic response retrieval and
|
SpotifyImplicitGrant, plus automatic response retrieval and
|
||||||
refreshable tokens. Only a few replacements need to be made:
|
refreshable tokens. Only a few replacements need to be made:
|
||||||
|
|
||||||
@ -1062,7 +1038,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("The OAuth standard no longer recommends the Implicit "
|
logger.warning("Spotify is deprecating 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 "
|
||||||
@ -1160,7 +1136,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
|
|||||||
|
|
||||||
urlparams = urllibparse.urlencode(payload)
|
urlparams = urllibparse.urlencode(payload)
|
||||||
|
|
||||||
return "%s?%s" % (self.OAUTH_AUTHORIZE_URL, urlparams)
|
return f"{self.OAUTH_AUTHORIZE_URL}?{urlparams}"
|
||||||
|
|
||||||
def parse_response_token(self, url, state=None):
|
def parse_response_token(self, url, state=None):
|
||||||
""" Parse the response code in the given response url """
|
""" Parse the response code in the given response url """
|
||||||
@ -1180,8 +1156,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
|
|||||||
form = dict(i.split('=') for i
|
form = dict(i.split('=') for i
|
||||||
in (fragment_s or query_s or url).split('&'))
|
in (fragment_s or query_s or url).split('&'))
|
||||||
if "error" in form:
|
if "error" in form:
|
||||||
raise SpotifyOauthError("Received error from auth server: "
|
raise SpotifyOauthError(f"Received error from auth server: {form['error']}",
|
||||||
"{}".format(form["error"]),
|
|
||||||
state=form["state"])
|
state=form["state"])
|
||||||
if "expires_in" in form:
|
if "expires_in" in form:
|
||||||
form["expires_in"] = int(form["expires_in"])
|
form["expires_in"] = int(form["expires_in"])
|
||||||
@ -1192,9 +1167,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("Opened %s in your browser", auth_url)
|
logger.info(f"Opened {auth_url} in your browser")
|
||||||
except webbrowser.Error:
|
except webbrowser.Error:
|
||||||
logger.error("Please navigate here: %s", auth_url)
|
logger.error(f"Please navigate here: {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 """
|
||||||
@ -1235,6 +1210,11 @@ 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 " +
|
||||||
@ -1273,24 +1253,26 @@ 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 = "failed ({})".format(self.server.error)
|
status = f"failed ({html.escape(str(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("""<html>
|
self._write(f"""<html>
|
||||||
<script>
|
<script>
|
||||||
window.close()
|
window.close()
|
||||||
</script>
|
</script>
|
||||||
<body>
|
<body>
|
||||||
<h1>Authentication status: {}</h1>
|
<h1>Authentication status: {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();">Close Window</button>
|
<button class="closeButton" style="cursor: pointer" onclick="window.close();">
|
||||||
|
Close Window
|
||||||
|
</button>
|
||||||
</body>
|
</body>
|
||||||
</html>""".format(status))
|
</html>""")
|
||||||
|
|
||||||
def _write(self, text):
|
def _write(self, text):
|
||||||
return self.wfile.write(text.encode("utf-8"))
|
return self.wfile.write(text.encode("utf-8"))
|
||||||
|
|||||||
@ -1,16 +1,20 @@
|
|||||||
# -*- coding: utf-8 -*-
|
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",
|
||||||
@ -19,6 +23,9 @@ 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,
|
||||||
@ -30,6 +37,22 @@ 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: "
|
||||||
@ -37,22 +60,7 @@ 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")
|
||||||
@ -64,7 +72,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:
|
||||||
@ -109,6 +117,12 @@ 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)
|
||||||
@ -120,6 +134,14 @@ 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(',')
|
||||||
@ -128,8 +150,36 @@ 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)
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import base64
|
import base64
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,10 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from spotipy import (
|
|
||||||
Spotify,
|
|
||||||
SpotifyClientCredentials,
|
|
||||||
SpotifyException
|
|
||||||
)
|
|
||||||
import spotipy
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
import spotipy
|
||||||
|
from spotipy import Spotify, SpotifyClientCredentials, SpotifyException
|
||||||
|
|
||||||
|
|
||||||
class AuthTestSpotipy(unittest.TestCase):
|
class AuthTestSpotipy(unittest.TestCase):
|
||||||
"""
|
"""
|
||||||
@ -39,12 +35,19 @@ 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'
|
||||||
@ -55,49 +58,41 @@ 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'
|
||||||
|
dune_id = '7iHfbu1YPACw6oZPAFJtqe'
|
||||||
|
dune_url = 'https://open.spotify.com/audiobook/7iHfbu1YPACw6oZPAFJtqe'
|
||||||
|
two_books = [
|
||||||
|
'spotify:audiobook:7iHfbu1YPACw6oZPAFJtqe',
|
||||||
|
'spotify:audiobook:67VtmjZitn25TWocsyAEyh']
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(self):
|
def setUpClass(self):
|
||||||
self.spotify = Spotify(
|
self.spotify = Spotify(
|
||||||
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')
|
||||||
@ -154,17 +149,6 @@ 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)
|
||||||
@ -221,6 +205,87 @@ class AuthTestSpotipy(unittest.TestCase):
|
|||||||
total_limited_results += len(results_limited[country]['artists']['items'])
|
total_limited_results += len(results_limited[country]['artists']['items'])
|
||||||
self.assertTrue(total_limited_results <= total)
|
self.assertTrue(total_limited_results <= total)
|
||||||
|
|
||||||
|
def test_multiple_types_search_with_multiple_markets(self):
|
||||||
|
total = 14
|
||||||
|
|
||||||
|
countries_list = ['GB', 'US', 'AU']
|
||||||
|
countries_tuple = ('GB', 'US', 'AU')
|
||||||
|
|
||||||
|
results_multiple = self.spotify.search_markets(q='abba', type='artist,track',
|
||||||
|
markets=countries_list)
|
||||||
|
results_all = self.spotify.search_markets(q='abba', type='artist,track')
|
||||||
|
results_tuple = self.spotify.search_markets(q='abba', type='artist,track',
|
||||||
|
markets=countries_tuple)
|
||||||
|
results_limited = self.spotify.search_markets(q='abba', limit=3, type='artist,track',
|
||||||
|
markets=countries_list, total=total)
|
||||||
|
|
||||||
|
# Asserts 'artists' property is present in all responses
|
||||||
|
self.assertTrue(
|
||||||
|
all('artists' in results_multiple[country] for country in results_multiple))
|
||||||
|
self.assertTrue(all('artists' in results_all[country] for country in results_all))
|
||||||
|
self.assertTrue(all('artists' in results_tuple[country] for country in results_tuple))
|
||||||
|
self.assertTrue(all('artists' in results_limited[country] for country in results_limited))
|
||||||
|
|
||||||
|
# Asserts 'tracks' property is present in all responses
|
||||||
|
self.assertTrue(
|
||||||
|
all('tracks' in results_multiple[country] for country in results_multiple))
|
||||||
|
self.assertTrue(all('tracks' in results_all[country] for country in results_all))
|
||||||
|
self.assertTrue(all('tracks' in results_tuple[country] for country in results_tuple))
|
||||||
|
self.assertTrue(all('tracks' in results_limited[country] for country in results_limited))
|
||||||
|
|
||||||
|
# Asserts 'artists' list is nonempty in unlimited searches
|
||||||
|
self.assertTrue(
|
||||||
|
all(len(results_multiple[country]['artists']['items']) > 0 for country in
|
||||||
|
results_multiple))
|
||||||
|
self.assertTrue(all(len(results_all[country]['artists']
|
||||||
|
['items']) > 0 for country in results_all))
|
||||||
|
self.assertTrue(
|
||||||
|
all(len(results_tuple[country]['artists']['items']) > 0 for country in results_tuple))
|
||||||
|
|
||||||
|
# Asserts 'tracks' list is nonempty in unlimited searches
|
||||||
|
self.assertTrue(
|
||||||
|
all(len(results_multiple[country]['tracks']['items']) > 0 for country in
|
||||||
|
results_multiple))
|
||||||
|
self.assertTrue(all(len(results_all[country]['tracks']
|
||||||
|
['items']) > 0 for country in results_all))
|
||||||
|
self.assertTrue(all(len(results_tuple[country]['tracks']
|
||||||
|
['items']) > 0 for country in results_tuple))
|
||||||
|
|
||||||
|
# Asserts artist name is the first artist result in all searches
|
||||||
|
self.assertTrue(all(results_multiple[country]['artists']['items']
|
||||||
|
[0]['name'] == 'ABBA' for country in results_multiple))
|
||||||
|
self.assertTrue(all(results_all[country]['artists']['items']
|
||||||
|
[0]['name'] == 'ABBA' for country in results_all))
|
||||||
|
self.assertTrue(all(results_tuple[country]['artists']['items']
|
||||||
|
[0]['name'] == 'ABBA' for country in results_tuple))
|
||||||
|
self.assertTrue(all(results_limited[country]['artists']['items']
|
||||||
|
[0]['name'] == 'ABBA' for country in results_limited))
|
||||||
|
|
||||||
|
# Asserts track name is present in responses from specified markets
|
||||||
|
self.assertTrue(all('Dancing Queen' in
|
||||||
|
[item['name'] for item in results_multiple[country]['tracks']['items']]
|
||||||
|
for country in results_multiple))
|
||||||
|
self.assertTrue(all('Dancing Queen' in
|
||||||
|
[item['name'] for item in results_tuple[country]['tracks']['items']]
|
||||||
|
for country in results_tuple))
|
||||||
|
|
||||||
|
# Asserts expected number of items are returned based on the total
|
||||||
|
# 3 artists + 3 tracks = 6 items returned from first market
|
||||||
|
# 3 artists + 3 tracks = 6 items returned from second market
|
||||||
|
# 2 artists + 0 tracks = 2 items returned from third market
|
||||||
|
# 14 items returned total
|
||||||
|
self.assertEqual(len(results_limited['GB']['artists']['items']), 3)
|
||||||
|
self.assertEqual(len(results_limited['GB']['tracks']['items']), 3)
|
||||||
|
self.assertEqual(len(results_limited['US']['artists']['items']), 3)
|
||||||
|
self.assertEqual(len(results_limited['US']['tracks']['items']), 3)
|
||||||
|
self.assertEqual(len(results_limited['AU']['artists']['items']), 2)
|
||||||
|
self.assertEqual(len(results_limited['AU']['tracks']['items']), 0)
|
||||||
|
|
||||||
|
item_count = sum([len(market_result['artists']['items']) + len(market_result['tracks']
|
||||||
|
['items']) for market_result in results_limited.values()])
|
||||||
|
|
||||||
|
self.assertEqual(item_count, total)
|
||||||
|
|
||||||
def test_artist_albums(self):
|
def test_artist_albums(self):
|
||||||
results = self.spotify.artist_albums(self.weezer_urn)
|
results = self.spotify.artist_albums(self.weezer_urn)
|
||||||
self.assertTrue('items' in results)
|
self.assertTrue('items' in results)
|
||||||
@ -228,7 +293,7 @@ class AuthTestSpotipy(unittest.TestCase):
|
|||||||
|
|
||||||
def find_album():
|
def find_album():
|
||||||
for album in results['items']:
|
for album in results['items']:
|
||||||
if album['name'] == 'Death to False Metal':
|
if 'Weezer' in album['name']: # Weezer has many albums containing Weezer
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -374,3 +439,28 @@ class AuthTestSpotipy(unittest.TestCase):
|
|||||||
self.assertTrue(isinstance(markets, list))
|
self.assertTrue(isinstance(markets, list))
|
||||||
self.assertIn("US", markets)
|
self.assertIn("US", markets)
|
||||||
self.assertIn("GB", markets)
|
self.assertIn("GB", markets)
|
||||||
|
|
||||||
|
def test_get_audiobook(self):
|
||||||
|
audiobook = self.spotify.get_audiobook(self.dune_urn, market="US")
|
||||||
|
self.assertTrue(audiobook['name'] ==
|
||||||
|
'Dune: Book One in the Dune Chronicles')
|
||||||
|
|
||||||
|
def test_get_audiobook_bad_urn(self):
|
||||||
|
with self.assertRaises(SpotifyException):
|
||||||
|
self.spotify.get_audiobook("bogus_urn", market="US")
|
||||||
|
|
||||||
|
def test_get_audiobooks(self):
|
||||||
|
results = self.spotify.get_audiobooks(self.two_books, market="US")
|
||||||
|
self.assertTrue('audiobooks' in results)
|
||||||
|
self.assertTrue(len(results['audiobooks']) == 2)
|
||||||
|
self.assertTrue(results['audiobooks'][0]['name']
|
||||||
|
== 'Dune: Book One in the Dune Chronicles')
|
||||||
|
self.assertTrue(results['audiobooks'][1]['name'] == 'The Helper')
|
||||||
|
|
||||||
|
def test_get_audiobook_chapters(self):
|
||||||
|
results = self.spotify.get_audiobook_chapters(
|
||||||
|
self.dune_urn, market="US", limit=10, offset=5)
|
||||||
|
self.assertTrue('items' in results)
|
||||||
|
self.assertTrue(len(results['items']) == 10)
|
||||||
|
self.assertTrue(results['items'][0]['chapter_number'] == 5)
|
||||||
|
self.assertTrue(results['items'][9]['chapter_number'] == 14)
|
||||||
|
|||||||
@ -1,14 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from spotipy import (
|
|
||||||
CLIENT_CREDS_ENV_VARS as CCEV,
|
|
||||||
prompt_for_user_token,
|
|
||||||
Spotify,
|
|
||||||
SpotifyException,
|
|
||||||
SpotifyImplicitGrant,
|
|
||||||
SpotifyPKCE
|
|
||||||
)
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
from spotipy import CLIENT_CREDS_ENV_VARS as CCEV
|
||||||
|
from spotipy import (Spotify, SpotifyException, SpotifyImplicitGrant,
|
||||||
|
SpotifyPKCE, prompt_for_user_token)
|
||||||
from tests import helpers
|
from tests import helpers
|
||||||
|
|
||||||
|
|
||||||
@ -253,7 +248,7 @@ class SpotipyLibraryApiTests(unittest.TestCase):
|
|||||||
tracks = self.spotify.current_user_saved_tracks()
|
tracks = self.spotify.current_user_saved_tracks()
|
||||||
self.assertGreaterEqual(len(tracks['items']), 0)
|
self.assertGreaterEqual(len(tracks['items']), 0)
|
||||||
|
|
||||||
def test_current_user_save_and_unsave_tracks(self):
|
def test_current_user_save_tracks(self):
|
||||||
tracks = self.spotify.current_user_saved_tracks()
|
tracks = self.spotify.current_user_saved_tracks()
|
||||||
total = tracks['total']
|
total = tracks['total']
|
||||||
self.spotify.current_user_saved_tracks_add(self.four_tracks)
|
self.spotify.current_user_saved_tracks_add(self.four_tracks)
|
||||||
@ -266,6 +261,19 @@ class SpotipyLibraryApiTests(unittest.TestCase):
|
|||||||
self.four_tracks)
|
self.four_tracks)
|
||||||
tracks = self.spotify.current_user_saved_tracks()
|
tracks = self.spotify.current_user_saved_tracks()
|
||||||
new_total = tracks['total']
|
new_total = tracks['total']
|
||||||
|
|
||||||
|
def test_current_user_unsave_tracks(self):
|
||||||
|
tracks = self.spotify.current_user_saved_tracks()
|
||||||
|
total = tracks['total']
|
||||||
|
self.spotify.current_user_saved_tracks_add(self.four_tracks)
|
||||||
|
|
||||||
|
tracks = self.spotify.current_user_saved_tracks()
|
||||||
|
new_total = tracks['total']
|
||||||
|
|
||||||
|
self.spotify.current_user_saved_tracks_delete(
|
||||||
|
self.four_tracks)
|
||||||
|
tracks = self.spotify.current_user_saved_tracks()
|
||||||
|
new_total = tracks['total']
|
||||||
self.assertEqual(new_total, total)
|
self.assertEqual(new_total, total)
|
||||||
|
|
||||||
def test_current_user_saved_albums(self):
|
def test_current_user_saved_albums(self):
|
||||||
@ -380,33 +388,6 @@ 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)
|
||||||
@ -419,10 +400,6 @@ 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
|
||||||
@ -545,3 +522,44 @@ 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)
|
||||||
|
|||||||
@ -1,19 +1,13 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import unittest
|
import unittest
|
||||||
|
import unittest.mock as mock
|
||||||
|
import urllib.parse as urllibparse
|
||||||
|
|
||||||
import six.moves.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,
|
||||||
from spotipy.oauth2 import SpotifyStateError
|
SpotifyStateError)
|
||||||
|
|
||||||
try:
|
|
||||||
import unittest.mock as mock
|
|
||||||
except ImportError:
|
|
||||||
import mock
|
|
||||||
|
|
||||||
patch = mock.patch
|
patch = mock.patch
|
||||||
DEFAULT = mock.DEFAULT
|
DEFAULT = mock.DEFAULT
|
||||||
@ -58,18 +52,21 @@ 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(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(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)
|
opener.assert_called_with(path, encoding='utf-8')
|
||||||
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)
|
||||||
@ -79,13 +76,15 @@ 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 = token_file
|
opener.return_value.__enter__ = mock.Mock(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)
|
||||||
@ -93,7 +92,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)
|
opener.assert_any_call(path, encoding='utf-8')
|
||||||
|
|
||||||
@patch.multiple(SpotifyOAuth,
|
@patch.multiple(SpotifyOAuth,
|
||||||
is_token_expired=DEFAULT, refresh_access_token=DEFAULT)
|
is_token_expired=DEFAULT, refresh_access_token=DEFAULT)
|
||||||
@ -105,29 +104,35 @@ 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)
|
||||||
|
|
||||||
opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
|
token_file = _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)
|
opener.assert_called_with(path, encoding='utf-8')
|
||||||
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')
|
opener.assert_called_with(path, 'w', encoding='utf-8')
|
||||||
self.assertTrue(fi.write.called)
|
self.assertTrue(fi.write.called)
|
||||||
|
|
||||||
@patch('spotipy.cache_handler.open', create=True)
|
@patch('spotipy.cache_handler.open', create=True)
|
||||||
@ -138,11 +143,13 @@ 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')
|
opener.assert_called_with(path, 'w', encoding='utf-8')
|
||||||
self.assertTrue(fi.write.called)
|
self.assertTrue(fi.write.called)
|
||||||
|
|
||||||
def test_cache_handler(self):
|
def test_cache_handler(self):
|
||||||
@ -258,32 +265,38 @@ class ImplicitGrantCacheTest(unittest.TestCase):
|
|||||||
path = ".cache-username"
|
path = ".cache-username"
|
||||||
tok = _make_fake_token(1, 1, scope)
|
tok = _make_fake_token(1, 1, scope)
|
||||||
|
|
||||||
opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
|
token_file = _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)
|
opener.assert_called_with(path, encoding='utf-8')
|
||||||
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)
|
opener.assert_any_call(path, encoding='utf-8')
|
||||||
self.assertIsNone(cached_tok)
|
self.assertIsNone(cached_tok)
|
||||||
|
|
||||||
@patch.object(SpotifyImplicitGrant, "is_token_expired", DEFAULT)
|
@patch.object(SpotifyImplicitGrant, "is_token_expired", DEFAULT)
|
||||||
@ -294,13 +307,16 @@ 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)
|
||||||
|
|
||||||
opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
|
token_file = _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)
|
opener.assert_called_with(path, encoding='utf-8')
|
||||||
self.assertIsNone(cached_tok)
|
self.assertIsNone(cached_tok)
|
||||||
|
|
||||||
@patch('spotipy.cache_handler.open', create=True)
|
@patch('spotipy.cache_handler.open', create=True)
|
||||||
@ -312,10 +328,12 @@ 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')
|
opener.assert_called_with(path, 'w', encoding='utf-8')
|
||||||
self.assertTrue(fi.write.called)
|
self.assertTrue(fi.write.called)
|
||||||
|
|
||||||
@patch('spotipy.cache_handler.open', create=True)
|
@patch('spotipy.cache_handler.open', create=True)
|
||||||
@ -326,11 +344,13 @@ 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')
|
opener.assert_called_with(path, 'w', encoding='utf-8')
|
||||||
self.assertTrue(fi.write.called)
|
self.assertTrue(fi.write.called)
|
||||||
|
|
||||||
|
|
||||||
@ -395,14 +415,17 @@ class SpotifyPKCECacheTest(unittest.TestCase):
|
|||||||
path = ".cache-username"
|
path = ".cache-username"
|
||||||
tok = _make_fake_token(1, 1, scope)
|
tok = _make_fake_token(1, 1, scope)
|
||||||
|
|
||||||
opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
|
token_file = _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)
|
opener.assert_called_with(path, encoding='utf-8')
|
||||||
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)
|
||||||
@ -418,7 +441,8 @@ 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 = token_file
|
opener.return_value.__enter__ = mock.Mock(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)
|
||||||
@ -426,7 +450,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)
|
opener.assert_any_call(path, encoding='utf-8')
|
||||||
|
|
||||||
@patch.multiple(SpotifyPKCE,
|
@patch.multiple(SpotifyPKCE,
|
||||||
is_token_expired=DEFAULT, refresh_access_token=DEFAULT)
|
is_token_expired=DEFAULT, refresh_access_token=DEFAULT)
|
||||||
@ -438,13 +462,16 @@ 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)
|
||||||
|
|
||||||
opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
|
token_file = _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)
|
opener.assert_called_with(path, encoding='utf-8')
|
||||||
self.assertIsNone(cached_tok)
|
self.assertIsNone(cached_tok)
|
||||||
self.assertEqual(refresh_access_token.call_count, 0)
|
self.assertEqual(refresh_access_token.call_count, 0)
|
||||||
|
|
||||||
@ -456,11 +483,12 @@ 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')
|
opener.assert_called_with(path, 'w', encoding='utf-8')
|
||||||
self.assertTrue(fi.write.called)
|
self.assertTrue(fi.write.called)
|
||||||
|
|
||||||
@patch('spotipy.cache_handler.open', create=True)
|
@patch('spotipy.cache_handler.open', create=True)
|
||||||
@ -471,11 +499,13 @@ 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')
|
opener.assert_called_with(path, 'w', encoding='utf-8')
|
||||||
self.assertTrue(fi.write.called)
|
self.assertTrue(fi.write.called)
|
||||||
|
|
||||||
|
|
||||||
@ -492,8 +522,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 hashlib
|
|
||||||
import base64
|
import base64
|
||||||
|
import hashlib
|
||||||
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,
|
||||||
|
|||||||
11
tox.ini
11
tox.ini
@ -1,15 +1,16 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist = py27,py34
|
envlist = py3{8,9,10,11,12}
|
||||||
[testenv]
|
[testenv]
|
||||||
deps=
|
deps=
|
||||||
requests
|
requests
|
||||||
six
|
commands=python -m unittest discover -v tests/unit
|
||||||
py27: mock
|
|
||||||
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
|
||||||
Loading…
Reference in New Issue
Block a user