Compare commits

...

151 Commits

Author SHA1 Message Date
Fabian Wunsch
351d4223d0
Reduce artist_album limit (#1232)
Reduces the limit so that a query called without explicit limit is valid
with the new changes to the spotify API.
2026-03-11 22:07:45 +01:00
Niko
6787aabe0f
Merge pull request #1230 from spotipy-dev/pr/1228
* #1227 - Updated methods related to API changes

- Updated /tracks endpoints to /items
- Switching IDs to URIs for /me/library endpoint
- Fixed playlist limit to 50 (according to API)
- Added warnings for deprecated methods

* #1227 - Formatted code and updated changelog

* #1227 - Improved HTTP URLs computation with kwargs

* bump version to 2.26.0

---------
2026-03-03 17:34:10 +01:00
Cédric Tonarelli
fa7049ea1d
Fix: Web API Changes of February 2026 (#1228)
* #1227 - Updated methods related to API changes

- Updated /tracks endpoints to /items
- Switching IDs to URIs for /me/library endpoint
- Fixed playlist limit to 50 (according to API)
- Added warnings for deprecated methods

* #1227 - Formatted code and updated changelog

* #1227 - Improved HTTP URLs computation with kwargs

* bump version to 2.26.0

---------

Co-authored-by: Niko <github@dieserniko.link>
2026-03-03 17:30:58 +01:00
dependabot[bot]
c52a29f6d2
Update sphinx-rtd-theme requirement from ~=3.0.2 to ~=3.1.0 (#1226)
Updates the requirements on [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme) to permit the latest version.
- [Changelog](https://github.com/readthedocs/sphinx_rtd_theme/blob/master/docs/changelog.rst)
- [Commits](https://github.com/readthedocs/sphinx_rtd_theme/compare/3.0.2...3.1.0)

---
updated-dependencies:
- dependency-name: sphinx-rtd-theme
  dependency-version: 3.1.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-16 20:28:54 +01:00
Niko
179d3e486c
Update skipLabel to skipLabels in workflow 2025-12-23 18:43:11 +01:00
Stephane Bruckert
9119b6a070 Fix lint workflow 2025-11-27 07:33:35 +00:00
Niko
b5be7fba6a
add advisory link 2025-11-26 21:16:13 +01:00
Niko
48dab6eb6b
bump version to 2.25.2 2025-11-26 21:11:15 +01:00
Yue (Knox) Liu
880b92d724
Merge commit from fork
* Sanitize error message output

Escape HTML characters in error message for security.

* Update CHANGELOG.md

---------

Co-authored-by: Niko <github@dieserniko.link>
2025-11-26 20:58:33 +01:00
john
a91d9feb51
Correct Spotify/Spotipy typo (#1209)
Correct Spotify/Spotipy typo. Document refers the user to the "Spotify GitHub repository" while the associated link is actually to the Spotipy GitHub repo.
2025-07-31 10:33:07 +02:00
Stéphane Bruckert
5a8b55f5e8
Deprecations in doc (#1202)
* Add documentation warnings to doc

* fix

* fix
2025-05-23 17:18:49 +01:00
Stéphane Bruckert
9dfb7177b8
Revert "Run integration tests from fork" (#1201)
This reverts commit 4f5759dbfb.
2025-05-15 18:02:54 +01:00
Shawn Falkner-Horine
6bc91ecf98
Fix README "examples" link. (#1195) (#1196) 2025-04-26 13:11:03 +02:00
Sebastian Held
1a8d9da033
add additional_types to current_user_playing_track() (#1193)
Co-authored-by: Sebastian Held <sebastian.held@imst.de>
Co-authored-by: Stéphane Bruckert <stephane.bruckert@gmail.com>
2025-04-21 22:41:02 +02:00
Stéphane Bruckert
5b018cf6af Fix main branch name 2025-04-21 15:52:33 +01:00
Stéphane Bruckert
743059989d Pin gha workflows to ubuntu-22.04 2025-04-21 15:48:54 +01:00
Stéphane Bruckert
4f5759dbfb
Run integration tests from fork 2025-04-21 15:44:04 +01:00
Niko
3ec8a2312c
Improve logging (#1191)
* improve retry warning

* changelog

* fix lint issue
2025-03-26 07:41:07 +01:00
Niko
4f01f7187d
Avoid garbage collection for requests.Session (#1189)
* workaround for garbage collection

* add missing import

* linting issues (wrong import order)

* fix imports
2025-03-07 20:02:43 +01:00
Stéphane Bruckert
103d6873fa Bump to 2.25.1 2025-02-27 07:30:09 +00:00
Aaron Lichtman
1ca453f6ef
Merge commit from fork
* security: set 600 permissions on auth token cache file

https://github.com/spotipy-dev/spotipy/security/advisories/GHSA-pwhh-q4h6-w599

* Update spotipy/cache_handler.py

Co-authored-by: Niko <github@dieserniko.link>

* add newline back in

---------

Co-authored-by: Niko <github@dieserniko.link>
2025-02-26 14:01:16 +01:00
Szymon Andrzejewski
668158f055
Fixed Spotify's deprecation of http and localhost (#1187)
* spotify's deprecation of http and localhost

* updated instead of changed

* pep8

* fixing thing that i broke
2025-02-19 06:31:42 +00:00
Stéphane Bruckert
a6f6841e62
Remove dead link 2025-02-19 02:31:14 +00:00
Niko
d9ec669d5b
added branch overview 2025-01-24 16:43:13 +01:00
Stéphane Bruckert
c738376b80
Check f-strings (#1184) 2025-01-24 15:11:58 +00:00
Niko
1d920ff5df
use f-strings 2025-01-23 19:05:27 +01:00
Stéphane Bruckert
81788935cb Add back submodule 2025-01-22 23:51:45 +00:00
508chris
1dbbbf65ec
Changed cache_handler.py to utilize Python's Context Management Protocol (#991) 2025-01-22 23:36:37 +00:00
Stéphane Bruckert
d319c6e09f Move examples to spotipy-dev/spotipy-examples, add as submodule 2025-01-19 17:21:18 +00:00
Stéphane Bruckert
e37b1a8a55 Marked as deprecated 2025-01-19 16:47:15 +00:00
Stéphane Bruckert
f5ebcc6fc6 Run tests on PR 2025-01-19 16:38:00 +00:00
Stéphane Bruckert
84f8b0a64b
Split test and lint workflows (#1183) 2025-01-18 15:21:51 +00:00
dependabot[bot]
2243e135df
Update sphinx requirements (#1166)
* Update sphinx-rtd-theme requirement from ~=2.0.0 to ~=3.0.2

Updates the requirements on [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme) to permit the latest version.
- [Changelog](https://github.com/readthedocs/sphinx_rtd_theme/blob/master/docs/changelog.rst)
- [Commits](https://github.com/readthedocs/sphinx_rtd_theme/compare/2.0.0...3.0.2)

---
updated-dependencies:
- dependency-name: sphinx-rtd-theme
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* Sphinx 8.1.3

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Stéphane Bruckert <stephane.bruckert@gmail.com>
2025-01-14 07:57:00 +00:00
Niko
2bc8d35fc6
Formatted strings (#1178)
* use newer string formatters (https://pyformat.info)

* fix typo

* went through the files again, additions from #1165 are also included

* replace .format() with f strings
2025-01-14 07:56:47 +01:00
Michael Rossetti
ca207e4f2d
Update docs for current_user_top_artists (#1171)
* Update docs

Add maximum value for limit parameter. See: https://developer.spotify.com/documentation/web-api/reference/get-users-top-artists-and-tracks

* Update CHANGELOG.md

---------

Co-authored-by: Stéphane Bruckert <stephane.bruckert@gmail.com>
2025-01-12 15:42:56 +00:00
jonathan-huston
ba01a6aee5
Examples directory updates (#1055)
* Fixed scripts in examples directory that didn't work, deleted any redundant examples.

* Added examples for methods related to audiobooks, shows and episodes

* Updated changelog

---------

Co-authored-by: Stéphane Bruckert <stephane.bruckert@gmail.com>
2025-01-12 14:47:10 +00:00
Niko
645ed6da56
Bump version to 2.25.0 2025-01-03 13:30:08 +01:00
Niko
14ab13d292
add deprecation warnings and FAQ to respond to Spotify's API changes (#1177)
* add deprecation warnings and FAQ to respond to Spotify's API changes

* remove test cases associated with deprecated methods

* added missing commas
2025-01-03 13:26:08 +01:00
Niko
db3fb9a5ee
created SpotifyBaseException and moved exceptions from oauth2.py to exceptions.py (#1161) 2024-10-08 18:40:53 +02:00
Stéphane Bruckert
d9da5af53c
Fix README tests badge 2024-07-30 16:46:22 +01:00
Stéphane Bruckert
c6209436d1
Add Discord badge to README (#1150) 2024-07-28 15:28:59 +01:00
dependabot[bot]
185140a812
Update sphinx requirement from ~=7.3.7 to ~=7.4.7 (#1148)
Updates the requirements on [sphinx](https://github.com/sphinx-doc/sphinx) to permit the latest version.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.3.7...v7.4.7)

---
updated-dependencies:
- dependency-name: sphinx
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-22 16:42:12 +02:00
Aref
51c5bd8d7d
Advanced examples (#986)
* Created a testing suite for the spotipy client.Spotify class

* Delete pyvenv.cfg

* Corrected error in description

* Added some advanced usage examples, some of which involve utilizing basic data analysis methods using other libraries such as Pandas

* Minor formatting fixes

---------

Co-authored-by: Stéphane Bruckert <stephane.bruckert@gmail.com>
2024-07-09 20:02:07 +01:00
JackDyre
1e05bdba67
ID support for current_user_unfollow_playlist() (#1143)
* ID support for `current_user_unfollow_playlist()`

* turned double quotes into single quotes

* Clarified changelog entry
2024-06-30 21:34:14 +02:00
Niko
66ad716595
Print warnings when a rate/request limit is reached (#1134)
* create custom urllib3.Retry class for printing warnings on rate/request limits

* move import urllib3 from client.py to util.py

* Using Retry.increment instead of Retry.is_retry. Shows the Retry-After value in the warning as well

* Making sure that max column <= 99

* add types.TracebackType

* Change warning in request/rate limit warning

* adding all parameters, just to make sure

* fixing length of line

* making sure that response is not None
2024-06-25 09:26:19 +02:00
Niko
5e09c78ccf
Added an explanation for rate/request limits to FAQ (#1133) 2024-06-24 23:43:54 +02:00
Stéphane Bruckert
ef282e2423
Removed outdated contributors from RTD 2024-06-23 22:30:54 +01:00
Brandon Parrott
d7640404a5
client.py docstring updates (#1060)
* Update client.py 

Added warnings in docstrings for functions that have been replaced and call other new functions.

* Update CHANGELOG.md

---------

Co-authored-by: Stéphane Bruckert <contact@stephanebruckert.com>
2024-06-23 22:29:28 +01:00
Stéphane Bruckert
c90ce4a875
Fix audiobook integration tests (#1141) 2024-06-23 22:13:44 +01:00
Stéphane Bruckert
c7856d0120
Don't run integration tests twice on PR 2024-06-23 20:39:17 +01:00
Brennan Pate
d92951b356
Update Directions In TUTORIAL.md; Add Unit Tests To non_user_endpoints test.py (#1136)
* Update navigational directions in Step 1(A)

* Combine directions in Step 1(B) and Step 2(C) and remove Step 2(C)

* Update navigational directions and verbage in Step 1(C)

* Change reference from Step 1(C) to Step 1(B) in Step 2(D)

* Update capitalization in Prerequisites Step 3

* Add directions for installing Spotipy in Prerequisites Step 1(A)

* List updates to TUTORIAL.md file in CHANGELOG.md

* Update docstrings for funcs in lines 340-585

* Add unit tests for artist ID and URL

* Add test_artists_mixed_ids

* Updated CHANGELOG.md and TUTORIAL.md as requested

* Update client.py and test.py

* Fix linting issue

* Remove duplicate line; Change order of prerequisites

* Update local repo

* Add test_artists_mixed_ids

* Add Radiohead ID and URL; Add qotsa URL

* Add test_artist_url

* Comment out three failing tests

* Fix linting errors

* Uncommenting out failed tests

* Add test_artist_id

* List changes in CHANGELOG.md

* Add line breaks at the end of files.

* Remove multiple spaces

Also I've removed a sentence that just doesn't make sense in my eyes, but was added before this PR.

---------

Co-authored-by: Niko <github@dieserniko.link>
2024-06-22 17:35:53 +02:00
Dianna
8f003147f7
Added and revised function docstrings for util.py (#1130)
* Added and revised function docstrings for util.py

* added credentials to gitignore

* Addressed request changes by reverting/modifying function docstrings to match the format used in client.py
2024-06-18 10:30:04 +02:00
Stephane Bruckert
c5a0943016 Uncomment temporarily failing test 2024-06-01 19:34:44 +01:00
Brandon Parrott
f94a89a995
Add unit tests for queue functions (#1059)
* Update test.py

Add tests for queue manipulation/retrieval functions

* Update CHANGELOG.md

* flake8

---------

Co-authored-by: Stéphane Bruckert <stephane.bruckert@gmail.com>
2024-05-31 17:46:19 +01:00
Stephane Bruckert
cb36133ca9 Comment out test failing with payment_required 2024-05-31 17:42:28 +01:00
Stéphane Bruckert
bb36290b8e
Fix TOC in readme 2024-05-31 08:11:58 +01:00
Stéphane Bruckert
9a8b8a7f02
Update README.md 2024-05-31 08:10:05 +01:00
Michał Górny
eee7ae7d0d
Remove obsolete mock dependency (#1127)
The package uses `unittest.mock` only, and that one is part
of the Python standard library.
2024-05-31 08:08:24 +01:00
Stephane Bruckert
8a40e038b9 Remove python2.7 from pypi build 2024-05-30 22:06:13 +01:00
Stephane Bruckert
c0343b8b80 Bump to 2.24.0 2024-05-30 22:03:18 +01:00
Allie
5fa2343162
TUTORIAL.md updates (#1009)
* Update TUTORIAL.md

Added instructions for installing Spotipy.

* Update TUTORIAL.md

Updated Spotify app creation steps.

---------

Co-authored-by: Stéphane Bruckert <stephane.bruckert@gmail.com>
2024-05-30 20:14:15 +01:00
Nick
22e3b7406d
Update current_user_follow_playlist (#1040)
* Update current_user_follow_playlist

The API specifies a `public` parameter as seen [here](https://developer.spotify.com/documentation/web-api/reference/follow-playlist)

* Update CHANGELOG.md

---------

Co-authored-by: Stéphane Bruckert <stephane.bruckert@gmail.com>
2024-05-30 19:43:10 +01:00
wouldube
a810edf5da
Corrections to Grammar Errors and Typos in Documentation (#1017)
* Fixed grammar errors and typos.

* Update CHANGELOG.md to mention documentation typo corrections.

---------

Co-authored-by: Stéphane Bruckert <stephane.bruckert@gmail.com>
2024-05-30 19:37:39 +01:00
Niko
126da62dd0
Fix API Reference (#1126)
* add base directory to PATH

* Check if build completes if redis is included
2024-05-30 20:33:17 +02:00
Shukie Li
df27fe93bf
Combined assertion tests, fixes #1051 (#1052)
* addressed issue 936

* publish branch

* 12/9/2023

* Update CHANGELOG.md

---------

Co-authored-by: Stéphane Bruckert <stephane.bruckert@gmail.com>
2024-05-30 19:29:32 +01:00
andrewcara
62a27a20e0
Added MemcacheCacheHandler (#1042)
* Added MemcacheCacheHandler

* Import MemcacheError where used

* Update index.rst

---------

Co-authored-by: Stéphane Bruckert <stephane.bruckert@gmail.com>
2024-05-30 19:01:16 +01:00
Stéphane Bruckert
913ae57275
Fix docs badge
Latest was removed on RTD and is equivalent to master
2024-05-29 08:49:27 +01:00
Stéphane Bruckert
1ce8c4f06b
RTD theme fix (#1122)
* pip sphinx-rtd-theme

* Bump sphinx

* Add extension

* Add docs/requirements.txt

* CL
2024-05-28 09:08:23 +01:00
Stephane Bruckert
2d1cb99be7 Use sphinx_rtd_theme 2024-05-28 08:36:22 +01:00
Stéphane Bruckert
cc24b4c22f
Fix RTD build 2024-05-27 13:06:21 +01:00
Stéphane Bruckert
677841b417
Create .readthedocs.yaml (#1120)
* Create .readthedocs.yaml

* Update CHANGELOG.md
2024-05-27 13:01:01 +01:00
jackattack825
e153dabe7c
Update index.rst (#1087)
spelling issue
2024-05-23 07:40:08 +02:00
Niko
939b7557a5
Updated _regex_spotify_url to ignore /intl-<countrycode> in Spotify links (#1100)
* Updated _regex_spotify_url to ignore /intl-<countrycode> in Spotify links

* Updated documentation link and added some additional information
2024-05-22 12:43:04 +02:00
Hugo van Kemenade
b109ca722c
Bump GitHub Actions (#1113)
Co-authored-by: Niko <github@dieserniko.link>
2024-05-21 23:01:02 +02:00
Hugo van Kemenade
85c9d74dc1
Drop support for EOL Python 3.7 (#1065)
* Add python_requires to help pip

* Update supported versions in tox.ini

* Upgrade Python syntax with pyupgrade --py37-plus

* Bump GitHub Actions

* Add Python 3.11 and 3.12 to CI

* Remove six dependency

* Remove redundant dependencies

* Remove redudant Python 3.5 code

* Drop support for EOL Python 3.7

* Upgrade Python syntax with pyupgrade --py38-plus

* Update CHANGELOG

* More f-strings

---------

Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>
2024-05-21 18:32:01 +02:00
melissa
958ff6ad2b
Reorganized index.rst for improved logical flow (#1054)
* reorganized sections without changing any header. Made a few small grammar fixes.

* cleaning up examples section.

* updating changelog for proposed edits
2024-05-21 10:28:38 +02:00
Dianna
74330dae6d
Updated TUTORIAL.md with an additional instructional step and Troubleshooting Section (#1104)
* updated TUTORIAL.md with Troubleshooting section and an added step to Prerequisite instructions

* Update TUTORIAL.md

Editing grammatical syntax and formatting

* Update CHANGELOG.md
2024-05-18 21:46:03 +02:00
Niko
070b54f494
Merge pull request #1108 from spotipy-dev/1107-artist_albums-include_groups
Replaced argument "album_type" with "include_groups" in "Spotify.artist_albums"
2024-05-17 14:39:26 +02:00
Niko
3b5708f5a0
Update spotipy/client.py
Co-authored-by: Stéphane Bruckert <stephane.bruckert@gmail.com>
2024-05-17 14:38:19 +02:00
Niko
52f2b923ba
Removing whitespace (new IDE, sorry) 2024-05-17 12:46:23 +02:00
Niko
f4c2b90a29
Made sure that the line column is not over 99. 2024-05-17 12:41:57 +02:00
Niko
0a8e7f635e
Added missing comma to artist_albums 2024-05-17 12:37:54 +02:00
Niko
2e54f2c138
Replaced argument "album_type" with "include_groups" in "Spotify.artist_albums" 2024-05-17 12:35:23 +02:00
Niko
028539e08a
Merge pull request #964 from johncheng2011/bug/inheriting-exceptions-in-init
Ignore Attribute error in Spotify.__del__
2024-05-15 16:21:46 +02:00
Niko
6d8d9d1f9b
Merge branch 'master' into bug/inheriting-exceptions-in-init 2024-05-15 16:16:08 +02:00
Niko
17893a6115
Update CHANGELOG.md 2024-05-15 16:14:30 +02:00
Niko
160a57a1d6
Merge pull request #1069 from democat3457/patch-1
Fix unused description parameter in playlist creation example
2024-05-14 11:03:58 +02:00
Niko
23bf3c9d88
Merge pull request #1068 from Jack-Dane/master
Fix broken example
2024-05-09 20:30:11 +02:00
Jack Dane
97c9917a02 Use the playlist_items function to retrieve tracks.
* It will request the same endpoint as sp.next with the same fields for consistency.
2024-05-06 10:43:21 +01:00
Colin Wong
d9a5f008ff
Update CHANGELOG.md 2024-01-06 00:25:18 -06:00
Colin Wong
e3629cdacb
Use unused description parameter in example 2024-01-06 00:20:53 -06:00
chaisupt
a14a28e10c
Improve usability on README.md (#983)
* Update README.md

Improve usability of the README.md file

* Update CHANGELOG.md

change log following README changes

* Update CHANGELOG.md

* Update README.md

Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>

* Update README.md

Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>

* Update README.md

Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>

* Update README.md

Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>

* Update README.md

• Removed the License Section from the table of contents due to the new GitHub tab UI
• Updated link to not specify any specific version

* Update CHANGELOG.md

---------

Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>
2023-12-29 15:50:13 +01:00
Dan Joseph
1416d47cba
Audiobook Support and CI Workflow Update (#1036)
* Implement audiobook endpoints

* Update GitHub CI Workflow: Removed Python v2.7

* Update GitHub CI Workflow: Removed Python v3.6

* Add integration tests for audiobook endpoints
2023-10-31 01:32:46 +00:00
John Cheng
f647ca75c2 update changelog 2023-05-26 19:51:59 -04:00
John Cheng
2063df0fa5
Merge branch 'spotipy-dev:master' into bug/inheriting-exceptions-in-init 2023-05-26 19:44:59 -04:00
adhil0
d31969108d
Update links in documentation (#969)
* updating links to Spotify documentation

* add changes to CHANGELOG.md
2023-05-02 22:26:40 +01:00
darwady2
3b6d14404e
Updates the video link to a high definition video (#967)
* Updates the video link to a high definition video for setting the SPOTIPY_CLIENT_ID and SPOTIPY_CLIENT_SECRET values and authenticating. The old video was getting some complaints about people not being able to read the text (since it was low quality), so I've re-recorded it and replaced it with this new version.

* Adds changes to Chanelog

---------

Co-authored-by: Stéphane Bruckert <stephane.bruckert@gmail.com>
2023-04-24 13:21:19 +01:00
John Cheng
6cc817af85 ignore if _session does not exist 2023-04-11 23:29:37 -04:00
Stéphane Bruckert
45f78ce7a6
Update dependabot.yml 2023-04-09 15:53:17 +01:00
Stéphane Bruckert
612b30efa3
Create .github/dependabot.yml 2023-04-09 15:52:22 +01:00
Stéphane Bruckert
796c03338f
Bump to 2.23.0 (#959) 2023-04-07 18:36:17 +01:00
Stéphane Bruckert
3bac7250ec
Publish to PyPI action (#958) 2023-04-07 12:33:32 +01:00
Beza Amare Amsalu
b7baaabf74
fixed contributing.md (#955) 2023-03-30 18:12:41 +01:00
Richard Ngo-Lam
fe438c0432
Update search_markets method to apply the total parameter to all types, add tests (#901)
* Update search_markets method to apply the total parameter to all types, fixes #534

* Add integration tests for searching multiple types in multiple markets

* Update search_markets method to apply the total parameter to all types, add tests

---------

Co-authored-by: Stéphane Bruckert <stephane.bruckert@gmail.com>
2023-03-15 23:46:08 +00:00
Ludwig Johansson
f2d23e2219
Fix regex to support detailed URI #947 (#949)
* Fix regex to support detailed URI as #947

* Added changes to changelog
2023-03-15 15:17:02 +00:00
Suyash Bajpai
ca332a538e
Fixing a typo in the docs (#945)
spotispy --> spotipy
2023-02-28 07:41:22 +00:00
Andrii Yurchuk
572195617b
Fix SpotifyPKCE docstring (#942) 2023-02-14 14:00:59 +00:00
Shawn Cruz
b3f308d289
Add encoder_cls argument to CacheFileHandler (#941)
Fixes
2023-02-12 22:58:55 +00:00
Stephane Bruckert
c53511bbbe Bump to 2.22.1 2023-01-23 19:00:55 +00:00
Stephane Bruckert
beec3dad1f Fix flake8 2023-01-23 18:54:40 +00:00
Shaderbug
b1db0b63d9
Merge pull request from GHSA-q764-g6fm-555v
* Improve URL and URI handling

* Back to SpotifyException for backward-compatibility

* fix copy paste typo

* TODO v3 comments

Co-authored-by: Stephane Bruckert <stephane.bruckert@gmail.com>
2023-01-23 18:50:07 +00:00
Mario Sessa
262e7a0443
Rename simple files (#933) 2023-01-23 12:48:37 +00:00
Archie Baldry
d884ae13c2
Fix typo in start_playback function (#930) 2023-01-15 23:30:37 +00:00
Stéphane Bruckert
f669966a72
Update SECURITY.md 2023-01-07 08:27:58 +00:00
Stéphane Bruckert
0b9062726c
Create SECURITY.md 2023-01-07 08:26:17 +00:00
kasowskc
d0bbe67302
Add additional video tutorial reference to documentation. (#921)
* Added additional video tutorial

Added additional link to video tutorial series by Ian Annase. The original tutorial is very low resolution and is hard to follow. The tutorial linked in this commit is entirely in HD and goes further in depth, including examples of possible functionality.

* Update index.rst

Adjusted link formatting.

* Update index.rst

Updated link formatting again because I'm an idiot.

* Update index.rst

Edited formatting to fix missing period and double wording.
2022-12-27 15:27:17 +01:00
oliveraw
922d51df02
modified docstring for playlist_add_items to no longer accept IDs 2022-12-16 19:17:37 +00:00
Mason Stephenson
edd3f29a2c
Getting Started Clarifications and Example Code File (#904)
* added URI link to README getting started

* revised comment

* added alternate module installation instruction

* installation troubleshooting comment

* scope clarification comment

* added playlist_add_items.py to examples folder

* reformatted changelog edits

* Relocated added contributions to added section in Changelog

* removed unnecessary library installation instruction

* Clarified alternative installation instruction
2022-12-10 16:27:29 +00:00
Stephane Bruckert
1377879972 Update contributing.md 2022-12-10 16:02:03 +00:00
Stephane Bruckert
1e840b6526 Bump to 2.22.0 2022-12-10 15:31:47 +00:00
mattgader
d2384c1c33
Add CODE_OF_CONDUCT.md (#897) 2022-12-10 14:22:45 +00:00
Kevin Sekuj
5201f58247
Add tests for global categories, releases, fix bugged tests (#893) 2022-11-26 09:29:53 +00:00
Stéphane Bruckert
edcd322845
Pin Github Actions Runner to Ubuntu 20 for Py27 (#894) 2022-11-26 08:40:49 +00:00
Sean Park
6f56504c2c
Update category tests to fix 1 failing and 1 error test results (#886)
* update category tests to fix 1 failing and 1 error test results

* update changelog
2022-11-13 10:42:25 +00:00
Stéphane Bruckert
1265d7b915
GHA integration tests (#876) 2022-11-01 21:57:40 +00:00
Stéphane Bruckert
e9fd81ce21
Update integration_tests.yml 2022-11-01 21:52:01 +00:00
Stéphane Bruckert
459be3d400
Integration tests workflow (2) 2022-11-01 21:36:43 +00:00
Stephane Bruckert
0c32cd74d9 Integration tests workflow 2022-11-01 21:26:01 +00:00
Stéphane Bruckert
9bf5e342c2
Add secrets to GHA workflow (#877) 2022-11-01 20:52:25 +00:00
Alberto Pasqualetto
cc5eaca191
Fix linting in test_non_user_endpoints (#841) 2022-10-29 15:50:56 +01:00
Bruno Alla
fa44fed76d
Fix a few typos in comments & strings (#866) 2022-10-29 14:23:43 +01:00
Pedro Rodrigues
dd69a48659
Create tutorial for beginners (#872)
* Create TUTORIAL.md

* remova blank space

Co-authored-by: avivaschmitz <39923753+avivaschmitz@users.noreply.github.com>
2022-10-29 14:20:50 +01:00
Job Doesburg
be759d3f50
Implement get-queue endpoint (#854) 2022-10-29 13:01:25 +01:00
Stephane Bruckert
5a7e5c5a43 Bump to 2.21.0, closes #836 2022-10-26 23:03:42 +01:00
Bruno Alla
fd8f97d46a
Add python 3.10 to GitHub Actions (#863) 2022-10-26 22:59:10 +01:00
Stephane Bruckert
06765c4f5a Fix flake8 2022-10-26 22:56:38 +01:00
Paulina Khew
be27391461
Add missing step to CONTRIBUTING.md to activate virtualenv (#848) 2022-08-30 23:16:38 +01:00
Bryan Malyn
7fc08809f0
Add FlaskSessionCacheHandler (#833)
Updated examples/app.py
Updated CHANGELOG.md and appropriate docs.
2022-06-25 20:00:08 +01:00
Stephane Bruckert
7337bf9352 Fix readthedocs 2022-06-19 09:03:13 +01:00
Ian Rolph
eab3c37b89
Updated documentation (#826)
* updated documentation

* updated changelog
2022-06-18 23:34:36 +01:00
Robinson Ibarra
ca73420248
Added documentation for windows users attempting to set environment variables (#822) 2022-06-18 23:30:07 +01:00
Stephane Bruckert
36ea3d9319 Fix lint 2022-06-18 23:29:12 +01:00
mattrost
1b0c426984
Addition of Test Cases (#818)
* Added test_categories_country to use optional country selection for categories method.

* Added test_categories_locale for testing locale method of categories.

* Added test_categories_limit_low and test_categories_limit_high to test the limit method of categories for edge cases.

* Rearranged for flow

* Added test_category_playlists_limit_low and test_category_playlists_limit_high for testing the limit method.
2022-06-18 23:28:08 +01:00
dnlruiz
a4cfd83946
show_featured_artists.py and update publishing docs (#821)
* show_featured_artists.py and update publishing docs

* Don't update version

Co-authored-by: Stéphane Bruckert <contact@stephanebruckert.com>
2022-06-18 23:26:43 +01:00
Stephane Bruckert
61c8cda006 Fix lint 2022-06-18 23:24:49 +01:00
kanari1
6164dbb628
Fixed and clarified docstrings for client.py (#820)
* fixed docstring for current_user_following_users_function

* Edited docstrings for playlist_cover_image, user_playlist, user_playlist_replace_tracks, user_playlist_reorder_tracks functions

* expanded docstring description of playlist_change_details function to include changes to collaborative state and/or description
2022-06-18 23:20:37 +01:00
darwady2
c6b56a658a
Add YT video URL to doc (#816)
For a final assignment in a class, I've created a How-To video that showcases how to get setup with API credentials, Redirect URIs, and Spotify Developer Accounts, all of which are necessary prerequisites to using the Spotipy library.

I wanted to do this because, in my past experience, the most frustrating thing about working with APIs and libraries is authentication. Spotipy does a nice job of wrapping some of this and making it easier on the developer, but it still takes some setup before successful use.

Specifically, my video goes through the following:
* Installing Spotipy
* Setting up a Spotify Developer Account
* Configuring the Redirect URI
* Getting the Client Secret and Public Keys
* Setting the envars for use with Spotipy
* Running a sample piece of code from Spotipy to show that things are setup properly
2022-06-18 23:20:11 +01:00
Logan O'Connell
13593c54cb
Added a new example: follow_playlist.py (#804)
* Create playlist_all_local_tracks.py

* Delete playlist_all_local_tracks.py

* Create follow_playlist.py
2022-06-18 23:19:27 +01:00
ivyadam
5175f19851
Updated documentation about ISO-639 language codes and address issue #753 (#800)
* Updated references to ISO 639 standard to clarify that the ISO 639-1 alpha-2 standard is used.

* Added `market` parameter to `album` and `albums`

* fix formatting change
2022-06-18 23:18:59 +01:00
Andrii Oriekhov
14ee53eacd
Add GitHub URL for PyPi (#787) 2022-06-18 23:18:20 +01:00
Mohammad Momeni
72a6cd9190
Fix text attribute for Response object (#811)
* Fix text attribute for Response object

* Add a changelog for the AttributeError PR
2022-06-18 23:17:58 +01:00
84 changed files with 2257 additions and 2230 deletions

14
.github/SECURITY.md vendored Normal file
View File

@ -0,0 +1,14 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 2.x | :white_check_mark: |
| 1.x | :x: |
## Reporting a Vulnerability
Report via https://github.com/spotipy-dev/spotipy/security/advisories.
Guidance: https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability.

11
.github/dependabot.yml vendored Normal file
View 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"

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

@ -0,0 +1,26 @@
name: Integration tests
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-22.04
env:
SPOTIPY_CLIENT_ID: ${{ secrets.SPOTIPY_CLIENT_ID }}
SPOTIPY_CLIENT_SECRET: ${{ secrets.SPOTIPY_CLIENT_SECRET }}
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 non user endpoints integration tests
run: |
python -m unittest discover -v tests/integration/non_user_endpoints

23
.github/workflows/lint.yml vendored Normal file
View 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
View 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 }}

View File

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

View File

@ -1,30 +0,0 @@
name: Tests
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [2.7, 3.6, 3.7, 3.8, 3.9]
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
View 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
View File

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

16
.readthedocs.yaml Normal file
View 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

View File

@ -6,33 +6,220 @@ 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).
## Unreleased
Add your changes below.
// Add your changes here and then delete this line
### Added
### Fixed
### 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
### Added
- Add alternative module installation instruction to README
- Added Comment to README - Getting Started for user to add URI to app in Spotify Developer Dashboard.
- Added playlist_add_tracks.py to example folder
### Changed
- Modified docstring for playlist_add_items() to accept "only URIs or URLs",
with intended deprecation for IDs in v3
### Fixed
- Path traversal vulnerability that may lead to type confusion in URI handling code
- Update contributing.md
## [2.22.0] - 2022-12-10
### Added
- Integration tests via GHA (non-user endpoints)
- Unit tests for new releases, passing limit parameter with minimum and maximum values of 1 and 50
- Unit tests for categories, omitting country code to test global releases
- Added `CODE_OF_CONDUCT.md`
### Fixed
- Incorrect `category_id` input for test_category
- Assertion value for `test_categories_limit_low` and `test_categories_limit_high`
- Pin GitHub Actions Runner to Ubuntu 20 for Py27
- Fixed potential error where `found` variable in `test_artist_related_artists` is undefined if for loop never evaluates to true
- Fixed false positive test `test_new_releases` which looks up the wrong property of the JSON response object and always evaluates to true
## [2.21.0] - 2022-09-26
### Added
- Added `market` parameter to `album` and `albums` to address ([#753](https://github.com/plamere/spotipy/issues/753)
- Added `show_featured_artists.py` to `/examples`.
- Expanded contribution and license sections of the documentation.
- Added `FlaskSessionCacheHandler`, a cache handler that stores the token info in a flask session.
- Added Python 3.10 in GitHub Actions
### Fixed
- Updated the documentation to specify ISO-639-1 language codes.
- Fix `AttributeError` for `text` attribute of the `Response` object
- Require redis v3 if python2.7 (fixes readthedocs)
## [2.20.0] - 2022-06-18
### Added
* Added `RedisCacheHandler`, a cache handler that stores the token info in Redis.
* Changed URI handling in `client.Spotify._get_id()` to remove qureies if provided by error.
* Added a new parameter to `RedisCacheHandler` to allow custom keys (instead of the default `token_info` key)
* Simplify check for existing token in `RedisCacheHandler`
- Added `RedisCacheHandler`, a cache handler that stores the token info in Redis.
- 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)
- Simplify check for existing token in `RedisCacheHandler`
### 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
### Added
* Added `MemoryCacheHandler`, a cache handler that simply stores the token info in memory as an instance attribute of this class.
* If a network request returns an error status code but the response body cannot be decoded into JSON, then fall back on decoding the body into a string.
* Added `DjangoSessionCacheHandler`, a cache handler that stores the token in the session framework provided by Django. Web apps using spotipy with Django can directly use this for cache handling.
- Added `MemoryCacheHandler`, a cache handler that simply stores the token info in memory as an instance attribute of this class.
- If a network request returns an error status code but the response body cannot be decoded into JSON, then fall back on decoding the body into a string.
- Added `DjangoSessionCacheHandler`, a cache handler that stores the token in the session framework provided by Django. Web apps using spotipy with Django can directly use this for cache handling.
### Fixed
* Fixed a bug in `CacheFileHandler.__init__`: The documentation says that the username will be retrieved from the environment, but it wasn't.
* Fixed a bug in the initializers for the auth managers that produced a spurious warning message if you provide a cache handler and you set a value for the "SPOTIPY_CLIENT_USERNAME" environment variable.
* Use generated MIT license and fix license type in `pip show`
- Fixed a bug in `CacheFileHandler.__init__`: The documentation says that the username will be retrieved from the environment, but it wasn't.
- Fixed a bug in the initializers for the auth managers that produced a spurious warning message if you provide a cache handler, and you set a value for the "SPOTIPY_CLIENT_USERNAME" environment variable.
- Use generated MIT license and fix license type in `pip show`
## [2.18.0] - 2021-04-13
@ -41,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
- Added a cache handler to `SpotifyClientCredentials`
- Added the following endpoints
* `Spotify.current_user_saved_episodes`
* `Spotify.current_user_saved_episodes_add`
* `Spotify.current_user_saved_episodes_delete`
* `Spotify.current_user_saved_episodes_contains`
* `Spotify.available_markets`
- `Spotify.current_user_saved_episodes`
- `Spotify.current_user_saved_episodes_add`
- `Spotify.current_user_saved_episodes_delete`
- `Spotify.current_user_saved_episodes_contains`
- `Spotify.available_markets`
### Changed
@ -53,9 +240,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
* Fixed the bugs in `SpotifyOAuth.refresh_access_token` and `SpotifyPKCE.refresh_access_token` which raised the incorrect exception upon receiving an error response from the server. This addresses #645.
* Fixed a bug in `RequestHandler.do_GET` in which the non-existent `state` attribute of `SpotifyOauthError` is accessed. This bug occurs when the user clicks "cancel" in the permissions dialog that opens in the browser.
* Cleaned up the documentation for `SpotifyClientCredentials.__init__`, `SpotifyOAuth.__init__`, and `SpotifyPKCE.__init__`.
- Fixed the bugs in `SpotifyOAuth.refresh_access_token` and `SpotifyPKCE.refresh_access_token` which raised the incorrect exception upon receiving an error response from the server. This addresses #645.
- Fixed a bug in `RequestHandler.do_GET` in which the non-existent `state` attribute of `SpotifyOauthError` is accessed. This bug occurs when the user clicks "cancel" in the permissions dialog that opens in the browser.
- Cleaned up the documentation for `SpotifyClientCredentials.__init__`, `SpotifyOAuth.__init__`, and `SpotifyPKCE.__init__`.
## [2.17.1] - 2021-02-28
@ -72,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"
- Changed docs for `search` to mention that you can provide multiple types to search for
- 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)
### Added
@ -111,7 +298,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- `SpotifyPKCE.parse_auth_response_url`, mirroring that method in
`SpotifyOAuth`
`SpotifyOAuth`
### Changed
@ -120,17 +307,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Using `SpotifyPKCE.get_authorization_url` will now generate a code
challenge if needed
challenge if needed
## [2.14.0] - 2020-08-29
### Added
- (experimental) Support to search multiple/all markets at once.
- Support to test whether the current user is following certain
users or artists
- Proper replacements for all deprecated playlist endpoints
(See https://developer.spotify.com/community/news/2018/06/12/changes-to-playlist-uris/ and below)
- (experimental) Support to search multiple/all markets at once.
- Support to test whether the current user is following certain
users or artists
- Proper replacements for all deprecated playlist endpoints
(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.
- Reason for 403 error in SpotifyException
- Support for the PKCE Auth Flow
@ -148,273 +335,312 @@ 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_reorder_tracks` in favor of `playlist_reorder_items`
- `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
`playlist_remove_specific_occurrences_of_items`
`playlist_remove_specific_occurrences_of_items`
- `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`
- `playlist_tracks` in favor of `playlist_items`
### Fixed
- fixed issue where episode URIs were being converted to track URIs in playlist calls
## [2.13.0] - 2020-06-25
### Added
- Added `SpotifyImplicitGrant` as an auth manager option. It provides
user authentication without a client secret but sacrifices the ability
to refresh the token without user input. (However, read the class
docstring for security advisory.)
- Added built-in verification of the `state` query parameter
- Added two new attributes: error and error_description to `SpotifyOauthError` exception class to show
authorization/authentication web api errors details.
- Added `SpotifyStateError` subclass of `SpotifyOauthError`
- Allow extending `SpotifyClientCredentials` and `SpotifyOAuth`
- Added the market paramter to `album_tracks`
- Added `SpotifyImplicitGrant` as an auth manager option. It provides
user authentication without a client secret but sacrifices the ability
to refresh the token without user input. (However, read the class
docstring for security advisory.)
- Added built-in verification of the `state` query parameter
- Added two new attributes: error and error_description to `SpotifyOauthError` exception class to show
authorization/authentication web api errors details.
- Added `SpotifyStateError` subclass of `SpotifyOauthError`
- Allow extending `SpotifyClientCredentials` and `SpotifyOAuth`
- Added the market parameter to `album_tracks`
### Deprecated
- Deprecated `util.prompt_for_user_token` in favor of `spotipy.Spotify(auth_manager=SpotifyOAuth())`
- Deprecated `util.prompt_for_user_token` in favor of `spotipy.Spotify(auth_manager=SpotifyOAuth())`
## [2.12.0] - 2020-04-26
### Added
- Added a method to update the auth token.
- Added a method to update the auth token.
### Fixed
- Logging regression due to the addition of `logging.basicConfig()` which was unneeded.
- Logging regression due to the addition of `logging.basicConfig()` which was unneeded.
## [2.11.2] - 2020-04-19
### Changed
- Updated the documentation to give more details on the authorization process and reflect
2020 Spotify Application jargon and practices.
- Updated the documentation to give more details on the authorization process and reflect
2020 Spotify Application jargon and practices.
- 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`.
- 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`.
### Fixed
- Issue where using `http://localhost` as redirect_uri would cause the authorization process to hang.
- Issue where using `http://localhost` as redirect_uri would cause the authorization process to hang.
## [2.11.1] - 2020-04-11
### Fixed
- Fixed miscellaneous issues with parsing of callback URL
- Fixed miscellaneous issues with parsing of callback URL
## [2.11.0] - 2020-04-11
### Added
- Support for shows/podcasts and episodes
- Added CONTRIBUTING.md
- Support for shows/podcasts and episodes
- Added CONTRIBUTING.md
### Changed
- 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:
- status_forcelist
- retries
- status_retries
- backoff_factor
- Spin up a local webserver to auto-fill authentication URL
- Use session in SpotifyAuthBase
- Logging used instead of print statements
- 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:
- status_forcelist
- retries
- status_retries
- backoff_factor
- Spin up a local webserver to autofill authentication URL
- Use session in SpotifyAuthBase
- Logging used instead of print statements
### Fixed
- Close session when Spotipy object is unloaded
- Propagate refresh token error
- Close session when Spotipy object is unloaded
- Propagate refresh token error
## [2.10.0] - 2020-03-18
### Added
- Support for `add_to_queue`
- **Parameters:**
- track uri, id, or url
- device id. If None, then the active device is used.
- Add CHANGELOG and LICENSE to released package
- Support for `add_to_queue`
- **Parameters:**
- track uri, id, or url
- device id. If None, then the active device is used.
- Add CHANGELOG and LICENSE to released package
## [2.9.0] - 2020-02-15
### Added
- Support `position_ms` optional parameter in `start_playback`
- Add `requests_timeout` parameter to authentication methods
- Make cache optional in `get_access_token`
- Support `position_ms` optional parameter in `start_playback`
- Add `requests_timeout` parameter to authentication methods
- Make cache optional in `get_access_token`
## [2.8.0] - 2020-02-12
### Added
- Support for `playlist_cover_image`
- Support `after` and `before` parameter in `current_user_recently_played`
- CI for unit tests
- Automatic `token` refresh
- `auth_manager` and `oauth_manager` optional parameters added to `Spotify`'s init.
- Optional `username` parameter to be passed to `SpotifyOAuth`, to infer a `cache_path` automatically
- Optional `as_dict` parameter to control `SpotifyOAuth`'s `get_access_token` output type. However, this is going to be deprecated in the future, and the method will always return a token string
- Optional `show_dialog` parameter to be passed to `SpotifyOAuth`
- Support for `playlist_cover_image`
- Support `after` and `before` parameter in `current_user_recently_played`
- CI for unit tests
- Automatic `token` refresh
- `auth_manager` and `oauth_manager` optional parameters added to `Spotify`'s init.
- Optional `username` parameter to be passed to `SpotifyOAuth`, to infer a `cache_path` automatically
- Optional `as_dict` parameter to control `SpotifyOAuth`'s `get_access_token` output type. However, this is going to be deprecated in the future, and the method will always return a token string
- Optional `show_dialog` parameter to be passed to `SpotifyOAuth`
### Changed
- Both `SpotifyClientCredentials` and `SpotifyOAuth` inherit from a common `SpotifyAuthBase` which handles common parameters and logics.
- Both `SpotifyClientCredentials` and `SpotifyOAuth` inherit from a common `SpotifyAuthBase` which handles common parameters and logics.
## [2.7.1] - 2020-01-20
### Changed
- PyPi release mistake without pulling last merge first
- PyPi release mistake without pulling last merge first
## [2.7.0] - 2020-01-20
### Added
- Support for `playlist_tracks`
- Support for `playlist_upload_cover_image`
- Support for `playlist_tracks`
- Support for `playlist_upload_cover_image`
### Changed
- `user_playlist_tracks` doesn't require a user anymore (accepts `None`)
- `user_playlist_tracks` doesn't require a user anymore (accepts `None`)
### Deprecated
- Deprecated `user_playlist` and `user_playlist_tracks`
- Deprecated `user_playlist` and `user_playlist_tracks`
## [2.6.3] - 2020-01-16
### Fixed
- Fixed broken doc in 2.6.2
- Fixed broken doc in 2.6.2
## [2.6.2] - 2020-01-16
### Fixed
- Fixed broken examples in README, examples and doc
- Fixed broken examples in README, examples and doc
### Changed
- Allow session keepalive
- Bump requests to 2.20.0
- Allow session keepalive
- Bump requests to 2.20.0
## [2.6.1] - 2020-01-13
### Fixed
- Fixed inconsistent behaviour with some API methods when
a full HTTP URL is passed.
- Fixed invalid calls to logging warn method
- Fixed inconsistent behaviour with some API methods when
a full HTTP URL is passed.
- Fixed invalid calls to logging warn method
### Removed
- `mock` no longer needed for install. Only used in `tox`.
- `mock` no longer needed for install. Only used in `tox`.
## [2.6.0] - 2020-01-12
### Added
- Support for `playlist` to get a playlist without specifying a user
- Support for `current_user_saved_albums_delete`
- Support for `current_user_saved_albums_contains`
- Support for `user_unfollow_artists`
- Support for `user_unfollow_users`
- Lint with flake8 using Github action
- Support for `playlist` to get a playlist without specifying a user
- Support for `current_user_saved_albums_delete`
- Support for `current_user_saved_albums_contains`
- Support for `user_unfollow_artists`
- Support for `user_unfollow_users`
- Lint with flake8 using GitHub action
### Changed
- Fix typos in doc
- Start following [SemVer](https://semver.org) properly
- Fix typos in doc
- 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
Added follow and player endpoints
## [2.4.4] - 2017-01-04
Python 3 fix
## [2.4.3] - 2017-01-02
Fixed proxy issue in standard auth flow
## [2.4.2] - 2017-01-02
Support getting audio features for a single track
## [2.4.1] - 2017-01-02
Incorporated proxy support
## [2.4.0] - 2016-12-31
Incorporated a number of PRs
## [2.3.8] - 2016-03-31
Added recs, audio features, user top lists
## [2.3.7] - 2015-08-10
Added current_user_followed_artists
## [2.3.6] - 2015-06-03
Support for offset/limit with album_tracks API
## [2.3.5] - 2015-04-28
Fixed bug in auto retry logic
## [2.3.3] - 2015-04-01
Aadded client credential flow
Added client credential flow
## [2.3.2] - 2015-03-31
Added auto retry logic
## [2.3.0] - 2015-01-05
Added session support added by akx.
## [2.2.0] - 2014-11-15
Added support for user_playlist_tracks
## [2.1.0] - 2014-10-25
Added support for new_releases and featured_playlists
## [2.0.2] - 2014-08-25
Moved to spotipy at pypi
## [1.2.0] - 2014-08-22
Upgraded APIs and docs to make it be a real library
## [1.310.0] - 2014-08-20
Added playlist replace and remove methods. Added auth tests. Improved API docs
## [1.301.0] - 2014-08-19
Upgraded version number to take precedence over previously botched release (sigh)
## [1.50.0] - 2014-08-14
Refactored util out of examples and into the main package
## [1.49.0] - 2014-07-23
Support for "Your Music" tracks (add, delete, get), with examples
## [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
Added show tracks.py example
## [1.43.0] - 2014-06-27
Fixed JSON handling issue
## [1.42.0] - 2014-06-19
Removed dependency on simplejson
## [1.40.0] - 2014-06-12
Initial public release.
## [1.4.2] - 2014-06-21
Added support for retrieving starred playlists
## [1.1.0] - 2014-06-17
Updates to match released API
## [1.1.0] - 2014-05-18
Repackaged for saner imports
## [1.0.0] - 2017-04-05
Initial release

63
CODE_OF_CONDUCT.md Normal file
View File

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

View File

@ -5,59 +5,90 @@ If you would like to contribute to spotipy follow these steps:
### Export the needed environment variables
```bash
# Linux or Mac
export SPOTIPY_CLIENT_ID=client_id_here
export SPOTIPY_CLIENT_SECRET=client_secret_here
export SPOTIPY_CLIENT_USERNAME=client_username_here # This is actually an id not spotify display name
export SPOTIPY_REDIRECT_URI=http://localhost:8080 # Make url is set in app you created to get your ID and SECRET
export SPOTIPY_CLIENT_USERNAME=client_username_here # This is actually an id not spotify display name and can be found [here](https://www.spotify.com/us/account/overview/)
export SPOTIPY_REDIRECT_URI=http://127.0.0.1:8080 # Make url is set in app you created to get your ID and SECRET
# Windows
$env:SPOTIPY_CLIENT_ID="client_id_here"
$env:SPOTIPY_CLIENT_SECRET="client_secret_here"
$env:SPOTIPY_CLIENT_USERNAME="client_username_here"
$env:SPOTIPY_REDIRECT_URI="http://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
$ virtualenv --python=python3.7 env
(env) $ pip install --user -e .
$ virtualenv --python=python3 env
$ source env/bin/activate
(env) $ pip install -e .
(env) $ python -m unittest discover -v tests
```
### 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 .
To verify the code style:
pip install flake8
flake8 .
To make sure if the import lists are stored correctly:
pip install isort
isort . -c -v
isort . -c
### Publishing (by maintainer)
Sort them automatically with:
- Bump version in setup.py
- Bump and date changelog
- Add to changelog:
## Unreleased
// Add your changes here and then delete this line
- Commit changes
- Package to pypi:
python setup.py sdist bdist_wheel
python3 setup.py sdist bdist_wheel
twine check dist/*
twine upload --repository-url https://upload.pypi.org/legacy/ --skip-existing dist/*.(whl|gz|zip)~dist/*linux*.whl
- Create github release https://github.com/plamere/spotipy/releases with the changelog content
for the version and a short name that describes the main addition
- Verify doc uses latest https://readthedocs.org/projects/spotipy/
isort .
### Changelog
Don't forget to add a short description of your change in the [CHANGELOG](CHANGELOG.md)
### Publishing (by maintainer)
- Bump version in setup.py
- Bump and date changelog
- Add to changelog:
## Unreleased
Add your changes below.
### Added
### Fixed
### Removed
- Commit changes
- Push tag to trigger PyPI build & release workflow
- 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
- Verify doc uses latest <https://readthedocs.org/projects/spotipy/>

34
FAQ.md
View File

@ -6,7 +6,7 @@ spotipy can only return fields documented on the Spotify web API https://develop
### How to use spotipy in an API?
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?
@ -36,7 +36,7 @@ Error:
Solution:
- 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
@ -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
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)

View File

@ -1,12 +1,20 @@
# 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.
![Tests](https://github.com/plamere/spotipy/workflows/Tests/badge.svg?branch=master) [![Documentation Status](https://readthedocs.org/projects/spotipy/badge/?version=latest)](https://spotipy.readthedocs.io/en/latest/?badge=latest)
![Integration tests](https://github.com/spotipy-dev/spotipy/actions/workflows/integration_tests.yml/badge.svg?branch=master) [![Documentation Status](https://readthedocs.org/projects/spotipy/badge/?version=master)](https://spotipy.readthedocs.io/en/latest/?badge=master) [![Discord server](https://img.shields.io/discord/1244611850700849183?style=flat&logo=discord&logoColor=7289DA&color=7289DA)](https://discord.gg/HP6xcPsTPJ)
## 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
@ -14,6 +22,12 @@ Spotipy's full documentation is online at [Spotipy Documentation](http://spotipy
pip install spotipy
```
alternatively, for Windows users
```bash
py -m pip install spotipy
```
or upgrade
```bash
@ -22,12 +36,11 @@ pip install spotipy --upgrade
## 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/.
Add your new ID and SECRET to your environment:
To get started, [install spotipy](#installation), create a new account or log in on https://developers.spotify.com/. Go to the [dashboard](https://developer.spotify.com/dashboard), create an app and add your new ID and SECRET (ID and SECRET can be found on an app setting) to your environment:
### Without user authentication
### Example without user authentication
```python
import spotipy
@ -40,8 +53,22 @@ results = sp.search(q='weezer', limit=20)
for idx, track in enumerate(results['tracks']['items']):
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.
```python
import spotipy
@ -57,6 +84,12 @@ for idx, item in enumerate(results['items']):
track = item['track']
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
@ -69,3 +102,9 @@ Dont 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,
file them [here](https://github.com/plamere/spotipy/issues).
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)

100
TUTORIAL.md Normal file
View File

@ -0,0 +1,100 @@
# Spotipy Tutorial for Beginners
Hello and welcome to the Spotipy Tutorial for Beginners. If you have limited experience coding in Python and have never used Spotipy or the Spotify API before, you've come to the right place. This tutorial will walk you through all the steps necessary to set up Spotipy and use it to accomplish a simple task.
## Prerequisites
In order to complete this tutorial successfully, there are a few things that you should already have installed:
**1. python3**
Spotipy is written in Python, so you'll need to have the latest version of Python installed in order to use Spotipy. Check if you already have Python installed with the Terminal command: python --version
If you see a version number, Python is already installed. If not, you can download it here: https://www.python.org/downloads/
**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.
Once those three setup items are taken care of, you're ready to start learning how to use Spotipy!
## Step 1. Creating a Spotify Account
Spotipy relies on the Spotify API. In order to use the Spotify API, you'll need to create a Spotify developer account.
A. Visit the [Spotify developer portal](https://developer.spotify.com/dashboard/). If you already have a Spotify account, click "Log in" and enter your username and password. Otherwise, click "Sign up" and follow the steps to create an account. After you've signed in or signed up, begin by clicking on your profile name at the top right of your screen and then click “Dashboard” to go to Spotifys Developer Dashboard.
B. Check the box "Accept the Spotify Developer Terms of Service" and then click "Accept the terms". On the next page, verify your email address if you haven't already. Click the "Create an App" button. Enter any name and description you'd like for your new app. Next, add "http://127.0.0.1:1234" (or any other port number of your choosing) to the "Redirect URI" secction. Check the box "I understand and agree with Spotify's Developer Terms of Service and Design Guidelines" and then click the "Save" button.
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.
## Step 2. Installation and Setup
A. Create a folder somewhere on your computer where you'd like to store the code for your Spotipy app. You can create a folder in terminal with this command: ```mkdir folder_name```
B. In 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. 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
from spotipy.oauth2 import SpotifyOAuth
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(client_id="YOUR_APP_CLIENT_ID",
client_secret="YOUR_APP_CLIENT_SECRET",
redirect_uri="YOUR_APP_REDIRECT_URI",
scope="user-library-read"))
```
D. Replace YOUR_APP_CLIENT_ID and YOUR_APP_CLIENT_SECRET with the values you copied and saved in step 1D. Replace YOUR_APP_REDIRECT_URI with the URI you set in step 1B.
## Step 3. Start Using Spotipy
After completing steps 1 and 2, your app is fully configured and ready to fetch data from the Spotify API. All that's left is to tell the API what data we're looking for, and we do that by adding some additional code to main.py. The code that follows is just an example - once you get it working, you should feel free to modify it in order to get different results.
For now, let's assume that we want to print the names of all the albums on Spotify by Taylor Swift:
A. First, we need to find Taylor Swift's Spotify URI (Uniform Resource Indicator). Every entity (artist, album, song, etc.) has a URI that can identify it. To find Taylor's URI, navigate to [her page on Spotify](https://open.spotify.com/artist/06HL4z0CvFAxyc27GXpf02) and look at the URI in your browser. Everything there that follows the last backslash in the URL path is Taylor's URI, in this case: 06HL4z0CvFAxyc27GXpf02
B. Add the URI as a variable in main.py. Notice the prefix added the URI:
```
taylor_uri = 'spotify:artist:06HL4z0CvFAxyc27GXpf02'
```
C. Add the following code that will get all of Taylor's album names from Spotify and iterate through them to print them all to standard output.
```
results = sp.artist_albums(taylor_uri, album_type='album')
albums = results['items']
while results['next']:
results = sp.next(results)
albums.extend(results['items'])
for album in albums:
print(album['name'])
```
D. Close main.py and return to the directory that contains main.py. You can then run your app by entering the following command: python main.py
E. You may see a window open in your browser asking you to authorize the application. Do so - you will only have to do this once.
F. Return to your terminal - you should see all of Taylor's albums printed out there.
## 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.

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# spotipy documentation build configuration file, created by
# sphinx-quickstart on Thu Aug 21 11:04:39 2014.
@ -11,23 +10,29 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
import os
import sys
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath(".."))
import spotipy
# -- General configuration -----------------------------------------------------
# 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
# 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.
templates_path = ['_templates']
@ -36,7 +41,7 @@ templates_path = ['_templates']
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
@ -56,68 +61,68 @@ release = '2.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# today = ''
# 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
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# 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.
#add_function_parentheses = True
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# 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
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# html_theme_options = {}
# 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
# "<project> v<release> documentation".
#html_title = None
# html_title = None
# 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
# 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
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# html_favicon = None
# 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,
@ -126,44 +131,44 @@ html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# 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
# typographically correct entities.
#html_use_smartypants = True
# html_use_smartypants = True
# 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
# template names.
#html_additional_pages = {}
# html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# html_domain_indices = True
# 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.
#html_split_index = False
# html_split_index = False
# 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.
#html_show_sphinx = True
# html_show_sphinx = 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
# contain a <link> tag referring to it. The value of this option must be the
# 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").
#html_file_suffix = None
# html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'spotipydoc'
@ -172,42 +177,42 @@ htmlhelp_basename = 'spotipydoc'
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'spotipy.tex', 'spotipy Documentation',
'Paul Lamere', 'manual'),
('index', 'spotipy.tex', 'spotipy Documentation',
'Paul Lamere', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# latex_show_pagerefs = False
# 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.
#latex_appendices = []
# latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
@ -220,7 +225,7 @@ man_pages = [
]
# If true, show URL addresses after external links.
#man_show_urls = False
# man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
@ -229,16 +234,16 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'spotipy', 'spotipy Documentation',
'Paul Lamere', 'spotipy', 'One line description of project.',
'Miscellaneous'),
('index', 'spotipy', 'spotipy Documentation',
'Paul Lamere', 'spotipy', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# texinfo_show_urls = 'footnote'

View File

@ -5,11 +5,201 @@ Welcome to Spotipy!
===================================
*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.
Assuming you set the ``SPOTIPY_CLIENT_ID`` and ``SPOTIPY_CLIENT_SECRET``
environment variables, here's a quick example of using *Spotipy* to list the
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/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'::
import spotipy
@ -64,186 +254,8 @@ artist's name::
artist = items[0]
print(artist['name'], artist['images'][0]['url'])
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
============
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 ``SET`` 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``
- ``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
There are many more examples of how to use *Spotipy* in the `spotipy-examples
repository <https://github.com/spotipy-dev/spotipy-examples>`_ on GitHub.
API Reference
==============
@ -284,39 +296,80 @@ You can ask questions about Spotipy on Stack Overflow. Dont forget to add t
http://stackoverflow.com/questions/ask
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
==========
Spotipy authored by Paul Lamere (plamere) with contributions by:
If you are a developer with Python experience, and you would like to contribute to Spotipy, please
be sure to follow the guidelines listed below:
Export the needed Environment variables:::
export SPOTIPY_CLIENT_ID=client_id_here
export SPOTIPY_CLIENT_SECRET=client_secret_here
export SPOTIPY_CLIENT_USERNAME=client_username_here # This is actually an id not spotify display name
export SPOTIPY_REDIRECT_URI=http://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:::
$ virtualenv --python=python3.12 env
(env) $ pip install --user -e .
(env) $ python -m unittest discover -v tests
**Lint**
To automatically fix the code style:::
pip install autopep8
autopep8 --in-place --aggressive --recursive .
To verify the code style:::
pip install flake8
flake8 .
To make sure if the import lists are stored correctly:::
pip install isort
isort . -c -v
**Publishing (by maintainer)**
- Bump version in setup.py
- Bump and date changelog
- Add to changelog:
::
## Unreleased
// Add your changes here and then delete this line
- Commit changes
- Package to pypi:
::
python setup.py sdist bdist_wheel
python3 setup.py sdist bdist_wheel
twine check dist/*
twine upload --repository-url https://upload.pypi.org/legacy/ --skip-existing dist/*.(whl|gz|zip)~dist/*linux*.whl
- Create github release https://github.com/plamere/spotipy/releases with the changelog content for the version and a short name that describes the main addition
- Build the documentation again to ensure it's on the latest version
**Changelog**
Don't forget to add a short description of your change in the `CHANGELOG <https://github.com/plamere/spotipy/blob/master/CHANGELOG.md>`_!
- Daniel Beaudry // danbeaudry
- Faruk Emre Sahin // fsahin
- George // rogueleaderr
- Henry Greville // sethaurus
- Hugo // hugovk
- José Manuel Pérez // JMPerez
- Lucas Nunno // lnunno
- Lynn Root // econchick
- Matt Dennewitz // mattdennewitz
- Matthew Duck // mattduck
- Michael Thelin // thelinmichael
- Ryan Choi // ryankicks
- Simon Metson // drsm79
- Steve Winton // swinton
- Tim Balzer // timbalzer
- corycorycory // corycorycory
- Nathan Coleman // nathancoleman
- Michael Birtwell // mbirtwell
- Harrison Hayes // Harrison97
- Stephane Bruckert // stephanebruckert
- Ritiek Malhotra // ritiek
License
=======
https://github.com/plamere/spotipy/blob/master/LICENSE.md
(Taken from https://github.com/plamere/spotipy/blob/master/LICENSE.md)::
MIT License
Copyright (c) 2021 Paul Lamere
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
(the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Indices and tables
@ -325,4 +378,3 @@ Indices and tables
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

3
docs/requirements.txt Normal file
View File

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

1
examples Submodule

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

View File

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

View File

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

View File

@ -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('-t', '--tids', 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.tids)
if __name__ == '__main__':
main()

View File

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

View File

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

View File

@ -1,73 +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()

View File

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

View File

@ -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,))

View File

@ -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,))

View File

@ -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,))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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']))

View File

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

View File

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

View File

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

View File

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

View File

@ -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'])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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]")

View File

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

View File

@ -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'])

View File

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

View File

@ -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'])

View File

@ -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']))

View File

@ -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'])

View File

@ -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'])

View File

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

View File

@ -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'])

View File

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

View File

@ -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'])

View File

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

View File

@ -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'])

View File

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

View File

@ -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

View File

@ -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.")

View File

@ -1,37 +1,42 @@
from setuptools import setup
with open("README.md", "r") as f:
with open("README.md") as f:
long_description = f.read()
test_reqs = [
'mock==2.0.0'
]
doc_reqs = [
'Sphinx>=1.5.2'
memcache_cache_reqs = [
'pymemcache>=3.5.2'
]
extra_reqs = {
'doc': doc_reqs,
'test': test_reqs
'memcache': [
'pymemcache>=3.5.2'
],
'test': [
'autopep8>=2.3.2',
'flake8>=7.3.0',
'flake8-use-fstring>=1.4',
'isort>=7.0.0'
]
}
setup(
name='spotipy',
version='2.20.0',
version='2.26.0',
description='A light weight Python library for the Spotify Web API',
long_description=long_description,
long_description_content_type="text/markdown",
author="@plamere",
author_email="paul@echonest.com",
url='https://spotipy.readthedocs.org/',
project_urls={
'Source': 'https://github.com/plamere/spotipy',
},
python_requires='>3.8',
install_requires=[
'redis>=3.5.3',
'requests>=2.25.0',
'six>=1.15.0',
'urllib3>=1.26.0'
"redis>=3.5.3", # TODO: Move to extras_require in v3
"requests>=2.25.0",
"urllib3>=1.26.0"
],
tests_require=test_reqs,
extras_require=extra_reqs,
license='MIT',
packages=['spotipy'])

View File

@ -2,17 +2,20 @@ __all__ = [
'CacheHandler',
'CacheFileHandler',
'DjangoSessionCacheHandler',
'FlaskSessionCacheHandler',
'MemoryCacheHandler',
'RedisCacheHandler']
'RedisCacheHandler',
'MemcacheCacheHandler']
import errno
import json
import logging
import os
from spotipy.util import CLIENT_CREDS_ENV_VARS
from redis import RedisError
from spotipy.util import CLIENT_CREDS_ENV_VARS
logger = logging.getLogger(__name__)
@ -38,7 +41,6 @@ class CacheHandler():
Save a token_info dictionary object to the cache and return None.
"""
raise NotImplementedError()
return None
class CacheFileHandler(CacheHandler):
@ -49,15 +51,18 @@ class CacheFileHandler(CacheHandler):
def __init__(self,
cache_path=None,
username=None):
username=None,
encoder_cls=None):
"""
Parameters:
* cache_path: May be supplied, will otherwise be generated
(takes precedence over `username`)
* username: May be supplied or set as environment variable
(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:
self.cache_path = cache_path
else:
@ -71,27 +76,30 @@ class CacheFileHandler(CacheHandler):
token_info = None
try:
f = open(self.cache_path)
token_info_string = f.read()
f.close()
with open(self.cache_path, encoding='utf-8') as f:
token_info_string = f.read()
token_info = json.loads(token_info_string)
except IOError as error:
except OSError as error:
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:
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
def save_token_to_cache(self, token_info):
try:
f = open(self.cache_path, "w")
f.write(json.dumps(token_info))
f.close()
except IOError:
logger.warning('Couldn\'t write token to cache at: %s',
self.cache_path)
with open(self.cache_path, "w", encoding='utf-8') as f:
f.write(json.dumps(token_info, cls=self.encoder_cls))
# https://github.com/spotipy-dev/spotipy/security/advisories/GHSA-pwhh-q4h6-w599
os.chmod(self.cache_path, 0o600)
except OSError:
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):
@ -144,7 +152,32 @@ class DjangoSessionCacheHandler(CacheHandler):
try:
self.request.session['token_info'] = token_info
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):
"""
A cache handler that stores the token info in the session framework
provided by flask.
"""
def __init__(self, session):
self.session = session
def get_cached_token(self):
token_info = None
try:
token_info = self.session["token_info"]
except KeyError:
logger.debug("Token not found in the session")
return token_info
def save_token_to_cache(self, token_info):
try:
self.session["token_info"] = token_info
except Exception as e:
logger.warning(f"Error saving token to cache: {e}")
class RedisCacheHandler(CacheHandler):
@ -170,7 +203,7 @@ class RedisCacheHandler(CacheHandler):
if token_info:
return json.loads(token_info)
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
@ -178,4 +211,36 @@ class RedisCacheHandler(CacheHandler):
try:
self.redis.set(self.key, json.dumps(token_info))
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

View File

@ -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):
self.http_status = http_status
@ -12,5 +16,29 @@ class SpotifyException(Exception):
self.headers = headers
def __str__(self):
return 'http status: {0}, code:{1} - {2}, reason: {3}'.format(
self.http_status, self.code, self.msg, self.reason)
return (f"http status: {self.http_status}, "
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)

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
__all__ = [
"SpotifyClientCredentials",
"SpotifyOAuth",
@ -10,68 +8,43 @@ __all__ = [
]
import base64
import html
import logging
import os
import time
import urllib.parse as urllibparse
import warnings
import webbrowser
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qsl, urlparse
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.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__)
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 recieved 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):
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):
env_val = CLIENT_CREDS_ENV_VARS[env_key]
_val = value or os.getenv(env_val)
if _val is None:
msg = "No %s. Pass it or set a %s environment variable." % (
env_key,
env_val,
)
msg = f"No {env_key}. Pass it or set a {env_val} environment variable."
raise SpotifyOauthError(msg)
return _val
class SpotifyAuthBase(object):
class SpotifyAuthBase:
def __init__(self, requests_session):
if isinstance(requests_session, requests.Session):
self._session = requests_session
@ -136,24 +109,22 @@ class SpotifyAuthBase(object):
error = error_payload.get('error')
error_description = error_payload.get('error_description')
except ValueError:
# if the response cannnot be decoded into JSON (which raises a ValueError),
# then try do decode it into text
# if the response cannot be decoded into JSON (which raises a ValueError),
# then try to decode it into text
# if we receive an empty string (which is falsy), then replace it with `None`
error = response.txt or None
error = response.text or None
error_description = None
raise SpotifyOauthError(
'error: {0}, error_description: {1}'.format(
error, error_description
),
f'error: {error}, error_description: {error_description}',
error=error,
error_description=error_description
)
def __del__(self):
"""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()
@ -196,7 +167,7 @@ class SpotifyClientCredentials(SpotifyAuthBase):
"""
super(SpotifyClientCredentials, self).__init__(requests_session)
super().__init__(requests_session)
self.client_id = client_id
self.client_secret = client_secret
@ -213,10 +184,10 @@ class SpotifyClientCredentials(SpotifyAuthBase):
def get_access_token(self, as_dict=True, check_cache=True):
"""
If a valid access token is in memory, returns it
Else feches a new token and returns it
Else fetches a new token and returns it
Parameters:
- 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 string.
"""
@ -248,10 +219,8 @@ class SpotifyClientCredentials(SpotifyAuthBase):
self.client_id, self.client_secret
)
logger.debug(
"sending POST request to %s with Headers: %s and Body: %r",
self.OAUTH_TOKEN_URL, headers, payload
)
logger.debug(f"Sending POST request to {self.OAUTH_TOKEN_URL} with Headers: "
f"{headers} and Body: {payload}")
try:
response = self._session.post(
@ -319,7 +288,7 @@ class SpotifyOAuth(SpotifyAuthBase):
* requests_session: A Requests session
* requests_timeout: Optional, tell Requests to stop waiting for a response after
a given number of seconds
* open_browser: Optional, whether or not the web browser should be opened to
* open_browser: Optional, whether the web browser should be opened to
authorize a user
* cache_handler: An instance of the `CacheHandler` class to handle
getting and saving cached authorization tokens.
@ -327,7 +296,7 @@ class SpotifyOAuth(SpotifyAuthBase):
(takes precedence over `cache_path` and `username`)
"""
super(SpotifyOAuth, self).__init__(requests_session)
super().__init__(requests_session)
self.client_id = client_id
self.client_secret = client_secret
@ -402,7 +371,7 @@ class SpotifyOAuth(SpotifyAuthBase):
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):
""" Parse the response code in the given response url
@ -421,8 +390,7 @@ class SpotifyOAuth(SpotifyAuthBase):
query_s = urlparse(url).query
form = dict(parse_qsl(query_s))
if "error" in form:
raise SpotifyOauthError("Received error from auth server: "
"{}".format(form["error"]),
raise SpotifyOauthError(f"Received error from auth server: {form['error']}",
error=form["error"])
return tuple(form.get(param) for param in ["state", "code"])
@ -433,9 +401,9 @@ class SpotifyOAuth(SpotifyAuthBase):
auth_url = self.get_authorize_url()
try:
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:
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):
if open_browser:
@ -444,8 +412,8 @@ class SpotifyOAuth(SpotifyAuthBase):
else:
url = self.get_authorize_url()
prompt = (
"Go to the following URL: {}\n"
"Enter the URL you were redirected to: ".format(url)
f"Go to the following URL: {url}\n"
"Enter the URL you were redirected to: "
)
response = self._get_user_input(prompt)
state, code = SpotifyOAuth.parse_auth_response_url(response)
@ -477,6 +445,17 @@ class SpotifyOAuth(SpotifyAuthBase):
redirect_info = urlparse(self.redirect_uri)
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:
open_browser = self.open_browser
@ -489,12 +468,11 @@ class SpotifyOAuth(SpotifyAuthBase):
if redirect_port:
return self._get_auth_response_local_server(redirect_port)
else:
logger.warning('Using `%s` as redirect URI without a port. '
'Specify a port (e.g. `%s:8080`) to allow '
logger.warning(f'Using `{redirect_host}` as redirect URI without a port. '
f'Specify a port (e.g. `{redirect_host}:8080`) to allow '
'automatic retrieval of authentication code '
'instead of having to copy and paste '
'the URL your browser is redirected to.',
redirect_host, redirect_host)
'the URL your browser is redirected to.')
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
Parameters:
- code - the response code
- as_dict - a boolean indicating if returning the access token
- code: the response code
- as_dict: (deprecated) a boolean indicating if returning the access token
as a token_info dictionary, otherwise it will be returned
as a string.
"""
@ -542,10 +520,8 @@ class SpotifyOAuth(SpotifyAuthBase):
headers = self._make_authorization_headers()
logger.debug(
"sending POST request to %s with Headers: %s and Body: %r",
self.OAUTH_TOKEN_URL, headers, payload
)
logger.debug(f"Sending POST request to {self.OAUTH_TOKEN_URL} with Headers: "
f"{headers} and Body: {payload}")
try:
response = self._session.post(
@ -572,10 +548,8 @@ class SpotifyOAuth(SpotifyAuthBase):
headers = self._make_authorization_headers()
logger.debug(
"sending POST request to %s with Headers: %s and Body: %r",
self.OAUTH_TOKEN_URL, headers, payload
)
logger.debug(f"Sending POST request to {self.OAUTH_TOKEN_URL} with Headers: "
f"{headers} and Body: {payload}")
try:
response = self._session.post(
@ -605,6 +579,11 @@ class SpotifyOAuth(SpotifyAuthBase):
return token_info
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 " +
"deprecated. Instead, please specify a CacheFileHandler instance as " +
"the cache_handler in SpotifyOAuth and use the CacheFileHandler's " +
@ -629,8 +608,8 @@ class SpotifyPKCE(SpotifyAuthBase):
""" Implements PKCE Authorization Flow for client apps
This auth manager enables *user and non-user* endpoints with only
a client secret, redirect uri, and username. When the app requests
an an access token for the first time, the user is prompted to
a client ID, redirect URI, and username. When the app requests
an access token for the first time, the user is prompted to
authorize the new client app. After authorizing the app, the client
app is then given both access and refresh tokens. This is the
preferred way of authorizing a mobile/desktop client.
@ -669,7 +648,7 @@ class SpotifyPKCE(SpotifyAuthBase):
* requests_timeout: Optional, tell Requests to stop waiting for a response after
a given number of seconds
* requests_session: A Requests session
* open_browser: Optional, thether or not the web browser should be opened to
* open_browser: Optional, whether the web browser should be opened to
authorize a user
* cache_handler: An instance of the `CacheHandler` class to handle
getting and saving cached authorization tokens.
@ -677,7 +656,7 @@ class SpotifyPKCE(SpotifyAuthBase):
(takes precedence over `cache_path` and `username`)
"""
super(SpotifyPKCE, self).__init__(requests_session)
super().__init__(requests_session)
self.client_id = client_id
self.redirect_uri = redirect_uri
self.state = state
@ -727,15 +706,8 @@ class SpotifyPKCE(SpotifyAuthBase):
length = random.randint(33, 96)
# The seeded length generates between a 44 and 128 base64 characters encoded string
try:
import secrets
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
import secrets
return secrets.token_urlsafe(length)
def _get_code_challenge(self):
""" Spotify PCKE code challenge - See step 1 of the reference guide below
@ -766,15 +738,15 @@ class SpotifyPKCE(SpotifyAuthBase):
if state is not None:
payload["state"] = state
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):
auth_url = self.get_authorize_url(state)
try:
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:
logger.error("Please navigate here: %s", auth_url)
logger.error(f"Please navigate here: {auth_url}")
def _get_auth_response(self, open_browser=None):
logger.info('User authentication requires interaction with your '
@ -789,6 +761,17 @@ class SpotifyPKCE(SpotifyAuthBase):
if open_browser is None:
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 (
open_browser
and redirect_host in ("127.0.0.1", "localhost")
@ -798,12 +781,11 @@ class SpotifyPKCE(SpotifyAuthBase):
if redirect_port:
return self._get_auth_response_local_server(redirect_port)
else:
logger.warning('Using `%s` as redirect URI without a port. '
'Specify a port (e.g. `%s:8080`) to allow '
logger.warning(f'Using `{redirect_host}` as redirect URI without a port. '
f'Specify a port (e.g. `{redirect_host}:8080`) to allow '
'automatic retrieval of authentication code '
'instead of having to copy and paste '
'the URL your browser is redirected to.',
redirect_host, redirect_host)
'the URL your browser is redirected to.')
return self._get_auth_response_interactive(open_browser=open_browser)
def _get_auth_response_local_server(self, redirect_port):
@ -817,7 +799,7 @@ class SpotifyPKCE(SpotifyAuthBase):
if server.auth_code is not None:
return server.auth_code
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:
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: "
else:
url = self.get_authorize_url()
prompt = (
"Go to the following URL: {}\n"
"Enter the URL you were redirected to: ".format(url)
)
prompt = (f"Go to the following URL: {url}\n"
f"Enter the URL you were redirected to: ")
response = self._get_user_input(prompt)
state, code = self.parse_auth_response_url(response)
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"}
logger.debug(
"sending POST request to %s with Headers: %s and Body: %r",
self.OAUTH_TOKEN_URL, headers, payload
)
logger.debug(f"Sending POST request to {self.OAUTH_TOKEN_URL} with Headers: "
f"{headers} and Body: {payload}")
try:
response = self._session.post(
@ -937,10 +915,8 @@ class SpotifyPKCE(SpotifyAuthBase):
headers = {"Content-Type": "application/x-www-form-urlencoded"}
logger.debug(
"sending POST request to %s with Headers: %s and Body: %r",
self.OAUTH_TOKEN_URL, headers, payload
)
logger.debug(f"Sending POST request to {self.OAUTH_TOKEN_URL} with Headers: "
f"{headers} and Body: {payload}")
try:
response = self._session.post(
@ -1012,7 +988,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
Authentication Code flow. Use the SpotifyPKCE auth manager instead
of SpotifyImplicitGrant.
SpotifyPKCE contains all of the functionality of
SpotifyPKCE contains all the functionality of
SpotifyImplicitGrant, plus automatic response retrieval and
refreshable tokens. Only a few replacements need to be made:
@ -1025,7 +1001,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
* parse_response_token(url) ->
get_access_token(parse_response_code(url)); get_cached_token()
The security concern in the Implict Grant flow is that the token is
The security concern in the Implicit Grant flow is that the token is
returned in the URL and can be intercepted through the browser. A
request with an authorization code and proof of origin could not be
easily intercepted without a compromised network.
@ -1043,7 +1019,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
cache_handler=None):
""" Creates Auth Manager using the Implicit Grant flow
**See help(SpotifyImplictGrant) for full Security Warning**
**See help(SpotifyImplicitGrant) for full Security Warning**
Parameters
----------
@ -1062,11 +1038,11 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
(will set `cache_path` to `.cache-{username}`)
* 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 "
"auth manager instead of SpotifyImplicitGrant. For "
"more details and a guide to switching, see "
"help(SpotifyImplictGrant).")
"help(SpotifyImplicitGrant).")
self.client_id = client_id
self.redirect_uri = redirect_uri
@ -1160,7 +1136,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
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):
""" Parse the response code in the given response url """
@ -1180,8 +1156,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
form = dict(i.split('=') for i
in (fragment_s or query_s or url).split('&'))
if "error" in form:
raise SpotifyOauthError("Received error from auth server: "
"{}".format(form["error"]),
raise SpotifyOauthError(f"Received error from auth server: {form['error']}",
state=form["state"])
if "expires_in" in form:
form["expires_in"] = int(form["expires_in"])
@ -1192,9 +1167,9 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
auth_url = self.get_authorize_url(state)
try:
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:
logger.error("Please navigate here: %s", auth_url)
logger.error(f"Please navigate here: {auth_url}")
def get_auth_response(self, state=None):
""" Gets a new auth **token** with user interaction """
@ -1235,6 +1210,11 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
return token_info
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 " +
"object will be deprecated. Instead, please specify a " +
"CacheFileHandler instance as the cache_handler in SpotifyOAuth " +
@ -1273,24 +1253,26 @@ class RequestHandler(BaseHTTPRequestHandler):
if self.server.auth_code:
status = "successful"
elif self.server.error:
status = "failed ({})".format(self.server.error)
status = f"failed ({html.escape(str(self.server.error))})"
else:
self._write("<html><body><h1>Invalid request</h1></body></html>")
return
self._write("""<html>
self._write(f"""<html>
<script>
window.close()
</script>
<body>
<h1>Authentication status: {}</h1>
<h1>Authentication status: {status}</h1>
This window can be closed.
<script>
window.close()
</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>
</html>""".format(status))
</html>""")
def _write(self, text):
return self.wfile.write(text.encode("utf-8"))

View File

@ -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"]
import logging
import os
import warnings
from types import TracebackType
import requests
import urllib3
import spotipy
LOGGER = logging.getLogger(__name__)
logger = logging.getLogger(__name__)
CLIENT_CREDS_ENV_VARS = {
"client_id": "SPOTIPY_CLIENT_ID",
@ -19,6 +23,9 @@ CLIENT_CREDS_ENV_VARS = {
"redirect_uri": "SPOTIPY_REDIRECT_URI",
}
# workaround for garbage collection
REQUESTS_SESSION = requests.Session
def prompt_for_user_token(
username=None,
@ -30,6 +37,22 @@ def prompt_for_user_token(
oauth_manager=None,
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(
"'prompt_for_user_token' is deprecated."
"Use the following instead: "
@ -37,22 +60,7 @@ def prompt_for_user_token(
" spotipy.Spotify(auth_manager=auth_manager)",
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 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")
if not client_id:
LOGGER.warning(
logger.warning(
"""
You need to set your Spotify API credentials.
You can do this by setting environment variables like so:
@ -90,7 +98,7 @@ def prompt_for_user_token(
)
# try to get a valid token for this user, from the cache,
# if not in the cache, the create a new (this will send
# if not in the cache, then create a new (this will send
# the user to a web page where they can authorize this app)
token_info = sp_oauth.validate_token(sp_oauth.cache_handler.get_cached_token())
@ -109,6 +117,12 @@ def prompt_for_user_token(
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:
host, port = netloc.split(":", 1)
port = int(port)
@ -120,6 +134,14 @@ def get_host_port(netloc):
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 isinstance(scope, str):
scopes = scope.split(',')
@ -128,8 +150,36 @@ def normalize_scope(scope):
else:
raise Exception(
"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))
else:
return None
class Retry(urllib3.Retry):
"""
Custom class for printing a warning when a rate/request limit is reached.
"""
def increment(
self,
method: str | None = None,
url: str | None = None,
response: urllib3.BaseHTTPResponse | None = None,
error: Exception | None = None,
_pool: urllib3.connectionpool.ConnectionPool | None = None,
_stacktrace: TracebackType | None = None,
) -> urllib3.Retry:
if response:
retry_header = response.headers.get("Retry-After")
if self.is_retry(method, response.status, bool(retry_header)):
retry_header = retry_header or 0
logger.warning("Your application has reached a rate/request limit. "
f"Retry will occur after: {retry_header} s")
return super().increment(method,
url,
response=response,
error=error,
_pool=_pool,
_stacktrace=_stacktrace)

View File

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

View File

@ -1,14 +1,10 @@
# -*- coding: utf-8 -*-
from spotipy import (
Spotify,
SpotifyClientCredentials,
SpotifyException
)
import spotipy
import unittest
import requests
import spotipy
from spotipy import Spotify, SpotifyClientCredentials, SpotifyException
class AuthTestSpotipy(unittest.TestCase):
"""
@ -39,12 +35,19 @@ class AuthTestSpotipy(unittest.TestCase):
creep_urn = 'spotify:track:6b2oQwSGFkzsMtQruIWm2p'
creep_id = '6b2oQwSGFkzsMtQruIWm2p'
creep_url = 'http://open.spotify.com/track/6b2oQwSGFkzsMtQruIWm2p'
el_scorcho_urn = 'spotify:track:0Svkvt5I79wficMFgaqEQJ'
el_scorcho_bad_urn = 'spotify:track:0Svkvt5I79wficMFgaqEQK'
pinkerton_urn = 'spotify:album:04xe676vyiTeYNXw15o9jT'
weezer_urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu'
pablo_honey_urn = 'spotify:album:6AZv3m27uyRxi8KyJSfUxL'
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'
heavyweight_urn = 'spotify:show:5c26B28vZMN8PG0Nppmn5G'
heavyweight_id = '5c26B28vZMN8PG0Nppmn5G'
@ -55,49 +58,41 @@ class AuthTestSpotipy(unittest.TestCase):
heavyweight_ep1_url = 'https://open.spotify.com/episode/68kq3bNz6hEuq8NtdfwERG'
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
def setUpClass(self):
self.spotify = Spotify(
client_credentials_manager=SpotifyClientCredentials())
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):
artist = self.spotify.artist(self.radiohead_urn)
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):
results = self.spotify.artists([self.weezer_urn, self.radiohead_urn])
self.assertTrue('artists' in results)
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):
album = self.spotify.album(self.pinkerton_urn)
self.assertTrue(album['name'] == 'Pinkerton')
@ -154,15 +149,6 @@ class AuthTestSpotipy(unittest.TestCase):
self.assertTrue('tracks' in results)
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)
for artist in results['artists']:
if artist['name'] == 'Jimmy Eat World':
found = True
self.assertTrue(found)
def test_artist_search(self):
results = self.spotify.search(q='weezer', type='artist')
self.assertTrue('artists' in results)
@ -219,27 +205,110 @@ class AuthTestSpotipy(unittest.TestCase):
total_limited_results += len(results_limited[country]['artists']['items'])
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):
results = self.spotify.artist_albums(self.weezer_urn)
self.assertTrue('items' in results)
self.assertTrue(len(results['items']) > 0)
found = False
for album in results['items']:
if album['name'] == 'Hurley':
found = True
def find_album():
for album in results['items']:
if 'Weezer' in album['name']: # Weezer has many albums containing Weezer
return True
return False
self.assertTrue(found)
self.assertTrue(find_album())
def test_search_timeout(self):
client_credentials_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(requests_timeout=0.01,
client_credentials_manager=client_credentials_manager)
# depending on the timing or bandwidth, this raises a timeout or connection error"
# depending on the timing or bandwidth, this raises a timeout or connection error
self.assertRaises((requests.exceptions.Timeout, requests.exceptions.ConnectionError),
lambda: sp.search(q='my*', type='track'))
@unittest.skip("flaky test, need a better method to test retries")
def test_max_retries_reached_get(self):
spotify_no_retry = Spotify(
client_credentials_manager=SpotifyClientCredentials(),
@ -370,3 +439,28 @@ class AuthTestSpotipy(unittest.TestCase):
self.assertTrue(isinstance(markets, list))
self.assertIn("US", 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)

View File

@ -1,14 +1,9 @@
import os
from spotipy import (
CLIENT_CREDS_ENV_VARS as CCEV,
prompt_for_user_token,
Spotify,
SpotifyException,
SpotifyImplicitGrant,
SpotifyPKCE
)
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
@ -253,7 +248,7 @@ class SpotipyLibraryApiTests(unittest.TestCase):
tracks = self.spotify.current_user_saved_tracks()
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()
total = tracks['total']
self.spotify.current_user_saved_tracks_add(self.four_tracks)
@ -266,6 +261,19 @@ class SpotipyLibraryApiTests(unittest.TestCase):
self.four_tracks)
tracks = self.spotify.current_user_saved_tracks()
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)
def test_current_user_saved_albums(self):
@ -352,29 +360,45 @@ class SpotipyBrowseApiTests(unittest.TestCase):
cls.spotify = Spotify(auth=token)
def test_category(self):
response = self.spotify.category('rock')
self.assertTrue('name' in response)
rock_cat_id = '0JQ5DAqbMKFDXXwE9BDJAr'
response = self.spotify.category(rock_cat_id)
self.assertEqual(response['name'], 'Rock')
def test_categories(self):
response = self.spotify.categories()
self.assertGreater(len(response['categories']), 0)
def test_category_playlists(self):
def test_categories_country(self):
response = self.spotify.categories(country='US')
self.assertGreater(len(response['categories']), 0)
def test_categories_global(self):
response = self.spotify.categories()
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)
self.assertGreater(len(response['categories']), 0)
def test_categories_locale(self):
response = self.spotify.categories(locale='en_US')
self.assertGreater(len(response['categories']), 0)
def test_categories_limit_low(self):
response = self.spotify.categories(limit=1)
self.assertEqual(len(response['categories']['items']), 1)
def test_categories_limit_high(self):
response = self.spotify.categories(limit=50)
self.assertLessEqual(len(response['categories']['items']), 50)
def test_new_releases(self):
response = self.spotify.new_releases()
self.assertGreater(len(response['albums']), 0)
self.assertGreater(len(response['albums']['items']), 0)
def test_featured_releases(self):
response = self.spotify.featured_playlists()
self.assertGreater(len(response['playlists']), 0)
def test_new_releases_limit_low(self):
response = self.spotify.new_releases(limit=1)
self.assertEqual(len(response['albums']['items']), 1)
def test_new_releases_limit_high(self):
response = self.spotify.new_releases(limit=50)
self.assertLessEqual(len(response['albums']['items']), 50)
class SpotipyFollowApiTests(unittest.TestCase):
@ -498,3 +522,44 @@ class SpotifyPKCETests(unittest.TestCase):
c_user = self.spotify.current_user()
user = self.spotify.user(c_user['id'])
self.assertEqual(c_user['display_name'], user['display_name'])
class SpotifyQueueApiTests(unittest.TestCase):
@classmethod
def setUp(self):
self.spotify = Spotify(auth="test_token")
def test_get_queue(self, mock_get):
# Mock the response from _get
mock_get.return_value = {'songs': ['song1', 'song2']}
# Call the queue function
response = self.spotify.queue()
# Check if the correct endpoint is called
mock_get.assert_called_with("me/player/queue")
# Check if the response is as expected
self.assertEqual(response, {'songs': ['song1', 'song2']})
def test_add_to_queue(self, mock_post):
test_uri = 'spotify:track:123'
# Call the add_to_queue function
self.spotify.add_to_queue(test_uri)
# Check if the correct endpoint is called
endpoint = f"me/player/queue?uri={test_uri}"
mock_post.assert_called_with(endpoint)
def test_add_to_queue_with_device_id(self, mock_post):
test_uri = 'spotify:track:123'
device_id = 'device123'
# Call the add_to_queue function with a device_id
self.spotify.add_to_queue(test_uri, device_id=device_id)
# Check if the correct endpoint is called
endpoint = f"me/player/queue?uri={test_uri}&device_id={device_id}"
mock_post.assert_called_with(endpoint)

View File

@ -1,19 +1,13 @@
# -*- coding: utf-8 -*-
import io
import json
import unittest
import unittest.mock as mock
import urllib.parse as urllibparse
import six.moves.urllib.parse as urllibparse
from spotipy import SpotifyOAuth, SpotifyImplicitGrant, SpotifyPKCE
from spotipy import SpotifyImplicitGrant, SpotifyOAuth, SpotifyPKCE
from spotipy.cache_handler import MemoryCacheHandler
from spotipy.oauth2 import SpotifyClientCredentials, SpotifyOauthError
from spotipy.oauth2 import SpotifyStateError
try:
import unittest.mock as mock
except ImportError:
import mock
from spotipy.oauth2 import (SpotifyClientCredentials, SpotifyOauthError,
SpotifyStateError)
patch = mock.patch
DEFAULT = mock.DEFAULT
@ -58,18 +52,21 @@ class OAuthCacheTest(unittest.TestCase):
@patch('spotipy.cache_handler.open', create=True)
def test_gets_from_cache_path(self, opener,
is_token_expired, refresh_access_token):
"""Test that the token is retrieved from the cache path."""
scope = "playlist-modify-private"
path = ".cache-username"
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
spot = _make_oauth(scope, path)
cached_tok = spot.validate_token(spot.cache_handler.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_legacy)
self.assertEqual(refresh_access_token.call_count, 0)
@ -79,13 +76,15 @@ class OAuthCacheTest(unittest.TestCase):
@patch('spotipy.cache_handler.open', create=True)
def test_expired_token_refreshes(self, opener,
is_token_expired, refresh_access_token):
"""Test that an expired token is refreshed."""
scope = "playlist-modify-private"
path = ".cache-username"
expired_tok = _make_fake_token(0, None, scope)
fresh_tok = _make_fake_token(1, 1, scope)
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
spot = _make_oauth(scope, path)
@ -93,7 +92,7 @@ class OAuthCacheTest(unittest.TestCase):
is_token_expired.assert_called_with(expired_tok)
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,
is_token_expired=DEFAULT, refresh_access_token=DEFAULT)
@ -105,29 +104,35 @@ class OAuthCacheTest(unittest.TestCase):
path = ".cache-username"
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
spot = _make_oauth(requested_scope, path)
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.assertEqual(refresh_access_token.call_count, 0)
@patch('spotipy.cache_handler.open', create=True)
def test_saves_to_cache_path(self, opener):
"""Test that the token is saved to the cache path."""
scope = "playlist-modify-private"
path = ".cache-username"
tok = _make_fake_token(1, 1, scope)
fi = _fake_file()
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.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)
@patch('spotipy.cache_handler.open', create=True)
@ -138,11 +143,13 @@ class OAuthCacheTest(unittest.TestCase):
fi = _fake_file()
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._save_token_info(tok)
opener.assert_called_with(path, 'w')
opener.assert_called_with(path, 'w', encoding='utf-8')
self.assertTrue(fi.write.called)
def test_cache_handler(self):
@ -258,32 +265,38 @@ class ImplicitGrantCacheTest(unittest.TestCase):
path = ".cache-username"
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
spot = _make_implicitgrantauth(scope, path)
cached_tok = spot.cache_handler.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_legacy)
@patch.object(SpotifyImplicitGrant, "is_token_expired", DEFAULT)
@patch('spotipy.cache_handler.open', create=True)
def test_expired_token_returns_none(self, opener, is_token_expired):
"""Test that an expired token returns None."""
scope = "playlist-modify-private"
path = ".cache-username"
expired_tok = _make_fake_token(0, None, scope)
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)
spot = _make_implicitgrantauth(scope, path)
cached_tok = spot.validate_token(spot.cache_handler.get_cached_token())
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)
@patch.object(SpotifyImplicitGrant, "is_token_expired", DEFAULT)
@ -294,13 +307,16 @@ class ImplicitGrantCacheTest(unittest.TestCase):
path = ".cache-username"
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
spot = _make_implicitgrantauth(requested_scope, path)
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)
@patch('spotipy.cache_handler.open', create=True)
@ -312,10 +328,12 @@ class ImplicitGrantCacheTest(unittest.TestCase):
fi = _fake_file()
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.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)
@patch('spotipy.cache_handler.open', create=True)
@ -326,11 +344,13 @@ class ImplicitGrantCacheTest(unittest.TestCase):
fi = _fake_file()
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._save_token_info(tok)
opener.assert_called_with(path, 'w')
opener.assert_called_with(path, 'w', encoding='utf-8')
self.assertTrue(fi.write.called)
@ -395,14 +415,17 @@ class SpotifyPKCECacheTest(unittest.TestCase):
path = ".cache-username"
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
spot = _make_pkceauth(scope, path)
cached_tok = spot.cache_handler.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_legacy)
self.assertEqual(refresh_access_token.call_count, 0)
@ -418,7 +441,8 @@ class SpotifyPKCECacheTest(unittest.TestCase):
fresh_tok = _make_fake_token(1, 1, scope)
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
spot = _make_pkceauth(scope, path)
@ -426,7 +450,7 @@ class SpotifyPKCECacheTest(unittest.TestCase):
is_token_expired.assert_called_with(expired_tok)
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,
is_token_expired=DEFAULT, refresh_access_token=DEFAULT)
@ -438,13 +462,16 @@ class SpotifyPKCECacheTest(unittest.TestCase):
path = ".cache-username"
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
spot = _make_pkceauth(requested_scope, path)
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.assertEqual(refresh_access_token.call_count, 0)
@ -456,11 +483,12 @@ class SpotifyPKCECacheTest(unittest.TestCase):
fi = _fake_file()
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.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)
@patch('spotipy.cache_handler.open', create=True)
@ -471,11 +499,13 @@ class SpotifyPKCECacheTest(unittest.TestCase):
fi = _fake_file()
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._save_token_info(tok)
opener.assert_called_with(path, 'w')
opener.assert_called_with(path, 'w', encoding='utf-8')
self.assertTrue(fi.write.called)
@ -492,8 +522,8 @@ class TestSpotifyPKCE(unittest.TestCase):
self.assertTrue(auth.code_challenge)
def test_code_verifier_and_code_challenge_are_correct(self):
import hashlib
import base64
import hashlib
auth = SpotifyPKCE("CLID", "REDIR")
auth.get_pkce_handshake_parameters()
self.assertEqual(auth.code_challenge,

11
tox.ini
View File

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