diff --git a/CHANGELOG.md b/CHANGELOG.md index fece821..309c825 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,46 +12,49 @@ Rebasing master onto v3 doesn't require a changelog update. ### Added -* `Scope` - An enum which contains all of the authorization scopes (see [here](https://github.com/plamere/spotipy/issues/652#issuecomment-797461311)). +- `Scope` - An enum which contains all of the authorization scopes (see [here](https://github.com/plamere/spotipy/issues/652#issuecomment-797461311)). ### Changed -* 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. - * [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 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. +- 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. + - [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 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. +- Replaced string concatenations and `str.format()` with f-strings ### 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 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`. +- 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] ### Added + - Added examples for audiobooks, shows and episodes methods to examples directory ### Fixed + - Fixed scripts in examples directory that didn't run correctly - Updated documentation for `Client.current_user_top_artists` to indicate maximum number of artists limit @@ -60,6 +63,7 @@ Rebasing master onto v3 doesn't require a changelog update. ## [2.25.0] - 2025-03-01 ### Added + - Added unit tests for queue functions - Added detailed function docstrings to 'util.py', including descriptions and special sections that lists arguments, returns, and raises. - Updated order of instructions for Python and pip package manager installation in TUTORIAL.md @@ -80,42 +84,50 @@ Rebasing master onto v3 doesn't require a changelog update. - Added FAQ entry for inaccessible playlists ### Fixed + - Audiobook integration tests - Edited docstrings for certain functions in client.py for functions that are no longer in use and have been replaced. - `current_user_unfollow_playlist()` now supports playlist IDs, URLs, and URIs rather than previously where it only supported playlist IDs. ### Removed + - `mock` no longer listed as a test dependency. Only built-in `unittest.mock` is actually used. ## [2.24.0] - 2024-05-30 ### Added + - Added `MemcacheCacheHandler`, a cache handler that stores the token info using pymemcache. - Added support for audiobook endpoints: `get_audiobook`, `get_audiobooks`, and `get_audiobook_chapters`. - Added integration tests for audiobook endpoints. - Added `update` field to `current_user_follow_playlist`. ### Changed + - Fixed error obfuscation when Spotify class is being inherited and an error is raised in the Child's `__init__` - Replaced `artist_albums(album_type=...)` with `artist_albums(include_groups=...)` due to an API change. - Updated `_regex_spotify_url` to ignore `/intl-` in Spotify links - Improved README, docs and examples ### Fixed + - Readthedocs build - Split `test_current_user_save_and_usave_tracks` unit test ### Removed + - Drop support for EOL Python 3.7 ## [2.23.0] - 2023-04-07 ### Added + - Added optional `encoder_cls` argument to `CacheFileHandler`, which overwrite default encoder for token before writing to disk - Integration tests for searching multiple types in multiple markets (non-user endpoints) - Publish to PyPI action ### Fixed + - Fixed the regex for matching playlist URIs with the format spotify:user:USERNAME:playlist:PLAYLISTID. - `search_markets` now factors the counts of all types in the `total` rather than just the first type ([#534](https://github.com/spotipy-dev/spotipy/issues/534)) @@ -124,7 +136,7 @@ Rebasing master onto v3 doesn't require a changelog update. ### Added - Add alternative module installation instruction to README -- Added Comment to README - Getting Started for user to add URI to app in Spotify Developer Dashboard. +- Added Comment to README - Getting Started for user to add URI to app in Spotify Developer Dashboard. - Added playlist_add_tracks.py to example folder ### Changed @@ -197,10 +209,6 @@ Rebasing master onto v3 doesn't require a changelog update. - Fixed a bug in the initializers for the auth managers that produced a spurious warning message if you provide a cache handler, and you set a value for the "SPOTIPY_CLIENT_USERNAME" environment variable. - Use generated MIT license and fix license type in `pip show` -### Fixed - -* 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 @@ -208,11 +216,11 @@ Rebasing master onto v3 doesn't require a changelog update. - Enabled using both short and long IDs for playlist_change_details - Added a cache handler to `SpotifyClientCredentials` - Added the following endpoints - - `Spotify.current_user_saved_episodes` - - `Spotify.current_user_saved_episodes_add` - - `Spotify.current_user_saved_episodes_delete` - - `Spotify.current_user_saved_episodes_contains` - - `Spotify.available_markets` + - `Spotify.current_user_saved_episodes` + - `Spotify.current_user_saved_episodes_add` + - `Spotify.current_user_saved_episodes_delete` + - `Spotify.current_user_saved_episodes_contains` + - `Spotify.available_markets` ### Changed @@ -297,7 +305,7 @@ Rebasing master onto v3 doesn't require a changelog update. - Support to test whether the current user is following certain users or artists - Proper replacements for all deprecated playlist endpoints - (See https://developer.spotify.com/community/news/2018/06/12/changes-to-playlist-uris/ and below) + (See and below) - Allow for OAuth 2.0 authorization by instructing the user to open the URL in a browser instead of opening the browser. - Reason for 403 error in SpotifyException - Support for the PKCE Auth Flow @@ -387,10 +395,10 @@ Rebasing master onto v3 doesn't require a changelog update. - Client retry logic has changed as it now uses urllib3's `Retry` in conjunction with requests `Session` - The session is customizable as it allows for: - - status_forcelist - - retries - - status_retries - - backoff_factor + - status_forcelist + - retries + - status_retries + - backoff_factor - Spin up a local webserver to autofill authentication URL - Use session in SpotifyAuthBase - Logging used instead of print statements @@ -405,9 +413,9 @@ Rebasing master onto v3 doesn't require a changelog update. ### Added - Support for `add_to_queue` - - **Parameters:** - - track uri, id, or url - - device id. If None, then the active device is used. + - **Parameters:** + - track uri, id, or url + - device id. If None, then the active device is used. - Add CHANGELOG and LICENSE to released package ## [2.9.0] - 2020-02-15 @@ -504,6 +512,7 @@ Rebasing master onto v3 doesn't require a changelog update. ### Changed - Made instructions in the CONTRIBUTING.md file more clear such that it is easier to onboard and there are no conflicts with TUTORIAL.md + ## [2.5.0] - 2020-01-11 Added follow and player endpoints diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 11558a0..53605fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,7 +18,7 @@ $env:SPOTIPY_CLIENT_USERNAME="client_username_here" $env:SPOTIPY_REDIRECT_URI="http://localhost:8080" ``` -### Create virtual environment, install dependencies, run tests: +### Create virtual environment, install dependencies, run tests ```bash $ virtualenv --python=python3 env @@ -50,9 +50,9 @@ Don't forget to add a short description of your change in the [CHANGELOG](CHANGE ### Publishing (by maintainer) - - Bump version in setup.py - - Bump and date changelog - - Add to changelog: +- Bump version in setup.py +- Bump and date changelog +- Add to changelog: ## Unreleased Add your changes below. @@ -63,9 +63,8 @@ Don't forget to add a short description of your change in the [CHANGELOG](CHANGE ### Removed - - Commit changes - - Push tag to trigger PyPI build & release workflow - - Create github release https://github.com/plamere/spotipy/releases with the changelog content +- Commit changes +- Push tag to trigger PyPI build & release workflow +- Create github release with the changelog content for the version and a short name that describes the main addition - - Verify doc uses latest https://readthedocs.org/projects/spotipy/ - \ No newline at end of file +- Verify doc uses latest diff --git a/README.md b/README.md index 66fa3ae..05b5489 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Spotipy -##### Spotipy is a lightweight Python library for the [Spotify Web API](https://developer.spotify.com/documentation/web-api). With Spotipy you get full access to all of the music data provided by the Spotify platform. +##### Spotipy is a lightweight Python library for the [Spotify Web API](https://developer.spotify.com/documentation/web-api). With Spotipy you get full access to all of the music data provided by the Spotify platform ![Integration tests](https://github.com/spotipy-dev/spotipy/actions/workflows/integration_tests.yml/badge.svg?branch=master) [![Documentation Status](https://readthedocs.org/projects/spotipy/badge/?version=master)](https://spotipy.readthedocs.io/en/latest/?badge=master) [![Discord server](https://img.shields.io/discord/1244611850700849183?style=flat&logo=discord&logoColor=7289DA&color=7289DA)](https://discord.gg/HP6xcPsTPJ) @@ -22,7 +22,7 @@ Spotipy supports all of the features of the Spotify Web API including access to pip install spotipy ``` -alternatively, for Windows users +alternatively, for Windows users ```bash py -m pip install spotipy @@ -38,7 +38,7 @@ pip install spotipy --upgrade A full set of examples can be found in the [online documentation](http://spotipy.readthedocs.org/) and in the [Spotipy examples directory](https://github.com/plamere/spotipy/tree/master/examples). -To get started, [install spotipy](#installation), create a new account or log in on https://developers.spotify.com/. Go to the [dashboard](https://developer.spotify.com/dashboard), create an app and add your new ID and SECRET (ID and SECRET can be found on an app setting) to your environment ([step-by-step video](https://www.youtube.com/watch?v=kaBVN8uP358)): +To get started, [install spotipy](#installation), create a new account or log in on [developers.spotify.com](https://developers.spotify.com/). Go to the [dashboard](https://developer.spotify.com/dashboard), create an app and add your new ID and SECRET (ID and SECRET can be found on an app setting) to your environment ([step-by-step video](https://www.youtube.com/watch?v=kaBVN8uP358)): ### Example without user authentication @@ -53,7 +53,9 @@ results = sp.search(q='weezer', limit=20) for idx, track in enumerate(results['tracks']['items']): print(idx, track['name']) ``` + Expected result: + ``` 0 Island In The Sun 1 Say It Ain't So @@ -65,7 +67,6 @@ Expected result: 19 Feels Like Summer ``` - ### Example with user authentication A redirect URI must be added to your application at [My Dashboard](https://developer.spotify.com/dashboard/applications) to access user authenticated features. @@ -84,13 +85,14 @@ for idx, item in enumerate(results['items']): track = item['track'] print(idx, track['artists'][0]['name'], " – ", track['name']) ``` + Expected result will be the list of music that you liked. For example if you liked Red and Sunflower, the result will be: + ``` 0 Post Malone – Sunflower - Spider-Man: Into the Spider-Verse 1 Taylor Swift – Red ``` - ## Reporting Issues For common questions please check our [FAQ](FAQ.md). diff --git a/TUTORIAL.md b/TUTORIAL.md index b3dd15d..f3c3e44 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -1,18 +1,20 @@ # Spotipy Tutorial for Beginners + Hello and welcome to the Spotipy Tutorial for Beginners. If you have limited experience coding in Python and have never used Spotipy or the Spotify API before, you've come to the right place. This tutorial will walk you through all the steps necessary to set up Spotipy and use it to accomplish a simple task. ## Prerequisites + In order to complete this tutorial successfully, there are a few things that you should already have installed: **1. python3** Spotipy is written in Python, so you'll need to have the latest version of Python installed in order to use Spotipy. Check if you already have Python installed with the Terminal command: python --version -If you see a version number, Python is already installed. If not, you can download it here: https://www.python.org/downloads/ +If you see a version number, Python is already installed. If not, you can download it here: -**2. pip package manager** +**2. pip package manager** You can check to see if you have pip installed by opening up Terminal and typing the following command: pip --version -If you see a version number, pip is installed, and you're ready to proceed. If not, instructions for downloading the latest version of pip can be found here: https://pip.pypa.io/en/stable/cli/pip_download/ +If you see a version number, pip is installed, and you're ready to proceed. If not, instructions for downloading the latest version of pip can be found here: A. After ensuring that pip is installed, run the following command in Terminal to install Spotipy: pip install spotipy --upgrade @@ -23,11 +25,12 @@ This tutorial will be easiest if you have some knowledge of how to use Linux com Once those three setup items are taken care of, you're ready to start learning how to use Spotipy! ## Step 1. Creating a Spotify Account + Spotipy relies on the Spotify API. In order to use the Spotify API, you'll need to create a Spotify developer account. A. Visit the [Spotify developer portal](https://developer.spotify.com/dashboard/). If you already have a Spotify account, click "Log in" and enter your username and password. Otherwise, click "Sign up" and follow the steps to create an account. After you've signed in or signed up, begin by clicking on your profile name at the top right of your screen and then click “Dashboard” to go to Spotify’s Developer Dashboard. -B. Check the box "Accept the Spotify Developer Terms of Service" and then click "Accept the terms". On the next page, verify your email address if you haven't already. Click the "Create an App" button. Enter any name and description you'd like for your new app. Next, add "http://localhost:1234" (or any other port number of your choosing) to the "Redirect URI" secction. Check the box "I understand and agree with Spotify's Developer Terms of Service and Design Guidelines" and then click the "Save" button. +B. Check the box "Accept the Spotify Developer Terms of Service" and then click "Accept the terms". On the next page, verify your email address if you haven't already. Click the "Create an App" button. Enter any name and description you'd like for your new app. Next, add "" (or any other port number of your choosing) to the "Redirect URI" secction. Check the box "I understand and agree with Spotify's Developer Terms of Service and Design Guidelines" and then click the "Save" button. C. Click on "Settings". Underneath "Client ID", you'll see a "View Client Secret" link. Click the link to reveal your Client secret and copy both your Client secret and your Client ID somewhere so that you can access them later. @@ -40,6 +43,7 @@ B. In your new folder, create a Python file named main.py. You can create the fi C. In that folder, create a Python file named main.py. You can create the file directly from Terminal using a built in text editor like Vim, which comes preinstalled on Linux operating systems. To create the file with Vim, ensure that you are in your new directory, then run: vim main.py D. Paste the following code into your main.py file: + ``` import spotipy from spotipy.oauth2 import SpotifyOAuth @@ -49,6 +53,7 @@ sp = spotipy.Spotify(auth_manager=SpotifyOAuth(client_id="YOUR_APP_CLIENT_ID", redirect_uri="YOUR_APP_REDIRECT_URI", scope="user-library-read")) ``` + D. Replace YOUR_APP_CLIENT_ID and YOUR_APP_CLIENT_SECRET with the values you copied and saved in step 1D. Replace YOUR_APP_REDIRECT_URI with the URI you set in step 1B. ## Step 3. Start Using Spotipy @@ -60,10 +65,13 @@ For now, let's assume that we want to print the names of all the albums on Spoti A. First, we need to find Taylor Swift's Spotify URI (Uniform Resource Indicator). Every entity (artist, album, song, etc.) has a URI that can identify it. To find Taylor's URI, navigate to [her page on Spotify](https://open.spotify.com/artist/06HL4z0CvFAxyc27GXpf02) and look at the URI in your browser. Everything there that follows the last backslash in the URL path is Taylor's URI, in this case: 06HL4z0CvFAxyc27GXpf02 B. Add the URI as a variable in main.py. Notice the prefix added the URI: + ``` taylor_uri = 'spotify:artist:06HL4z0CvFAxyc27GXpf02' ``` + C. Add the following code that will get all of Taylor's album names from Spotify and iterate through them to print them all to standard output. + ``` results = sp.artist_albums(taylor_uri, album_type='album') albums = results['items'] @@ -82,19 +90,22 @@ E. You may see a window open in your browser asking you to authorize the applica F. Return to your terminal - you should see all of Taylor's albums printed out there. ## Troubleshooting Tips + A. Command not found running the application "zsh: command not found: python" Check which Python version that you have by running the command: -```python --version ``` or ```python3 --version```. +```python --version``` or ```python3 --version```. In most cases, the recent Python version is Python 3. You may need to update Python. Once you have updated Python to the most recent version, run the command: -``` python3 main.py``` +```python3 main.py``` B. Encountering package error: If you are seeing an error "ModuleNotFoundError: No module named 'spotipy'", this means you have not installed the package. Run the command: + ``` pip install spotipy ``` + After the package is installed, run the app again. diff --git a/docs/conf.py b/docs/conf.py index 69d9943..4892717 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -246,4 +246,4 @@ texinfo_documents = [ # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -# texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index c7ef2bc..8bab9c0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -377,4 +377,4 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` -* :ref:`search` +* :ref:`search` \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index ed65226..7060d83 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,3 @@ Sphinx~=7.4.7 sphinx-rtd-theme~=2.0.0 -redis>=3.5.3 +redis>=3.5.3 \ No newline at end of file diff --git a/examples/app.py b/examples/app.py index 821f3fb..c4fd379 100644 --- a/examples/app.py +++ b/examples/app.py @@ -88,9 +88,7 @@ def currently_playing(): return redirect('/') spotify = spotipy.Spotify(auth_manager=auth_manager) track = spotify.current_user_playing_track() - if not track is None: - return track - return "No track currently playing." + return track if track is not None else "No track currently playing." @app.route('/current_user') diff --git a/examples/artist_albums.py b/examples/artist_albums.py index 81d56df..7d0691e 100644 --- a/examples/artist_albums.py +++ b/examples/artist_albums.py @@ -18,12 +18,9 @@ def get_args(): def get_artist(name): - results = sp.search(q='artist:' + name, type='artist') + results = sp.search(q=f'artist:{name}', type='artist') items = results['artists']['items'] - if len(items) > 0: - return items[0] - else: - return None + return items[0] if len(items) > 0 else None def show_artist_albums(artist): diff --git a/examples/artist_discography.py b/examples/artist_discography.py index 3104af5..bbcd860 100644 --- a/examples/artist_discography.py +++ b/examples/artist_discography.py @@ -18,12 +18,9 @@ def get_args(): def get_artist(name): - results = sp.search(q='artist:' + name, type='artist') + results = sp.search(q=f'artist:{name}', type='artist') items = results['artists']['items'] - if len(items) > 0: - return items[0] - else: - return None + return items[0] if len(items) > 0 else None def show_album_tracks(album): diff --git a/examples/artist_recommendations.py b/examples/artist_recommendations.py index 4b4c6e0..3933d1c 100644 --- a/examples/artist_recommendations.py +++ b/examples/artist_recommendations.py @@ -20,12 +20,9 @@ def get_args(): def get_artist(name): - results = sp.search(q='artist:' + name, type='artist') + results = sp.search(q=f'artist:{name}', type='artist') items = results['artists']['items'] - if len(items) > 0: - return items[0] - else: - return None + return items[0] if len(items) > 0 else None def show_recommendations_for_artist(artist): diff --git a/examples/playlist_add_items.py b/examples/playlist_add_items.py new file mode 100644 index 0000000..8d10c5b --- /dev/null +++ b/examples/playlist_add_items.py @@ -0,0 +1,12 @@ +# Add a list of items (URI) to a playlist (URI) + +import spotipy +from spotipy.oauth2 import SpotifyOAuth + +sp = spotipy.Spotify(auth_manager=SpotifyOAuth(client_id="YOUR_APP_CLIENT_ID", + client_secret="YOUR_APP_CLIENT_SECRET", + redirect_uri="YOUR_APP_REDIRECT_URI", + scope="playlist-modify-private" + )) + +sp.playlist_add_items('playlist_id', ['list_of_items']) diff --git a/examples/playlist_all_non_local_tracks.py b/examples/playlist_all_non_local_tracks.py index 1349c91..48797fb 100644 --- a/examples/playlist_all_non_local_tracks.py +++ b/examples/playlist_all_non_local_tracks.py @@ -25,6 +25,5 @@ for item in tracks: tracks.remove(item) i += 1 - # print result -print("Playlist length: " + str(len(tracks)) + "\nExcluding: " + str(i)) +print(f"Playlist length: {len(tracks)}\nExcluding: {i}") diff --git a/examples/remove_specific_tracks_from_playlist.py b/examples/remove_specific_tracks_from_playlist.py index 340f379..e51f736 100644 --- a/examples/remove_specific_tracks_from_playlist.py +++ b/examples/remove_specific_tracks_from_playlist.py @@ -14,8 +14,7 @@ if len(sys.argv) > 2: tid, pos = t_pos.split(',') track_ids.append({"uri": tid, "positions": [int(pos)]}) else: - print( - f"Usage: {sys.argv[0]} playlist_id track_id,pos track_id,pos ...") + print(f"Usage: {sys.argv[0]} playlist_id track_id,pos track_id,pos ...") sys.exit() scope = 'playlist-modify-public' diff --git a/examples/search.py b/examples/search.py index f1e3d9b..a504635 100644 --- a/examples/search.py +++ b/examples/search.py @@ -5,11 +5,7 @@ import spotipy import sys import pprint -if len(sys.argv) > 1: - search_str = sys.argv[1] -else: - search_str = 'Radiohead' - +search_str = sys.argv[1] if len(sys.argv) > 1 else 'Radiohead' sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials()) result = sp.search(search_str) pprint.pprint(result) diff --git a/examples/show_featured_playlists.py b/examples/show_featured_playlists.py index d2d5965..fc0e2c4 100644 --- a/examples/show_featured_playlists.py +++ b/examples/show_featured_playlists.py @@ -13,7 +13,4 @@ while response: for i, item in enumerate(playlists['items']): print(playlists['offset'] + i, item['name']) - if playlists['next']: - response = sp.next(playlists) - else: - response = None + response = sp.next(playlists) if playlists['next'] else None diff --git a/examples/show_new_releases.py b/examples/show_new_releases.py index e4ab63e..b11bb4e 100644 --- a/examples/show_new_releases.py +++ b/examples/show_new_releases.py @@ -12,7 +12,4 @@ while response: for i, item in enumerate(albums['items']): print(albums['offset'] + i, item['name']) - if albums['next']: - response = sp.next(albums) - else: - response = None + response = sp.next(albums) if albums['next'] else None diff --git a/examples/show_related.py b/examples/show_related.py index 423af3b..c1ebb8b 100644 --- a/examples/show_related.py +++ b/examples/show_related.py @@ -4,14 +4,10 @@ from spotipy.oauth2 import SpotifyClientCredentials import spotipy import sys -if len(sys.argv) > 1: - artist_name = sys.argv[1] -else: - artist_name = 'weezer' - +artist_name = sys.argv[1] if len(sys.argv) > 1 else 'weezer' auth_manager = SpotifyClientCredentials() sp = spotipy.Spotify(auth_manager=auth_manager) -result = sp.search(q='artist:' + artist_name, type='artist') +result = sp.search(q=f'artist:{artist_name}', type='artist') try: name = result['artists']['items'][0]['name'] uri = result['artists']['items'][0]['uri'] diff --git a/examples/show_user.py b/examples/show_user.py index 288f271..30c3041 100644 --- a/examples/show_user.py +++ b/examples/show_user.py @@ -5,11 +5,7 @@ import spotipy import sys import pprint -if len(sys.argv) > 1: - username = sys.argv[1] -else: - username = 'plamere' - +username = sys.argv[1] if len(sys.argv) > 1 else 'plamere' auth_manager = SpotifyClientCredentials() sp = spotipy.Spotify(auth_manager=auth_manager) sp.trace = True diff --git a/examples/simple_search_artist_image_url.py b/examples/simple_search_artist_image_url.py index c3a9cb3..945c384 100644 --- a/examples/simple_search_artist_image_url.py +++ b/examples/simple_search_artist_image_url.py @@ -6,12 +6,8 @@ import spotipy sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials()) -if len(sys.argv) > 1: - name = ' '.join(sys.argv[1:]) -else: - name = 'Radiohead' - -results = sp.search(q='artist:' + name, type='artist') +name = ' '.join(sys.argv[1:]) if len(sys.argv) > 1 else 'Radiohead' +results = sp.search(q=f'artist:{name}', type='artist') items = results['artists']['items'] if len(items) > 0: artist = items[0] diff --git a/examples/track_recommendations.py b/examples/track_recommendations.py index 38cf4be..c159eb7 100644 --- a/examples/track_recommendations.py +++ b/examples/track_recommendations.py @@ -39,7 +39,5 @@ recommendations = sp.recommendations( # Display the recommendations for i, track in enumerate(recommendations['tracks']): print( - "{}. {} by {}" - .format(i+1, track['name'], ', ' - .join([artist['name'] for artist in track['artists']])) + f"{i+1}. {track['name']} by {', '.join([artist['name'] for artist in track['artists']])}" ) diff --git a/examples/user_public_playlists.py b/examples/user_public_playlists.py index 5685bdd..a0a95bb 100644 --- a/examples/user_public_playlists.py +++ b/examples/user_public_playlists.py @@ -9,11 +9,7 @@ from spotipy.oauth2 import SpotifyClientCredentials auth_manager = SpotifyClientCredentials() sp = spotipy.Spotify(auth_manager=auth_manager) -user = 'spotify' - -if len(sys.argv) > 1: - user = sys.argv[1] - +user = sys.argv[1] if len(sys.argv) > 1 else 'spotify' playlists = sp.user_playlists(user) while playlists: @@ -25,7 +21,4 @@ while playlists: playlists['offset'], playlist['uri'], playlist['name'])) - if playlists['next']: - playlists = sp.next(playlists) - else: - playlists = None + playlists = sp.next(playlists) if playlists['next'] else None diff --git a/spotipy/cache_handler.py b/spotipy/cache_handler.py index 99f0720..051f451 100644 --- a/spotipy/cache_handler.py +++ b/spotipy/cache_handler.py @@ -68,7 +68,7 @@ class CacheFileHandler(CacheHandler): cache_path = ".cache" username = (username or os.getenv(CLIENT_CREDS_ENV_VARS["client_username"])) if username: - cache_path += "-" + str(username) + cache_path += f"-{username}" self.cache_path = cache_path def get_cached_token(self): @@ -82,9 +82,9 @@ class CacheFileHandler(CacheHandler): except OSError as error: if error.errno == errno.ENOENT: - logger.debug("cache does not exist at: %s", self.cache_path) + logger.debug(f"cache does not exist at: {self.cache_path}") else: - logger.warning("Couldn't read cache at: %s", self.cache_path) + logger.warning(f"Couldn't read cache at: {self.cache_path}") return token_info @@ -94,8 +94,7 @@ class CacheFileHandler(CacheHandler): f.write(json.dumps(token_info, cls=self.encoder_cls)) f.close() except OSError: - logger.warning('Couldn\'t write token to cache at: %s', - self.cache_path) + logger.warning(f'Couldn\'t write token to cache at: {self.cache_path}') class MemoryCacheHandler(CacheHandler): @@ -148,7 +147,7 @@ class DjangoSessionCacheHandler(CacheHandler): try: self.request.session['token_info'] = token_info except Exception as e: - logger.warning("Error saving token to cache: " + str(e)) + logger.warning(f"Error saving token to cache: {str(e)}") class FlaskSessionCacheHandler(CacheHandler): @@ -173,7 +172,7 @@ class FlaskSessionCacheHandler(CacheHandler): try: self.session["token_info"] = token_info except Exception as e: - logger.warning("Error saving token to cache: " + str(e)) + logger.warning(f"Error saving token to cache: {str(e)}") class RedisCacheHandler(CacheHandler): @@ -199,7 +198,7 @@ class RedisCacheHandler(CacheHandler): if token_info: return json.loads(token_info) except RedisError as e: - logger.warning('Error getting token from cache: ' + str(e)) + logger.warning(f'Error getting token from cache: {str(e)}') return token_info @@ -207,12 +206,13 @@ class RedisCacheHandler(CacheHandler): try: self.redis.set(self.key, json.dumps(token_info)) except RedisError as e: - logger.warning('Error saving token to cache: ' + str(e)) + logger.warning(f'Error saving token to cache: {str(e)}') class MemcacheCacheHandler(CacheHandler): """A Cache handler that stores the token info in Memcache using the pymemcache client """ + def __init__(self, memcache, key=None) -> None: """ Parameters: @@ -222,7 +222,7 @@ class MemcacheCacheHandler(CacheHandler): (takes precedence over `token_info`) """ self.memcache = memcache - self.key = key if key else 'token_info' + self.key = key or 'token_info' def get_cached_token(self): from pymemcache import MemcacheError @@ -231,11 +231,11 @@ class MemcacheCacheHandler(CacheHandler): if token_info: return json.loads(token_info.decode()) except MemcacheError as e: - logger.warning('Error getting token from cache' + str(e)) + logger.warning(f'Error getting token from cache: {str(e)}') def save_token_to_cache(self, token_info): from pymemcache import MemcacheError try: self.memcache.set(self.key, json.dumps(token_info)) except MemcacheError as e: - logger.warning('Error saving token to cache' + str(e)) + logger.warning(f'Error saving token to cache: {str(e)}') diff --git a/spotipy/client.py b/spotipy/client.py index c63822a..ad3a556 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -189,11 +189,10 @@ class Spotify: if isinstance(requests_session, requests.Session): self._session = requests_session - else: - if requests_session: # Build a new session. - self._build_session() - else: # Use the Requests API module as a "session". - self._session = requests.api + elif requests_session: # Build a new session. + self._build_session() + else: # Use the Requests API module as a "session". + self._session = requests.api def __del__(self): """Make sure the connection (pool) gets closed""" @@ -220,7 +219,7 @@ class Spotify: def _auth_headers(self): if self.access_token: - return {"Authorization": "Bearer {0}".format(self.access_token)} + return {"Authorization": f"Bearer {self.access_token}"} if not self.auth_manager: return {} try: @@ -332,10 +331,7 @@ class Spotify: Parameters: - result - a previously returned paged result """ - if result["next"]: - return self._get(result["next"]) - else: - return None + return self._get(result["next"]) if result["next"] else None def previous(self, result): """ returns the previous result given a paged result @@ -343,10 +339,7 @@ class Spotify: Parameters: - result - a previously returned paged result """ - if result["previous"]: - return self._get(result["previous"]) - else: - return None + return self._get(result["previous"]) if result["previous"] else None def track(self, track_id, market=None): """ returns a single track given the track's ID, URI or URL @@ -357,7 +350,7 @@ class Spotify: """ trid = self._get_id("track", track_id) - return self._get("tracks/" + trid, market=market) + return self._get(f"tracks/{trid}", market=market) def tracks(self, tracks, market=None): """ returns a list of tracks given a list of track IDs, URIs, or URLs @@ -368,7 +361,7 @@ class Spotify: """ tlist = [self._get_id("track", t) for t in tracks] - return self._get("tracks/?ids=" + ",".join(tlist), market=market) + return self._get(f"tracks/?ids={','.join(tlist)}", market=market) def artist(self, artist_id): """ returns a single artist given the artist's ID, URI or URL @@ -378,7 +371,7 @@ class Spotify: """ trid = self._get_id("artist", artist_id) - return self._get("artists/" + trid) + return self._get(f"artists/{trid}") def artists(self, artists): """ returns a list of artists given the artist IDs, URIs, or URLs @@ -388,7 +381,7 @@ class Spotify: """ tlist = [self._get_id("artist", a) for a in artists] - return self._get("artists/?ids=" + ",".join(tlist)) + return self._get(f"artists/?ids={','.join(tlist)}") def artist_albums( self, artist_id, album_type=None, include_groups=None, country=None, limit=20, offset=0 @@ -416,7 +409,8 @@ class Spotify: trid = self._get_id("artist", artist_id) return self._get( - "artists/" + trid + "/albums", + f"artists/{trid}/albums", + album_type=album_type, include_groups=include_groups, country=country, limit=limit, @@ -433,7 +427,7 @@ class Spotify: """ trid = self._get_id("artist", artist_id) - return self._get("artists/" + trid + "/top-tracks", country=country) + return self._get(f"artists/{trid}/top-tracks", country=country) def artist_related_artists(self, artist_id): """ Get Spotify catalog information about artists similar to an @@ -449,7 +443,7 @@ class Spotify: DeprecationWarning ) trid = self._get_id("artist", artist_id) - return self._get("artists/" + trid + "/related-artists") + return self._get(f"artists/{trid}/related-artists") def album(self, album_id, market=None): """ returns a single album given the album's ID, URIs or URL @@ -461,9 +455,9 @@ class Spotify: trid = self._get_id("album", album_id) if market is not None: - return self._get("albums/" + trid + '?market=' + market) + return self._get(f"albums/{trid}?market={market}") else: - return self._get("albums/" + trid) + return self._get(f"albums/{trid}") def album_tracks(self, album_id, limit=50, offset=0, market=None): """ Get Spotify catalog information about an album's tracks @@ -478,7 +472,7 @@ class Spotify: trid = self._get_id("album", album_id) return self._get( - "albums/" + trid + "/tracks/", limit=limit, offset=offset, market=market + f"albums/{trid}/tracks/", limit=limit, offset=offset, market=market ) def albums(self, albums, market=None): @@ -491,9 +485,9 @@ class Spotify: tlist = [self._get_id("album", a) for a in albums] if market is not None: - return self._get("albums/?ids=" + ",".join(tlist) + '&market=' + market) + return self._get(f"albums/?ids={','.join(tlist)}&market={market}") else: - return self._get("albums/?ids=" + ",".join(tlist)) + return self._get(f"albums/?ids={','.join(tlist)}") def show(self, show_id, market=None): """ returns a single show given the show's ID, URIs or URL @@ -508,7 +502,7 @@ class Spotify: """ trid = self._get_id("show", show_id) - return self._get("shows/" + trid, market=market) + return self._get(f"shows/{trid}", market=market) def shows(self, shows, market=None): """ returns a list of shows given the show IDs, URIs, or URLs @@ -523,7 +517,7 @@ class Spotify: """ tlist = [self._get_id("show", s) for s in shows] - return self._get("shows/?ids=" + ",".join(tlist), market=market) + return self._get(f"shows/?ids={','.join(tlist)}", market=market) def show_episodes(self, show_id, limit=50, offset=0, market=None): """ Get Spotify catalog information about a show's episodes @@ -541,7 +535,7 @@ class Spotify: trid = self._get_id("show", show_id) return self._get( - "shows/" + trid + "/episodes/", limit=limit, offset=offset, market=market + f"shows/{trid}/episodes/", limit=limit, offset=offset, market=market ) def episode(self, episode_id, market=None): @@ -557,7 +551,7 @@ class Spotify: """ trid = self._get_id("episode", episode_id) - return self._get("episodes/" + trid, market=market) + return self._get(f"episodes/{trid}", market=market) def episodes(self, episodes, market=None): """ returns a list of episodes given the episode IDs, URIs, or URLs @@ -572,7 +566,7 @@ class Spotify: """ tlist = [self._get_id("episode", e) for e in episodes] - return self._get("episodes/?ids=" + ",".join(tlist), market=market) + return self._get(f"episodes/?ids={','.join(tlist)}", market=market) def search(self, q, limit=10, offset=0, type="track", market=None): """ searches for an item @@ -616,7 +610,7 @@ class Spotify: if not markets: markets = self.country_codes - if not (isinstance(markets, list) or isinstance(markets, tuple)): + if not (isinstance(markets, (list, tuple))): markets = [] warnings.warn( @@ -631,7 +625,7 @@ class Spotify: Parameters: - user - the id of the usr """ - return self._get("users/" + user) + return self._get(f"users/{user}") def current_user_playlists(self, limit=50, offset=0): """ Get current user playlists without required getting his profile @@ -687,7 +681,7 @@ class Spotify: offset=offset, fields=fields, market=market, - additional_types=",".join(additional_types) + additional_types=",".join(additional_types), ) def playlist_cover_image(self, playlist_id): @@ -722,9 +716,7 @@ class Spotify: - limit - the number of items to return - offset - the index of the first item to return """ - return self._get( - f"users/{user}/playlists", limit=limit, offset=offset - ) + return self._get(f"users/{user}/playlists", limit=limit, offset=offset) def user_playlist_create(self, user, name, public=True, collaborative=False, description=""): """ Creates a playlist for a user @@ -801,9 +793,7 @@ class Spotify: plid = self._get_id("playlist", playlist_id) ftracks = [self._get_uri("track", tid) for tid in items] return self._post( - f"playlists/{plid}/tracks", - payload=ftracks, - position=position, + f"playlists/{plid}/tracks", payload=ftracks, position=position ) def playlist_replace_items(self, playlist_id, items): @@ -816,9 +806,7 @@ class Spotify: plid = self._get_id("playlist", playlist_id) ftracks = [self._get_uri("track", tid) for tid in items] payload = {"uris": ftracks} - return self._put( - f"playlists/{plid}/tracks", payload=payload - ) + return self._put(f"playlists/{plid}/tracks", payload=payload) def playlist_reorder_items( self, @@ -847,9 +835,7 @@ class Spotify: } if snapshot_id: payload["snapshot_id"] = snapshot_id - return self._put( - f"playlists/{plid}/tracks", payload=payload - ) + return self._put(f"playlists/{plid}/tracks", payload=payload) def playlist_remove_all_occurrences_of_items( self, playlist_id, items, snapshot_id=None @@ -868,9 +854,7 @@ class Spotify: payload = {"tracks": [{"uri": track} for track in ftracks]} if snapshot_id: payload["snapshot_id"] = snapshot_id - return self._delete( - f"playlists/{plid}/tracks", payload=payload - ) + return self._delete(f"playlists/{plid}/tracks", payload=payload) def playlist_remove_specific_occurrences_of_items( self, playlist_id, items, snapshot_id=None @@ -888,20 +872,17 @@ class Spotify: """ plid = self._get_id("playlist", playlist_id) - ftracks = [] - for tr in items: - ftracks.append( - { - "uri": self._get_uri("track", tr["uri"]), - "positions": tr["positions"], - } - ) + ftracks = [ + { + "uri": self._get_uri("track", tr["uri"]), + "positions": tr["positions"], + } + for tr in items + ] payload = {"tracks": ftracks} if snapshot_id: payload["snapshot_id"] = snapshot_id - return self._delete( - f"playlists/{plid}/tracks", payload=payload - ) + return self._delete(f"playlists/{plid}/tracks", payload=payload) def current_user_follow_playlist(self, playlist_id, public=True): """ @@ -928,9 +909,8 @@ class Spotify: if they follow the playlist. Maximum: 5 ids. """ - endpoint = "playlists/{}/followers/contains?ids={}" return self._get( - endpoint.format(playlist_id, ",".join(user_ids)) + f"playlists/{playlist_id}/followers/contains?ids={','.join(user_ids)}" ) def me(self): @@ -962,35 +942,41 @@ class Spotify: """ return self._get("me/albums", limit=limit, offset=offset, market=market) - def current_user_saved_albums_add(self, albums=[]): + def current_user_saved_albums_add(self, albums=None): """ Add one or more albums to the current user's "Your Music" library. Parameters: - albums - a list of album URIs, URLs or IDs """ + if albums is None: + albums = [] alist = [self._get_id("album", a) for a in albums] - return self._put("me/albums?ids=" + ",".join(alist)) + return self._put(f"me/albums?ids={','.join(alist)}") - def current_user_saved_albums_delete(self, albums=[]): + def current_user_saved_albums_delete(self, albums=None): """ Remove one or more albums from the current user's "Your Music" library. Parameters: - albums - a list of album URIs, URLs or IDs """ + if albums is None: + albums = [] alist = [self._get_id("album", a) for a in albums] - return self._delete("me/albums/?ids=" + ",".join(alist)) + return self._delete(f"me/albums/?ids={','.join(alist)}") - def current_user_saved_albums_contains(self, albums=[]): + def current_user_saved_albums_contains(self, albums=None): """ Check if one or more albums is already saved in the current Spotify user’s “Your Music” library. Parameters: - albums - a list of album URIs, URLs or IDs """ + if albums is None: + albums = [] alist = [self._get_id("album", a) for a in albums] - return self._get("me/albums/contains?ids=" + ",".join(alist)) + return self._get(f"me/albums/contains?ids={','.join(alist)}") def current_user_saved_tracks(self, limit=20, offset=0, market=None): """ Gets a list of the tracks saved in the current authorized user's @@ -1010,10 +996,8 @@ class Spotify: Parameters: - tracks - a list of track URIs, URLs or IDs """ - tlist = [] - if tracks is not None: - tlist = [self._get_id("track", t) for t in tracks] - return self._put("me/tracks/?ids=" + ",".join(tlist)) + tlist = [] if tracks is None else [self._get_id("track", t) for t in tracks] + return self._put(f"me/tracks/?ids={','.join(tlist)}") def current_user_saved_tracks_delete(self, tracks=None): """ Remove one or more tracks from the current user's @@ -1022,10 +1006,8 @@ class Spotify: Parameters: - tracks - a list of track URIs, URLs or IDs """ - tlist = [] - if tracks is not None: - tlist = [self._get_id("track", t) for t in tracks] - return self._delete("me/tracks/?ids=" + ",".join(tlist)) + tlist = [] if tracks is None else [self._get_id("track", t) for t in tracks] + return self._delete(f"me/tracks/?ids={','.join(tlist)}") def current_user_saved_tracks_contains(self, tracks=None): """ Check if one or more tracks is already saved in @@ -1034,10 +1016,8 @@ class Spotify: Parameters: - tracks - a list of track URIs, URLs or IDs """ - tlist = [] - if tracks is not None: - tlist = [self._get_id("track", t) for t in tracks] - return self._get("me/tracks/contains?ids=" + ",".join(tlist)) + tlist = [] if tracks is None else [self._get_id("track", t) for t in tracks] + return self._get(f"me/tracks/contains?ids={','.join(tlist)}") def current_user_saved_episodes(self, limit=20, offset=0, market=None): """ Gets a list of the episodes saved in the current authorized user's @@ -1061,7 +1041,7 @@ class Spotify: elist = [] if episodes is not None: elist = [self._get_id("episode", e) for e in episodes] - return self._put("me/episodes/?ids=" + ",".join(elist)) + return self._put(f"me/episodes/?ids={','.join(elist)}") def current_user_saved_episodes_delete(self, episodes=None): """ Remove one or more episodes from the current user's @@ -1073,7 +1053,7 @@ class Spotify: elist = [] if episodes is not None: elist = [self._get_id("episode", e) for e in episodes] - return self._delete("me/episodes/?ids=" + ",".join(elist)) + return self._delete(f"me/episodes/?ids={','.join(elist)}") def current_user_saved_episodes_contains(self, episodes=None): """ Check if one or more episodes is already saved in @@ -1085,7 +1065,7 @@ class Spotify: elist = [] if episodes is not None: elist = [self._get_id("episode", e) for e in episodes] - return self._get("me/episodes/contains?ids=" + ",".join(elist)) + return self._get(f"me/episodes/contains?ids={','.join(elist)}") def current_user_saved_shows(self, limit=20, offset=0, market=None): """ Gets a list of the shows saved in the current authorized user's @@ -1099,26 +1079,30 @@ class Spotify: """ return self._get("me/shows", limit=limit, offset=offset, market=market) - def current_user_saved_shows_add(self, shows=[]): + def current_user_saved_shows_add(self, shows=None): """ Add one or more albums to the current user's "Your Music" library. Parameters: - shows - a list of show URIs, URLs or IDs """ + if shows is None: + shows = [] slist = [self._get_id("show", s) for s in shows] - return self._put("me/shows?ids=" + ",".join(slist)) + return self._put(f"me/shows?ids={','.join(slist)}") - def current_user_saved_shows_delete(self, shows=[]): + def current_user_saved_shows_delete(self, shows=None): """ Remove one or more shows from the current user's "Your Music" library. Parameters: - shows - a list of show URIs, URLs or IDs """ + if shows is None: + shows = [] slist = [self._get_id("show", s) for s in shows] - return self._delete("me/shows/?ids=" + ",".join(slist)) + return self._delete(f"me/shows/?ids={','.join(slist)}") - def current_user_saved_shows_contains(self, shows=[]): + def current_user_saved_shows_contains(self, shows=None): """ Check if one or more shows is already saved in the current Spotify user’s “Your Music” library. @@ -1126,8 +1110,10 @@ class Spotify: - shows - a list of show URIs, URLs or IDs """ + if shows is None: + shows = [] slist = [self._get_id("show", s) for s in shows] - return self._get("me/shows/contains?ids=" + ",".join(slist)) + return self._get(f"me/shows/contains?ids={','.join(slist)}") def current_user_followed_artists(self, limit=20, after=None): """ Gets a list of the artists followed by the current authorized user @@ -1138,9 +1124,7 @@ class Spotify: request """ - return self._get( - "me/following", type="artist", limit=limit, after=after - ) + return self._get(f"me/following?type=artist&limit={limit}&after={after}") def current_user_following_artists(self, ids=None): """ Check if the current user is following certain artists @@ -1150,9 +1134,7 @@ class Spotify: Parameters: - ids - a list of artist URIs, URLs or IDs """ - idlist = [] - if ids is not None: - idlist = [self._get_id("artist", i) for i in ids] + idlist = [self._get_id("artist", i) for i in ids] if ids is not None else [] return self._get( "me/following/contains", ids=",".join(idlist), type="artist" ) @@ -1165,9 +1147,7 @@ class Spotify: Parameters: - ids - a list of user URIs, URLs or IDs """ - idlist = [] - if ids is not None: - idlist = [self._get_id("user", i) for i in ids] + idlist = [self._get_id("user", i) for i in ids] if ids is not None else [] return self._get( "me/following/contains", ids=",".join(idlist), type="user" ) @@ -1221,33 +1201,41 @@ class Spotify: before=before, ) - def user_follow_artists(self, ids=[]): + def user_follow_artists(self, ids=None): """ Follow one or more artists Parameters: - ids - a list of artist IDs """ - return self._put("me/following?type=artist&ids=" + ",".join(ids)) + if ids is None: + ids = [] + return self._put(f"me/following?type=artist&ids={','.join(ids)}") - def user_follow_users(self, ids=[]): + def user_follow_users(self, ids=None): """ Follow one or more users Parameters: - ids - a list of user IDs """ - return self._put("me/following?type=user&ids=" + ",".join(ids)) + if ids is None: + ids = [] + return self._put(f"me/following?type=user&ids={','.join(ids)}") - def user_unfollow_artists(self, ids=[]): + def user_unfollow_artists(self, ids=None): """ Unfollow one or more artists Parameters: - ids - a list of artist IDs """ - return self._delete("me/following?type=artist&ids=" + ",".join(ids)) + if ids is None: + ids = [] + return self._delete(f"me/following?type=artist&ids={','.join(ids)}") - def user_unfollow_users(self, ids=[]): + def user_unfollow_users(self, ids=None): """ Unfollow one or more users Parameters: - ids - a list of user IDs """ - return self._delete("me/following?type=user&ids=" + ",".join(ids)) + if ids is None: + ids = [] + return self._delete(f"me/following?type=user&ids={','.join(ids)}") def featured_playlists( self, locale=None, country=None, timestamp=None, limit=20, offset=0 @@ -1316,9 +1304,7 @@ class Spotify: by an underscore. """ return self._get( - "browse/categories/" + category_id, - country=country, - locale=locale, + f"browse/categories/{category_id}", country=country, locale=locale ) def categories(self, country=None, locale=None, limit=20, offset=0): @@ -1368,7 +1354,7 @@ class Spotify: DeprecationWarning, ) return self._get( - "browse/categories/" + category_id + "/playlists", + f"browse/categories/{category_id}/playlists", country=country, limit=limit, offset=offset, @@ -1462,9 +1448,9 @@ class Spotify: DeprecationWarning, ) trid = self._get_id("track", track_id) - return self._get("audio-analysis/" + trid) + return self._get(f"audio-analysis/{trid}") - def audio_features(self, tracks=[]): + def audio_features(self, tracks=None): """ Get audio features for one or multiple tracks based upon their Spotify IDs Parameters: - tracks - a list of track URIs, URLs or IDs, maximum: 100 ids @@ -1475,12 +1461,15 @@ class Spotify: DeprecationWarning, ) + if tracks is None: + tracks = [] + if isinstance(tracks, str): trackid = self._get_id("track", tracks) - results = self._get("audio-features/?ids=" + trackid) + results = self._get(f"audio-features/?ids={trackid}") else: tlist = [self._get_id("track", t) for t in tracks] - results = self._get("audio-features/?ids=" + ",".join(tlist)) + results = self._get(f"audio-features/?ids={','.join(tlist)}") # the response has changed, look for the new style first, and if # it's not there, fallback on the old style if "audio_features" in results: @@ -1620,11 +1609,7 @@ class Spotify: if state not in ["track", "context", "off"]: logger.warning("Invalid state") return - self._put( - self._append_device_id( - f"me/player/repeat?state={state}", device_id - ) - ) + self._put(self._append_device_id(f"me/player/repeat?state={state}", device_id)) def volume(self, volume_percent, device_id=None): """ Set playback volume. @@ -1641,8 +1626,7 @@ class Spotify: return self._put( self._append_device_id( - f"me/player/volume?volume_percent={volume_percent}", - device_id, + f"me/player/volume?volume_percent={volume_percent}", device_id ) ) @@ -1658,9 +1642,7 @@ class Spotify: return state = str(state).lower() self._put( - self._append_device_id( - f"me/player/shuffle?state={state}", device_id - ) + self._append_device_id(f"me/player/shuffle?state={state}", device_id) ) def queue(self): @@ -1705,10 +1687,7 @@ class Spotify: - device_id - device id to append """ if device_id: - if "?" in path: - path += f"&device_id={device_id}" - else: - path += f"?device_id={device_id}" + path += f"&device_id={device_id}" if "?" in path else f"?device_id={device_id}" return path def _get_id(self, type, id): @@ -1736,10 +1715,7 @@ class Spotify: raise SpotifyException(400, -1, "Unsupported URL / URI.") def _get_uri(self, type, id): - if self._is_uri(id): - return id - else: - return "spotify:" + type + ":" + self._get_id(type, id) + return id if self._is_uri(id) else f"spotify:{type}:{self._get_id(type, id)}" def _is_uri(self, uri): return re.search(Spotify._regex_spotify_uri, uri) is not None @@ -1748,13 +1724,12 @@ class Spotify: if total and limit > total: limit = total warnings.warn( - "limit was auto-adjusted to equal {} as it must not be higher than total".format( - total), + f"limit was auto-adjusted to equal {total} as it must not be higher than total", UserWarning, ) results = defaultdict(dict) - item_types = [item_type + "s" for item_type in type.split(",")] + item_types = [f"{item_type}s" for item_type in type.split(",")] count = 0 for country in markets: diff --git a/spotipy/oauth2.py b/spotipy/oauth2.py index 8937a3a..47a035a 100644 --- a/spotipy/oauth2.py +++ b/spotipy/oauth2.py @@ -19,14 +19,17 @@ from urllib.parse import parse_qsl, urlparse from spotipy.cache_handler import CacheFileHandler, CacheHandler from spotipy.exceptions import SpotifyOauthError, SpotifyStateError -from spotipy.util import CLIENT_CREDS_ENV_VARS, get_host_port, normalize_scope +from spotipy.util import CLIENT_CREDS_ENV_VARS, get_host_port +from spotipy.scope import Scope +from typing import Iterable +import re logger = logging.getLogger(__name__) def _make_authorization_headers(client_id, client_secret): auth_header = base64.b64encode( - str(client_id + ":" + client_secret).encode("ascii") + f"{client_id}:{client_secret}".encode("ascii") ) return {"Authorization": f"Basic {auth_header.decode('ascii')}"} @@ -41,18 +44,51 @@ def _ensure_value(value, env_key): class SpotifyAuthBase: + def __init__(self, requests_session): if isinstance(requests_session, requests.Session): self._session = requests_session - else: - if requests_session: # Build a new session. - self._session = requests.Session() - else: # Use the Requests API module as a "session". - from requests import api - self._session = api + elif requests_session: # Build a new session. + self._session = requests.Session() + else: # Use the Requests API module as a "session". + from requests import api + self._session = api def _normalize_scope(self, scope): - return normalize_scope(scope) + """ + Accepts a string of scopes, or an iterable with elements of type + `Scope` or `str` and returns a space-separated string of scopes. + Returns `None` if the argument is `None`. + """ + + # TODO: do we need to sort the scopes? + + if isinstance(scope, str): + # allow for any separator(s) between the scopes other than a word + # character or a hyphen + scopes = re.split(pattern=r"[^\w-]+", string=scope) + return " ".join(sorted(scopes)) + + if isinstance(scope, Iterable): + + # Assume all of the iterable's elements are of the same type. + # If the iterable is empty, then return None. + first_element = next(iter(scope), None) + + if isinstance(first_element, str): + return " ".join(sorted(scope)) + if isinstance(first_element, Scope): + return Scope.make_string(scope) + if first_element is None: + return "" + + elif scope is None: + return None + + raise TypeError( + "Unsupported type for scopes: %s. Expected either a string of scopes, or " + "an Iterable with elements of type `Scope` or `str`." % type(scope) + ) @property def client_id(self): @@ -169,9 +205,10 @@ class SpotifyClientCredentials(SpotifyAuthBase): self.proxies = proxies self.requests_timeout = requests_timeout if cache_handler: - assert issubclass(cache_handler.__class__, CacheHandler), \ - "cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \ - + " != " + str(CacheHandler) + assert issubclass( + cache_handler.__class__, CacheHandler + ), f"cache_handler must be a subclass of CacheHandler:\ + {str(type(cache_handler))} != {str(CacheHandler)}" self.cache_handler = cache_handler else: self.cache_handler = CacheFileHandler() @@ -289,12 +326,14 @@ class SpotifyOAuth(SpotifyAuthBase): self.scope = self._normalize_scope(scope) if cache_handler: - assert issubclass(cache_handler.__class__, CacheHandler), \ - "cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \ - + " != " + str(CacheHandler) + assert issubclass( + cache_handler.__class__, CacheHandler + ), f"cache_handler must be a subclass of CacheHandler:\ + {str(type(cache_handler))} != {str(CacheHandler)}" self.cache_handler = cache_handler else: self.cache_handler = CacheFileHandler() + self.proxies = proxies self.requests_timeout = requests_timeout self.show_dialog = show_dialog @@ -345,18 +384,17 @@ class SpotifyOAuth(SpotifyAuthBase): - url - the response url """ _, code = self.parse_auth_response_url(url) - if code is None: - return url - else: - return code + return url if code is None else code @staticmethod def parse_auth_response_url(url): query_s = urlparse(url).query form = dict(parse_qsl(query_s)) if "error" in form: - raise SpotifyOauthError(f"Received error from auth server: {form['error']}", - error=form["error"]) + raise SpotifyOauthError( + f"Received error from auth server: {form['error']}", + error=form["error"] + ) return tuple(form.get(param) for param in ["state", "code"]) def _make_authorization_headers(self): @@ -376,10 +414,7 @@ class SpotifyOAuth(SpotifyAuthBase): prompt = "Enter the URL you were redirected to: " else: url = self.get_authorize_url() - prompt = ( - "Go to the following URL: {}\n" - "Enter the URL you were redirected to: ".format(url) - ) + prompt = f"Go to the following URL: {url}\nEnter the URL you were redirected to: " response = self._get_user_input(prompt) state, code = SpotifyOAuth.parse_auth_response_url(response) if self.state is not None and self.state != state: @@ -565,6 +600,16 @@ class SpotifyPKCE(SpotifyAuthBase): * scope: Optional, either a string of scopes, or an iterable with elements of type `Scope` or `str`. E.g., {Scope.user_modify_playback_state, Scope.user_library_read} + * cache_path: (deprecated) Optional, will otherwise be generated + (takes precedence over `username`) + * username: (deprecated) Optional or set as environment variable + (will set `cache_path` to `.cache-{username}`) + * proxies: Optional, proxy for the requests library to route through + * requests_timeout: Optional, tell Requests to stop waiting for a response after + a given number of seconds + * requests_session: A Requests session + * open_browser: Optional, whether the web browser should be opened to + authorize a user * cache_handler: An instance of the `CacheHandler` class to handle getting and saving cached authorization tokens. Optional, will otherwise use `CacheFileHandler`. @@ -575,7 +620,7 @@ class SpotifyPKCE(SpotifyAuthBase): 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 + * open_browser: Optional, whether the web browser should be opened to authorize a user """ @@ -584,12 +629,15 @@ class SpotifyPKCE(SpotifyAuthBase): self.redirect_uri = redirect_uri self.state = state self.scope = self._normalize_scope(scope) + if cache_handler: - assert issubclass(type(cache_handler), CacheHandler), \ - "type(cache_handler): " + str(type(cache_handler)) + " != " + str(CacheHandler) + assert issubclass(cache_handler.__class__, CacheHandler), \ + "cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \ + + " != " + str(CacheHandler) self.cache_handler = cache_handler else: self.cache_handler = CacheFileHandler() + self.proxies = proxies self.requests_timeout = requests_timeout @@ -703,10 +751,7 @@ class SpotifyPKCE(SpotifyAuthBase): prompt = "Enter the URL you were redirected to: " else: url = self.get_authorize_url() - prompt = ( - "Go to the following URL: {}\n" - "Enter the URL you were redirected to: ".format(url) - ) + prompt = f"Go to the following URL: {url}\nEnter the URL you were redirected to: " response = self._get_user_input(prompt) state, code = self.parse_auth_response_url(response) if self.state is not None and self.state != state: @@ -843,10 +888,7 @@ class SpotifyPKCE(SpotifyAuthBase): - url - the response url """ _, code = self.parse_auth_response_url(url) - if code is None: - return url - else: - return code + return url if code is None else code @staticmethod def parse_auth_response_url(url): diff --git a/spotipy/util.py b/spotipy/util.py index 6626947..37ccbc7 100644 --- a/spotipy/util.py +++ b/spotipy/util.py @@ -2,8 +2,6 @@ from __future__ import annotations from spotipy.scope import Scope -""" Shows a user's playlists. This needs to be authenticated via OAuth. """ - __all__ = ["CLIENT_CREDS_ENV_VARS", "get_host_port", "normalize_scope", "Retry"] import logging @@ -70,6 +68,7 @@ class Retry(urllib3.Retry): """ Custom class for printing a warning when a rate/request limit is reached. """ + def increment( self, method: str | None = None, diff --git a/tests/integration/non_user_endpoints/test.py b/tests/integration/non_user_endpoints/test.py index 4b50ae6..bf15545 100644 --- a/tests/integration/non_user_endpoints/test.py +++ b/tests/integration/non_user_endpoints/test.py @@ -68,11 +68,9 @@ class AuthTestSpotipy(unittest.TestCase): 'spotify:audiobook:67VtmjZitn25TWocsyAEyh'] @classmethod - def setUpClass(self): - self.spotify = Spotify( - auth_manager=SpotifyClientCredentials() - ) - self.spotify.trace = False + def setUpClass(cls): + cls.spotify = Spotify(auth_manager=SpotifyClientCredentials()) + cls.spotify.trace = False def test_artist_urn(self): artist = self.spotify.artist(self.radiohead_urn) @@ -204,8 +202,10 @@ class AuthTestSpotipy(unittest.TestCase): [0]['name'] == 'Weezer' for country in results_limited)) total_limited_results = 0 - for country in results_limited: - total_limited_results += len(results_limited[country]['artists']['items']) + total_limited_results = sum( + len(results_limited[country]['artists']['items']) + for country in results_limited + ) self.assertTrue(total_limited_results <= total) def test_multiple_types_search_with_multiple_markets(self): @@ -316,7 +316,7 @@ class AuthTestSpotipy(unittest.TestCase): spotify_no_retry = Spotify( auth_manager=SpotifyClientCredentials(), retries=0) - for i in range(100): + for _ in range(100): try: spotify_no_retry.search(q='foo') except SpotifyException as e: @@ -414,7 +414,7 @@ class AuthTestSpotipy(unittest.TestCase): with self.assertRaises(SpotifyException) as cm: self.spotify.user_playlist_create( "spotify", "Best hits of the 90s") - self.assertTrue(cm.exception.http_status == 401 or cm.exception.http_status == 403) + self.assertTrue(cm.exception.http_status in [401, 403]) def test_custom_requests_session(self): sess = requests.Session() diff --git a/tests/unit/test_scopes.py b/tests/unit/test_scopes.py index 874dee3..441c793 100644 --- a/tests/unit/test_scopes.py +++ b/tests/unit/test_scopes.py @@ -20,8 +20,8 @@ class SpotipyScopeTest(TestCase): normalized_scope_string_2 = self.normalize_scope(scope_string) self.assertEqual(scope_string, "") - self.assertEqual(normalized_scope_string, None) - self.assertEqual(normalized_scope_string_2, None) + self.assertEqual(normalized_scope_string, "") + self.assertEqual(normalized_scope_string_2, "") converted_scopes = Scope.from_string(scope_string) self.assertEqual(converted_scopes, set()) @@ -82,10 +82,10 @@ class SpotipyScopeTest(TestCase): def test_normalize_scope(self): normalized_scope_string = self.normalize_scope([]) - self.assertEqual(normalized_scope_string, None) + self.assertEqual(normalized_scope_string, "") normalized_scope_string_2 = self.normalize_scope(()) - self.assertEqual(normalized_scope_string_2, None) + self.assertEqual(normalized_scope_string_2, "") self.assertIsNone(self.normalize_scope(None))