mirror of
https://github.com/spotipy-dev/spotipy.git
synced 2026-06-19 09:13:53 +00:00
commit
d7e8dd1e74
115
CHANGELOG.md
115
CHANGELOG.md
@ -7,8 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## Unreleased [3.0.0-alpha]
|
## Unreleased [3.0.0-alpha]
|
||||||
|
|
||||||
While this is unreleased, please only add v3 features here.
|
While this is unreleased, please only add v3 features here. Rebasing master onto v3 doesn't require a changelog update.
|
||||||
Rebasing master onto v3 doesn't require a changelog update.
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
@ -17,16 +16,50 @@ Rebasing master onto v3 doesn't require a changelog update.
|
|||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* Made `CacheHandler` an abstract base class
|
* Made `CacheHandler` an abstract base class
|
||||||
|
|
||||||
* Modified the return structure of the `audio_features` function (wrapping the [Get Audio Features for Several Tracks](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-audio-features) API) to conform to the return structure of the similar methods listed below. The functions wrapping these APIs do not unwrap the single key JSON response, and this is currently the only function that does this.
|
* Modified the return structure of the `audio_features` function (wrapping the [Get Audio Features for Several Tracks](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-audio-features) API) to conform to the return structure of the similar methods listed below. The functions wrapping these APIs do not unwrap the single key JSON response, and this is currently the only function that does this.
|
||||||
* [Get Several Tracks](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-tracks)
|
* [Get Several Tracks](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-tracks)
|
||||||
* [Get Multiple Artists](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-artists)
|
* [Get Multiple Artists](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-artists)
|
||||||
* [Get Multiple Albums](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-albums)
|
* [Get Multiple Albums](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-albums)
|
||||||
|
* Renamed the `auth` parameter of `Spotify.__init__` to `access_token` for better clarity.
|
||||||
|
* Removed the `client_credentials_manager` and `oauth_manager` parameters because they are redundant.
|
||||||
|
* Replaced the `set_auth` and `auth_manager` properties with standard attributes.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
* Removed the following deprecated methods from `Spotify`:
|
||||||
|
* `playlist_tracks`
|
||||||
|
* `user_playlist`
|
||||||
|
* `user_playlist_tracks`
|
||||||
|
* `user_playlist_change_details`
|
||||||
|
* `user_playlist_unfollow`
|
||||||
|
* `user_playlist_add_tracks`
|
||||||
|
* `user_playlist_replace_tracks`
|
||||||
|
* `user_playlist_reorder_tracks`
|
||||||
|
* `user_playlist_remove_all_occurrences_of_tracks`
|
||||||
|
* `user_playlist_remove_specific_occurrences_of_tracks`
|
||||||
|
* `user_playlist_follow_playlist`
|
||||||
|
* `user_playlist_is_following`
|
||||||
|
|
||||||
|
* Removed the deprecated `as_dict` parameter from the `get_access_token` method of `SpotifyOAuth` and `SpotifyPKCE`.
|
||||||
|
|
||||||
|
* Removed the deprecated `get_cached_token` and `_save_token_info` methods of `SpotifyOAuth` and `SpotifyPKCE`.
|
||||||
|
* Removed `SpotifyImplicitGrant`.
|
||||||
|
* Removed `prompt_for_user_token`.
|
||||||
|
|
||||||
## Unreleased [2.x.x]
|
## Unreleased [2.x.x]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
* Added `MemoryCacheHandler`, a cache handler that simply stores the token info in memory as an instance attribute of this class.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* Fixed a bug in `CacheFileHandler.__init__`: The documentation says that the username will be retrieved from the environment, but it wasn't.
|
||||||
|
|
||||||
|
## [2.18.0] - 2021-04-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
- Enabled using both short and long IDs for playlist_change_details
|
- Enabled using both short and long IDs for playlist_change_details
|
||||||
- Added a cache handler to `SpotifyClientCredentials`
|
- Added a cache handler to `SpotifyClientCredentials`
|
||||||
- Added the following endpoints
|
- Added the following endpoints
|
||||||
@ -43,13 +76,9 @@ Rebasing master onto v3 doesn't require a changelog update.
|
|||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
* Fixed the bugs in `SpotifyOAuth.refresh_access_token` and `SpotifyPKCE.refresh_access_token` which raised the incorrect exception upon receiving an error response from the server. This addresses #645.
|
* Fixed the bugs in `SpotifyOAuth.refresh_access_token` and `SpotifyPKCE.refresh_access_token` which raised the incorrect exception upon receiving an error response from the server. This addresses #645.
|
||||||
|
|
||||||
* Fixed a bug in `RequestHandler.do_GET` in which the non-existent `state` attribute of `SpotifyOauthError` is accessed. This bug occurs when the user clicks "cancel" in the permissions dialog that opens in the browser.
|
* Fixed a bug in `RequestHandler.do_GET` in which the non-existent `state` attribute of `SpotifyOauthError` is accessed. This bug occurs when the user clicks "cancel" in the permissions dialog that opens in the browser.
|
||||||
|
|
||||||
* Cleaned up the documentation for `SpotifyClientCredentials.__init__`, `SpotifyOAuth.__init__`, and `SpotifyPKCE.__init__`.
|
* Cleaned up the documentation for `SpotifyClientCredentials.__init__`, `SpotifyOAuth.__init__`, and `SpotifyPKCE.__init__`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.17.1] - 2021-02-28
|
## [2.17.1] - 2021-02-28
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
@ -104,7 +133,7 @@ Rebasing master onto v3 doesn't require a changelog update.
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `SpotifyPKCE.parse_auth_response_url`, mirroring that method in
|
- `SpotifyPKCE.parse_auth_response_url`, mirroring that method in
|
||||||
`SpotifyOAuth`
|
`SpotifyOAuth`
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@ -113,7 +142,7 @@ Rebasing master onto v3 doesn't require a changelog update.
|
|||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Using `SpotifyPKCE.get_authorization_url` will now generate a code
|
- Using `SpotifyPKCE.get_authorization_url` will now generate a code
|
||||||
challenge if needed
|
challenge if needed
|
||||||
|
|
||||||
## [2.14.0] - 2020-08-29
|
## [2.14.0] - 2020-08-29
|
||||||
|
|
||||||
@ -121,9 +150,9 @@ Rebasing master onto v3 doesn't require a changelog update.
|
|||||||
|
|
||||||
- (experimental) Support to search multiple/all markets at once.
|
- (experimental) Support to search multiple/all markets at once.
|
||||||
- Support to test whether the current user is following certain
|
- Support to test whether the current user is following certain
|
||||||
users or artists
|
users or artists
|
||||||
- Proper replacements for all deprecated playlist endpoints
|
- Proper replacements for all deprecated playlist endpoints
|
||||||
(See https://developer.spotify.com/community/news/2018/06/12/changes-to-playlist-uris/ and below)
|
(See https://developer.spotify.com/community/news/2018/06/12/changes-to-playlist-uris/ and below)
|
||||||
- Allow for OAuth 2.0 authorization by instructing the user to open the URL in a browser instead of opening the browser.
|
- Allow for OAuth 2.0 authorization by instructing the user to open the URL in a browser instead of opening the browser.
|
||||||
- Reason for 403 error in SpotifyException
|
- Reason for 403 error in SpotifyException
|
||||||
- Support for the PKCE Auth Flow
|
- Support for the PKCE Auth Flow
|
||||||
@ -141,15 +170,16 @@ Rebasing master onto v3 doesn't require a changelog update.
|
|||||||
- `user_playlist_replace_tracks` in favor of `playlist_replace_items`
|
- `user_playlist_replace_tracks` in favor of `playlist_replace_items`
|
||||||
- `user_playlist_reorder_tracks` in favor of `playlist_reorder_items`
|
- `user_playlist_reorder_tracks` in favor of `playlist_reorder_items`
|
||||||
- `user_playlist_remove_all_occurrences_of_tracks` in favor of
|
- `user_playlist_remove_all_occurrences_of_tracks` in favor of
|
||||||
`playlist_remove_all_occurrences_of_items`
|
`playlist_remove_all_occurrences_of_items`
|
||||||
- `user_playlist_remove_specific_occurrences_of_tracks` in favor of
|
- `user_playlist_remove_specific_occurrences_of_tracks` in favor of
|
||||||
`playlist_remove_specific_occurrences_of_items`
|
`playlist_remove_specific_occurrences_of_items`
|
||||||
- `user_playlist_follow_playlist` in favor of
|
- `user_playlist_follow_playlist` in favor of
|
||||||
`current_user_follow_playlist`
|
`current_user_follow_playlist`
|
||||||
- `user_playlist_is_following` in favor of `playlist_is_following`
|
- `user_playlist_is_following` in favor of `playlist_is_following`
|
||||||
- `playlist_tracks` in favor of `playlist_items`
|
- `playlist_tracks` in favor of `playlist_items`
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- fixed issue where episode URIs were being converted to track URIs in playlist calls
|
- fixed issue where episode URIs were being converted to track URIs in playlist calls
|
||||||
|
|
||||||
## [2.13.0] - 2020-06-25
|
## [2.13.0] - 2020-06-25
|
||||||
@ -157,12 +187,12 @@ Rebasing master onto v3 doesn't require a changelog update.
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added `SpotifyImplicitGrant` as an auth manager option. It provides
|
- Added `SpotifyImplicitGrant` as an auth manager option. It provides
|
||||||
user authentication without a client secret but sacrifices the ability
|
user authentication without a client secret but sacrifices the ability
|
||||||
to refresh the token without user input. (However, read the class
|
to refresh the token without user input. (However, read the class
|
||||||
docstring for security advisory.)
|
docstring for security advisory.)
|
||||||
- Added built-in verification of the `state` query parameter
|
- Added built-in verification of the `state` query parameter
|
||||||
- Added two new attributes: error and error_description to `SpotifyOauthError` exception class to show
|
- Added two new attributes: error and error_description to `SpotifyOauthError` exception class to show
|
||||||
authorization/authentication web api errors details.
|
authorization/authentication web api errors details.
|
||||||
- Added `SpotifyStateError` subclass of `SpotifyOauthError`
|
- Added `SpotifyStateError` subclass of `SpotifyOauthError`
|
||||||
- Allow extending `SpotifyClientCredentials` and `SpotifyOAuth`
|
- Allow extending `SpotifyClientCredentials` and `SpotifyOAuth`
|
||||||
- Added the market paramter to `album_tracks`
|
- Added the market paramter to `album_tracks`
|
||||||
@ -186,10 +216,10 @@ Rebasing master onto v3 doesn't require a changelog update.
|
|||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Updated the documentation to give more details on the authorization process and reflect
|
- Updated the documentation to give more details on the authorization process and reflect
|
||||||
2020 Spotify Application jargon and practices.
|
2020 Spotify Application jargon and practices.
|
||||||
|
|
||||||
- The local webserver is only started for localhost redirect_uri which specify a port,
|
- The local webserver is only started for localhost redirect_uri which specify a port,
|
||||||
i.e. it is started for `http://localhost:8080` or `http://127.0.0.1:8080`, not for `http://localhost`.
|
i.e. it is started for `http://localhost:8080` or `http://127.0.0.1:8080`, not for `http://localhost`.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
@ -258,6 +288,7 @@ Rebasing master onto v3 doesn't require a changelog update.
|
|||||||
- Optional `show_dialog` parameter to be passed to `SpotifyOAuth`
|
- Optional `show_dialog` parameter to be passed to `SpotifyOAuth`
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Both `SpotifyClientCredentials` and `SpotifyOAuth` inherit from a common `SpotifyAuthBase` which handles common parameters and logics.
|
- Both `SpotifyClientCredentials` and `SpotifyOAuth` inherit from a common `SpotifyAuthBase` which handles common parameters and logics.
|
||||||
|
|
||||||
## [2.7.1] - 2020-01-20
|
## [2.7.1] - 2020-01-20
|
||||||
@ -301,16 +332,19 @@ Rebasing master onto v3 doesn't require a changelog update.
|
|||||||
## [2.6.1] - 2020-01-13
|
## [2.6.1] - 2020-01-13
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed inconsistent behaviour with some API methods when
|
- Fixed inconsistent behaviour with some API methods when
|
||||||
a full HTTP URL is passed.
|
a full HTTP URL is passed.
|
||||||
- Fixed invalid calls to logging warn method
|
- Fixed invalid calls to logging warn method
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
- `mock` no longer needed for install. Only used in `tox`.
|
- `mock` no longer needed for install. Only used in `tox`.
|
||||||
|
|
||||||
## [2.6.0] - 2020-01-12
|
## [2.6.0] - 2020-01-12
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Support for `playlist` to get a playlist without specifying a user
|
- Support for `playlist` to get a playlist without specifying a user
|
||||||
- Support for `current_user_saved_albums_delete`
|
- Support for `current_user_saved_albums_delete`
|
||||||
- Support for `current_user_saved_albums_contains`
|
- Support for `current_user_saved_albums_contains`
|
||||||
@ -319,95 +353,126 @@ Rebasing master onto v3 doesn't require a changelog update.
|
|||||||
- Lint with flake8 using Github action
|
- Lint with flake8 using Github action
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Fix typos in doc
|
- Fix typos in doc
|
||||||
- Start following [SemVer](https://semver.org) properly
|
- Start following [SemVer](https://semver.org) properly
|
||||||
|
|
||||||
## [2.5.0] - 2020-01-11
|
## [2.5.0] - 2020-01-11
|
||||||
|
|
||||||
Added follow and player endpoints
|
Added follow and player endpoints
|
||||||
|
|
||||||
## [2.4.4] - 2017-01-04
|
## [2.4.4] - 2017-01-04
|
||||||
|
|
||||||
Python 3 fix
|
Python 3 fix
|
||||||
|
|
||||||
## [2.4.3] - 2017-01-02
|
## [2.4.3] - 2017-01-02
|
||||||
|
|
||||||
Fixed proxy issue in standard auth flow
|
Fixed proxy issue in standard auth flow
|
||||||
|
|
||||||
## [2.4.2] - 2017-01-02
|
## [2.4.2] - 2017-01-02
|
||||||
|
|
||||||
Support getting audio features for a single track
|
Support getting audio features for a single track
|
||||||
|
|
||||||
## [2.4.1] - 2017-01-02
|
## [2.4.1] - 2017-01-02
|
||||||
|
|
||||||
Incorporated proxy support
|
Incorporated proxy support
|
||||||
|
|
||||||
## [2.4.0] - 2016-12-31
|
## [2.4.0] - 2016-12-31
|
||||||
|
|
||||||
Incorporated a number of PRs
|
Incorporated a number of PRs
|
||||||
|
|
||||||
## [2.3.8] - 2016-03-31
|
## [2.3.8] - 2016-03-31
|
||||||
|
|
||||||
Added recs, audio features, user top lists
|
Added recs, audio features, user top lists
|
||||||
|
|
||||||
## [2.3.7] - 2015-08-10
|
## [2.3.7] - 2015-08-10
|
||||||
|
|
||||||
Added current_user_followed_artists
|
Added current_user_followed_artists
|
||||||
|
|
||||||
## [2.3.6] - 2015-06-03
|
## [2.3.6] - 2015-06-03
|
||||||
|
|
||||||
Support for offset/limit with album_tracks API
|
Support for offset/limit with album_tracks API
|
||||||
|
|
||||||
## [2.3.5] - 2015-04-28
|
## [2.3.5] - 2015-04-28
|
||||||
|
|
||||||
Fixed bug in auto retry logic
|
Fixed bug in auto retry logic
|
||||||
|
|
||||||
## [2.3.3] - 2015-04-01
|
## [2.3.3] - 2015-04-01
|
||||||
|
|
||||||
Aadded client credential flow
|
Aadded client credential flow
|
||||||
|
|
||||||
## [2.3.2] - 2015-03-31
|
## [2.3.2] - 2015-03-31
|
||||||
|
|
||||||
Added auto retry logic
|
Added auto retry logic
|
||||||
|
|
||||||
## [2.3.0] - 2015-01-05
|
## [2.3.0] - 2015-01-05
|
||||||
|
|
||||||
Added session support added by akx.
|
Added session support added by akx.
|
||||||
|
|
||||||
## [2.2.0] - 2014-11-15
|
## [2.2.0] - 2014-11-15
|
||||||
|
|
||||||
Added support for user_playlist_tracks
|
Added support for user_playlist_tracks
|
||||||
|
|
||||||
## [2.1.0] - 2014-10-25
|
## [2.1.0] - 2014-10-25
|
||||||
|
|
||||||
Added support for new_releases and featured_playlists
|
Added support for new_releases and featured_playlists
|
||||||
|
|
||||||
## [2.0.2] - 2014-08-25
|
## [2.0.2] - 2014-08-25
|
||||||
|
|
||||||
Moved to spotipy at pypi
|
Moved to spotipy at pypi
|
||||||
|
|
||||||
## [1.2.0] - 2014-08-22
|
## [1.2.0] - 2014-08-22
|
||||||
|
|
||||||
Upgraded APIs and docs to make it be a real library
|
Upgraded APIs and docs to make it be a real library
|
||||||
|
|
||||||
## [1.310.0] - 2014-08-20
|
## [1.310.0] - 2014-08-20
|
||||||
|
|
||||||
Added playlist replace and remove methods. Added auth tests. Improved API docs
|
Added playlist replace and remove methods. Added auth tests. Improved API docs
|
||||||
|
|
||||||
## [1.301.0] - 2014-08-19
|
## [1.301.0] - 2014-08-19
|
||||||
|
|
||||||
Upgraded version number to take precedence over previously botched release (sigh)
|
Upgraded version number to take precedence over previously botched release (sigh)
|
||||||
|
|
||||||
## [1.50.0] - 2014-08-14
|
## [1.50.0] - 2014-08-14
|
||||||
|
|
||||||
Refactored util out of examples and into the main package
|
Refactored util out of examples and into the main package
|
||||||
|
|
||||||
## [1.49.0] - 2014-07-23
|
## [1.49.0] - 2014-07-23
|
||||||
|
|
||||||
Support for "Your Music" tracks (add, delete, get), with examples
|
Support for "Your Music" tracks (add, delete, get), with examples
|
||||||
|
|
||||||
## [1.45.0] - 2014-07-07
|
## [1.45.0] - 2014-07-07
|
||||||
|
|
||||||
Support for related artists endpoint. Don't use cache auth codes when scope changes
|
Support for related artists endpoint. Don't use cache auth codes when scope changes
|
||||||
|
|
||||||
## [1.44.0] - 2014-07-03
|
## [1.44.0] - 2014-07-03
|
||||||
|
|
||||||
Added show tracks.py example
|
Added show tracks.py example
|
||||||
|
|
||||||
## [1.43.0] - 2014-06-27
|
## [1.43.0] - 2014-06-27
|
||||||
|
|
||||||
Fixed JSON handling issue
|
Fixed JSON handling issue
|
||||||
|
|
||||||
## [1.42.0] - 2014-06-19
|
## [1.42.0] - 2014-06-19
|
||||||
|
|
||||||
Removed dependency on simplejson
|
Removed dependency on simplejson
|
||||||
|
|
||||||
## [1.40.0] - 2014-06-12
|
## [1.40.0] - 2014-06-12
|
||||||
|
|
||||||
Initial public release.
|
Initial public release.
|
||||||
|
|
||||||
## [1.4.2] - 2014-06-21
|
## [1.4.2] - 2014-06-21
|
||||||
|
|
||||||
Added support for retrieving starred playlists
|
Added support for retrieving starred playlists
|
||||||
|
|
||||||
## [1.1.0] - 2014-06-17
|
## [1.1.0] - 2014-06-17
|
||||||
|
|
||||||
Updates to match released API
|
Updates to match released API
|
||||||
|
|
||||||
## [1.1.0] - 2014-05-18
|
## [1.1.0] - 2014-05-18
|
||||||
|
|
||||||
Repackaged for saner imports
|
Repackaged for saner imports
|
||||||
|
|
||||||
## [1.0.0] - 2017-04-05
|
## [1.0.0] - 2017-04-05
|
||||||
Initial release
|
|
||||||
|
Initial release
|
||||||
@ -7,7 +7,7 @@ import spotipy
|
|||||||
logger = logging.getLogger('examples.artist_albums')
|
logger = logging.getLogger('examples.artist_albums')
|
||||||
logging.basicConfig(level='INFO')
|
logging.basicConfig(level='INFO')
|
||||||
|
|
||||||
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials())
|
sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
|
||||||
|
|
||||||
|
|
||||||
def get_args():
|
def get_args():
|
||||||
|
|||||||
@ -68,6 +68,6 @@ def main():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
client_credentials_manager = SpotifyClientCredentials()
|
auth_manager = SpotifyClientCredentials()
|
||||||
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
|
sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||||
main()
|
main()
|
||||||
|
|||||||
@ -8,8 +8,8 @@ from spotipy.oauth2 import SpotifyClientCredentials
|
|||||||
logger = logging.getLogger('examples.artist_recommendations')
|
logger = logging.getLogger('examples.artist_recommendations')
|
||||||
logging.basicConfig(level='INFO')
|
logging.basicConfig(level='INFO')
|
||||||
|
|
||||||
client_credentials_manager = SpotifyClientCredentials()
|
auth_manager = SpotifyClientCredentials()
|
||||||
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
|
sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||||
|
|
||||||
|
|
||||||
def get_args():
|
def get_args():
|
||||||
|
|||||||
@ -8,8 +8,8 @@ import time
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
client_credentials_manager = SpotifyClientCredentials()
|
auth_manager = SpotifyClientCredentials()
|
||||||
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
|
sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||||
|
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
tid = sys.argv[1]
|
tid = sys.argv[1]
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import time
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
client_credentials_manager = SpotifyClientCredentials()
|
auth_manager = SpotifyClientCredentials()
|
||||||
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
|
sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||||
sp.trace = False
|
sp.trace = False
|
||||||
|
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
|
|||||||
@ -10,8 +10,8 @@ import time
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
client_credentials_manager = SpotifyClientCredentials()
|
auth_manager = SpotifyClientCredentials()
|
||||||
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
|
sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||||
sp.trace = True
|
sp.trace = True
|
||||||
|
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
|
|||||||
@ -2,8 +2,8 @@ from spotipy.oauth2 import SpotifyClientCredentials
|
|||||||
import spotipy
|
import spotipy
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
client_credentials_manager = SpotifyClientCredentials()
|
auth_manager = SpotifyClientCredentials()
|
||||||
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
|
sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||||
|
|
||||||
search_str = 'Muse'
|
search_str = 'Muse'
|
||||||
result = sp.search(search_str)
|
result = sp.search(search_str)
|
||||||
|
|||||||
@ -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())
|
|
||||||
@ -4,7 +4,7 @@ from pprint import pprint
|
|||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
scope = "user-read-playback-state,user-modify-playback-state"
|
scope = "user-read-playback-state,user-modify-playback-state"
|
||||||
sp = spotipy.Spotify(client_credentials_manager=SpotifyOAuth(scope=scope))
|
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
|
||||||
|
|
||||||
# Shows playing devices
|
# Shows playing devices
|
||||||
res = sp.devices()
|
res = sp.devices()
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import spotipy
|
|||||||
PlaylistExample = '37i9dQZEVXbMDoHDwVN2tF'
|
PlaylistExample = '37i9dQZEVXbMDoHDwVN2tF'
|
||||||
|
|
||||||
# create spotipy client
|
# create spotipy client
|
||||||
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials())
|
sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
|
||||||
|
|
||||||
# load the first 100 songs
|
# load the first 100 songs
|
||||||
tracks = []
|
tracks = []
|
||||||
|
|||||||
@ -2,7 +2,7 @@ from spotipy.oauth2 import SpotifyClientCredentials
|
|||||||
import spotipy
|
import spotipy
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials())
|
sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
|
||||||
|
|
||||||
pl_id = 'spotify:playlist:5RIbzhG2QqdkaP24iXLnZX'
|
pl_id = 'spotify:playlist:5RIbzhG2QqdkaP24iXLnZX'
|
||||||
offset = 0
|
offset = 0
|
||||||
@ -18,4 +18,4 @@ while True:
|
|||||||
|
|
||||||
pprint(response['items'])
|
pprint(response['items'])
|
||||||
offset = offset + len(response['items'])
|
offset = offset + len(response['items'])
|
||||||
print(offset, "/", response['total'])
|
print(offset, "/", response['total'])
|
||||||
|
|||||||
@ -2,8 +2,8 @@ from spotipy.oauth2 import SpotifyClientCredentials
|
|||||||
import spotipy
|
import spotipy
|
||||||
import json
|
import json
|
||||||
|
|
||||||
client_credentials_manager = SpotifyClientCredentials()
|
auth_manager = SpotifyClientCredentials()
|
||||||
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
|
sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||||
|
|
||||||
playlist_id = 'spotify:user:spotifycharts:playlist:37i9dQZEVXbJiZcmkrIHGU'
|
playlist_id = 'spotify:user:spotifycharts:playlist:37i9dQZEVXbJiZcmkrIHGU'
|
||||||
results = sp.playlist(playlist_id)
|
results = sp.playlist(playlist_id)
|
||||||
|
|||||||
@ -10,6 +10,6 @@ if len(sys.argv) > 1:
|
|||||||
else:
|
else:
|
||||||
search_str = 'Radiohead'
|
search_str = 'Radiohead'
|
||||||
|
|
||||||
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials())
|
sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
|
||||||
result = sp.search(search_str)
|
result = sp.search(search_str)
|
||||||
pprint.pprint(result)
|
pprint.pprint(result)
|
||||||
|
|||||||
@ -11,6 +11,6 @@ if len(sys.argv) > 1:
|
|||||||
else:
|
else:
|
||||||
urn = 'spotify:album:5yTx83u3qerZF7GRJu7eFk'
|
urn = 'spotify:album:5yTx83u3qerZF7GRJu7eFk'
|
||||||
|
|
||||||
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials())
|
sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
|
||||||
album = sp.album(urn)
|
album = sp.album(urn)
|
||||||
pprint(album)
|
pprint(album)
|
||||||
|
|||||||
@ -10,7 +10,7 @@ if len(sys.argv) > 1:
|
|||||||
else:
|
else:
|
||||||
urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu'
|
urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu'
|
||||||
|
|
||||||
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials())
|
sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
|
||||||
|
|
||||||
artist = sp.artist(urn)
|
artist = sp.artist(urn)
|
||||||
pprint(artist)
|
pprint(artist)
|
||||||
|
|||||||
@ -9,7 +9,7 @@ if len(sys.argv) > 1:
|
|||||||
else:
|
else:
|
||||||
urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu'
|
urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu'
|
||||||
|
|
||||||
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials())
|
sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
|
||||||
response = sp.artist_top_tracks(urn)
|
response = sp.artist_top_tracks(urn)
|
||||||
|
|
||||||
for track in response['tracks']:
|
for track in response['tracks']:
|
||||||
|
|||||||
@ -10,8 +10,8 @@ if len(sys.argv) > 1:
|
|||||||
else:
|
else:
|
||||||
artist_name = 'weezer'
|
artist_name = 'weezer'
|
||||||
|
|
||||||
client_credentials_manager = SpotifyClientCredentials()
|
auth_manager = SpotifyClientCredentials()
|
||||||
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
|
sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||||
result = sp.search(q='artist:' + artist_name, type='artist')
|
result = sp.search(q='artist:' + artist_name, type='artist')
|
||||||
try:
|
try:
|
||||||
name = result['artists']['items'][0]['name']
|
name = result['artists']['items'][0]['name']
|
||||||
|
|||||||
@ -10,7 +10,7 @@ if len(sys.argv) > 1:
|
|||||||
else:
|
else:
|
||||||
urn = 'spotify:track:0Svkvt5I79wficMFgaqEQJ'
|
urn = 'spotify:track:0Svkvt5I79wficMFgaqEQJ'
|
||||||
|
|
||||||
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials())
|
sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
|
||||||
|
|
||||||
track = sp.track(urn)
|
track = sp.track(urn)
|
||||||
pprint(track)
|
pprint(track)
|
||||||
|
|||||||
@ -15,8 +15,8 @@ if __name__ == '__main__':
|
|||||||
file = sys.stdin
|
file = sys.stdin
|
||||||
tids = file.read().split()
|
tids = file.read().split()
|
||||||
|
|
||||||
client_credentials_manager = SpotifyClientCredentials()
|
auth_manager = SpotifyClientCredentials()
|
||||||
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
|
sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||||
for start in range(0, len(tids), max_tracks_per_call):
|
for start in range(0, len(tids), max_tracks_per_call):
|
||||||
results = sp.tracks(tids[start: start + max_tracks_per_call])
|
results = sp.tracks(tids[start: start + max_tracks_per_call])
|
||||||
for track in results['tracks']:
|
for track in results['tracks']:
|
||||||
|
|||||||
@ -10,8 +10,8 @@ if len(sys.argv) > 1:
|
|||||||
else:
|
else:
|
||||||
username = 'plamere'
|
username = 'plamere'
|
||||||
|
|
||||||
client_credentials_manager = SpotifyClientCredentials()
|
auth_manager = SpotifyClientCredentials()
|
||||||
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
|
sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||||
sp.trace = True
|
sp.trace = True
|
||||||
user = sp.user(username)
|
user = sp.user(username)
|
||||||
pprint.pprint(user)
|
pprint.pprint(user)
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
from spotipy.oauth2 import SpotifyClientCredentials
|
from spotipy.oauth2 import SpotifyClientCredentials
|
||||||
import spotipy
|
import spotipy
|
||||||
|
|
||||||
client_credentials_manager = SpotifyClientCredentials()
|
auth_manager = SpotifyClientCredentials()
|
||||||
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
|
sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||||
|
|
||||||
results = sp.search(q='weezer', limit=20)
|
results = sp.search(q='weezer', limit=20)
|
||||||
for i, t in enumerate(results['tracks']['items']):
|
for i, t in enumerate(results['tracks']['items']):
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import spotipy
|
|||||||
|
|
||||||
birdy_uri = 'spotify:artist:2WX2uTcsvV5OnS0inACecP'
|
birdy_uri = 'spotify:artist:2WX2uTcsvV5OnS0inACecP'
|
||||||
|
|
||||||
client_credentials_manager = SpotifyClientCredentials()
|
auth_manager = SpotifyClientCredentials()
|
||||||
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
|
sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||||
|
|
||||||
results = sp.artist_albums(birdy_uri, album_type='album')
|
results = sp.artist_albums(birdy_uri, album_type='album')
|
||||||
albums = results['items']
|
albums = results['items']
|
||||||
|
|||||||
@ -4,8 +4,8 @@ import spotipy
|
|||||||
|
|
||||||
lz_uri = 'spotify:artist:36QJpDe2go2KgaRleHCDTp'
|
lz_uri = 'spotify:artist:36QJpDe2go2KgaRleHCDTp'
|
||||||
|
|
||||||
client_credentials_manager = SpotifyClientCredentials()
|
auth_manager = SpotifyClientCredentials()
|
||||||
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
|
sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||||
|
|
||||||
results = sp.artist_top_tracks(lz_uri)
|
results = sp.artist_top_tracks(lz_uri)
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import sys
|
|||||||
from spotipy.oauth2 import SpotifyClientCredentials
|
from spotipy.oauth2 import SpotifyClientCredentials
|
||||||
import spotipy
|
import spotipy
|
||||||
|
|
||||||
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials())
|
sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
|
||||||
|
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
name = ' '.join(sys.argv[1:])
|
name = ' '.join(sys.argv[1:])
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import random
|
|||||||
usage: python title_chain.py [song name]
|
usage: python title_chain.py [song name]
|
||||||
'''
|
'''
|
||||||
|
|
||||||
client_credentials_manager = SpotifyClientCredentials()
|
auth_manager = SpotifyClientCredentials()
|
||||||
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
|
sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||||
|
|
||||||
|
|
||||||
skiplist = set(['dm', 'remix'])
|
skiplist = set(['dm', 'remix'])
|
||||||
|
|||||||
@ -6,8 +6,8 @@ from spotipy.oauth2 import SpotifyClientCredentials
|
|||||||
import spotipy
|
import spotipy
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
client_credentials_manager = SpotifyClientCredentials()
|
auth_manager = SpotifyClientCredentials()
|
||||||
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
|
sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||||
|
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
artist_name = ' '.join(sys.argv[1:])
|
artist_name = ' '.join(sys.argv[1:])
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import sys
|
|||||||
import spotipy
|
import spotipy
|
||||||
from spotipy.oauth2 import SpotifyClientCredentials
|
from spotipy.oauth2 import SpotifyClientCredentials
|
||||||
|
|
||||||
client_credentials_manager = SpotifyClientCredentials()
|
auth_manager = SpotifyClientCredentials()
|
||||||
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
|
sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||||
|
|
||||||
user = 'spotify'
|
user = 'spotify'
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
__all__ = ['CacheHandler', 'CacheFileHandler']
|
__all__ = ['CacheHandler', 'CacheFileHandler', 'MemoryCacheHandler']
|
||||||
|
|
||||||
import errno
|
import errno
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
from spotipy.util import CLIENT_CREDS_ENV_VARS
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -54,6 +56,7 @@ class CacheFileHandler(CacheHandler):
|
|||||||
self.cache_path = cache_path
|
self.cache_path = cache_path
|
||||||
else:
|
else:
|
||||||
cache_path = ".cache"
|
cache_path = ".cache"
|
||||||
|
username = (username or os.getenv(CLIENT_CREDS_ENV_VARS["client_username"]))
|
||||||
if username:
|
if username:
|
||||||
cache_path += "-" + str(username)
|
cache_path += "-" + str(username)
|
||||||
self.cache_path = cache_path
|
self.cache_path = cache_path
|
||||||
@ -83,3 +86,24 @@ class CacheFileHandler(CacheHandler):
|
|||||||
except IOError:
|
except IOError:
|
||||||
logger.warning('Couldn\'t write token to cache at: %s',
|
logger.warning('Couldn\'t write token to cache at: %s',
|
||||||
self.cache_path)
|
self.cache_path)
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryCacheHandler(CacheHandler):
|
||||||
|
"""
|
||||||
|
A cache handler that simply stores the token info in memory as an
|
||||||
|
instance attribute of this class. The token info will be lost when this
|
||||||
|
instance is freed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, token_info=None):
|
||||||
|
"""
|
||||||
|
Parameters:
|
||||||
|
* token_info: The token info to store in memory. Can be None.
|
||||||
|
"""
|
||||||
|
self.token_info = token_info
|
||||||
|
|
||||||
|
def get_cached_token(self):
|
||||||
|
return self.token_info
|
||||||
|
|
||||||
|
def save_token_to_cache(self, token_info):
|
||||||
|
self.token_info = token_info
|
||||||
|
|||||||
@ -98,11 +98,9 @@ class Spotify(object):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
auth=None,
|
access_token=None,
|
||||||
requests_session=True,
|
|
||||||
client_credentials_manager=None,
|
|
||||||
oauth_manager=None,
|
|
||||||
auth_manager=None,
|
auth_manager=None,
|
||||||
|
requests_session=True,
|
||||||
proxies=None,
|
proxies=None,
|
||||||
requests_timeout=5,
|
requests_timeout=5,
|
||||||
status_forcelist=None,
|
status_forcelist=None,
|
||||||
@ -114,19 +112,16 @@ class Spotify(object):
|
|||||||
"""
|
"""
|
||||||
Creates a Spotify API client.
|
Creates a Spotify API client.
|
||||||
|
|
||||||
:param auth: An access token (optional)
|
:param access_token: An access token (optional). If not None, then this parameter
|
||||||
|
will override the auth_manager parameter. Prefer `auth_manager` over this parameter
|
||||||
|
because otherwise you cannot refresh the `access_token`.
|
||||||
|
:param auth_manager:
|
||||||
|
SpotifyOauth, SpotifyClientCredentials, or SpotifyPKCE object
|
||||||
:param requests_session:
|
:param requests_session:
|
||||||
A Requests session object or a truthy value to create one.
|
A Requests session object or a true value to create one.
|
||||||
A falsy value disables sessions.
|
A false value disables sessions.
|
||||||
It should generally be a good idea to keep sessions enabled
|
It should generally be a good idea to keep sessions enabled
|
||||||
for performance reasons (connection pooling).
|
for performance reasons (connection pooling).
|
||||||
:param client_credentials_manager:
|
|
||||||
SpotifyClientCredentials object
|
|
||||||
:param oauth_manager:
|
|
||||||
SpotifyOAuth object
|
|
||||||
:param auth_manager:
|
|
||||||
SpotifyOauth, SpotifyClientCredentials,
|
|
||||||
or SpotifyImplicitGrant object
|
|
||||||
:param proxies:
|
:param proxies:
|
||||||
Definition of proxies (optional).
|
Definition of proxies (optional).
|
||||||
See Requests doc https://2.python-requests.org/en/master/user/advanced/#proxies
|
See Requests doc https://2.python-requests.org/en/master/user/advanced/#proxies
|
||||||
@ -146,17 +141,23 @@ class Spotify(object):
|
|||||||
The language parameter advertises what language the user prefers to see.
|
The language parameter advertises what language the user prefers to see.
|
||||||
See ISO-639 language code: https://www.loc.gov/standards/iso639-2/php/code_list.php
|
See ISO-639 language code: https://www.loc.gov/standards/iso639-2/php/code_list.php
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if access_token is not None and auth_manager is not None:
|
||||||
|
warnings.warn(
|
||||||
|
"Either `access_token` or `auth_manager` should be provided, "
|
||||||
|
"not both. `auth_manager` will be ignored.",
|
||||||
|
UserWarning
|
||||||
|
)
|
||||||
|
|
||||||
self.prefix = "https://api.spotify.com/v1/"
|
self.prefix = "https://api.spotify.com/v1/"
|
||||||
self._auth = auth
|
self.access_token = access_token
|
||||||
self.client_credentials_manager = client_credentials_manager
|
|
||||||
self.oauth_manager = oauth_manager
|
|
||||||
self.auth_manager = auth_manager
|
self.auth_manager = auth_manager
|
||||||
self.proxies = proxies
|
self.proxies = proxies
|
||||||
self.requests_timeout = requests_timeout
|
self.requests_timeout = requests_timeout
|
||||||
self.status_forcelist = status_forcelist or self.default_retry_codes
|
self.status_forcelist = status_forcelist or self.default_retry_codes
|
||||||
self.backoff_factor = backoff_factor
|
|
||||||
self.retries = retries
|
self.retries = retries
|
||||||
self.status_retries = status_retries
|
self.status_retries = status_retries
|
||||||
|
self.backoff_factor = backoff_factor
|
||||||
self.language = language
|
self.language = language
|
||||||
|
|
||||||
if isinstance(requests_session, requests.Session):
|
if isinstance(requests_session, requests.Session):
|
||||||
@ -167,22 +168,6 @@ class Spotify(object):
|
|||||||
else: # Use the Requests API module as a "session".
|
else: # Use the Requests API module as a "session".
|
||||||
self._session = requests.api
|
self._session = requests.api
|
||||||
|
|
||||||
def set_auth(self, auth):
|
|
||||||
self._auth = auth
|
|
||||||
|
|
||||||
@property
|
|
||||||
def auth_manager(self):
|
|
||||||
return self._auth_manager
|
|
||||||
|
|
||||||
@auth_manager.setter
|
|
||||||
def auth_manager(self, auth_manager):
|
|
||||||
if auth_manager is not None:
|
|
||||||
self._auth_manager = auth_manager
|
|
||||||
else:
|
|
||||||
self._auth_manager = (
|
|
||||||
self.client_credentials_manager or self.oauth_manager
|
|
||||||
)
|
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
"""Make sure the connection (pool) gets closed"""
|
"""Make sure the connection (pool) gets closed"""
|
||||||
if isinstance(self._session, requests.Session):
|
if isinstance(self._session, requests.Session):
|
||||||
@ -204,12 +189,12 @@ class Spotify(object):
|
|||||||
self._session.mount('https://', adapter)
|
self._session.mount('https://', adapter)
|
||||||
|
|
||||||
def _auth_headers(self):
|
def _auth_headers(self):
|
||||||
if self._auth:
|
if self.access_token:
|
||||||
return {"Authorization": "Bearer {0}".format(self._auth)}
|
return {"Authorization": "Bearer {0}".format(self.access_token)}
|
||||||
if not self.auth_manager:
|
if not self.auth_manager:
|
||||||
return {}
|
return {}
|
||||||
try:
|
try:
|
||||||
token = self.auth_manager.get_access_token(as_dict=False)
|
token = self.auth_manager.get_access_token()
|
||||||
except TypeError:
|
except TypeError:
|
||||||
token = self.auth_manager.get_access_token()
|
token = self.auth_manager.get_access_token()
|
||||||
return {"Authorization": "Bearer {0}".format(token)}
|
return {"Authorization": "Bearer {0}".format(token)}
|
||||||
@ -615,34 +600,6 @@ class Spotify(object):
|
|||||||
additional_types=",".join(additional_types),
|
additional_types=",".join(additional_types),
|
||||||
)
|
)
|
||||||
|
|
||||||
def playlist_tracks(
|
|
||||||
self,
|
|
||||||
playlist_id,
|
|
||||||
fields=None,
|
|
||||||
limit=100,
|
|
||||||
offset=0,
|
|
||||||
market=None,
|
|
||||||
additional_types=("track",)
|
|
||||||
):
|
|
||||||
""" Get full details of the tracks of a playlist.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- playlist_id - the id of the playlist
|
|
||||||
- fields - which fields to return
|
|
||||||
- limit - the maximum number of tracks to return
|
|
||||||
- offset - the index of the first track to return
|
|
||||||
- market - an ISO 3166-1 alpha-2 country code.
|
|
||||||
- additional_types - list of item types to return.
|
|
||||||
valid types are: track and episode
|
|
||||||
"""
|
|
||||||
warnings.warn(
|
|
||||||
"You should use `playlist_items(playlist_id, ...,"
|
|
||||||
"additional_types=('track',))` instead",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
return self.playlist_items(playlist_id, fields, limit, offset,
|
|
||||||
market, additional_types)
|
|
||||||
|
|
||||||
def playlist_items(
|
def playlist_items(
|
||||||
self,
|
self,
|
||||||
playlist_id,
|
playlist_id,
|
||||||
@ -697,55 +654,6 @@ class Spotify(object):
|
|||||||
content_type="image/jpeg",
|
content_type="image/jpeg",
|
||||||
)
|
)
|
||||||
|
|
||||||
def user_playlist(self, user, playlist_id=None, fields=None, market=None):
|
|
||||||
warnings.warn(
|
|
||||||
"You should use `playlist(playlist_id)` instead",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
|
|
||||||
""" Gets playlist of a user
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- user - the id of the user
|
|
||||||
- playlist_id - the id of the playlist
|
|
||||||
- fields - which fields to return
|
|
||||||
"""
|
|
||||||
if playlist_id is None:
|
|
||||||
return self._get("users/%s/starred" % user)
|
|
||||||
return self.playlist(playlist_id, fields=fields, market=market)
|
|
||||||
|
|
||||||
def user_playlist_tracks(
|
|
||||||
self,
|
|
||||||
user=None,
|
|
||||||
playlist_id=None,
|
|
||||||
fields=None,
|
|
||||||
limit=100,
|
|
||||||
offset=0,
|
|
||||||
market=None,
|
|
||||||
):
|
|
||||||
warnings.warn(
|
|
||||||
"You should use `playlist_tracks(playlist_id)` instead",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
|
|
||||||
""" Get full details of the tracks of a playlist owned by a user.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- user - the id of the user
|
|
||||||
- playlist_id - the id of the playlist
|
|
||||||
- fields - which fields to return
|
|
||||||
- limit - the maximum number of tracks to return
|
|
||||||
- offset - the index of the first track to return
|
|
||||||
- market - an ISO 3166-1 alpha-2 country code.
|
|
||||||
"""
|
|
||||||
return self.playlist_tracks(
|
|
||||||
playlist_id,
|
|
||||||
limit=limit,
|
|
||||||
offset=offset,
|
|
||||||
fields=fields,
|
|
||||||
market=market,
|
|
||||||
)
|
|
||||||
|
|
||||||
def user_playlists(self, user, limit=50, offset=0):
|
def user_playlists(self, user, limit=50, offset=0):
|
||||||
""" Gets playlists of a user
|
""" Gets playlists of a user
|
||||||
|
|
||||||
@ -777,197 +685,6 @@ class Spotify(object):
|
|||||||
|
|
||||||
return self._post("users/%s/playlists" % (user,), payload=data)
|
return self._post("users/%s/playlists" % (user,), payload=data)
|
||||||
|
|
||||||
def user_playlist_change_details(
|
|
||||||
self,
|
|
||||||
user,
|
|
||||||
playlist_id,
|
|
||||||
name=None,
|
|
||||||
public=None,
|
|
||||||
collaborative=None,
|
|
||||||
description=None,
|
|
||||||
):
|
|
||||||
warnings.warn(
|
|
||||||
"You should use `playlist_change_details(playlist_id, ...)` instead",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
""" Changes a playlist's name and/or public/private state
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- user - the id of the user
|
|
||||||
- playlist_id - the id of the playlist
|
|
||||||
- name - optional name of the playlist
|
|
||||||
- public - optional is the playlist public
|
|
||||||
- collaborative - optional is the playlist collaborative
|
|
||||||
- description - optional description of the playlist
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.playlist_change_details(playlist_id, name, public,
|
|
||||||
collaborative, description)
|
|
||||||
|
|
||||||
def user_playlist_unfollow(self, user, playlist_id):
|
|
||||||
""" Unfollows (deletes) a playlist for a user
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- user - the id of the user
|
|
||||||
- name - the name of the playlist
|
|
||||||
"""
|
|
||||||
warnings.warn(
|
|
||||||
"You should use `current_user_unfollow_playlist(playlist_id)` instead",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
return self.current_user_unfollow_playlist(playlist_id)
|
|
||||||
|
|
||||||
def user_playlist_add_tracks(
|
|
||||||
self, user, playlist_id, tracks, position=None
|
|
||||||
):
|
|
||||||
warnings.warn(
|
|
||||||
"You should use `playlist_add_items(playlist_id, tracks)` instead",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
""" Adds tracks to a playlist
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- user - the id of the user
|
|
||||||
- playlist_id - the id of the playlist
|
|
||||||
- tracks - a list of track URIs, URLs or IDs
|
|
||||||
- position - the position to add the tracks
|
|
||||||
"""
|
|
||||||
return self.playlist_add_items(playlist_id, tracks, position)
|
|
||||||
|
|
||||||
def user_playlist_replace_tracks(self, user, playlist_id, tracks):
|
|
||||||
""" Replace all tracks in a playlist
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- user - the id of the user
|
|
||||||
- playlist_id - the id of the playlist
|
|
||||||
- tracks - the list of track ids to add to the playlist
|
|
||||||
"""
|
|
||||||
warnings.warn(
|
|
||||||
"You should use `playlist_replace_items(playlist_id, tracks)` instead",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
return self.playlist_replace_items(playlist_id, tracks)
|
|
||||||
|
|
||||||
def user_playlist_reorder_tracks(
|
|
||||||
self,
|
|
||||||
user,
|
|
||||||
playlist_id,
|
|
||||||
range_start,
|
|
||||||
insert_before,
|
|
||||||
range_length=1,
|
|
||||||
snapshot_id=None,
|
|
||||||
):
|
|
||||||
""" Reorder tracks in a playlist
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- user - the id of the user
|
|
||||||
- playlist_id - the id of the playlist
|
|
||||||
- range_start - the position of the first track to be reordered
|
|
||||||
- range_length - optional the number of tracks to be reordered
|
|
||||||
(default: 1)
|
|
||||||
- insert_before - the position where the tracks should be
|
|
||||||
inserted
|
|
||||||
- snapshot_id - optional playlist's snapshot ID
|
|
||||||
"""
|
|
||||||
warnings.warn(
|
|
||||||
"You should use `playlist_reorder_items(playlist_id, ...)` instead",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
return self.playlist_reorder_items(playlist_id, range_start,
|
|
||||||
insert_before, range_length,
|
|
||||||
snapshot_id)
|
|
||||||
|
|
||||||
def user_playlist_remove_all_occurrences_of_tracks(
|
|
||||||
self, user, playlist_id, tracks, snapshot_id=None
|
|
||||||
):
|
|
||||||
""" Removes all occurrences of the given tracks from the given playlist
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- user - the id of the user
|
|
||||||
- playlist_id - the id of the playlist
|
|
||||||
- tracks - the list of track ids to remove from the playlist
|
|
||||||
- snapshot_id - optional id of the playlist snapshot
|
|
||||||
|
|
||||||
"""
|
|
||||||
warnings.warn(
|
|
||||||
"You should use `playlist_remove_all_occurrences_of_items"
|
|
||||||
"(playlist_id, tracks)` instead",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
return self.playlist_remove_all_occurrences_of_items(playlist_id,
|
|
||||||
tracks,
|
|
||||||
snapshot_id)
|
|
||||||
|
|
||||||
def user_playlist_remove_specific_occurrences_of_tracks(
|
|
||||||
self, user, playlist_id, tracks, snapshot_id=None
|
|
||||||
):
|
|
||||||
""" Removes all occurrences of the given tracks from the given playlist
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- user - the id of the user
|
|
||||||
- playlist_id - the id of the playlist
|
|
||||||
- tracks - an array of objects containing Spotify URIs of the
|
|
||||||
tracks to remove with their current positions in the
|
|
||||||
playlist. For example:
|
|
||||||
[ { "uri":"4iV5W9uYEdYUVa79Axb7Rh", "positions":[2] },
|
|
||||||
{ "uri":"1301WleyT98MSxVHPZCA6M", "positions":[7] } ]
|
|
||||||
- snapshot_id - optional id of the playlist snapshot
|
|
||||||
"""
|
|
||||||
warnings.warn(
|
|
||||||
"You should use `playlist_remove_specific_occurrences_of_items"
|
|
||||||
"(playlist_id, tracks)` instead",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
plid = self._get_id("playlist", playlist_id)
|
|
||||||
ftracks = []
|
|
||||||
for tr in tracks:
|
|
||||||
ftracks.append(
|
|
||||||
{
|
|
||||||
"uri": self._get_uri("track", tr["uri"]),
|
|
||||||
"positions": tr["positions"],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
payload = {"tracks": ftracks}
|
|
||||||
if snapshot_id:
|
|
||||||
payload["snapshot_id"] = snapshot_id
|
|
||||||
return self._delete(
|
|
||||||
"users/%s/playlists/%s/tracks" % (user, plid), payload=payload
|
|
||||||
)
|
|
||||||
|
|
||||||
def user_playlist_follow_playlist(self, playlist_owner_id, playlist_id):
|
|
||||||
"""
|
|
||||||
Add the current authenticated user as a follower of a playlist.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- playlist_owner_id - the user id of the playlist owner
|
|
||||||
- playlist_id - the id of the playlist
|
|
||||||
|
|
||||||
"""
|
|
||||||
warnings.warn(
|
|
||||||
"You should use `current_user_follow_playlist(playlist_id)` instead",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
return self.current_user_follow_playlist(playlist_id)
|
|
||||||
|
|
||||||
def user_playlist_is_following(
|
|
||||||
self, playlist_owner_id, playlist_id, user_ids
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Check to see if the given users are following the given playlist
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- playlist_owner_id - the user id of the playlist owner
|
|
||||||
- playlist_id - the id of the playlist
|
|
||||||
- user_ids - the ids of the users that you want to check to see
|
|
||||||
if they follow the playlist. Maximum: 5 ids.
|
|
||||||
|
|
||||||
"""
|
|
||||||
warnings.warn(
|
|
||||||
"You should use `playlist_is_following(playlist_id, user_ids)` instead",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
return self.playlist_is_following(playlist_id, user_ids)
|
|
||||||
|
|
||||||
def playlist_change_details(
|
def playlist_change_details(
|
||||||
self,
|
self,
|
||||||
playlist_id,
|
playlist_id,
|
||||||
|
|||||||
@ -5,7 +5,6 @@ __all__ = [
|
|||||||
"SpotifyOAuth",
|
"SpotifyOAuth",
|
||||||
"SpotifyOauthError",
|
"SpotifyOauthError",
|
||||||
"SpotifyStateError",
|
"SpotifyStateError",
|
||||||
"SpotifyImplicitGrant",
|
|
||||||
"SpotifyPKCE"
|
"SpotifyPKCE"
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -13,7 +12,6 @@ import base64
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import warnings
|
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@ -179,10 +177,10 @@ class SpotifyClientCredentials(SpotifyAuthBase):
|
|||||||
self,
|
self,
|
||||||
client_id=None,
|
client_id=None,
|
||||||
client_secret=None,
|
client_secret=None,
|
||||||
|
cache_handler=None,
|
||||||
proxies=None,
|
proxies=None,
|
||||||
requests_session=True,
|
requests_session=True,
|
||||||
requests_timeout=None,
|
requests_timeout=None
|
||||||
cache_handler=None
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Creates a Client Credentials Flow Manager.
|
Creates a Client Credentials Flow Manager.
|
||||||
@ -200,14 +198,16 @@ class SpotifyClientCredentials(SpotifyAuthBase):
|
|||||||
Parameters:
|
Parameters:
|
||||||
* client_id: Must be supplied or set as environment variable
|
* client_id: Must be supplied or set as environment variable
|
||||||
* client_secret: Must be supplied or set as environment variable
|
* client_secret: Must be supplied or set as environment variable
|
||||||
* proxies: Optional, proxy for the requests library to route through
|
|
||||||
* requests_session: A Requests session
|
|
||||||
* requests_timeout: Optional, tell Requests to stop waiting for a response after
|
|
||||||
a given number of seconds
|
|
||||||
* cache_handler: An instance of the `CacheHandler` class to handle
|
* cache_handler: An instance of the `CacheHandler` class to handle
|
||||||
getting and saving cached authorization tokens.
|
getting and saving cached authorization tokens.
|
||||||
Optional, will otherwise use `CacheFileHandler`.
|
Optional, will otherwise use `CacheFileHandler`.
|
||||||
(takes precedence over `cache_path` and `username`)
|
* proxies: Optional, proxy for the requests library to route through
|
||||||
|
* requests_session: A Requests session object or a true value to create one.
|
||||||
|
A false value disables sessions.
|
||||||
|
It should generally be a good idea to keep sessions enabled
|
||||||
|
for performance reasons (connection pooling).
|
||||||
|
* requests_timeout: Optional, tell Requests to stop waiting for a response after
|
||||||
|
a given number of seconds
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -225,35 +225,25 @@ class SpotifyClientCredentials(SpotifyAuthBase):
|
|||||||
else:
|
else:
|
||||||
self.cache_handler = CacheFileHandler()
|
self.cache_handler = CacheFileHandler()
|
||||||
|
|
||||||
def get_access_token(self, as_dict=True, check_cache=True):
|
def get_access_token(self, check_cache=True):
|
||||||
"""
|
"""
|
||||||
If a valid access token is in memory, returns it
|
If a valid access token is in memory, returns it
|
||||||
Else feches a new token and returns it
|
Else feches a new token and returns it
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
- as_dict - a boolean indicating if returning the access token
|
- check_cache - if true, checks for a locally stored token
|
||||||
as a token_info dictionary, otherwise it will be returned
|
before requesting a new token.
|
||||||
as a string.
|
|
||||||
"""
|
"""
|
||||||
if as_dict:
|
|
||||||
warnings.warn(
|
|
||||||
"You're using 'as_dict = True'."
|
|
||||||
"get_access_token will return the token string directly in future "
|
|
||||||
"versions. Please adjust your code accordingly, or use "
|
|
||||||
"get_cached_token instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
if check_cache:
|
if check_cache:
|
||||||
token_info = self.cache_handler.get_cached_token()
|
token_info = self.cache_handler.get_cached_token()
|
||||||
if token_info and not self.is_token_expired(token_info):
|
if token_info and not self.is_token_expired(token_info):
|
||||||
return token_info if as_dict else token_info["access_token"]
|
return token_info["access_token"]
|
||||||
|
|
||||||
token_info = self._request_access_token()
|
token_info = self._request_access_token()
|
||||||
token_info = self._add_custom_values_to_token_info(token_info)
|
token_info = self._add_custom_values_to_token_info(token_info)
|
||||||
self.cache_handler.save_token_to_cache(token_info)
|
self.cache_handler.save_token_to_cache(token_info)
|
||||||
return token_info if as_dict else token_info["access_token"]
|
return token_info["access_token"]
|
||||||
|
|
||||||
def _request_access_token(self):
|
def _request_access_token(self):
|
||||||
"""Gets client credentials access token """
|
"""Gets client credentials access token """
|
||||||
@ -309,14 +299,12 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
redirect_uri=None,
|
redirect_uri=None,
|
||||||
state=None,
|
state=None,
|
||||||
scope=None,
|
scope=None,
|
||||||
cache_path=None,
|
cache_handler=None,
|
||||||
username=None,
|
|
||||||
proxies=None,
|
proxies=None,
|
||||||
show_dialog=False,
|
show_dialog=False,
|
||||||
requests_session=True,
|
requests_session=True,
|
||||||
requests_timeout=None,
|
requests_timeout=None,
|
||||||
open_browser=True,
|
open_browser=True
|
||||||
cache_handler=None
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Creates a SpotifyOAuth object
|
Creates a SpotifyOAuth object
|
||||||
@ -329,24 +317,19 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
* scope: Optional, either a string of scopes, or an iterable with elements of type
|
* scope: Optional, either a string of scopes, or an iterable with elements of type
|
||||||
`Scope` or `str`. E.g.,
|
`Scope` or `str`. E.g.,
|
||||||
{Scope.user_modify_playback_state, Scope.user_library_read}
|
{Scope.user_modify_playback_state, Scope.user_library_read}
|
||||||
|
* cache_handler: An instance of the `CacheHandler` class to handle
|
||||||
iterable of scopes or comma separated string of scopes.
|
getting and saving cached authorization tokens.
|
||||||
e.g, "playlist-read-private,playlist-read-collaborative"
|
Optional, will otherwise use `CacheFileHandler`.
|
||||||
* cache_path: (deprecated) Optional, will otherwise be generated
|
|
||||||
(takes precedence over `username`)
|
|
||||||
* username: (deprecated) Optional or set as environment variable
|
|
||||||
(will set `cache_path` to `.cache-{username}`)
|
|
||||||
* proxies: Optional, proxy for the requests library to route through
|
* proxies: Optional, proxy for the requests library to route through
|
||||||
* show_dialog: Optional, interpreted as boolean
|
* show_dialog: Optional, interpreted as boolean
|
||||||
* requests_session: A Requests session
|
* requests_session: A Requests session object or a true value to create one.
|
||||||
|
A false value disables sessions.
|
||||||
|
It should generally be a good idea to keep sessions enabled
|
||||||
|
for performance reasons (connection pooling).
|
||||||
* requests_timeout: Optional, tell Requests to stop waiting for a response after
|
* requests_timeout: Optional, tell Requests to stop waiting for a response after
|
||||||
a given number of seconds
|
a given number of seconds
|
||||||
* open_browser: Optional, whether or not the web browser should be opened to
|
* open_browser: Optional, whether or not the web browser should be opened to
|
||||||
authorize a user
|
authorize a user
|
||||||
* cache_handler: An instance of the `CacheHandler` class to handle
|
|
||||||
getting and saving cached authorization tokens.
|
|
||||||
Optional, will otherwise use `CacheFileHandler`.
|
|
||||||
(takes precedence over `cache_path` and `username`)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super(SpotifyOAuth, self).__init__(requests_session)
|
super(SpotifyOAuth, self).__init__(requests_session)
|
||||||
@ -356,34 +339,13 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
self.redirect_uri = redirect_uri
|
self.redirect_uri = redirect_uri
|
||||||
self.state = state
|
self.state = state
|
||||||
self.scope = self._normalize_scope(scope)
|
self.scope = self._normalize_scope(scope)
|
||||||
username = (username or os.getenv(CLIENT_CREDS_ENV_VARS["client_username"]))
|
|
||||||
if username or cache_path:
|
|
||||||
warnings.warn("Specifying cache_path or username as arguments to SpotifyOAuth " +
|
|
||||||
"will be deprecated. Instead, please create a CacheFileHandler " +
|
|
||||||
"instance with the desired cache_path and username and pass it " +
|
|
||||||
"to SpotifyOAuth as the cache_handler. For example:\n\n" +
|
|
||||||
"\tfrom spotipy.oauth2 import CacheFileHandler\n" +
|
|
||||||
"\thandler = CacheFileHandler(cache_path=cache_path, " +
|
|
||||||
"username=username)\n" +
|
|
||||||
"\tsp = spotipy.SpotifyOAuth(client_id, client_secret, " +
|
|
||||||
"redirect_uri," +
|
|
||||||
" cache_handler=handler)",
|
|
||||||
DeprecationWarning
|
|
||||||
)
|
|
||||||
if cache_handler:
|
|
||||||
warnings.warn("A cache_handler has been specified along with a cache_path or " +
|
|
||||||
"username. The cache_path and username arguments will be ignored.")
|
|
||||||
if cache_handler:
|
if cache_handler:
|
||||||
assert issubclass(cache_handler.__class__, CacheHandler), \
|
assert issubclass(cache_handler.__class__, CacheHandler), \
|
||||||
"cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \
|
"cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \
|
||||||
+ " != " + str(CacheHandler)
|
+ " != " + str(CacheHandler)
|
||||||
self.cache_handler = cache_handler
|
self.cache_handler = cache_handler
|
||||||
else:
|
else:
|
||||||
|
self.cache_handler = CacheFileHandler()
|
||||||
self.cache_handler = CacheFileHandler(
|
|
||||||
username=username,
|
|
||||||
cache_path=cache_path
|
|
||||||
)
|
|
||||||
self.proxies = proxies
|
self.proxies = proxies
|
||||||
self.requests_timeout = requests_timeout
|
self.requests_timeout = requests_timeout
|
||||||
self.show_dialog = show_dialog
|
self.show_dialog = show_dialog
|
||||||
@ -527,24 +489,14 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
return self.parse_response_code(response)
|
return self.parse_response_code(response)
|
||||||
return self.get_auth_response()
|
return self.get_auth_response()
|
||||||
|
|
||||||
def get_access_token(self, code=None, as_dict=True, check_cache=True):
|
def get_access_token(self, code=None, check_cache=True):
|
||||||
""" Gets the access token for the app given the code
|
""" Gets the access token for the app given the code
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
- code - the response code
|
- code - the response code
|
||||||
- as_dict - a boolean indicating if returning the access token
|
- check_cache - if true, checks for a locally stored token
|
||||||
as a token_info dictionary, otherwise it will be returned
|
before requesting a new token
|
||||||
as a string.
|
|
||||||
"""
|
"""
|
||||||
if as_dict:
|
|
||||||
warnings.warn(
|
|
||||||
"You're using 'as_dict = True'."
|
|
||||||
"get_access_token will return the token string directly in future "
|
|
||||||
"versions. Please adjust your code accordingly, or use "
|
|
||||||
"get_cached_token instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
if check_cache:
|
if check_cache:
|
||||||
token_info = self.validate_token(self.cache_handler.get_cached_token())
|
token_info = self.validate_token(self.cache_handler.get_cached_token())
|
||||||
if token_info is not None:
|
if token_info is not None:
|
||||||
@ -552,7 +504,7 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
token_info = self.refresh_access_token(
|
token_info = self.refresh_access_token(
|
||||||
token_info["refresh_token"]
|
token_info["refresh_token"]
|
||||||
)
|
)
|
||||||
return token_info if as_dict else token_info["access_token"]
|
return token_info["access_token"]
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"redirect_uri": self.redirect_uri,
|
"redirect_uri": self.redirect_uri,
|
||||||
@ -589,7 +541,7 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
token_info = response.json()
|
token_info = response.json()
|
||||||
token_info = self._add_custom_values_to_token_info(token_info)
|
token_info = self._add_custom_values_to_token_info(token_info)
|
||||||
self.cache_handler.save_token_to_cache(token_info)
|
self.cache_handler.save_token_to_cache(token_info)
|
||||||
return token_info if as_dict else token_info["access_token"]
|
return token_info["access_token"]
|
||||||
|
|
||||||
def refresh_access_token(self, refresh_token):
|
def refresh_access_token(self, refresh_token):
|
||||||
payload = {
|
payload = {
|
||||||
@ -636,26 +588,6 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
token_info["scope"] = self.scope
|
token_info["scope"] = self.scope
|
||||||
return token_info
|
return token_info
|
||||||
|
|
||||||
def get_cached_token(self):
|
|
||||||
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 " +
|
|
||||||
"get_cached_token method. You can replace:\n\tsp.get_cached_token()" +
|
|
||||||
"\n\nWith:\n\tsp.validate_token(sp.cache_handler.get_cached_token())",
|
|
||||||
DeprecationWarning
|
|
||||||
)
|
|
||||||
return self.validate_token(self.cache_handler.get_cached_token())
|
|
||||||
|
|
||||||
def _save_token_info(self, token_info):
|
|
||||||
warnings.warn("Calling _save_token_info 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 " +
|
|
||||||
"save_token_to_cache method.",
|
|
||||||
DeprecationWarning
|
|
||||||
)
|
|
||||||
self.cache_handler.save_token_to_cache(token_info)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class SpotifyPKCE(SpotifyAuthBase):
|
class SpotifyPKCE(SpotifyAuthBase):
|
||||||
""" Implements PKCE Authorization Flow for client apps
|
""" Implements PKCE Authorization Flow for client apps
|
||||||
@ -672,18 +604,18 @@ class SpotifyPKCE(SpotifyAuthBase):
|
|||||||
OAUTH_AUTHORIZE_URL = "https://accounts.spotify.com/authorize"
|
OAUTH_AUTHORIZE_URL = "https://accounts.spotify.com/authorize"
|
||||||
OAUTH_TOKEN_URL = "https://accounts.spotify.com/api/token"
|
OAUTH_TOKEN_URL = "https://accounts.spotify.com/api/token"
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(
|
||||||
client_id=None,
|
self,
|
||||||
redirect_uri=None,
|
client_id=None,
|
||||||
state=None,
|
redirect_uri=None,
|
||||||
scope=None,
|
state=None,
|
||||||
cache_path=None,
|
scope=None,
|
||||||
username=None,
|
cache_handler=None,
|
||||||
proxies=None,
|
proxies=None,
|
||||||
requests_timeout=None,
|
requests_timeout=None,
|
||||||
requests_session=True,
|
requests_session=True,
|
||||||
open_browser=True,
|
open_browser=True
|
||||||
cache_handler=None):
|
):
|
||||||
"""
|
"""
|
||||||
Creates Auth Manager with the PKCE Auth flow.
|
Creates Auth Manager with the PKCE Auth flow.
|
||||||
|
|
||||||
@ -694,20 +626,18 @@ class SpotifyPKCE(SpotifyAuthBase):
|
|||||||
* scope: Optional, either a string of scopes, or an iterable with elements of type
|
* scope: Optional, either a string of scopes, or an iterable with elements of type
|
||||||
`Scope` or `str`. E.g.,
|
`Scope` or `str`. E.g.,
|
||||||
{Scope.user_modify_playback_state, Scope.user_library_read}
|
{Scope.user_modify_playback_state, Scope.user_library_read}
|
||||||
* cache_path: (deprecated) Optional, will otherwise be generated
|
|
||||||
(takes precedence over `username`)
|
|
||||||
* username: (deprecated) Optional or set as environment variable
|
|
||||||
(will set `cache_path` to `.cache-{username}`)
|
|
||||||
* proxies: Optional, proxy for the requests library to route through
|
|
||||||
* requests_timeout: Optional, tell Requests to stop waiting for a response after
|
|
||||||
a given number of seconds
|
|
||||||
* requests_session: A Requests session
|
|
||||||
* open_browser: Optional, thether or not the web browser should be opened to
|
|
||||||
authorize a user
|
|
||||||
* cache_handler: An instance of the `CacheHandler` class to handle
|
* cache_handler: An instance of the `CacheHandler` class to handle
|
||||||
getting and saving cached authorization tokens.
|
getting and saving cached authorization tokens.
|
||||||
Optional, will otherwise use `CacheFileHandler`.
|
Optional, will otherwise use `CacheFileHandler`.
|
||||||
(takes precedence over `cache_path` and `username`)
|
* proxies: Optional, proxy for the requests library to route through
|
||||||
|
* requests_timeout: Optional, tell Requests to stop waiting for a response after
|
||||||
|
a given number of seconds
|
||||||
|
* requests_session: A Requests session object or a true value to create one.
|
||||||
|
A false value disables sessions.
|
||||||
|
It should generally be a good idea to keep sessions enabled
|
||||||
|
for performance reasons (connection pooling).
|
||||||
|
* open_browser: Optional, thether or not the web browser should be opened to
|
||||||
|
authorize a user
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super(SpotifyPKCE, self).__init__(requests_session)
|
super(SpotifyPKCE, self).__init__(requests_session)
|
||||||
@ -715,31 +645,13 @@ class SpotifyPKCE(SpotifyAuthBase):
|
|||||||
self.redirect_uri = redirect_uri
|
self.redirect_uri = redirect_uri
|
||||||
self.state = state
|
self.state = state
|
||||||
self.scope = self._normalize_scope(scope)
|
self.scope = self._normalize_scope(scope)
|
||||||
username = (username or os.getenv(CLIENT_CREDS_ENV_VARS["client_username"]))
|
|
||||||
if username or cache_path:
|
|
||||||
warnings.warn("Specifying cache_path or username as arguments to SpotifyPKCE " +
|
|
||||||
"will be deprecated. Instead, please create a CacheFileHandler " +
|
|
||||||
"instance with the desired cache_path and username and pass it " +
|
|
||||||
"to SpotifyPKCE as the cache_handler. For example:\n\n" +
|
|
||||||
"\tfrom spotipy.oauth2 import CacheFileHandler\n" +
|
|
||||||
"\thandler = CacheFileHandler(cache_path=cache_path, " +
|
|
||||||
"username=username)\n" +
|
|
||||||
"\tsp = spotipy.SpotifyImplicitGrant(client_id, client_secret, " +
|
|
||||||
"redirect_uri, cache_handler=handler)",
|
|
||||||
DeprecationWarning
|
|
||||||
)
|
|
||||||
if cache_handler:
|
|
||||||
warnings.warn("A cache_handler has been specified along with a cache_path or " +
|
|
||||||
"username. The cache_path and username arguments will be ignored.")
|
|
||||||
if cache_handler:
|
if cache_handler:
|
||||||
assert issubclass(type(cache_handler), CacheHandler), \
|
assert issubclass(cache_handler.__class__, CacheHandler), \
|
||||||
"type(cache_handler): " + str(type(cache_handler)) + " != " + str(CacheHandler)
|
"cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \
|
||||||
|
+ " != " + str(CacheHandler)
|
||||||
self.cache_handler = cache_handler
|
self.cache_handler = cache_handler
|
||||||
else:
|
else:
|
||||||
self.cache_handler = CacheFileHandler(
|
self.cache_handler = CacheFileHandler()
|
||||||
username=username,
|
|
||||||
cache_path=cache_path
|
|
||||||
)
|
|
||||||
self.proxies = proxies
|
self.proxies = proxies
|
||||||
self.requests_timeout = requests_timeout
|
self.requests_timeout = requests_timeout
|
||||||
|
|
||||||
@ -1018,286 +930,6 @@ class SpotifyPKCE(SpotifyAuthBase):
|
|||||||
def parse_auth_response_url(url):
|
def parse_auth_response_url(url):
|
||||||
return SpotifyOAuth.parse_auth_response_url(url)
|
return SpotifyOAuth.parse_auth_response_url(url)
|
||||||
|
|
||||||
def get_cached_token(self):
|
|
||||||
warnings.warn("Calling get_cached_token directly on the SpotifyPKCE object will be " +
|
|
||||||
"deprecated. Instead, please specify a CacheFileHandler instance as " +
|
|
||||||
"the cache_handler in SpotifyOAuth and use the CacheFileHandler's " +
|
|
||||||
"get_cached_token method. You can replace:\n\tsp.get_cached_token()" +
|
|
||||||
"\n\nWith:\n\tsp.validate_token(sp.cache_handler.get_cached_token())",
|
|
||||||
DeprecationWarning
|
|
||||||
)
|
|
||||||
return self.validate_token(self.cache_handler.get_cached_token())
|
|
||||||
|
|
||||||
def _save_token_info(self, token_info):
|
|
||||||
warnings.warn("Calling _save_token_info 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 " +
|
|
||||||
"save_token_to_cache method.",
|
|
||||||
DeprecationWarning
|
|
||||||
)
|
|
||||||
self.cache_handler.save_token_to_cache(token_info)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class SpotifyImplicitGrant(SpotifyAuthBase):
|
|
||||||
""" Implements Implicit Grant Flow for client apps
|
|
||||||
|
|
||||||
This auth manager enables *user and non-user* endpoints with only
|
|
||||||
a client secret, redirect uri, and username. The user will need to
|
|
||||||
copy and paste a URI from the browser every hour.
|
|
||||||
|
|
||||||
Security Warning
|
|
||||||
-----------------
|
|
||||||
The OAuth standard no longer recommends the Implicit Grant Flow for
|
|
||||||
client-side code. Spotify has implemented the OAuth-suggested PKCE
|
|
||||||
extension that removes the need for a client secret in the
|
|
||||||
Authentication Code flow. Use the SpotifyPKCE auth manager instead
|
|
||||||
of SpotifyImplicitGrant.
|
|
||||||
|
|
||||||
SpotifyPKCE contains all of the functionality of
|
|
||||||
SpotifyImplicitGrant, plus automatic response retrieval and
|
|
||||||
refreshable tokens. Only a few replacements need to be made:
|
|
||||||
|
|
||||||
* get_auth_response()['access_token'] ->
|
|
||||||
get_access_token(get_authorization_code())
|
|
||||||
* get_auth_response() ->
|
|
||||||
get_access_token(get_authorization_code()); get_cached_token()
|
|
||||||
* parse_response_token(url)['access_token'] ->
|
|
||||||
get_access_token(parse_response_code(url))
|
|
||||||
* 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
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
OAUTH_AUTHORIZE_URL = "https://accounts.spotify.com/authorize"
|
|
||||||
|
|
||||||
def __init__(self,
|
|
||||||
client_id=None,
|
|
||||||
redirect_uri=None,
|
|
||||||
state=None,
|
|
||||||
scope=None,
|
|
||||||
cache_path=None,
|
|
||||||
username=None,
|
|
||||||
show_dialog=False,
|
|
||||||
cache_handler=None):
|
|
||||||
""" Creates Auth Manager using the Implicit Grant flow
|
|
||||||
|
|
||||||
**See help(SpotifyImplictGrant) for full Security Warning**
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
* client_id: Must be supplied or set as environment variable
|
|
||||||
* redirect_uri: Must be supplied or set as environment variable
|
|
||||||
* state: May be supplied, no verification is performed
|
|
||||||
* scope: Optional, either a string of scopes, or an iterable with elements of type
|
|
||||||
`Scope` or `str`. E.g.,
|
|
||||||
{Scope.user_modify_playback_state, Scope.user_library_read}
|
|
||||||
* cache_handler: An instance of the `CacheHandler` class to handle
|
|
||||||
getting and saving cached authorization tokens.
|
|
||||||
May be supplied, will otherwise use `CacheFileHandler`.
|
|
||||||
(takes precedence over `cache_path` and `username`)
|
|
||||||
* cache_path: (deprecated) May be supplied, will otherwise be generated
|
|
||||||
(takes precedence over `username`)
|
|
||||||
* username: (deprecated) May be supplied or set as environment variable
|
|
||||||
(will set `cache_path` to `.cache-{username}`)
|
|
||||||
* show_dialog: Interpreted as boolean
|
|
||||||
"""
|
|
||||||
logger.warning("The OAuth standard no longer recommends 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).")
|
|
||||||
|
|
||||||
self.client_id = client_id
|
|
||||||
self.redirect_uri = redirect_uri
|
|
||||||
self.state = state
|
|
||||||
username = (username or os.getenv(CLIENT_CREDS_ENV_VARS["client_username"]))
|
|
||||||
if username or cache_path:
|
|
||||||
warnings.warn("Specifying cache_path or username as arguments to " +
|
|
||||||
"SpotifyImplicitGrant will be deprecated. Instead, please create " +
|
|
||||||
"a CacheFileHandler instance with the desired cache_path and " +
|
|
||||||
"username and pass it to SpotifyImplicitGrant as the " +
|
|
||||||
"cache_handler. For example:\n\n" +
|
|
||||||
"\tfrom spotipy.oauth2 import CacheFileHandler\n" +
|
|
||||||
"\thandler = CacheFileHandler(cache_path=cache_path, " +
|
|
||||||
"username=username)\n" +
|
|
||||||
"\tsp = spotipy.SpotifyImplicitGrant(client_id, client_secret, " +
|
|
||||||
"redirect_uri, cache_handler=handler)",
|
|
||||||
DeprecationWarning
|
|
||||||
)
|
|
||||||
if cache_handler:
|
|
||||||
warnings.warn("A cache_handler has been specified along with a cache_path or " +
|
|
||||||
"username. The cache_path and username arguments will be ignored.")
|
|
||||||
if cache_handler:
|
|
||||||
assert issubclass(type(cache_handler), CacheHandler), \
|
|
||||||
"type(cache_handler): " + str(type(cache_handler)) + " != " + str(CacheHandler)
|
|
||||||
self.cache_handler = cache_handler
|
|
||||||
else:
|
|
||||||
self.cache_handler = CacheFileHandler(
|
|
||||||
username=username,
|
|
||||||
cache_path=cache_path
|
|
||||||
)
|
|
||||||
self.scope = self._normalize_scope(scope)
|
|
||||||
self.show_dialog = show_dialog
|
|
||||||
self._session = None # As to not break inherited __del__
|
|
||||||
|
|
||||||
def validate_token(self, token_info):
|
|
||||||
if token_info is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# if scopes don't match, then bail
|
|
||||||
if "scope" not in token_info or not self._is_scope_subset(
|
|
||||||
self.scope, token_info["scope"]
|
|
||||||
):
|
|
||||||
return None
|
|
||||||
|
|
||||||
if self.is_token_expired(token_info):
|
|
||||||
return None
|
|
||||||
|
|
||||||
return token_info
|
|
||||||
|
|
||||||
def get_access_token(self,
|
|
||||||
state=None,
|
|
||||||
response=None,
|
|
||||||
check_cache=True):
|
|
||||||
""" Gets Auth Token from cache (preferred) or user interaction
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
* state: May be given, overrides (without changing) self.state
|
|
||||||
* response: URI with token, can break expiration checks
|
|
||||||
* check_cache: Interpreted as boolean
|
|
||||||
"""
|
|
||||||
if check_cache:
|
|
||||||
token_info = self.validate_token(self.cache_handler.get_cached_token())
|
|
||||||
if not (token_info is None or self.is_token_expired(token_info)):
|
|
||||||
return token_info["access_token"]
|
|
||||||
|
|
||||||
if response:
|
|
||||||
token_info = self.parse_response_token(response)
|
|
||||||
else:
|
|
||||||
token_info = self.get_auth_response(state)
|
|
||||||
token_info = self._add_custom_values_to_token_info(token_info)
|
|
||||||
self.cache_handler.save_token_to_cache(token_info)
|
|
||||||
|
|
||||||
return token_info["access_token"]
|
|
||||||
|
|
||||||
def get_authorize_url(self, state=None):
|
|
||||||
""" Gets the URL to use to authorize this app """
|
|
||||||
payload = {
|
|
||||||
"client_id": self.client_id,
|
|
||||||
"response_type": "token",
|
|
||||||
"redirect_uri": self.redirect_uri,
|
|
||||||
}
|
|
||||||
if self.scope:
|
|
||||||
payload["scope"] = self.scope
|
|
||||||
if state is None:
|
|
||||||
state = self.state
|
|
||||||
if state is not None:
|
|
||||||
payload["state"] = state
|
|
||||||
if self.show_dialog:
|
|
||||||
payload["show_dialog"] = True
|
|
||||||
|
|
||||||
urlparams = urllibparse.urlencode(payload)
|
|
||||||
|
|
||||||
return "%s?%s" % (self.OAUTH_AUTHORIZE_URL, urlparams)
|
|
||||||
|
|
||||||
def parse_response_token(self, url, state=None):
|
|
||||||
""" Parse the response code in the given response url """
|
|
||||||
remote_state, token, t_type, exp_in = self.parse_auth_response_url(url)
|
|
||||||
if state is None:
|
|
||||||
state = self.state
|
|
||||||
if state is not None and remote_state != state:
|
|
||||||
raise SpotifyStateError(state, remote_state)
|
|
||||||
return {"access_token": token, "token_type": t_type,
|
|
||||||
"expires_in": exp_in, "state": state}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_auth_response_url(url):
|
|
||||||
url_components = urlparse(url)
|
|
||||||
fragment_s = url_components.fragment
|
|
||||||
query_s = url_components.query
|
|
||||||
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"]),
|
|
||||||
state=form["state"])
|
|
||||||
if "expires_in" in form:
|
|
||||||
form["expires_in"] = int(form["expires_in"])
|
|
||||||
return tuple(form.get(param) for param in ["state", "access_token",
|
|
||||||
"token_type", "expires_in"])
|
|
||||||
|
|
||||||
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)
|
|
||||||
except webbrowser.Error:
|
|
||||||
logger.error("Please navigate here: %s", auth_url)
|
|
||||||
|
|
||||||
def get_auth_response(self, state=None):
|
|
||||||
""" Gets a new auth **token** with user interaction """
|
|
||||||
logger.info('User authentication requires interaction with your '
|
|
||||||
'web browser. Once you enter your credentials and '
|
|
||||||
'give authorization, you will be redirected to '
|
|
||||||
'a url. Paste that url you were directed to to '
|
|
||||||
'complete the authorization.')
|
|
||||||
|
|
||||||
redirect_info = urlparse(self.redirect_uri)
|
|
||||||
redirect_host, redirect_port = get_host_port(redirect_info.netloc)
|
|
||||||
# Implicit Grant tokens are returned in a hash fragment
|
|
||||||
# which is only available to the browser. Therefore, interactive
|
|
||||||
# URL retrieval is required.
|
|
||||||
if (redirect_host in ("127.0.0.1", "localhost")
|
|
||||||
and redirect_info.scheme == "http" and redirect_port):
|
|
||||||
logger.warning('Using a local redirect URI with a '
|
|
||||||
'port, likely expecting automatic '
|
|
||||||
'retrieval. Due to technical limitations, '
|
|
||||||
'the authentication token cannot be '
|
|
||||||
'automatically retrieved and must be '
|
|
||||||
'copied and pasted.')
|
|
||||||
|
|
||||||
self._open_auth_url(state)
|
|
||||||
logger.info('Paste that url you were directed to in order to '
|
|
||||||
'complete the authorization')
|
|
||||||
response = SpotifyImplicitGrant._get_user_input("Enter the URL you "
|
|
||||||
"were redirected to: ")
|
|
||||||
return self.parse_response_token(response, state)
|
|
||||||
|
|
||||||
def _add_custom_values_to_token_info(self, token_info):
|
|
||||||
"""
|
|
||||||
Store some values that aren't directly provided by a Web API
|
|
||||||
response.
|
|
||||||
"""
|
|
||||||
token_info["expires_at"] = int(time.time()) + token_info["expires_in"]
|
|
||||||
token_info["scope"] = self.scope
|
|
||||||
return token_info
|
|
||||||
|
|
||||||
def get_cached_token(self):
|
|
||||||
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 " +
|
|
||||||
"and use the CacheFileHandler's get_cached_token method. " +
|
|
||||||
"You can replace:\n\tsp.get_cached_token()" +
|
|
||||||
"\n\nWith:\n\tsp.validate_token(sp.cache_handler.get_cached_token())",
|
|
||||||
DeprecationWarning
|
|
||||||
)
|
|
||||||
return self.validate_token(self.cache_handler.get_cached_token())
|
|
||||||
|
|
||||||
def _save_token_info(self, token_info):
|
|
||||||
warnings.warn("Calling _save_token_info directly on the SpotifyImplicitGrant " +
|
|
||||||
"object will be deprecated. Instead, please specify a " +
|
|
||||||
"CacheFileHandler instance as the cache_handler in SpotifyOAuth " +
|
|
||||||
"and use the CacheFileHandler's save_token_to_cache method.",
|
|
||||||
DeprecationWarning
|
|
||||||
)
|
|
||||||
self.cache_handler.save_token_to_cache(token_info)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class RequestHandler(BaseHTTPRequestHandler):
|
class RequestHandler(BaseHTTPRequestHandler):
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
|
|||||||
@ -2,12 +2,9 @@
|
|||||||
|
|
||||||
""" Shows a user's playlists (need to be authenticated via oauth) """
|
""" Shows a user's playlists (need to be authenticated via oauth) """
|
||||||
|
|
||||||
__all__ = ["CLIENT_CREDS_ENV_VARS", "prompt_for_user_token"]
|
__all__ = ["CLIENT_CREDS_ENV_VARS"]
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import warnings
|
|
||||||
import spotipy
|
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -19,94 +16,6 @@ CLIENT_CREDS_ENV_VARS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def prompt_for_user_token(
|
|
||||||
username=None,
|
|
||||||
scope=None,
|
|
||||||
client_id=None,
|
|
||||||
client_secret=None,
|
|
||||||
redirect_uri=None,
|
|
||||||
cache_path=None,
|
|
||||||
oauth_manager=None,
|
|
||||||
show_dialog=False
|
|
||||||
):
|
|
||||||
warnings.warn(
|
|
||||||
"'prompt_for_user_token' is deprecated."
|
|
||||||
"Use the following instead: "
|
|
||||||
" auth_manager=SpotifyOAuth(scope=scope)"
|
|
||||||
" 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")
|
|
||||||
|
|
||||||
if not client_secret:
|
|
||||||
client_secret = os.getenv("SPOTIPY_CLIENT_SECRET")
|
|
||||||
|
|
||||||
if not redirect_uri:
|
|
||||||
redirect_uri = os.getenv("SPOTIPY_REDIRECT_URI")
|
|
||||||
|
|
||||||
if not client_id:
|
|
||||||
LOGGER.warning(
|
|
||||||
"""
|
|
||||||
You need to set your Spotify API credentials.
|
|
||||||
You can do this by setting environment variables like so:
|
|
||||||
|
|
||||||
export SPOTIPY_CLIENT_ID='your-spotify-client-id'
|
|
||||||
export SPOTIPY_CLIENT_SECRET='your-spotify-client-secret'
|
|
||||||
export SPOTIPY_REDIRECT_URI='your-app-redirect-url'
|
|
||||||
|
|
||||||
Get your credentials at
|
|
||||||
https://developer.spotify.com/my-applications
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
raise spotipy.SpotifyException(550, -1, "no credentials set")
|
|
||||||
|
|
||||||
sp_oauth = oauth_manager or spotipy.SpotifyOAuth(
|
|
||||||
client_id,
|
|
||||||
client_secret,
|
|
||||||
redirect_uri,
|
|
||||||
scope=scope,
|
|
||||||
cache_path=cache_path,
|
|
||||||
username=username,
|
|
||||||
show_dialog=show_dialog
|
|
||||||
)
|
|
||||||
|
|
||||||
# try to get a valid token for this user, from the cache,
|
|
||||||
# if not in the cache, the 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())
|
|
||||||
|
|
||||||
if not token_info:
|
|
||||||
code = sp_oauth.get_auth_response()
|
|
||||||
token = sp_oauth.get_access_token(code, as_dict=False)
|
|
||||||
else:
|
|
||||||
return token_info["access_token"]
|
|
||||||
|
|
||||||
# Auth'ed API request
|
|
||||||
if token:
|
|
||||||
return token
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_host_port(netloc):
|
def get_host_port(netloc):
|
||||||
if ":" in netloc:
|
if ":" in netloc:
|
||||||
host, port = netloc.split(":", 1)
|
host, port = netloc.split(":", 1)
|
||||||
|
|||||||
@ -58,7 +58,8 @@ class AuthTestSpotipy(unittest.TestCase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(self):
|
def setUpClass(self):
|
||||||
self.spotify = Spotify(
|
self.spotify = Spotify(
|
||||||
client_credentials_manager=SpotifyClientCredentials())
|
auth_manager=SpotifyClientCredentials()
|
||||||
|
)
|
||||||
self.spotify.trace = False
|
self.spotify.trace = False
|
||||||
|
|
||||||
def test_audio_analysis(self):
|
def test_audio_analysis(self):
|
||||||
@ -232,9 +233,9 @@ class AuthTestSpotipy(unittest.TestCase):
|
|||||||
self.assertTrue(found)
|
self.assertTrue(found)
|
||||||
|
|
||||||
def test_search_timeout(self):
|
def test_search_timeout(self):
|
||||||
client_credentials_manager = SpotifyClientCredentials()
|
auth_manager = SpotifyClientCredentials()
|
||||||
sp = spotipy.Spotify(requests_timeout=0.01,
|
sp = spotipy.Spotify(requests_timeout=0.01,
|
||||||
client_credentials_manager=client_credentials_manager)
|
auth_manager=auth_manager)
|
||||||
|
|
||||||
# depending on the timing or bandwidth, this raises a timeout or connection error"
|
# depending on the timing or bandwidth, this raises a timeout or connection error"
|
||||||
self.assertRaises((requests.exceptions.Timeout, requests.exceptions.ConnectionError),
|
self.assertRaises((requests.exceptions.Timeout, requests.exceptions.ConnectionError),
|
||||||
@ -242,17 +243,15 @@ class AuthTestSpotipy(unittest.TestCase):
|
|||||||
|
|
||||||
def test_max_retries_reached_get(self):
|
def test_max_retries_reached_get(self):
|
||||||
spotify_no_retry = Spotify(
|
spotify_no_retry = Spotify(
|
||||||
client_credentials_manager=SpotifyClientCredentials(),
|
auth_manager=SpotifyClientCredentials(),
|
||||||
retries=0)
|
retries=0)
|
||||||
i = 0
|
for i in range(100):
|
||||||
while i < 100:
|
|
||||||
try:
|
try:
|
||||||
spotify_no_retry.search(q='foo')
|
spotify_no_retry.search(q='foo')
|
||||||
except SpotifyException as e:
|
except SpotifyException as e:
|
||||||
self.assertIsInstance(e, SpotifyException)
|
self.assertIsInstance(e, SpotifyException)
|
||||||
self.assertEqual(e.http_status, 429)
|
self.assertEqual(e.http_status, 429)
|
||||||
return
|
return
|
||||||
i += 1
|
|
||||||
self.fail()
|
self.fail()
|
||||||
|
|
||||||
def test_album_search(self):
|
def test_album_search(self):
|
||||||
@ -350,7 +349,7 @@ class AuthTestSpotipy(unittest.TestCase):
|
|||||||
sess = requests.Session()
|
sess = requests.Session()
|
||||||
sess.headers["user-agent"] = "spotipy-test"
|
sess.headers["user-agent"] = "spotipy-test"
|
||||||
with_custom_session = spotipy.Spotify(
|
with_custom_session = spotipy.Spotify(
|
||||||
client_credentials_manager=SpotifyClientCredentials(),
|
auth_manager=SpotifyClientCredentials(),
|
||||||
requests_session=sess)
|
requests_session=sess)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
with_custom_session.user(
|
with_custom_session.user(
|
||||||
@ -359,7 +358,7 @@ class AuthTestSpotipy(unittest.TestCase):
|
|||||||
|
|
||||||
def test_force_no_requests_session(self):
|
def test_force_no_requests_session(self):
|
||||||
with_no_session = spotipy.Spotify(
|
with_no_session = spotipy.Spotify(
|
||||||
client_credentials_manager=SpotifyClientCredentials(),
|
auth_manager=SpotifyClientCredentials(),
|
||||||
requests_session=False)
|
requests_session=False)
|
||||||
self.assertNotIsInstance(with_no_session._session, requests.Session)
|
self.assertNotIsInstance(with_no_session._session, requests.Session)
|
||||||
user = with_no_session.user(user="akx")
|
user = with_no_session.user(user="akx")
|
||||||
|
|||||||
@ -2,16 +2,33 @@ import os
|
|||||||
|
|
||||||
from spotipy import (
|
from spotipy import (
|
||||||
CLIENT_CREDS_ENV_VARS as CCEV,
|
CLIENT_CREDS_ENV_VARS as CCEV,
|
||||||
prompt_for_user_token,
|
|
||||||
Spotify,
|
Spotify,
|
||||||
SpotifyException,
|
SpotifyException,
|
||||||
SpotifyImplicitGrant,
|
SpotifyOAuth,
|
||||||
SpotifyPKCE
|
SpotifyPKCE,
|
||||||
|
CacheFileHandler
|
||||||
)
|
)
|
||||||
import unittest
|
import unittest
|
||||||
from tests import helpers
|
from tests import helpers
|
||||||
|
|
||||||
|
|
||||||
|
def _make_spotify(scopes=None, retries=None):
|
||||||
|
|
||||||
|
retries = retries or Spotify.max_retries
|
||||||
|
|
||||||
|
auth_manager = SpotifyOAuth(
|
||||||
|
scope=scopes,
|
||||||
|
cache_handler=CacheFileHandler()
|
||||||
|
)
|
||||||
|
|
||||||
|
spotify = Spotify(
|
||||||
|
auth_manager=auth_manager,
|
||||||
|
retries=retries
|
||||||
|
)
|
||||||
|
|
||||||
|
return spotify
|
||||||
|
|
||||||
|
|
||||||
class SpotipyPlaylistApiTest(unittest.TestCase):
|
class SpotipyPlaylistApiTest(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
@ -48,10 +65,9 @@ class SpotipyPlaylistApiTest(unittest.TestCase):
|
|||||||
'user-read-playback-state'
|
'user-read-playback-state'
|
||||||
)
|
)
|
||||||
|
|
||||||
token = prompt_for_user_token(cls.username, scope=scope)
|
cls.spotify = _make_spotify(scopes=scope)
|
||||||
|
cls.spotify_no_retry = _make_spotify(scopes=scope, retries=0)
|
||||||
|
|
||||||
cls.spotify = Spotify(auth=token)
|
|
||||||
cls.spotify_no_retry = Spotify(auth=token, retries=0)
|
|
||||||
cls.new_playlist_name = 'spotipy-playlist-test'
|
cls.new_playlist_name = 'spotipy-playlist-test'
|
||||||
cls.new_playlist = helpers.get_spotify_playlist(
|
cls.new_playlist = helpers.get_spotify_playlist(
|
||||||
cls.spotify, cls.new_playlist_name, cls.username) or \
|
cls.spotify, cls.new_playlist_name, cls.username) or \
|
||||||
@ -119,8 +135,7 @@ class SpotipyPlaylistApiTest(unittest.TestCase):
|
|||||||
self.assertEqual(pl["tracks"]["total"], 0)
|
self.assertEqual(pl["tracks"]["total"], 0)
|
||||||
|
|
||||||
def test_max_retries_reached_post(self):
|
def test_max_retries_reached_post(self):
|
||||||
i = 0
|
for i in range(500):
|
||||||
while i < 500:
|
|
||||||
try:
|
try:
|
||||||
self.spotify_no_retry.playlist_change_details(
|
self.spotify_no_retry.playlist_change_details(
|
||||||
self.new_playlist['id'], description="test")
|
self.new_playlist['id'], description="test")
|
||||||
@ -128,7 +143,6 @@ class SpotipyPlaylistApiTest(unittest.TestCase):
|
|||||||
self.assertIsInstance(e, SpotifyException)
|
self.assertIsInstance(e, SpotifyException)
|
||||||
self.assertEqual(e.http_status, 429)
|
self.assertEqual(e.http_status, 429)
|
||||||
return
|
return
|
||||||
i += 1
|
|
||||||
self.fail()
|
self.fail()
|
||||||
|
|
||||||
def test_playlist_add_items(self):
|
def test_playlist_add_items(self):
|
||||||
@ -188,17 +202,6 @@ class SpotipyPlaylistApiTest(unittest.TestCase):
|
|||||||
return
|
return
|
||||||
self.fail()
|
self.fail()
|
||||||
|
|
||||||
def test_deprecated_starred(self):
|
|
||||||
pl = self.spotify.user_playlist(self.username)
|
|
||||||
self.assertTrue(pl["tracks"] is None)
|
|
||||||
self.assertTrue(pl["owner"] is None)
|
|
||||||
|
|
||||||
def test_deprecated_user_playlist(self):
|
|
||||||
# Test without user due to change from
|
|
||||||
# https://developer.spotify.com/community/news/2018/06/12/changes-to-playlist-uris/
|
|
||||||
pl = self.spotify.user_playlist(None, self.new_playlist['id'])
|
|
||||||
self.assertEqual(pl["tracks"]["total"], 0)
|
|
||||||
|
|
||||||
|
|
||||||
class SpotipyLibraryApiTests(unittest.TestCase):
|
class SpotipyLibraryApiTests(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -227,10 +230,7 @@ class SpotipyLibraryApiTests(unittest.TestCase):
|
|||||||
'ugc-image-upload '
|
'ugc-image-upload '
|
||||||
'user-read-playback-state'
|
'user-read-playback-state'
|
||||||
)
|
)
|
||||||
|
cls.spotify = _make_spotify(scopes=scope)
|
||||||
token = prompt_for_user_token(cls.username, scope=scope)
|
|
||||||
|
|
||||||
cls.spotify = Spotify(auth=token)
|
|
||||||
|
|
||||||
def test_track_bad_id(self):
|
def test_track_bad_id(self):
|
||||||
with self.assertRaises(SpotifyException):
|
with self.assertRaises(SpotifyException):
|
||||||
@ -305,9 +305,7 @@ class SpotipyUserApiTests(unittest.TestCase):
|
|||||||
'user-read-playback-state'
|
'user-read-playback-state'
|
||||||
)
|
)
|
||||||
|
|
||||||
token = prompt_for_user_token(cls.username, scope=scope)
|
cls.spotify = _make_spotify(scopes=scope)
|
||||||
|
|
||||||
cls.spotify = Spotify(auth=token)
|
|
||||||
|
|
||||||
def test_basic_user_profile(self):
|
def test_basic_user_profile(self):
|
||||||
user = self.spotify.user(self.username)
|
user = self.spotify.user(self.username)
|
||||||
@ -335,9 +333,7 @@ class SpotipyUserApiTests(unittest.TestCase):
|
|||||||
class SpotipyBrowseApiTests(unittest.TestCase):
|
class SpotipyBrowseApiTests(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
username = os.getenv(CCEV['client_username'])
|
cls.spotify = _make_spotify()
|
||||||
token = prompt_for_user_token(username)
|
|
||||||
cls.spotify = Spotify(auth=token)
|
|
||||||
|
|
||||||
def test_category(self):
|
def test_category(self):
|
||||||
response = self.spotify.category('rock')
|
response = self.spotify.category('rock')
|
||||||
@ -383,9 +379,7 @@ class SpotipyFollowApiTests(unittest.TestCase):
|
|||||||
'user-read-playback-state'
|
'user-read-playback-state'
|
||||||
)
|
)
|
||||||
|
|
||||||
token = prompt_for_user_token(cls.username, scope=scope)
|
cls.spotify = _make_spotify(scopes=scope)
|
||||||
|
|
||||||
cls.spotify = Spotify(auth=token)
|
|
||||||
|
|
||||||
def test_current_user_follows(self):
|
def test_current_user_follows(self):
|
||||||
response = self.spotify.current_user_followed_artists()
|
response = self.spotify.current_user_followed_artists()
|
||||||
@ -438,9 +432,7 @@ class SpotipyPlayerApiTests(unittest.TestCase):
|
|||||||
'user-read-playback-state'
|
'user-read-playback-state'
|
||||||
)
|
)
|
||||||
|
|
||||||
token = prompt_for_user_token(cls.username, scope=scope)
|
cls.spotify = _make_spotify(scopes=scope)
|
||||||
|
|
||||||
cls.spotify = Spotify(auth=token)
|
|
||||||
|
|
||||||
def test_devices(self):
|
def test_devices(self):
|
||||||
# No devices playing by default
|
# No devices playing by default
|
||||||
@ -454,39 +446,6 @@ class SpotipyPlayerApiTests(unittest.TestCase):
|
|||||||
# not much more to test if account is inactive and has no recently played tracks
|
# not much more to test if account is inactive and has no recently played tracks
|
||||||
|
|
||||||
|
|
||||||
class SpotipyImplicitGrantTests(unittest.TestCase):
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
scope = (
|
|
||||||
'user-follow-read '
|
|
||||||
'user-follow-modify '
|
|
||||||
)
|
|
||||||
auth_manager = SpotifyImplicitGrant(scope=scope,
|
|
||||||
cache_path=".cache-implicittest")
|
|
||||||
cls.spotify = Spotify(auth_manager=auth_manager)
|
|
||||||
|
|
||||||
def test_user_follows_and_unfollows_artist(self):
|
|
||||||
# Initially follows 1 artist
|
|
||||||
current_user_followed_artists = self.spotify.current_user_followed_artists()[
|
|
||||||
'artists']['total']
|
|
||||||
|
|
||||||
# Follow 2 more artists
|
|
||||||
artists = ["6DPYiyq5kWVQS4RGwxzPC7", "0NbfKEOTQCcwd6o7wSDOHI"]
|
|
||||||
self.spotify.user_follow_artists(artists)
|
|
||||||
res = self.spotify.current_user_followed_artists()
|
|
||||||
self.assertEqual(res['artists']['total'], current_user_followed_artists + len(artists))
|
|
||||||
|
|
||||||
# Unfollow these 2 artists
|
|
||||||
self.spotify.user_unfollow_artists(artists)
|
|
||||||
res = self.spotify.current_user_followed_artists()
|
|
||||||
self.assertEqual(res['artists']['total'], current_user_followed_artists)
|
|
||||||
|
|
||||||
def test_current_user(self):
|
|
||||||
c_user = self.spotify.current_user()
|
|
||||||
user = self.spotify.user(c_user['id'])
|
|
||||||
self.assertEqual(c_user['display_name'], user['display_name'])
|
|
||||||
|
|
||||||
|
|
||||||
class SpotifyPKCETests(unittest.TestCase):
|
class SpotifyPKCETests(unittest.TestCase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -495,7 +454,8 @@ class SpotifyPKCETests(unittest.TestCase):
|
|||||||
'user-follow-read '
|
'user-follow-read '
|
||||||
'user-follow-modify '
|
'user-follow-modify '
|
||||||
)
|
)
|
||||||
auth_manager = SpotifyPKCE(scope=scope, cache_path=".cache-pkcetest")
|
cache_handler = CacheFileHandler(cache_path=".cache-pkcetest")
|
||||||
|
auth_manager = SpotifyPKCE(scope=scope, cache_handler=cache_handler)
|
||||||
cls.spotify = Spotify(auth_manager=auth_manager)
|
cls.spotify = Spotify(auth_manager=auth_manager)
|
||||||
|
|
||||||
def test_user_follows_and_unfollows_artist(self):
|
def test_user_follows_and_unfollows_artist(self):
|
||||||
|
|||||||
@ -5,10 +5,10 @@ import unittest
|
|||||||
|
|
||||||
import six.moves.urllib.parse as urllibparse
|
import six.moves.urllib.parse as urllibparse
|
||||||
|
|
||||||
from spotipy import SpotifyOAuth, SpotifyImplicitGrant, SpotifyPKCE
|
from spotipy import SpotifyOAuth, SpotifyPKCE
|
||||||
from spotipy.cache_handler import CacheHandler
|
|
||||||
from spotipy.oauth2 import SpotifyClientCredentials, SpotifyOauthError
|
from spotipy.oauth2 import SpotifyClientCredentials, SpotifyOauthError
|
||||||
from spotipy.oauth2 import SpotifyStateError
|
from spotipy.oauth2 import SpotifyStateError
|
||||||
|
from spotipy import MemoryCacheHandler, CacheFileHandler
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import unittest.mock as mock
|
import unittest.mock as mock
|
||||||
@ -43,26 +43,10 @@ def _make_oauth(*args, **kwargs):
|
|||||||
return SpotifyOAuth("CLID", "CLISEC", "REDIR", "STATE", *args, **kwargs)
|
return SpotifyOAuth("CLID", "CLISEC", "REDIR", "STATE", *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def _make_implicitgrantauth(*args, **kwargs):
|
|
||||||
return SpotifyImplicitGrant("CLID", "REDIR", "STATE", *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def _make_pkceauth(*args, **kwargs):
|
def _make_pkceauth(*args, **kwargs):
|
||||||
return SpotifyPKCE("CLID", "REDIR", "STATE", *args, **kwargs)
|
return SpotifyPKCE("CLID", "REDIR", "STATE", *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class MemoryCache(CacheHandler):
|
|
||||||
def __init__(self, token_info=None):
|
|
||||||
self.token_info = token_info
|
|
||||||
|
|
||||||
def get_cached_token(self):
|
|
||||||
return self.token_info
|
|
||||||
|
|
||||||
def save_token_to_cache(self, token_info):
|
|
||||||
self.token_info = token_info
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class OAuthCacheTest(unittest.TestCase):
|
class OAuthCacheTest(unittest.TestCase):
|
||||||
|
|
||||||
@patch.multiple(SpotifyOAuth,
|
@patch.multiple(SpotifyOAuth,
|
||||||
@ -77,13 +61,12 @@ class OAuthCacheTest(unittest.TestCase):
|
|||||||
opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
|
opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
|
||||||
is_token_expired.return_value = False
|
is_token_expired.return_value = False
|
||||||
|
|
||||||
spot = _make_oauth(scope, path)
|
cache_handler = CacheFileHandler(cache_path=path)
|
||||||
|
spot = _make_oauth(scope, cache_handler=cache_handler)
|
||||||
cached_tok = spot.validate_token(spot.cache_handler.get_cached_token())
|
cached_tok = spot.validate_token(spot.cache_handler.get_cached_token())
|
||||||
cached_tok_legacy = spot.get_cached_token()
|
|
||||||
|
|
||||||
opener.assert_called_with(path)
|
opener.assert_called_with(path)
|
||||||
self.assertIsNotNone(cached_tok)
|
self.assertIsNotNone(cached_tok)
|
||||||
self.assertIsNotNone(cached_tok_legacy)
|
|
||||||
self.assertEqual(refresh_access_token.call_count, 0)
|
self.assertEqual(refresh_access_token.call_count, 0)
|
||||||
|
|
||||||
@patch.multiple(SpotifyOAuth,
|
@patch.multiple(SpotifyOAuth,
|
||||||
@ -100,7 +83,8 @@ class OAuthCacheTest(unittest.TestCase):
|
|||||||
opener.return_value = token_file
|
opener.return_value = token_file
|
||||||
refresh_access_token.return_value = fresh_tok
|
refresh_access_token.return_value = fresh_tok
|
||||||
|
|
||||||
spot = _make_oauth(scope, path)
|
cache_handler = CacheFileHandler(cache_path=path)
|
||||||
|
spot = _make_oauth(scope, cache_handler=cache_handler)
|
||||||
spot.validate_token(spot.cache_handler.get_cached_token())
|
spot.validate_token(spot.cache_handler.get_cached_token())
|
||||||
|
|
||||||
is_token_expired.assert_called_with(expired_tok)
|
is_token_expired.assert_called_with(expired_tok)
|
||||||
@ -120,7 +104,9 @@ class OAuthCacheTest(unittest.TestCase):
|
|||||||
opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
|
opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
|
||||||
is_token_expired.return_value = False
|
is_token_expired.return_value = False
|
||||||
|
|
||||||
spot = _make_oauth(requested_scope, path)
|
cache_handler = CacheFileHandler(cache_path=path)
|
||||||
|
|
||||||
|
spot = _make_oauth(requested_scope, cache_handler=cache_handler)
|
||||||
cached_tok = spot.validate_token(spot.cache_handler.get_cached_token())
|
cached_tok = spot.validate_token(spot.cache_handler.get_cached_token())
|
||||||
|
|
||||||
opener.assert_called_with(path)
|
opener.assert_called_with(path)
|
||||||
@ -136,32 +122,18 @@ class OAuthCacheTest(unittest.TestCase):
|
|||||||
fi = _fake_file()
|
fi = _fake_file()
|
||||||
opener.return_value = fi
|
opener.return_value = fi
|
||||||
|
|
||||||
spot = SpotifyOAuth("CLID", "CLISEC", "REDIR", "STATE", scope, path)
|
cache_handler = CacheFileHandler(cache_path=path)
|
||||||
|
spot = SpotifyOAuth("CLID", "CLISEC", "REDIR", "STATE", scope, cache_handler=cache_handler)
|
||||||
spot.cache_handler.save_token_to_cache(tok)
|
spot.cache_handler.save_token_to_cache(tok)
|
||||||
|
|
||||||
opener.assert_called_with(path, 'w')
|
opener.assert_called_with(path, 'w')
|
||||||
self.assertTrue(fi.write.called)
|
self.assertTrue(fi.write.called)
|
||||||
|
|
||||||
@patch('spotipy.cache_handler.open', create=True)
|
|
||||||
def test_saves_to_cache_path_legacy(self, opener):
|
|
||||||
scope = "playlist-modify-private"
|
|
||||||
path = ".cache-username"
|
|
||||||
tok = _make_fake_token(1, 1, scope)
|
|
||||||
|
|
||||||
fi = _fake_file()
|
|
||||||
opener.return_value = fi
|
|
||||||
|
|
||||||
spot = SpotifyOAuth("CLID", "CLISEC", "REDIR", "STATE", scope, path)
|
|
||||||
spot._save_token_info(tok)
|
|
||||||
|
|
||||||
opener.assert_called_with(path, 'w')
|
|
||||||
self.assertTrue(fi.write.called)
|
|
||||||
|
|
||||||
def test_cache_handler(self):
|
def test_cache_handler(self):
|
||||||
scope = "playlist-modify-private"
|
scope = "playlist-modify-private"
|
||||||
tok = _make_fake_token(1, 1, scope)
|
tok = _make_fake_token(1, 1, scope)
|
||||||
|
|
||||||
spot = _make_oauth(scope, cache_handler=MemoryCache())
|
spot = _make_oauth(scope, cache_handler=MemoryCacheHandler())
|
||||||
spot.cache_handler.save_token_to_cache(tok)
|
spot.cache_handler.save_token_to_cache(tok)
|
||||||
cached_tok = spot.cache_handler.get_cached_token()
|
cached_tok = spot.cache_handler.get_cached_token()
|
||||||
|
|
||||||
@ -261,141 +233,6 @@ class TestSpotifyClientCredentials(unittest.TestCase):
|
|||||||
self.assertEqual(error.exception.error, 'invalid_client')
|
self.assertEqual(error.exception.error, 'invalid_client')
|
||||||
|
|
||||||
|
|
||||||
class ImplicitGrantCacheTest(unittest.TestCase):
|
|
||||||
|
|
||||||
@patch.object(SpotifyImplicitGrant, "is_token_expired", DEFAULT)
|
|
||||||
@patch('spotipy.cache_handler.open', create=True)
|
|
||||||
def test_gets_from_cache_path(self, opener, is_token_expired):
|
|
||||||
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))
|
|
||||||
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)
|
|
||||||
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):
|
|
||||||
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
|
|
||||||
|
|
||||||
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)
|
|
||||||
self.assertIsNone(cached_tok)
|
|
||||||
|
|
||||||
@patch.object(SpotifyImplicitGrant, "is_token_expired", DEFAULT)
|
|
||||||
@patch('spotipy.cache_handler.open', create=True)
|
|
||||||
def test_badly_scoped_token_bails(self, opener, is_token_expired):
|
|
||||||
token_scope = "playlist-modify-public"
|
|
||||||
requested_scope = "playlist-modify-private"
|
|
||||||
path = ".cache-username"
|
|
||||||
tok = _make_fake_token(1, 1, token_scope)
|
|
||||||
|
|
||||||
opener.return_value = _token_file(json.dumps(tok, ensure_ascii=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)
|
|
||||||
self.assertIsNone(cached_tok)
|
|
||||||
|
|
||||||
@patch('spotipy.cache_handler.open', create=True)
|
|
||||||
def test_saves_to_cache_path(self, opener):
|
|
||||||
scope = "playlist-modify-private"
|
|
||||||
path = ".cache-username"
|
|
||||||
tok = _make_fake_token(1, 1, scope)
|
|
||||||
|
|
||||||
fi = _fake_file()
|
|
||||||
opener.return_value = fi
|
|
||||||
|
|
||||||
spot = SpotifyImplicitGrant("CLID", "REDIR", "STATE", scope, path)
|
|
||||||
spot.cache_handler.save_token_to_cache(tok)
|
|
||||||
|
|
||||||
opener.assert_called_with(path, 'w')
|
|
||||||
self.assertTrue(fi.write.called)
|
|
||||||
|
|
||||||
@patch('spotipy.cache_handler.open', create=True)
|
|
||||||
def test_saves_to_cache_path_legacy(self, opener):
|
|
||||||
scope = "playlist-modify-private"
|
|
||||||
path = ".cache-username"
|
|
||||||
tok = _make_fake_token(1, 1, scope)
|
|
||||||
|
|
||||||
fi = _fake_file()
|
|
||||||
opener.return_value = fi
|
|
||||||
|
|
||||||
spot = SpotifyImplicitGrant("CLID", "REDIR", "STATE", scope, path)
|
|
||||||
spot._save_token_info(tok)
|
|
||||||
|
|
||||||
opener.assert_called_with(path, 'w')
|
|
||||||
self.assertTrue(fi.write.called)
|
|
||||||
|
|
||||||
|
|
||||||
class TestSpotifyImplicitGrant(unittest.TestCase):
|
|
||||||
|
|
||||||
def test_get_authorize_url_doesnt_pass_state_by_default(self):
|
|
||||||
auth = SpotifyImplicitGrant("CLID", "REDIR")
|
|
||||||
|
|
||||||
url = auth.get_authorize_url()
|
|
||||||
|
|
||||||
parsed_url = urllibparse.urlparse(url)
|
|
||||||
parsed_qs = urllibparse.parse_qs(parsed_url.query)
|
|
||||||
self.assertNotIn('state', parsed_qs)
|
|
||||||
|
|
||||||
def test_get_authorize_url_passes_state_from_constructor(self):
|
|
||||||
state = "STATE"
|
|
||||||
auth = SpotifyImplicitGrant("CLID", "REDIR", state)
|
|
||||||
|
|
||||||
url = auth.get_authorize_url()
|
|
||||||
|
|
||||||
parsed_url = urllibparse.urlparse(url)
|
|
||||||
parsed_qs = urllibparse.parse_qs(parsed_url.query)
|
|
||||||
self.assertEqual(parsed_qs['state'][0], state)
|
|
||||||
|
|
||||||
def test_get_authorize_url_passes_state_from_func_call(self):
|
|
||||||
state = "STATE"
|
|
||||||
auth = SpotifyImplicitGrant("CLID", "REDIR", "NOT STATE")
|
|
||||||
|
|
||||||
url = auth.get_authorize_url(state=state)
|
|
||||||
|
|
||||||
parsed_url = urllibparse.urlparse(url)
|
|
||||||
parsed_qs = urllibparse.parse_qs(parsed_url.query)
|
|
||||||
self.assertEqual(parsed_qs['state'][0], state)
|
|
||||||
|
|
||||||
def test_get_authorize_url_does_not_show_dialog_by_default(self):
|
|
||||||
auth = SpotifyImplicitGrant("CLID", "REDIR")
|
|
||||||
|
|
||||||
url = auth.get_authorize_url()
|
|
||||||
|
|
||||||
parsed_url = urllibparse.urlparse(url)
|
|
||||||
parsed_qs = urllibparse.parse_qs(parsed_url.query)
|
|
||||||
self.assertNotIn('show_dialog', parsed_qs)
|
|
||||||
|
|
||||||
def test_get_authorize_url_shows_dialog_when_requested(self):
|
|
||||||
auth = SpotifyImplicitGrant("CLID", "REDIR", show_dialog=True)
|
|
||||||
|
|
||||||
url = auth.get_authorize_url()
|
|
||||||
|
|
||||||
parsed_url = urllibparse.urlparse(url)
|
|
||||||
parsed_qs = urllibparse.parse_qs(parsed_url.query)
|
|
||||||
self.assertTrue(parsed_qs['show_dialog'])
|
|
||||||
|
|
||||||
|
|
||||||
class SpotifyPKCECacheTest(unittest.TestCase):
|
class SpotifyPKCECacheTest(unittest.TestCase):
|
||||||
|
|
||||||
@patch.multiple(SpotifyPKCE,
|
@patch.multiple(SpotifyPKCE,
|
||||||
@ -410,13 +247,12 @@ class SpotifyPKCECacheTest(unittest.TestCase):
|
|||||||
opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
|
opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
|
||||||
is_token_expired.return_value = False
|
is_token_expired.return_value = False
|
||||||
|
|
||||||
spot = _make_pkceauth(scope, path)
|
cache_handler = CacheFileHandler(cache_path=path)
|
||||||
|
spot = _make_pkceauth(scope, cache_handler=cache_handler)
|
||||||
cached_tok = spot.cache_handler.get_cached_token()
|
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)
|
||||||
self.assertIsNotNone(cached_tok)
|
self.assertIsNotNone(cached_tok)
|
||||||
self.assertIsNotNone(cached_tok_legacy)
|
|
||||||
self.assertEqual(refresh_access_token.call_count, 0)
|
self.assertEqual(refresh_access_token.call_count, 0)
|
||||||
|
|
||||||
@patch.multiple(SpotifyPKCE,
|
@patch.multiple(SpotifyPKCE,
|
||||||
@ -433,7 +269,8 @@ class SpotifyPKCECacheTest(unittest.TestCase):
|
|||||||
opener.return_value = token_file
|
opener.return_value = token_file
|
||||||
refresh_access_token.return_value = fresh_tok
|
refresh_access_token.return_value = fresh_tok
|
||||||
|
|
||||||
spot = _make_pkceauth(scope, path)
|
cache_handler = CacheFileHandler(cache_path=path)
|
||||||
|
spot = _make_pkceauth(scope, cache_handler=cache_handler)
|
||||||
spot.validate_token(spot.cache_handler.get_cached_token())
|
spot.validate_token(spot.cache_handler.get_cached_token())
|
||||||
|
|
||||||
is_token_expired.assert_called_with(expired_tok)
|
is_token_expired.assert_called_with(expired_tok)
|
||||||
@ -453,7 +290,8 @@ class SpotifyPKCECacheTest(unittest.TestCase):
|
|||||||
opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
|
opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
|
||||||
is_token_expired.return_value = False
|
is_token_expired.return_value = False
|
||||||
|
|
||||||
spot = _make_pkceauth(requested_scope, path)
|
cache_handler = CacheFileHandler(cache_path=path)
|
||||||
|
spot = _make_pkceauth(requested_scope, cache_handler=cache_handler)
|
||||||
cached_tok = spot.validate_token(spot.cache_handler.get_cached_token())
|
cached_tok = spot.validate_token(spot.cache_handler.get_cached_token())
|
||||||
|
|
||||||
opener.assert_called_with(path)
|
opener.assert_called_with(path)
|
||||||
@ -469,27 +307,13 @@ class SpotifyPKCECacheTest(unittest.TestCase):
|
|||||||
fi = _fake_file()
|
fi = _fake_file()
|
||||||
opener.return_value = fi
|
opener.return_value = fi
|
||||||
|
|
||||||
spot = SpotifyPKCE("CLID", "REDIR", "STATE", scope, path)
|
cache_handler = CacheFileHandler(cache_path=path)
|
||||||
|
spot = SpotifyPKCE("CLID", "REDIR", "STATE", scope, cache_handler=cache_handler)
|
||||||
spot.cache_handler.save_token_to_cache(tok)
|
spot.cache_handler.save_token_to_cache(tok)
|
||||||
|
|
||||||
opener.assert_called_with(path, 'w')
|
opener.assert_called_with(path, 'w')
|
||||||
self.assertTrue(fi.write.called)
|
self.assertTrue(fi.write.called)
|
||||||
|
|
||||||
@patch('spotipy.cache_handler.open', create=True)
|
|
||||||
def test_saves_to_cache_path_legacy(self, opener):
|
|
||||||
scope = "playlist-modify-private"
|
|
||||||
path = ".cache-username"
|
|
||||||
tok = _make_fake_token(1, 1, scope)
|
|
||||||
|
|
||||||
fi = _fake_file()
|
|
||||||
opener.return_value = fi
|
|
||||||
|
|
||||||
spot = SpotifyPKCE("CLID", "REDIR", "STATE", scope, path)
|
|
||||||
spot._save_token_info(tok)
|
|
||||||
|
|
||||||
opener.assert_called_with(path, 'w')
|
|
||||||
self.assertTrue(fi.write.called)
|
|
||||||
|
|
||||||
|
|
||||||
class TestSpotifyPKCE(unittest.TestCase):
|
class TestSpotifyPKCE(unittest.TestCase):
|
||||||
|
|
||||||
|
|||||||
@ -88,3 +88,28 @@ class SpotipyScopeTest(TestCase):
|
|||||||
self.assertEqual(normalized_scope_string_2, "")
|
self.assertEqual(normalized_scope_string_2, "")
|
||||||
|
|
||||||
self.assertIsNone(self.normalize_scope(None))
|
self.assertIsNone(self.normalize_scope(None))
|
||||||
|
|
||||||
|
def test_all_scopes(self):
|
||||||
|
expected_scopes = {
|
||||||
|
Scope.user_read_currently_playing,
|
||||||
|
Scope.playlist_read_collaborative,
|
||||||
|
Scope.playlist_modify_private,
|
||||||
|
Scope.user_read_playback_position,
|
||||||
|
Scope.user_library_modify,
|
||||||
|
Scope.user_top_read,
|
||||||
|
Scope.user_read_playback_state,
|
||||||
|
Scope.user_read_email,
|
||||||
|
Scope.ugc_image_upload,
|
||||||
|
Scope.user_read_private,
|
||||||
|
Scope.playlist_modify_public,
|
||||||
|
Scope.user_library_read,
|
||||||
|
Scope.streaming,
|
||||||
|
Scope.user_read_recently_played,
|
||||||
|
Scope.user_follow_read,
|
||||||
|
Scope.user_follow_modify,
|
||||||
|
Scope.app_remote_control,
|
||||||
|
Scope.playlist_read_private,
|
||||||
|
Scope.user_modify_playback_state,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertEqual(expected_scopes, Scope.all())
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user