├── .github └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── docs ├── README.md ├── examples │ ├── access-token-with-authorization-code-flow.md │ ├── access-token-with-client-credentials-flow.md │ ├── access-token-with-pkce-flow.md │ ├── controlling-user-playback.md │ ├── fetching-album-information.md │ ├── fetching-artist-information.md │ ├── fetching-audiobook-information.md │ ├── fetching-podcast-information.md │ ├── fetching-spotify-featured-content.md │ ├── fetching-track-information.md │ ├── following-artists-playlists-and-users.md │ ├── handling-errors.md │ ├── managing-user-library.md │ ├── managing-user-playlists.md │ ├── managing-user-profiles.md │ ├── passing-a-custom-request-instance.md │ ├── refreshing-access-tokens.md │ ├── searching-the-spotify-catalog.md │ ├── setting-custom-curl-options.md │ ├── setting-options.md │ └── working-with-scopes.md ├── getting-started.md └── method-reference │ ├── Request.md │ ├── Session.md │ ├── SpotifyWebAPI.md │ ├── SpotifyWebAPIAuthException.md │ └── SpotifyWebAPIException.md ├── phpcs.xml ├── phpunit.dist.xml ├── phpunit.php ├── src ├── Request.php ├── Session.php ├── SpotifyWebAPI.php ├── SpotifyWebAPIAuthException.php ├── SpotifyWebAPIException.php └── cacert.pem └── tests ├── RequestTest.php ├── SessionTest.php ├── SpotifyWebAPITest.php └── fixtures ├── access-token.json ├── album-tracks.json ├── album.json ├── albums.json ├── artist-albums.json ├── artist-related-artists.json ├── artist-top-tracks.json ├── artist.json ├── artists.json ├── audio-analysis.json ├── audio-features.json ├── audiobook.json ├── audiobooks.json ├── available-genre-seeds.json ├── categories-list.json ├── category-playlists.json ├── category.json ├── chapter.json ├── chapters.json ├── episode.json ├── episodes.json ├── featured-playlists.json ├── markets.json ├── multiple-audio-features.json ├── my-playlists.json ├── my-queue.json ├── new-releases.json ├── playlist-cover-image.json ├── recently-played.json ├── recommendations.json ├── refresh-token-no-refresh-token.json ├── refresh-token.json ├── search-album.json ├── show-episodes.json ├── show.json ├── shows.json ├── snapshot-id.json ├── top-artists-and-tracks.json ├── track.json ├── tracks.json ├── user-albums-contains.json ├── user-albums.json ├── user-current-playback-info.json ├── user-current-track.json ├── user-devices.json ├── user-episodes-contains.json ├── user-episodes.json ├── user-followed-artists.json ├── user-follows-playlist.json ├── user-follows.json ├── user-playlist-tracks.json ├── user-playlist.json ├── user-playlists.json ├── user-shows-contains.json ├── user-shows.json ├── user-tracks-contains.json ├── user-tracks.json ├── user.json └── users-follows-playlist.json /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | php-versions: ['8.4', '8.3', '8.2', '8.1'] 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: ${{ matrix.php-versions }} 21 | coverage: pcov 22 | 23 | - run: composer install 24 | - run: composer test 25 | - run: php vendor/bin/php-coveralls -v 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .phpunit.cache 3 | build 4 | composer.lock 5 | phpunit.xml 6 | vendor 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | ## Issues 4 | Please submit all your bug reports, feature requests and pull requests here but note that this isn't the place for support requests. Please use [Stack Overflow](http://stackoverflow.com/) for this. 5 | 6 | ## Bug reports 7 | 1. Search the issues, has it already been reported? 8 | 2. Download the latest source, did this solve the problem? 9 | 4. If the answer to all of the above questions are "No" then open a bug report and include the following: 10 | * A short, descriptive title. 11 | * A summary of the problem. 12 | * The steps to reproduce the problem. 13 | * Possible solutions or other relevant information/suggestions. 14 | 15 | ## New features 16 | If you have an idea for a new feature, please file an issue first to see if it fits the scope of this project. That way no one's time needs to be wasted. 17 | 18 | ## Coding Guidelines 19 | We follow the coding standards outlined in [PSR-1](https://www.php-fig.org/psr/psr-1/) and [PSR-12](https://www.php-fig.org/psr/psr-12/). Please follow these guidelines when committing new code. 20 | 21 | In addition to the PSR guidelines we try to adhere to the following points: 22 | * We order all methods by visibility and then alphabetically, `private`/`protected` methods first and then `public`. For example: 23 | 24 | ``` 25 | protected function b() {} 26 | 27 | public function a() {} 28 | ``` 29 | 30 | instead of 31 | 32 | ``` 33 | public function a() {} 34 | 35 | protected function b() {} 36 | ``` 37 | 38 | * We strive to keep the inline documentation language consistent, take a look at existing docs for examples. 39 | 40 | Before committing any code, be sure to run `composer test` to ensure that the code style is consistent and all the tests pass. 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) Jonathan Wilsson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spotify Web API PHP 2 | 3 | [![Packagist](https://img.shields.io/packagist/v/jwilsson/spotify-web-api-php.svg)](https://packagist.org/packages/jwilsson/spotify-web-api-php) 4 | ![build](https://github.com/jwilsson/spotify-web-api-php/workflows/build/badge.svg) 5 | [![Coverage Status](https://coveralls.io/repos/jwilsson/spotify-web-api-php/badge.svg?branch=main)](https://coveralls.io/r/jwilsson/spotify-web-api-php?branch=main) 6 | 7 | This is a PHP wrapper for [Spotify's Web API](https://developer.spotify.com/web-api/). It includes the following: 8 | 9 | * Helper methods for all API endpoints: 10 | * Information about artists, albums, tracks, podcasts, audiobooks, and users. 11 | * List music featured by Spotify. 12 | * Playlist and user music library management. 13 | * Spotify catalog search. 14 | * User playback control. 15 | * Authorization flow helpers. 16 | * Automatic refreshing of access tokens. 17 | * Automatic retry of rate limited requests. 18 | * PSR-4 autoloading support. 19 | 20 | ## Requirements 21 | * PHP 8.1 or later. 22 | * PHP [cURL extension](http://php.net/manual/en/book.curl.php) (Usually included with PHP). 23 | 24 | ## Installation 25 | Install it using [Composer](https://getcomposer.org/): 26 | 27 | ```sh 28 | composer require jwilsson/spotify-web-api-php 29 | ``` 30 | 31 | ## Usage 32 | Before using the Spotify Web API, you'll need to create an app at [Spotify’s developer site](https://developer.spotify.com/web-api/). 33 | 34 | *Note: Applications created after 2021-05-27 [might need to perform some extra steps](https://developer.spotify.com/community/news/2021/05/27/improving-the-developer-and-user-experience-for-third-party-apps/).* 35 | 36 | Simple example displaying a user's profile: 37 | ```php 38 | require 'vendor/autoload.php'; 39 | 40 | $session = new SpotifyWebAPI\Session( 41 | 'CLIENT_ID', 42 | 'CLIENT_SECRET', 43 | 'REDIRECT_URI' 44 | ); 45 | 46 | $api = new SpotifyWebAPI\SpotifyWebAPI(); 47 | 48 | if (isset($_GET['code'])) { 49 | $session->requestAccessToken($_GET['code']); 50 | $api->setAccessToken($session->getAccessToken()); 51 | 52 | print_r($api->me()); 53 | } else { 54 | $options = [ 55 | 'scope' => [ 56 | 'user-read-email', 57 | ], 58 | ]; 59 | 60 | header('Location: ' . $session->getAuthorizeUrl($options)); 61 | die(); 62 | } 63 | ``` 64 | 65 | For more instructions and examples, check out the [documentation](/docs/). 66 | 67 | The [Spotify Web API Console](https://developer.spotify.com/web-api/console/) can also be of great help when trying out the API. 68 | 69 | ## Contributing 70 | Contributions are more than welcome! See [CONTRIBUTING.md](/CONTRIBUTING.md) for more info. 71 | 72 | ## License 73 | MIT license. Please see [LICENSE.md](LICENSE.md) for more info. 74 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jwilsson/spotify-web-api-php", 3 | "description": "A PHP wrapper for Spotify's Web API.", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": [ 7 | "spotify" 8 | ], 9 | "homepage": "https://github.com/jwilsson/spotify-web-api-php", 10 | "authors": [ 11 | { 12 | "name": "Jonathan Wilsson", 13 | "email": "jonathan.wilsson@gmail.com" 14 | } 15 | ], 16 | "require": { 17 | "php": "^8.1", 18 | "ext-curl": "*" 19 | }, 20 | "require-dev": { 21 | "php-coveralls/php-coveralls": "^2.5", 22 | "php-mock/php-mock-phpunit": "^2.7", 23 | "phpunit/phpunit": "^10.2", 24 | "squizlabs/php_codesniffer": "^3.0" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "SpotifyWebAPI\\": "src/" 29 | } 30 | }, 31 | "scripts": { 32 | "test": "phpcs src -v && phpunit" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Spotify Web API for PHP Documentation 2 | There are a lot of things possible with the Spotify Web API and these pages exist to give you an overview of how to make the most out of them. Which methods and options are available, how to use them, and what to do when something goes wrong. 3 | 4 | First, start by checking out the [Getting started guide](/docs/getting-started.md) before continuing with the examples below. 5 | 6 | ## Examples 7 | * **Authorization** 8 | * [Obtaining an access token using the Authorization Code Flow](/docs/examples/access-token-with-authorization-code-flow.md) 9 | * [Obtaining an access token using the Client Credentials Flow](/docs/examples/access-token-with-client-credentials-flow.md) 10 | * [Obtaining an access token using the Proof Key for Code Exchange (PKCE) Flow](/docs/examples/access-token-with-pkce-flow.md) 11 | * [Refreshing access tokens](/docs/examples/refreshing-access-tokens.md) 12 | * [Working with scopes](/docs/examples/working-with-scopes.md) 13 | * **Fetching data** 14 | * [Fetching information about albums](/docs/examples/fetching-album-information.md) 15 | * [Fetching information about artists](/docs/examples/fetching-artist-information.md) 16 | * [Fetching information about audiobooks](/docs/examples/fetching-audiobook-information.md) 17 | * [Fetching information about podcasts](/docs/examples/fetching-podcast-information.md) 18 | * [Fetching information about tracks](/docs/examples/fetching-track-information.md) 19 | * [Fetching Spotify featured content](/docs/examples/fetching-spotify-featured-content.md) 20 | * [Searching the Spotify catalog](/docs/examples/searching-the-spotify-catalog.md) 21 | * **Managing users** 22 | * [Controlling user playback](/docs/examples/controlling-user-playback.md) 23 | * [Following artists, playlists, and users](/docs/examples/following-artists-playlists-and-users.md) 24 | * [Managing a user's library](/docs/examples/managing-user-library.md) 25 | * [Managing a user's playlists](/docs/examples/managing-user-playlists.md) 26 | * [Managing a user's profile](/docs/examples/managing-user-profiles.md) 27 | * **Working with the API** 28 | * [Handling errors](/docs/examples/handling-errors.md) 29 | * [Passing a custom Request instance](/docs/examples/passing-a-custom-request-instance.md) 30 | * [Setting custom cURL options](/docs/examples/setting-custom-curl-options.md) 31 | * [Setting options](/docs/examples/setting-options.md) 32 | 33 | ## Method Reference 34 | A full method reference listing all public methods is [available here](/docs/method-reference/). 35 | -------------------------------------------------------------------------------- /docs/examples/access-token-with-authorization-code-flow.md: -------------------------------------------------------------------------------- 1 | # Authorization Using the Authorization Code Flow 2 | 3 | All API methods require authorization. Before using these methods you'll need to create an app at [Spotify's developer site](https://developer.spotify.com/documentation/web-api/). 4 | 5 | The Authorization Code flow method requires some interaction from the user but in turn allows access to user information. There are two steps required to authenticate the user. The first step is to request access to the user's account and data (known as *scopes*) and redirecting them to your app's authorize URL (also known as the callback URL). 6 | 7 | ### Step 1 8 | Put the following code in its own file, lets call it `auth.php`. Replace `CLIENT_ID` and `CLIENT_SECRET` with the values given to you by Spotify. The `REDIRECT_URI` is the one you entered when creating the Spotify app, make sure it's an exact match. 9 | 10 | ```php 11 | require 'vendor/autoload.php'; 12 | 13 | $session = new SpotifyWebAPI\Session( 14 | 'CLIENT_ID', 15 | 'CLIENT_SECRET', 16 | 'REDIRECT_URI' 17 | ); 18 | 19 | $state = $session->generateState(); 20 | $options = [ 21 | 'scope' => [ 22 | 'playlist-read-private', 23 | 'user-read-private', 24 | ], 25 | 'state' => $state, 26 | ]; 27 | 28 | header('Location: ' . $session->getAuthorizeUrl($options)); 29 | die(); 30 | ``` 31 | 32 | __Note:__ The `state` parameter is optional but highly recommended to prevent CSRF attacks. The value will need to be stored between requests and verfied when the user is redirected back to your application from Spotify. 33 | 34 | To read more about scopes, see [Working with Scopes](/docs/examples/working-with-scopes.md). To see all of the available options for `getAuthorizeUrl()`, refer to the [method reference](/docs/method-reference/Session.md#getauthorizeurl). 35 | 36 | ### Step 2 37 | When the user has approved your app, Spotify will redirect the user together with a `code` to the specifed redirect URI. You'll need to use this code to request a access token from Spotify. 38 | 39 | __Note:__ The API wrapper does not include any token management. It's up to you to save the access token somewhere (in a database, a PHP session, or wherever appropriate for your application) and request a new access token when the old one has expired. 40 | 41 | Lets put this code in a new file called `callback.php`: 42 | 43 | ```php 44 | require 'vendor/autoload.php'; 45 | 46 | $session = new SpotifyWebAPI\Session( 47 | 'CLIENT_ID', 48 | 'CLIENT_SECRET', 49 | 'REDIRECT_URI' 50 | ); 51 | 52 | $state = $_GET['state']; 53 | 54 | // Fetch the stored state value from somewhere. A session for example 55 | 56 | if ($state !== $storedState) { 57 | // The state returned isn't the same as the one we've stored, we shouldn't continue 58 | die('State mismatch'); 59 | } 60 | 61 | // Request a access token using the code from Spotify 62 | $session->requestAccessToken($_GET['code']); 63 | 64 | $accessToken = $session->getAccessToken(); 65 | $refreshToken = $session->getRefreshToken(); 66 | 67 | // Store the access and refresh tokens somewhere. In a session for example 68 | 69 | // Send the user along and fetch some data! 70 | header('Location: app.php'); 71 | die(); 72 | ``` 73 | 74 | When requesting a access token, a **refresh token** will also be included. This can be used to extend the validity of access tokens. It's recommended to also store this somewhere persistent, in a database for example. [Read more about refresh tokens here](refreshing-access-tokens.md). 75 | 76 | ### Step 3 77 | In a third file, `app.php`, tell the API wrapper which access token to use, and then make some API calls! 78 | 79 | ```php 80 | require 'vendor/autoload.php'; 81 | 82 | $api = new SpotifyWebAPI\SpotifyWebAPI(); 83 | 84 | // Fetch the saved access token from somewhere. A session for example. 85 | $api->setAccessToken($accessToken); 86 | 87 | // It's now possible to request data about the currently authenticated user 88 | print_r( 89 | $api->me() 90 | ); 91 | 92 | // Getting Spotify catalog data is of course also possible 93 | print_r( 94 | $api->getTrack('7EjyzZcbLxW7PaaLua9Ksb') 95 | ); 96 | ``` 97 | 98 | For more in-depth technical information about the Authorization Code flow, please refer to the [Spotify Web API documentation](https://developer.spotify.com/documentation/general/guides/authorization/code-flow/). 99 | -------------------------------------------------------------------------------- /docs/examples/access-token-with-client-credentials-flow.md: -------------------------------------------------------------------------------- 1 | # Authorization Using the Client Credentials Flow 2 | 3 | All API methods require authorization. Before using these methods you'll need to create an app at [Spotify's developer site](https://developer.spotify.com/documentation/web-api/). 4 | 5 | This method doesn't require any user interaction and no access to user information is therefore granted. This is the recommended method if you only need access to Spotify catalog data. 6 | 7 | ## Step 1 8 | The first step is to request a access token. Put the following code in its own file, lets call it `auth.php`. Replace `CLIENT_ID` and `CLIENT_SECRET` with the values given to you by Spotify. 9 | 10 | __Note:__ The API wrapper does not include any token management. It's up to you to save the access token somewhere (in a database, a PHP session, or wherever appropriate for your application) and request a new access token when the old one has expired. 11 | 12 | ```php 13 | require 'vendor/autoload.php'; 14 | 15 | $session = new SpotifyWebAPI\Session( 16 | 'CLIENT_ID', 17 | 'CLIENT_SECRET' 18 | ); 19 | 20 | $session->requestCredentialsToken(); 21 | $accessToken = $session->getAccessToken(); 22 | 23 | // Store the access token somewhere. In a database for example. 24 | 25 | // Send the user along and fetch some data! 26 | header('Location: app.php'); 27 | die(); 28 | ``` 29 | 30 | You'll notice the missing redirect URI when initializing the `Session`. When using the Client Credentials Flow, it isn't needed and can simply be omitted from the constructor call. 31 | 32 | ## Step 2 33 | In a second file, `app.php`, tell the API wrapper which access token to use, and then make some API calls! 34 | 35 | ```php 36 | require 'vendor/autoload.php'; 37 | 38 | // Fetch the saved access token from somewhere. A database for example. 39 | 40 | $api = new SpotifyWebAPI\SpotifyWebAPI(); 41 | $api->setAccessToken($accessToken); 42 | 43 | // It's now possible to request data from the Spotify catalog 44 | print_r( 45 | $api->getTrack('7EjyzZcbLxW7PaaLua9Ksb') 46 | ); 47 | ``` 48 | 49 | For more in-depth technical information about the Client Credentials Flow, please refer to the [Spotify Web API documentation](https://developer.spotify.com/documentation/general/guides/authorization/client-credentials/). 50 | -------------------------------------------------------------------------------- /docs/examples/access-token-with-pkce-flow.md: -------------------------------------------------------------------------------- 1 | # Authorization Using the Proof Key for Code Exchange (PKCE) Flow 2 | 3 | All API methods require authorization. Before using these methods you'll need to create an app at [Spotify's developer site](https://developer.spotify.com/documentation/web-api/). 4 | 5 | The Proof Key for Code Exchange Flow is very similar to the [Authorization Code flow](access-token-with-authorization-code-flow.md), but instead of using a client secret which might not always be viable it uses a code challenge flow. 6 | 7 | Just like the Authorization Code flow, this method requires some interaction from the user but in turn allows access to user information. There are two steps required to authenticate the user. The first step is to request access to the user's account and data (known as *scopes*) and redirecting them to your app's authorize URL (also known as the callback URL). 8 | 9 | ### Step 1 10 | Put the following code in its own file, lets call it `auth.php`. Replace `CLIENT_ID` with the value given to you by Spotify. The `REDIRECT_URI` is the one you entered when creating the Spotify app, make sure it's an exact match. You'll also need to create a *code verifier* and store it somewhere between requests. It will be used again in the second step. 11 | 12 | ```php 13 | require 'vendor/autoload.php'; 14 | 15 | $session = new SpotifyWebAPI\Session( 16 | 'CLIENT_ID', 17 | '', // Normally the client secret, but this value can be omitted when using the PKCE flow 18 | 'REDIRECT_URI' 19 | ); 20 | 21 | $verifier = $session->generateCodeVerifier(); // Store this value somewhere, a session for example 22 | $challenge = $session->generateCodeChallenge($verifier); 23 | $state = $session->generateState(); 24 | 25 | $options = [ 26 | 'code_challenge' => $challenge, 27 | 'scope' => [ 28 | 'playlist-read-private', 29 | 'user-read-private', 30 | ], 31 | 'state' => $state, 32 | ]; 33 | 34 | header('Location: ' . $session->getAuthorizeUrl($options)); 35 | die(); 36 | ``` 37 | 38 | __Note:__ The `state` parameter is optional but highly recommended to prevent CSRF attacks. The value will need to be stored between requests and verfied when the user is redirected back to your application from Spotify. 39 | 40 | To read more about scopes, see [Working with Scopes](/docs/examples/working-with-scopes.md). To see all of the available options for `getAuthorizeUrl()`, refer to the [method reference](/docs/method-reference/Session.md#getauthorizeurl). 41 | 42 | ### Step 2 43 | When the user has approved your app, Spotify will redirect the user together with a `code` to the specifed redirect URI. You'll need to use this code to request a access token from Spotify. The *code verifier* created in the previous step will also be needed. 44 | 45 | __Note:__ The API wrapper does not include any token management. It's up to you to save the access token somewhere (in a database, a PHP session, or wherever appropriate for your application) and request a new access token when the old one has expired. 46 | 47 | Lets put this code in a new file called `callback.php`: 48 | 49 | ```php 50 | require 'vendor/autoload.php'; 51 | 52 | $session = new SpotifyWebAPI\Session( 53 | 'CLIENT_ID', 54 | 'CLIENT_SECRET', 55 | 'REDIRECT_URI' 56 | ); 57 | 58 | $state = $_GET['state']; 59 | 60 | // Fetch the stored state value from somewhere. A session for example 61 | 62 | if ($state !== $storedState) { 63 | // The state returned isn't the same as the one we've stored, we shouldn't continue 64 | die('State mismatch'); 65 | } 66 | 67 | // Request a access token using the code from Spotify and the previously created code verifier 68 | $session->requestAccessToken($_GET['code'], $verifier); 69 | 70 | $accessToken = $session->getAccessToken(); 71 | $refreshToken = $session->getRefreshToken(); 72 | 73 | // Store the access and refresh tokens somewhere. In a session for example 74 | 75 | // Send the user along and fetch some data! 76 | header('Location: app.php'); 77 | die(); 78 | ``` 79 | 80 | When requesting a access token, a **refresh token** will also be included. This can be used to extend the validity of access tokens. It's recommended to also store this somewhere persistent, in a database for example. [Read more about refresh tokens here](refreshing-access-tokens.md). 81 | 82 | ### Step 3 83 | In a third file, `app.php`, tell the API wrapper which access token to use, and then make some API calls! 84 | 85 | ```php 86 | require 'vendor/autoload.php'; 87 | 88 | $api = new SpotifyWebAPI\SpotifyWebAPI(); 89 | 90 | // Fetch the saved access token from somewhere. A session for example. 91 | $api->setAccessToken($accessToken); 92 | 93 | // It's now possible to request data about the currently authenticated user 94 | print_r( 95 | $api->me() 96 | ); 97 | 98 | // Getting Spotify catalog data is of course also possible 99 | print_r( 100 | $api->getTrack('7EjyzZcbLxW7PaaLua9Ksb') 101 | ); 102 | ``` 103 | 104 | For more in-depth technical information about the Proof Key for Code Exchange flow, please refer to the [Spotify Web API documentation](https://developer.spotify.com/documentation/general/guides/authorization/code-flow/). 105 | -------------------------------------------------------------------------------- /docs/examples/controlling-user-playback.md: -------------------------------------------------------------------------------- 1 | # Controlling User Playback 2 | 3 | Using Spotify Connect, it's possible to control the playback of the currently authenticated user. 4 | 5 | ## Getting user devices 6 | 7 | The `SpotifyWebAPI::getMyDevices()` method can be used to list out a user's devices. 8 | 9 | ```php 10 | // Get the Devices 11 | $api->getMyDevices(); 12 | ``` 13 | 14 | It is worth noting that if all devices have `is_active` set to `false` then using `SpotifyWebApi::play()` will fail. 15 | 16 | ## Start and stop playback 17 | ```php 18 | // With Device ID 19 | $api->play($deviceId, [ 20 | 'uris' => ['TRACK_URI'], 21 | ]); 22 | 23 | // Without Device ID 24 | $api->play(false, [ 25 | 'uris' => ['TRACK_URI'], 26 | ]); 27 | 28 | $api->pause(); 29 | ``` 30 | 31 | ## Playing the next or previous track 32 | ```php 33 | $api->previous(); 34 | 35 | $api->next(); 36 | ``` 37 | 38 | ## Adding a track to the queue 39 | ```php 40 | $api->queue('TRACK_ID'); 41 | ``` 42 | 43 | ## Get info about the queue 44 | ```php 45 | $api->getMyQueue(); 46 | ``` 47 | 48 | ## Get the currently playing track 49 | ```php 50 | $api->getMyCurrentTrack(); 51 | ``` 52 | 53 | ## Get info about the current playback 54 | ```php 55 | $api->getMyCurrentPlaybackInfo(); 56 | ``` 57 | 58 | ## Move to a specific position in a track 59 | ```php 60 | $api->seek([ 61 | 'position_ms' => 60000 + 37000, // Move to the 1.37 minute mark 62 | ]); 63 | ``` 64 | 65 | ## Set repeat and shuffle mode 66 | ```php 67 | $api->repeat([ 68 | 'state' => 'track', 69 | ]); 70 | 71 | $api->shuffle([ 72 | 'state' => false, 73 | ]); 74 | ``` 75 | 76 | ## Control the volume 77 | ```php 78 | $api->changeVolume([ 79 | 'volume_percent' => 78, 80 | ]); 81 | ``` 82 | 83 | ## Retrying API calls 84 | Sometimes, a API call might return a `202 Accepted` response code. When this occurs, you should retry the request after a few seconds. For example: 85 | 86 | ```php 87 | try { 88 | $wasPaused = $api->pause(): 89 | 90 | if (!$wasPaused) { 91 | $lastResponse = $api->getLastResponse(); 92 | 93 | if ($lastResponse['status'] == 202) { 94 | // Perform some logic to retry the request after a few seconds 95 | } 96 | } 97 | } catch (Exception $e) { 98 | $reason = $e->getReason(); 99 | 100 | // Check the reason for the failure and handle the error 101 | } 102 | ``` 103 | 104 | Read more about working with Spotify Connect in the [Spotify API docs](https://developer.spotify.com/documentation/web-api/guides/using-connect-web-api/). 105 | -------------------------------------------------------------------------------- /docs/examples/fetching-album-information.md: -------------------------------------------------------------------------------- 1 | # Fetching Information About Albums 2 | 3 | There are a few methods for retrieving information about one or more albums from the Spotify catalog. For example, info about a albums's artist or all the tracks on an album. 4 | 5 | ## Getting info about a single album 6 | 7 | ```php 8 | $album = $api->getAlbum('ALBUM_ID'); 9 | 10 | echo '' . $album->name . ''; 11 | ``` 12 | 13 | ## Getting info about multiple albums 14 | 15 | ```php 16 | $albums = $api->getAlbums([ 17 | 'ALBUM_ID', 18 | 'ALBUM_ID', 19 | ]); 20 | 21 | foreach ($albums->albums as $album) { 22 | echo '' . $album->name . '
'; 23 | } 24 | ``` 25 | 26 | ## Getting all tracks on an album 27 | 28 | ```php 29 | $tracks = $api->getAlbumTracks('ALBUM_ID'); 30 | 31 | foreach ($tracks->items as $track) { 32 | echo '' . $track->name . '
'; 33 | } 34 | ``` 35 | 36 | Please see the [method reference](/docs/method-reference/SpotifyWebAPI.md) for more available options for each method. 37 | -------------------------------------------------------------------------------- /docs/examples/fetching-artist-information.md: -------------------------------------------------------------------------------- 1 | # Fetching Information About Artists 2 | 3 | There are a few methods for retrieving information about one or more albums from the Spotify catalog. For example, info about a single artist or an artist's top tracks in a country. 4 | 5 | ## Getting info about a single artist 6 | 7 | ```php 8 | $artist = $api->getArtist('ARTIST_ID'); 9 | 10 | echo '' . $artist->name . ''; 11 | ``` 12 | 13 | ## Getting info about multiple artists 14 | 15 | ```php 16 | $artists = $api->getArtists([ 17 | 'ARTIST_ID', 18 | 'ARTIST_ID', 19 | ]); 20 | 21 | foreach ($artists->artists as $artist) { 22 | echo '' . $artist->name . '
'; 23 | } 24 | ``` 25 | 26 | ## Getting an artist's albums 27 | 28 | ```php 29 | $albums = $api->getArtistAlbums('ARTIST_ID'); 30 | 31 | foreach ($albums->items as $album) { 32 | echo '' . $album->name . '
'; 33 | } 34 | ``` 35 | 36 | ## Getting an artist's related artists 37 | 38 | ```php 39 | $artists = $api->getArtistRelatedArtists('ARTIST_ID'); 40 | 41 | foreach ($artists->artists as $artist) { 42 | echo '' . $artist->name . '
'; 43 | } 44 | ``` 45 | 46 | ## Getting an artist’s top tracks in a country 47 | 48 | ```php 49 | $tracks = $api->getArtistTopTracks('ARTIST_ID', [ 50 | 'country' => 'se', 51 | ]); 52 | 53 | foreach ($tracks->tracks as $track) { 54 | echo '' . $track->name . '
'; 55 | } 56 | ``` 57 | 58 | ## Getting recommendations based on artists 59 | 60 | ```php 61 | $seedArtist = ['ARTIST_ID', 'ARTIST_ID']; 62 | 63 | $recommendations = $api->getRecommendations([ 64 | 'seed_artists' => $seedArtist, 65 | ]); 66 | 67 | print_r($recommendations); 68 | ``` 69 | 70 | It's also possible to fetch recommendations based on genres and tracks, see the [Spotify docs](https://developer.spotify.com/documentation/web-api/reference/get-recommendations) for more info. 71 | 72 | Please see the [method reference](/docs/method-reference/SpotifyWebAPI.md) for more available options for each method. 73 | -------------------------------------------------------------------------------- /docs/examples/fetching-audiobook-information.md: -------------------------------------------------------------------------------- 1 | # Fetching Information About Audiobooks 2 | 3 | There are a few methods for retrieving information about one or more audiobooks from the Spotify catalog. For example, the description of a audiobook or all of a audiobook's chapters. 4 | 5 | ## Getting info about a single audiobook 6 | 7 | ```php 8 | $audiobook = $api->getAudiobook('AUDIOBOOK_ID'); 9 | 10 | echo '' . $audiobook->name . ''; 11 | ``` 12 | 13 | ## Getting info about multiple audiobooks 14 | 15 | ```php 16 | $audiobooks = $api->getAudiobooks([ 17 | 'AUDIOBOOK_ID', 18 | 'AUDIOBOOK_ID', 19 | ]); 20 | 21 | foreach ($audiobooks->audiobooks as $audiobook) { 22 | echo '' . $audiobook->name . '
'; 23 | } 24 | ``` 25 | 26 | ## Getting info about a single audiobook chapter 27 | 28 | ```php 29 | $chapter = $api->getChapter('CHAPTER_ID'); 30 | 31 | echo '' . $chapter->name . ''; 32 | ``` 33 | 34 | ## Getting info about multiple audiobook chapters 35 | 36 | ```php 37 | $chapters = $api->getChapters([ 38 | 'CHAPTER_ID', 39 | 'CHAPTER_ID', 40 | ]); 41 | 42 | foreach ($chapters->chapters as $chapter) { 43 | echo '' . $chapter->name . '
'; 44 | } 45 | ``` 46 | 47 | Please see the [method reference](/docs/method-reference/SpotifyWebAPI.md) for more available options for each method. 48 | -------------------------------------------------------------------------------- /docs/examples/fetching-podcast-information.md: -------------------------------------------------------------------------------- 1 | # Fetching Information About Podcasts 2 | 3 | There are a few methods for retrieving information about one or more podcasts from the Spotify catalog. For example, the description of a podcast or all of a podcast's episodes. 4 | 5 | ## Getting info about a single podcast show 6 | 7 | ```php 8 | $show = $api->getShow('SHOW_ID'); 9 | 10 | echo '' . $show->name . ''; 11 | ``` 12 | 13 | ## Getting info about multiple podcast shows 14 | 15 | ```php 16 | $shows = $api->getShows([ 17 | 'SHOW_ID', 18 | 'SHOW_ID', 19 | ]); 20 | 21 | foreach ($shows->shows as $show) { 22 | echo '' . $show->name . '
'; 23 | } 24 | ``` 25 | 26 | ## Getting info about a single podcast episode 27 | 28 | ```php 29 | $episode = $api->getEpisode('EPISODE_ID'); 30 | 31 | echo '' . $episode->name . ''; 32 | ``` 33 | 34 | ## Getting info about multiple podcast episodes 35 | 36 | ```php 37 | $episodes = $api->getEpisodeS([ 38 | 'EPISODE_ID', 39 | 'EPISODE_ID', 40 | ]); 41 | 42 | foreach ($episodes->episodes as $episode) { 43 | echo '' . $episode->name . '
'; 44 | } 45 | ``` 46 | 47 | ## Getting a podcast show's episodes 48 | 49 | ```php 50 | $episodes = $api->getShowEpisodes('SHOW_ID'); 51 | 52 | foreach ($episodes->items as $episode) { 53 | echo '' . $episode->name . '
'; 54 | } 55 | ``` 56 | 57 | Please see the [method reference](/docs/method-reference/SpotifyWebAPI.md) for more available options for each method. 58 | -------------------------------------------------------------------------------- /docs/examples/fetching-spotify-featured-content.md: -------------------------------------------------------------------------------- 1 | # Fetching Spotify Featured Content 2 | 3 | If you wish to access content that's featured and/or curated by Spotify there are a number of methods available to achieve that. 4 | 5 | ## Getting a list of new releases 6 | 7 | ```php 8 | $releases = $api->getNewReleases([ 9 | 'country' => 'se', 10 | ]); 11 | 12 | foreach ($releases->albums->items as $album) { 13 | echo '' . $album->name . '
'; 14 | } 15 | ``` 16 | 17 | ## Getting a list of featured playlists 18 | 19 | ```php 20 | $playlists = $api->getFeaturedPlaylists([ 21 | 'country' => 'se', 22 | 'locale' => 'sv_SE', 23 | 'timestamp' => '2015-01-17T21:00:00', // Saturday night 24 | ]); 25 | 26 | foreach ($playlists->playlists->items as $playlist) { 27 | echo '' . $playlist->name . '
'; 28 | } 29 | ``` 30 | 31 | ## Getting a list of Spotify categories 32 | 33 | ```php 34 | $categories = $api->getCategoriesList([ 35 | 'country' => 'se', 36 | 'locale' => 'sv_SE', 37 | 'limit' => 10, 38 | 'offset' => 0, 39 | ]); 40 | 41 | foreach ($categories->categories->items as $category) { 42 | echo '' . $category->name . '
'; 43 | } 44 | 45 | ## Getting a single Spotify category 46 | 47 | ```php 48 | $category = $api->getCategory('dinner', [ 49 | 'country' => 'se', 50 | ]); 51 | 52 | echo '' . $category->name . ''; 53 | ``` 54 | 55 | ## Getting a category's playlists 56 | 57 | ```php 58 | $playlists = $api->getCategoryPlaylists('dinner', [ 59 | 'country' => 'se', 60 | 'limit' => 10, 61 | 'offset' => 0 62 | ]); 63 | 64 | foreach ($playlists->playlists->items as $playlist) { 65 | echo '' . $playlist->name . '
'; 66 | } 67 | ``` 68 | 69 | Please see the [method reference](/docs/method-reference/SpotifyWebAPI.md) for more available options for each method. 70 | -------------------------------------------------------------------------------- /docs/examples/fetching-track-information.md: -------------------------------------------------------------------------------- 1 | # Fetching Information About Tracks 2 | 3 | There are a few methods for retrieving information about one or more albums from the Spotify catalog. For example, info about a track's artist or recommendations on similar tracks. 4 | 5 | ## Getting info about a single track 6 | 7 | ```php 8 | $track = $api->getTrack('TRACK_ID'); 9 | 10 | echo '' . $track->name . ' by ' . $track->artists[0]->name . ''; 11 | ``` 12 | 13 | ## Getting info about multiple tracks 14 | 15 | ```php 16 | $tracks = $api->getTracks([ 17 | 'TRACK_ID', 18 | 'TRACK_ID', 19 | ]); 20 | 21 | foreach ($tracks->tracks as $track) { 22 | echo '' . $track->name . ' by ' . $track->artists[0]->name . '
'; 23 | } 24 | ``` 25 | 26 | ## Getting the audio analysis of a track 27 | 28 | ```php 29 | $analysis = $api->getAudioAnalysis('TRACK_ID'); 30 | 31 | print_r($analysis); 32 | ``` 33 | 34 | ## Getting the audio features of a track 35 | 36 | ```php 37 | $features = $api->getAudioFeatures('TRACK_ID'); 38 | 39 | print_r($features); 40 | ``` 41 | 42 | ## Getting the audio features of multiple tracks 43 | 44 | ```php 45 | $tracks = [ 46 | 'TRACK_ID', 47 | 'TRACK_ID', 48 | ]; 49 | 50 | $features = $api->getMultipleAudioFeatures($tracks); 51 | 52 | print_r($features); 53 | ``` 54 | 55 | ## Getting recommendations based on tracks 56 | 57 | ```php 58 | $seedTracks = ['TRACK_ID', 'TRACK_ID']; 59 | 60 | $recommendations = $api->getRecommendations([ 61 | 'seed_tracks' => $seedTracks, 62 | ]); 63 | 64 | print_r($recommendations); 65 | ``` 66 | 67 | It's also possible to fetch recommendations based on genres and artists, see the [Spotify docs](https://developer.spotify.com/documentation/web-api/reference/get-recommendations) for more info. 68 | 69 | Please see the [method reference](/docs/method-reference/SpotifyWebAPI.md) for more available options for each method. 70 | -------------------------------------------------------------------------------- /docs/examples/following-artists-playlists-and-users.md: -------------------------------------------------------------------------------- 1 | # Following Artists, Playlists, and Users 2 | 3 | A Spotify user can follow artists, playlists, and users. The API contains methods for all of this functionality. 4 | 5 | ## Following an artist or user 6 | 7 | ```php 8 | $api->followArtistsOrUsers('artist', 'ARTIST_ID'); 9 | ``` 10 | 11 | ## Unfollowing an artist or user 12 | 13 | ```php 14 | $api->unfollowArtistsOrUsers('artist', 'ARTIST_ID'); 15 | ``` 16 | 17 | ## Checking if a user is following an artist or user 18 | 19 | ```php 20 | $following = $api->currentUserFollows('user', 'spotify'); 21 | 22 | var_dump($following); 23 | ``` 24 | 25 | ## Following a playlist 26 | 27 | ```php 28 | $api->followPlaylist('PLAYLIST_ID'); 29 | ``` 30 | 31 | ## Unfollowing a playlist 32 | 33 | ```php 34 | $api->unfollowPlaylist('PLAYLIST_ID'); 35 | ``` 36 | 37 | ## Checking if user(s) are following a playlist 38 | 39 | ```php 40 | $users = [ 41 | 'USER_1', 42 | 'USER_2', 43 | ]; 44 | 45 | $api->usersFollowsPlaylist('PLAYLIST_ID', [ 46 | 'ids' => $users, 47 | ]); 48 | ``` 49 | 50 | Please see the [method reference](/docs/method-reference/SpotifyWebAPI.md) for more available options for each method. 51 | -------------------------------------------------------------------------------- /docs/examples/handling-errors.md: -------------------------------------------------------------------------------- 1 | # Handling Errors 2 | 3 | Whenever the API returns a error of some sort, a `SpotifyWebAPIException` extending from the native [PHP Exception](http://php.net/manual/en/language.exceptions.php) will be thrown. 4 | 5 | The `message` property will be set to the error message returned by the Spotify API and the `code` property will be set to the HTTP status code returned by the Spotify API. 6 | 7 | ```php 8 | try { 9 | $track = $api->getTrack('non-existing-track'); 10 | } catch (SpotifyWebAPI\SpotifyWebAPIException $e) { 11 | echo 'Spotify API Error: ' . $e->getCode(); // Will be 404 12 | } 13 | ``` 14 | 15 | When an authentication error occurs, a `SpotifyWebAPIAuthException` will be thrown. This will contain the same properties as above. 16 | 17 | ## Handling expired access tokens 18 | _As of version `2.11.0` it's possible to automatically refresh expired access tokens. [Read more here](refreshing-access-tokens.md#automatically-refreshing-access-tokens)._ 19 | 20 | When the access token has expired you'll get an error back. The `SpotifyWebAPIException` class supplies a helper method to easily check if an expired access token is the issue. 21 | 22 | ```php 23 | try { 24 | $track = $api->me(); 25 | } catch (SpotifyWebAPI\SpotifyWebAPIException $e) { 26 | if ($e->hasExpiredToken()) { 27 | // Refresh the access token 28 | } else { 29 | // Some other kind of error 30 | } 31 | } 32 | ``` 33 | 34 | Read more about how to [refresh access tokens](refreshing-access-tokens.md). 35 | 36 | ## Handling rate limit errors 37 | _As of version `2.12.0` it's possible to automatically retry rate limited requests by setting the `auto_retry` option to `true`._ 38 | 39 | If your application should hit the Spotify API rate limit, you will get an error back and the number of seconds you need to wait before sending another request. 40 | 41 | Here's an example of how to handle this: 42 | 43 | ```php 44 | try { 45 | $track = $api->getTrack('7EjyzZcbLxW7PaaLua9Ksb'); 46 | } catch (SpotifyWebAPI\SpotifyWebAPIException $e) { 47 | if ($e->getCode() == 429) { // 429 is Too Many Requests 48 | $lastResponse = $api->getRequest()->getLastResponse(); 49 | 50 | $retryAfter = $lastResponse['headers']['retry-after']; // Number of seconds to wait before sending another request 51 | } else { 52 | // Some other kind of error 53 | } 54 | } 55 | ``` 56 | 57 | Read more about the exact mechanics of rate limiting in the [Spotify API docs](https://developer.spotify.com/documentation/web-api/guides/rate-limits/). 58 | -------------------------------------------------------------------------------- /docs/examples/managing-user-library.md: -------------------------------------------------------------------------------- 1 | # Managing a User's Library 2 | 3 | There are lots of operations involving a user's library that can be performed. Remember to request the correct [scopes](working-with-scopes.md) beforehand. 4 | 5 | ## Listing the tracks in a user's library 6 | 7 | ```php 8 | $tracks = $api->getMySavedTracks([ 9 | 'limit' => 5, 10 | ]); 11 | 12 | foreach ($tracks->items as $track) { 13 | $track = $track->track; 14 | 15 | echo '' . $track->name . '
'; 16 | } 17 | ``` 18 | 19 | It's also possible to list the albums, podcast episodes, or podcast shows in a user's library using `getMySavedAlbums`, `getMySavedEpisodes`, or `getMySavedShows`. 20 | 21 | ## Adding tracks to a user's library 22 | 23 | ```php 24 | $api->addMyTracks([ 25 | 'TRACK_ID', 26 | 'TRACK_ID', 27 | ]); 28 | ``` 29 | 30 | It's also possible to add an album, a podcast episode, or a podcast show to a user's library using `addMyAlbums`, `addMyEpisodes`, or `addMyShows`. 31 | 32 | ## Deleting tracks from a user's library 33 | 34 | ```php 35 | $api->deleteMyTracks([ 36 | 'TRACK_ID', 37 | 'TRACK_ID', 38 | ]); 39 | ``` 40 | 41 | It's also possible to delete an album, a podcast episode, or a podcast show from a user's library using `deleteMyAlbums`, `deleteMyEpisodes`, or `deleteMyShows`. 42 | 43 | ## Checking if tracks are present in a user's library 44 | 45 | ```php 46 | $contains = $api->myTracksContains([ 47 | 'TRACK_ID', 48 | 'TRACK_ID', 49 | ]); 50 | 51 | var_dump($contains); 52 | ``` 53 | 54 | It's also possible to check if an album, a podcast episode, or a podcast show is present in a user's library using `myAlbumsContains`, `myEpisodesContains`, or `myShowsContains`. 55 | 56 | Please see the [method reference](/docs/method-reference/SpotifyWebAPI.md) for more available options for each method. 57 | -------------------------------------------------------------------------------- /docs/examples/managing-user-playlists.md: -------------------------------------------------------------------------------- 1 | # Managing a User's Playlists 2 | 3 | There are lots of operations involving user's playlists that can be performed. Remember to request the correct [scopes](working-with-scopes.md) beforehand. 4 | 5 | ## Listing a user's playlists 6 | 7 | ```php 8 | $playlists = $api->getUserPlaylists('USER_ID', [ 9 | 'limit' => 5 10 | ]); 11 | 12 | foreach ($playlists->items as $playlist) { 13 | echo '' . $playlist->name . '
'; 14 | } 15 | ``` 16 | 17 | ## Getting info about a specific playlist 18 | 19 | ```php 20 | $playlist = $api->getPlaylist('PLAYLIST_ID'); 21 | 22 | echo $playlist->name; 23 | ``` 24 | 25 | ## Getting the image of a user's playlist 26 | ```php 27 | $playlistImage = $api->getPlaylistImage('PLAYLIST_ID'); 28 | ``` 29 | 30 | ## Getting all tracks in a playlist 31 | 32 | ```php 33 | $playlistTracks = $api->getPlaylistTracks('PLAYLIST_ID'); 34 | 35 | foreach ($playlistTracks->items as $track) { 36 | $track = $track->track; 37 | 38 | echo '' . $track->name . '
'; 39 | } 40 | ``` 41 | 42 | ## Creating a new playlist 43 | 44 | ```php 45 | $api->createPlaylist('USER_ID', [ 46 | 'name' => 'My shiny playlist' 47 | ]); 48 | ``` 49 | 50 | ## Updating the details of a user's playlist 51 | 52 | ```php 53 | $api->updatePlaylist('PLAYLIST_ID', [ 54 | 'name' => 'New name' 55 | ]); 56 | ``` 57 | 58 | ## Updating the image of a user's playlist 59 | ```php 60 | $imageData = base64_encode(file_get_contents('image.jpg')); 61 | 62 | $api->updatePlaylistImage('PLAYLIST_ID', $imageData); 63 | ``` 64 | 65 | ## Adding tracks to a user's playlist 66 | 67 | ```php 68 | $api->addPlaylistTracks('PLAYLIST_ID', [ 69 | 'TRACK_ID', 70 | 'EPISODE_URI' 71 | ]); 72 | ``` 73 | 74 | ## Delete tracks from a user's playlist based on IDs 75 | 76 | ```php 77 | $tracks = [ 78 | 'tracks' => [ 79 | ['uri' => 'TRACK_ID'], 80 | ['uri' => 'EPISODE_URI'], 81 | ], 82 | ]; 83 | 84 | $api->deletePlaylistTracks('PLAYLIST_ID', $tracks, 'SNAPSHOT_ID'); 85 | ``` 86 | 87 | ## Delete tracks from a user's playlist based on positions 88 | 89 | ```php 90 | $trackOptions = [ 91 | 'positions' => [ 92 | 5, 93 | 12, 94 | ], 95 | ]; 96 | 97 | $api->deletePlaylistTracks('PLAYLIST_ID', $trackOptions, 'SNAPSHOT_ID'); 98 | ``` 99 | 100 | ## Replacing all tracks in a user's playlist with new ones 101 | 102 | ```php 103 | $api->replacePlaylistTracks('PLAYLIST_ID', [ 104 | 'TRACK_ID', 105 | 'EPISODE_URI' 106 | ]); 107 | ``` 108 | 109 | ## Reorder the tracks in a user's playlist 110 | 111 | ```php 112 | $api->reorderPlaylistTracks('PLAYLIST_ID', [ 113 | 'range_start' => 1, 114 | 'range_length' => 5, 115 | 'insert_before' => 10, 116 | 'snapshot_id' => 'SNAPSHOT_ID' 117 | ]); 118 | ``` 119 | 120 | Please see the [method reference](/docs/method-reference/SpotifyWebAPI.md) for more available options for each method. 121 | -------------------------------------------------------------------------------- /docs/examples/managing-user-profiles.md: -------------------------------------------------------------------------------- 1 | # Managing a User's Profile 2 | 3 | There are lots of operations involving a user's profile that can be performed. Remember to request the correct [scopes](working-with-scopes.md) beforehand. 4 | 5 | ### Getting the current user's profile 6 | 7 | ```php 8 | $me = $api->me(); 9 | 10 | echo $me->display_name; 11 | ``` 12 | 13 | ### Getting any user's profile 14 | 15 | ```php 16 | $user = $api->getUser('USER_ID'); 17 | 18 | echo $user->display_name; 19 | ``` 20 | 21 | Please see the [method reference](/docs/method-reference/SpotifyWebAPI.md) for more available options for each method. 22 | -------------------------------------------------------------------------------- /docs/examples/passing-a-custom-request-instance.md: -------------------------------------------------------------------------------- 1 | # Passing a Custom Request Instance 2 | 3 | Sometimes you want to pass a custom `Request` instance to `SpotifyWebAPI`. For example to use another HTTP library or provide additional logging. 4 | 5 | This is possible by extending the `Request` class and providing your own `send` method. 6 | 7 | ```php 8 | class MyRequest extends SpotifyWebAPI\Request 9 | { 10 | public function send(string $method, string $url, string|array|object $parameters = [], array $headers = []): array 11 | { 12 | // Do your thing here 13 | 14 | // But be sure to set the lastResponse property for other parts to work correctly 15 | $this->lastResponse = [ 16 | 'body' => $body, // The JSON response body parsed to a PHP value 17 | 'headers' => $headers, // An array of the headers returned 18 | 'status' => $status, // The HTTP response status code 19 | 'url' => $url, // The requested URL 20 | ]; 21 | 22 | return $this->lastResponse; 23 | } 24 | } 25 | ``` 26 | 27 | Then when you wish to use it, pass it to the `SpotifyWebAPI` and `Session` constructors. 28 | 29 | ```php 30 | $request = new MyRequest(); 31 | 32 | $session = new SpotifyWebAPI\Session( 33 | 'CLIENT_ID', 34 | 'CLIENT_SECRET', 35 | 'REDIRECT_URI', 36 | $request 37 | ); 38 | 39 | $api = new SpotifyWebAPI\SpotifyWebAPI( 40 | $options, 41 | $session, 42 | $request 43 | ); 44 | ``` 45 | 46 | ## Helper functions 47 | The `Request` class provides a few helper functions that can be useful when working with the responses from Spotify. 48 | 49 | ### handleResponseError 50 | `protected function handleResponseError(string $body, int $status): void` 51 | 52 | Takes the unparsed response body and tries to figure out what kind of error occured in order to provide some additional info in the error thrown. 53 | 54 | ### parseBody 55 | `protected function parseBody(string $body): mixed` 56 | 57 | Takes the unparsed response body and parses it, taking the [`return_assoc`](/docs/examples/setting-options.md#return_assoc) option into account. 58 | 59 | ### parseHeaders 60 | `protected function parseHeaders(string $headers): array` 61 | 62 | Takes the HTTP header block, parses it to a key-value array while normalizing header names. If you're using an external HTTP library it will most definitely already include a method for this. 63 | 64 | ### splitResponse 65 | `protected function splitResponse(string $response): array` 66 | 67 | Takes the full HTTP response and splits it into `headers` and `body` while stripping additional headers sometimes added by proxy servers. 68 | -------------------------------------------------------------------------------- /docs/examples/refreshing-access-tokens.md: -------------------------------------------------------------------------------- 1 | # Refreshing Access Tokens 2 | When requesting access tokens using the [Authorization Code](access-token-with-authorization-code-flow.md) or [Proof Key for Code Exchange (PKCE)](access-token-with-pkce-flow.md) flows, a _refresh token_ will also be included. This token can be used to request a new access token when the previous one has expired, but without any user interaction. 3 | 4 | ```php 5 | require 'vendor/autoload.php'; 6 | 7 | $session = new SpotifyWebAPI\Session( 8 | 'CLIENT_ID', 9 | 'CLIENT_SECRET', 10 | 'REDIRECT_URI' 11 | ); 12 | 13 | // Fetch the refresh token from somewhere. A database for example. 14 | 15 | $session->refreshAccessToken($refreshToken); 16 | 17 | $accessToken = $session->getAccessToken(); 18 | $refreshToken = $session->getRefreshToken(); 19 | 20 | // Set our new access token on the API wrapper and continue to use the API as usual 21 | $api->setAccessToken($accessToken); 22 | 23 | // Store the new refresh token somewhere for later use 24 | ``` 25 | 26 | ## Automatically Refreshing Access Tokens 27 | _Note: This feature is available as of version `2.11.0`._ 28 | 29 | Start off by requesting an access token as usual. But instead of setting the access token on a `SpotifyWebAPI` instance, pass the complete `Session` instance when initializing a new `SpotifyWebAPI` instance or by using the `setSession()` method. Remember to also set the `auto_refresh` option to `true`. For example: 30 | 31 | ```php 32 | $session = new SpotifyWebAPI\Session( 33 | 'CLIENT_ID', 34 | 'CLIENT_SECRET', 35 | 'REDIRECT_URI' 36 | ); 37 | 38 | $options = [ 39 | 'auto_refresh' => true, 40 | ]; 41 | 42 | $api = new SpotifyWebAPI\SpotifyWebAPI($options, $session); 43 | 44 | // You can also call setSession on an existing SpotifyWebAPI instance 45 | $api->setSession($session); 46 | 47 | // Call the API as usual 48 | $api->me(); 49 | 50 | // Remember to grab the tokens afterwards, they might have been updated 51 | $newAccessToken = $session->getAccessToken(); 52 | $newRefreshToken = $session->getRefreshToken(); 53 | ``` 54 | 55 | ### With an existing refresh token 56 | 57 | When you already have existing access and refresh tokens, add them to the `Session` instance and call the API. 58 | 59 | ```php 60 | $session = new SpotifyWebAPI\Session( 61 | 'CLIENT_ID', 62 | 'CLIENT_SECRET' 63 | ); 64 | 65 | // Use previously requested tokens fetched from somewhere. A database for example. 66 | if ($accessToken) { 67 | $session->setAccessToken($accessToken); 68 | $session->setRefreshToken($refreshToken); 69 | } else { 70 | // Or request a new access token 71 | $session->refreshAccessToken($refreshToken); 72 | } 73 | 74 | $options = [ 75 | 'auto_refresh' => true, 76 | ]; 77 | 78 | $api = new SpotifyWebAPI\SpotifyWebAPI($options, $session); 79 | 80 | // You can also call setSession on an existing SpotifyWebAPI instance 81 | $api->setSession($session); 82 | 83 | // Call the API as usual 84 | $api->me(); 85 | 86 | // Remember to grab the tokens afterwards, they might have been updated 87 | $newAccessToken = $session->getAccessToken(); 88 | $newRefreshToken = $session->getRefreshToken(); 89 | ``` 90 | -------------------------------------------------------------------------------- /docs/examples/searching-the-spotify-catalog.md: -------------------------------------------------------------------------------- 1 | # Searching the Spotify Catalog 2 | 3 | The whole Spotify catalog, including playlists, can be searched in various ways. Since the Spotify search contains so many features, this page just includes a basic example and one should refer to the 4 | [Spotify documentation](https://developer.spotify.com/documentation/web-api/reference/search) and [method reference](/docs/method-reference/SpotifyWebAPI.md) for more information. 5 | 6 | ```php 7 | $results = $api->search('blur', 'artist'); 8 | 9 | foreach ($results->artists->items as $artist) { 10 | echo $artist->name, '
'; 11 | } 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/examples/setting-custom-curl-options.md: -------------------------------------------------------------------------------- 1 | # Setting custom cURL options 2 | 3 | Sometimes, you need to override the default cURL options. For example increasing the timeout or setting some proxy setting. 4 | 5 | In order to set custom cURL options, you'll need to instantiate a `Request` object yourself and passing it to `SpotifyWebAPI` instead of letting it set it up itself. 6 | 7 | For example: 8 | ```php 9 | $options = [ 10 | 'curl_options' => [ 11 | CURLOPT_TIMEOUT => 60, 12 | ], 13 | ]; 14 | 15 | $request = new SpotifyWebAPI\Request($options); 16 | 17 | // You can also call setOptions on an existing Request instance 18 | $request->setOptions($options); 19 | 20 | // Then, pass the $request when instantiating Session and SpotifyWebAPI 21 | $session = new SpotifyWebAPI\Session( 22 | 'CLIENT_ID', 23 | 'CLIENT_SECRET', 24 | 'REDIRECT_URI', 25 | $request 26 | ); 27 | 28 | $api = new SpotifyWebAPI\SpotifyWebAPI([], null, $request); 29 | 30 | // And continue as usual 31 | ``` 32 | 33 | The options you pass in `curl_options` will be merged with the default ones and existing options with the same key will be overwritten by the ones passed by you. 34 | 35 | Refer to the [PHP docs](https://www.php.net/manual/en/function.curl-setopt.php) for a complete list of cURL options. 36 | -------------------------------------------------------------------------------- /docs/examples/setting-options.md: -------------------------------------------------------------------------------- 1 | # Setting options 2 | 3 | There are a few options that can be used to control the behaviour of the API. All options can be set when initializing a new `SpotifyWebAPI` instance or by using the `setOptions()` method. Both approaches will merge the new options with the defaults and multiple calls to `setOptions()` will merge the new options with the ones already set. 4 | 5 | ```php 6 | $options = [ 7 | 'auto_refresh' => true, 8 | ]; 9 | 10 | // Options can be set using the SpotifyWebAPI constructor 11 | $api = new SpotifyWebAPI\SpotifyWebAPI($options); 12 | 13 | // Or by using the setOptions method 14 | $api->setOptions($options); 15 | ``` 16 | 17 | ## Available options 18 | 19 | ### `auto_refresh` 20 | 21 | * Possible values: `true`/`false` (default) 22 | 23 | Used to control [automatic refresh of access tokens](refreshing-access-tokens.md#automatically-refreshing-access-tokens). 24 | 25 | ### `auto_retry` 26 | 27 | * Possible values: `true`/`false` (default) 28 | 29 | Used to control automatic retries of [rate limited requests](https://developer.spotify.com/documentation/web-api/guides/rate-limits/). 30 | 31 | ### `return_assoc` 32 | 33 | * Possible values: `true`/`false` (default) 34 | 35 | Used to control return type of API calls. Setting it to `true` will return associative arrays instead of objects. 36 | -------------------------------------------------------------------------------- /docs/examples/working-with-scopes.md: -------------------------------------------------------------------------------- 1 | # Working with Scopes 2 | 3 | All operations involving a user or their information requires your app to request one or more scopes. You request scopes at the same time as the access token, for example: 4 | 5 | ```php 6 | require 'vendor/autoload.php'; 7 | 8 | $session = new SpotifyWebAPI\Session( 9 | 'CLIENT_ID', 10 | 'CLIENT_SECRET', 11 | 'REDIRECT_URI' 12 | ); 13 | 14 | $options = [ 15 | 'scope' => [ 16 | 'user-library-modify', 17 | 'user-read-birthdate', 18 | ], 19 | ]; 20 | 21 | header('Location: ' . $session->getAuthorizeUrl($options)); 22 | die(); 23 | ``` 24 | 25 | It's possible to request more scopes at any time, simply add new ones to the list. This will ask the user to approve your app again. 26 | 27 | Please refer to the Spotify docs for a full list of [all available scopes](https://developer.spotify.com/documentation/general/guides/authorization/scopes/). 28 | 29 | ## Checking Requested Scopes 30 | If you wish to check which scopes are granted for an access token, the `Session::getScope()` method can be used. For example, put the following code in your callback where the user will be redirected to by Spotify: 31 | 32 | ```php 33 | $accessToken = $session->requestAccessToken($_GET['code']); 34 | $scopes = $session->getScope(); 35 | ``` 36 | 37 | The same method can also be used after refreshing an access token, for example: 38 | 39 | ```php 40 | $session->refreshAccessToken($refreshToken); 41 | 42 | $scopes = $session->getScope(); 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Requirements 4 | * PHP 8.1 or later. 5 | * PHP [cURL extension](http://php.net/manual/en/book.curl.php) (Usually included with PHP). 6 | 7 | ## Autoloading 8 | The Spotify Web API for PHP is compatible with [PSR-4](http://www.php-fig.org/psr/psr-4/). This means that the code makes heavy use of namespaces and the correct files can be loaded automatically. All examples throughout this documentation will assume the use of a PSR-4 compatible autoloader, for example via [Composer](https://getcomposer.org/). 9 | 10 | ## Installation 11 | 12 | ### Installation via Composer 13 | This is the preferred way of installing the Spotify Web API for PHP. Run the following command in the root of your project: 14 | 15 | ```sh 16 | composer require jwilsson/spotify-web-api-php 17 | ``` 18 | 19 | Then, in every file where you wish to use the Spotify Web API for PHP, include the following line: 20 | 21 | ```php 22 | require_once 'vendor/autoload.php'; 23 | ``` 24 | 25 | ### Manual installation 26 | 27 | Download the latest release from the [releases page](https://github.com/jwilsson/spotify-web-api-php/releases). Unzip the files somewhere in your project and include a [PSR-4 compatible autoloader](http://www.php-fig.org/psr/psr-4/examples/) in your project. 28 | 29 | ## Configuration and setup 30 | First off, make sure you've created an app on [Spotify's developer site](https://developer.spotify.com/documentation/web-api/). 31 | 32 | *Note: Applications created after 2021-05-27 [might need to perform some extra steps](https://developer.spotify.com/community/news/2021/05/27/improving-the-developer-and-user-experience-for-third-party-apps/).* 33 | 34 | Now, before sending requests to Spotify, we need to create a session using your app info: 35 | 36 | ```php 37 | $session = new SpotifyWebAPI\Session( 38 | 'CLIENT_ID', 39 | 'CLIENT_SECRET', 40 | 'REDIRECT_URI' 41 | ); 42 | ``` 43 | 44 | Replace the values here with the ones given to you from Spotify. 45 | 46 | ## Authentication and authorization 47 | After creating a session it's time to request access to the Spotify Web API. There are three ways to request an access token. 48 | 49 | The first method is called *Proof Key for Code Exchange (PKCE)* and requires some interaction from the user, but in turn gives you some access to the user's account. This is the recommended method if you need access to a user's account. 50 | 51 | The second method is called the *Authorization Code Flow* and just like the PKCE method it requires some interaction from the user, but will also give you access to the user's account. 52 | 53 | The last method is called the *Client Credentials Flow* and doesn't require any user interaction but also doesn't provide any user information. This method is the recommended one if you just need access to Spotify catalog data. 54 | 55 | For more info about each authorization method, checkout these examples: 56 | * [Obtaining an access token using the Proof Key for Code Exchange (PKCE) Flow](/docs/examples/access-token-with-pkce-flow.md) 57 | * [Obtaining an access token using the Authorization Code Flow](/docs/examples/access-token-with-authorization-code-flow.md) 58 | * [Obtaining an access token using the Client Credentials Flow](/docs/examples/access-token-with-client-credentials-flow.md) 59 | 60 | ## Making requests to the Spotify API 61 | Assuming you've followed one of the authorization guides above and successfully requested an access token, now it's time to create a new file called `app.php`. In this file we'll tell the API wrapper about the access token to use and then request some data from Spotify! 62 | 63 | ```php 64 | require 'vendor/autoload.php'; 65 | 66 | // Fetch your access token from somewhere. A session for example. 67 | 68 | $api = new SpotifyWebAPI\SpotifyWebAPI(); 69 | $api->setAccessToken($accessToken); 70 | 71 | print_r( 72 | $api->getTrack('4uLU6hMCjMI75M1A2tKUQC') 73 | ); 74 | ``` 75 | 76 | Congratulations! You now know how to use the Spotify Web API for PHP. The next step is to check out [some examples](/docs/examples/) and the [method reference](/docs/method-reference/). 77 | -------------------------------------------------------------------------------- /docs/method-reference/Request.md: -------------------------------------------------------------------------------- 1 | # Request 2 | 3 | ## Table of Contents 4 | * [__construct](#__construct) 5 | * [account](#account) 6 | * [api](#api) 7 | * [getLastResponse](#getlastresponse) 8 | * [send](#send) 9 | * [setOptions](#setoptions) 10 | 11 | ## Constants 12 | * **ACCOUNT_URL** 13 | * **API_URL** 14 | 15 | ## Methods 16 | ### __construct 17 | 18 | 19 | ```php 20 | Request::__construct($options) 21 | ``` 22 | 23 | Constructor
24 | Set options. 25 | 26 | #### Arguments 27 | * `$options` **array\|object** - Optional. Options to set. 28 | 29 | 30 | --- 31 | ### account 32 | 33 | 34 | ```php 35 | Request::account($method, $uri, $parameters, $headers) 36 | ``` 37 | 38 | Make a request to the "account" endpoint. 39 | 40 | #### Arguments 41 | * `$method` **string** - The HTTP method to use. 42 | * `$uri` **string** - The URI to request. 43 | * `$parameters` **string\|array** - Optional. Query string parameters or HTTP body, depending on $method. 44 | * `$headers` **array** - Optional. HTTP headers. 45 | 46 | #### Return values 47 | * **array** Response data. 48 | * array\|object body The response body. Type is controlled by the `return_assoc` option. 49 | * array headers Response headers. 50 | * int status HTTP status code. 51 | * string url The requested URL. 52 | 53 | --- 54 | ### api 55 | 56 | 57 | ```php 58 | Request::api($method, $uri, $parameters, $headers) 59 | ``` 60 | 61 | Make a request to the "api" endpoint. 62 | 63 | #### Arguments 64 | * `$method` **string** - The HTTP method to use. 65 | * `$uri` **string** - The URI to request. 66 | * `$parameters` **string\|array** - Optional. Query string parameters or HTTP body, depending on $method. 67 | * `$headers` **array** - Optional. HTTP headers. 68 | 69 | #### Return values 70 | * **array** Response data. 71 | * array\|object body The response body. Type is controlled by the `return_assoc` option. 72 | * array headers Response headers. 73 | * int status HTTP status code. 74 | * string url The requested URL. 75 | 76 | --- 77 | ### getLastResponse 78 | 79 | 80 | ```php 81 | Request::getLastResponse() 82 | ``` 83 | 84 | Get the latest full response from the Spotify API. 85 | 86 | 87 | #### Return values 88 | * **array** Response data. 89 | * array\|object body The response body. Type is controlled by the `return_assoc` option. 90 | * array headers Response headers. 91 | * int status HTTP status code. 92 | * string url The requested URL. 93 | 94 | --- 95 | ### send 96 | 97 | 98 | ```php 99 | Request::send($method, $url, $parameters, $headers) 100 | ``` 101 | 102 | Make a request to Spotify.
103 | You'll probably want to use one of the convenience methods instead. 104 | 105 | #### Arguments 106 | * `$method` **string** - The HTTP method to use. 107 | * `$url` **string** - The URL to request. 108 | * `$parameters` **string\|array\|object** - Optional. Query string parameters or HTTP body, depending on $method. 109 | * `$headers` **array** - Optional. HTTP headers. 110 | 111 | #### Return values 112 | * **array** Response data. 113 | * array\|object body The response body. Type is controlled by the `return_assoc` option. 114 | * array headers Response headers. 115 | * int status HTTP status code. 116 | * string url The requested URL. 117 | 118 | --- 119 | ### setOptions 120 | 121 | 122 | ```php 123 | Request::setOptions($options) 124 | ``` 125 | 126 | Set options 127 | 128 | #### Arguments 129 | * `$options` **array\|object** - Options to set. 130 | 131 | #### Return values 132 | * **self** 133 | 134 | --- 135 | -------------------------------------------------------------------------------- /docs/method-reference/Session.md: -------------------------------------------------------------------------------- 1 | # Session 2 | 3 | ## Table of Contents 4 | * [__construct](#__construct) 5 | * [generateCodeChallenge](#generatecodechallenge) 6 | * [generateCodeVerifier](#generatecodeverifier) 7 | * [generateState](#generatestate) 8 | * [getAuthorizeUrl](#getauthorizeurl) 9 | * [getAccessToken](#getaccesstoken) 10 | * [getClientId](#getclientid) 11 | * [getClientSecret](#getclientsecret) 12 | * [getTokenExpiration](#gettokenexpiration) 13 | * [getRedirectUri](#getredirecturi) 14 | * [getRefreshToken](#getrefreshtoken) 15 | * [getScope](#getscope) 16 | * [refreshAccessToken](#refreshaccesstoken) 17 | * [requestAccessToken](#requestaccesstoken) 18 | * [requestCredentialsToken](#requestcredentialstoken) 19 | * [setAccessToken](#setaccesstoken) 20 | * [setClientId](#setclientid) 21 | * [setClientSecret](#setclientsecret) 22 | * [setRedirectUri](#setredirecturi) 23 | * [setRefreshToken](#setrefreshtoken) 24 | 25 | ## Constants 26 | 27 | ## Methods 28 | ### __construct 29 | 30 | 31 | ```php 32 | Session::__construct($clientId, $clientSecret, $redirectUri, $request) 33 | ``` 34 | 35 | Constructor
36 | Set up client credentials. 37 | 38 | #### Arguments 39 | * `$clientId` **string** - The client ID. 40 | * `$clientSecret` **string** - Optional. The client secret. 41 | * `$redirectUri` **string** - Optional. The redirect URI. 42 | * `$request` **\SpotifyWebAPI\Request** - Optional. The Request object to use. 43 | 44 | 45 | --- 46 | ### generateCodeChallenge 47 | 48 | 49 | ```php 50 | Session::generateCodeChallenge($codeVerifier, $hashAlgo) 51 | ``` 52 | 53 | Generate a code challenge from a code verifier for use with the PKCE flow. 54 | 55 | #### Arguments 56 | * `$codeVerifier` **string** - The code verifier to create a challenge from. 57 | * `$hashAlgo` **string** - Optional. The hash algorithm to use. Defaults to "sha256". 58 | 59 | #### Return values 60 | * **string** The code challenge. 61 | 62 | --- 63 | ### generateCodeVerifier 64 | 65 | 66 | ```php 67 | Session::generateCodeVerifier($length) 68 | ``` 69 | 70 | Generate a code verifier for use with the PKCE flow. 71 | 72 | #### Arguments 73 | * `$length` **int** - Optional. Code verifier length. Must be between 43 and 128 characters long, default is 128. 74 | 75 | #### Return values 76 | * **string** A code verifier string. 77 | 78 | --- 79 | ### generateState 80 | 81 | 82 | ```php 83 | Session::generateState($length) 84 | ``` 85 | 86 | Generate a random state value. 87 | 88 | #### Arguments 89 | * `$length` **int** - Optional. Length of the state. Default is 16 characters. 90 | 91 | #### Return values 92 | * **string** A random state value. 93 | 94 | --- 95 | ### getAuthorizeUrl 96 | 97 | 98 | ```php 99 | Session::getAuthorizeUrl($options) 100 | ``` 101 | 102 | Get the authorization URL. 103 | 104 | #### Arguments 105 | * `$options` **array\|object** - Optional. Options for the authorization URL. 106 | * string code_challenge Optional. A PKCE code challenge. 107 | * array scope Optional. Scope(s) to request from the user. 108 | * boolean show_dialog Optional. Whether or not to force the user to always approve the app. Default is false. 109 | * string state Optional. A CSRF token. 110 | 111 | #### Return values 112 | * **string** The authorization URL. 113 | 114 | --- 115 | ### getAccessToken 116 | 117 | 118 | ```php 119 | Session::getAccessToken() 120 | ``` 121 | 122 | Get the access token. 123 | 124 | 125 | #### Return values 126 | * **string** The access token. 127 | 128 | --- 129 | ### getClientId 130 | 131 | 132 | ```php 133 | Session::getClientId() 134 | ``` 135 | 136 | Get the client ID. 137 | 138 | 139 | #### Return values 140 | * **string** The client ID. 141 | 142 | --- 143 | ### getClientSecret 144 | 145 | 146 | ```php 147 | Session::getClientSecret() 148 | ``` 149 | 150 | Get the client secret. 151 | 152 | 153 | #### Return values 154 | * **string** The client secret. 155 | 156 | --- 157 | ### getTokenExpiration 158 | 159 | 160 | ```php 161 | Session::getTokenExpiration() 162 | ``` 163 | 164 | Get the access token expiration time. 165 | 166 | 167 | #### Return values 168 | * **int** A Unix timestamp indicating the token expiration time. 169 | 170 | --- 171 | ### getRedirectUri 172 | 173 | 174 | ```php 175 | Session::getRedirectUri() 176 | ``` 177 | 178 | Get the client's redirect URI. 179 | 180 | 181 | #### Return values 182 | * **string** The redirect URI. 183 | 184 | --- 185 | ### getRefreshToken 186 | 187 | 188 | ```php 189 | Session::getRefreshToken() 190 | ``` 191 | 192 | Get the refresh token. 193 | 194 | 195 | #### Return values 196 | * **string** The refresh token. 197 | 198 | --- 199 | ### getScope 200 | 201 | 202 | ```php 203 | Session::getScope() 204 | ``` 205 | 206 | Get the scope for the current access token 207 | 208 | 209 | #### Return values 210 | * **array** The scope for the current access token 211 | 212 | --- 213 | ### refreshAccessToken 214 | 215 | 216 | ```php 217 | Session::refreshAccessToken($refreshToken) 218 | ``` 219 | 220 | Refresh an access token. 221 | 222 | #### Arguments 223 | * `$refreshToken` **string** - Optional. The refresh token to use. 224 | 225 | #### Return values 226 | * **bool** Whether the access token was successfully refreshed. 227 | 228 | --- 229 | ### requestAccessToken 230 | 231 | 232 | ```php 233 | Session::requestAccessToken($authorizationCode, $codeVerifier) 234 | ``` 235 | 236 | Request an access token given an authorization code. 237 | 238 | #### Arguments 239 | * `$authorizationCode` **string** - The authorization code from Spotify. 240 | * `$codeVerifier` **string** - Optional. A previously generated code verifier. Will assume a PKCE flow if passed. 241 | 242 | #### Return values 243 | * **bool** True when the access token was successfully granted, false otherwise. 244 | 245 | --- 246 | ### requestCredentialsToken 247 | 248 | 249 | ```php 250 | Session::requestCredentialsToken() 251 | ``` 252 | 253 | Request an access token using the Client Credentials Flow. 254 | 255 | 256 | #### Return values 257 | * **bool** True when an access token was successfully granted, false otherwise. 258 | 259 | --- 260 | ### setAccessToken 261 | 262 | 263 | ```php 264 | Session::setAccessToken($accessToken) 265 | ``` 266 | 267 | Set the access token. 268 | 269 | #### Arguments 270 | * `$accessToken` **string** - The access token 271 | 272 | #### Return values 273 | * **self** 274 | 275 | --- 276 | ### setClientId 277 | 278 | 279 | ```php 280 | Session::setClientId($clientId) 281 | ``` 282 | 283 | Set the client ID. 284 | 285 | #### Arguments 286 | * `$clientId` **string** - The client ID. 287 | 288 | #### Return values 289 | * **self** 290 | 291 | --- 292 | ### setClientSecret 293 | 294 | 295 | ```php 296 | Session::setClientSecret($clientSecret) 297 | ``` 298 | 299 | Set the client secret. 300 | 301 | #### Arguments 302 | * `$clientSecret` **string** - The client secret. 303 | 304 | #### Return values 305 | * **self** 306 | 307 | --- 308 | ### setRedirectUri 309 | 310 | 311 | ```php 312 | Session::setRedirectUri($redirectUri) 313 | ``` 314 | 315 | Set the client's redirect URI. 316 | 317 | #### Arguments 318 | * `$redirectUri` **string** - The redirect URI. 319 | 320 | #### Return values 321 | * **self** 322 | 323 | --- 324 | ### setRefreshToken 325 | 326 | 327 | ```php 328 | Session::setRefreshToken($refreshToken) 329 | ``` 330 | 331 | Set the session's refresh token. 332 | 333 | #### Arguments 334 | * `$refreshToken` **string** - The refresh token. 335 | 336 | #### Return values 337 | * **self** 338 | 339 | --- 340 | -------------------------------------------------------------------------------- /docs/method-reference/SpotifyWebAPIAuthException.md: -------------------------------------------------------------------------------- 1 | # SpotifyWebAPIAuthException 2 | 3 | ## Table of Contents 4 | * [hasInvalidCredentials](#hasinvalidcredentials) 5 | * [hasInvalidRefreshToken](#hasinvalidrefreshtoken) 6 | * [getReason](#getreason) 7 | * [hasExpiredToken](#hasexpiredtoken) 8 | * [isRateLimited](#isratelimited) 9 | * [setReason](#setreason) 10 | 11 | ## Constants 12 | * **INVALID_CLIENT** 13 | * **INVALID_CLIENT_SECRET** 14 | * **INVALID_REFRESH_TOKEN** 15 | * **TOKEN_EXPIRED** 16 | * **RATE_LIMIT_STATUS** 17 | 18 | ## Methods 19 | ### hasInvalidCredentials 20 | 21 | 22 | ```php 23 | SpotifyWebAPIAuthException::hasInvalidCredentials() 24 | ``` 25 | 26 | Returns whether the exception was thrown because of invalid credentials. 27 | 28 | 29 | #### Return values 30 | * **bool** 31 | 32 | --- 33 | ### hasInvalidRefreshToken 34 | 35 | 36 | ```php 37 | SpotifyWebAPIAuthException::hasInvalidRefreshToken() 38 | ``` 39 | 40 | Returns whether the exception was thrown because of an invalid refresh token. 41 | 42 | 43 | #### Return values 44 | * **bool** 45 | 46 | --- 47 | ### getReason 48 | 49 | 50 | ```php 51 | SpotifyWebAPIAuthException::getReason() 52 | ``` 53 | 54 | Returns the reason string from a player request's error object. 55 | 56 | 57 | #### Return values 58 | * **string** 59 | 60 | --- 61 | ### hasExpiredToken 62 | 63 | 64 | ```php 65 | SpotifyWebAPIAuthException::hasExpiredToken() 66 | ``` 67 | 68 | Returns whether the exception was thrown because of an expired access token. 69 | 70 | 71 | #### Return values 72 | * **bool** 73 | 74 | --- 75 | ### isRateLimited 76 | 77 | 78 | ```php 79 | SpotifyWebAPIAuthException::isRateLimited() 80 | ``` 81 | 82 | Returns whether the exception was thrown because of rate limiting. 83 | 84 | 85 | #### Return values 86 | * **bool** 87 | 88 | --- 89 | ### setReason 90 | 91 | 92 | ```php 93 | SpotifyWebAPIAuthException::setReason($reason) 94 | ``` 95 | 96 | Set the reason string. 97 | 98 | #### Arguments 99 | * `$reason` **string** 100 | 101 | #### Return values 102 | * **void** 103 | 104 | --- 105 | -------------------------------------------------------------------------------- /docs/method-reference/SpotifyWebAPIException.md: -------------------------------------------------------------------------------- 1 | # SpotifyWebAPIException 2 | 3 | ## Table of Contents 4 | * [getReason](#getreason) 5 | * [hasExpiredToken](#hasexpiredtoken) 6 | * [isRateLimited](#isratelimited) 7 | * [setReason](#setreason) 8 | 9 | ## Constants 10 | * **TOKEN_EXPIRED** 11 | * **RATE_LIMIT_STATUS** 12 | 13 | ## Methods 14 | ### getReason 15 | 16 | 17 | ```php 18 | SpotifyWebAPIException::getReason() 19 | ``` 20 | 21 | Returns the reason string from a player request's error object. 22 | 23 | 24 | #### Return values 25 | * **string** 26 | 27 | --- 28 | ### hasExpiredToken 29 | 30 | 31 | ```php 32 | SpotifyWebAPIException::hasExpiredToken() 33 | ``` 34 | 35 | Returns whether the exception was thrown because of an expired access token. 36 | 37 | 38 | #### Return values 39 | * **bool** 40 | 41 | --- 42 | ### isRateLimited 43 | 44 | 45 | ```php 46 | SpotifyWebAPIException::isRateLimited() 47 | ``` 48 | 49 | Returns whether the exception was thrown because of rate limiting. 50 | 51 | 52 | #### Return values 53 | * **bool** 54 | 55 | --- 56 | ### setReason 57 | 58 | 59 | ```php 60 | SpotifyWebAPIException::setReason($reason) 61 | ``` 62 | 63 | Set the reason string. 64 | 65 | #### Arguments 66 | * `$reason` **string** 67 | 68 | #### Return values 69 | * **void** 70 | 71 | --- 72 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /phpunit.dist.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ./tests/ 25 | 26 | 27 | 28 | 29 | 30 | ./src/ 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /phpunit.php: -------------------------------------------------------------------------------- 1 | [], 15 | 'return_assoc' => false, 16 | ]; 17 | 18 | /** 19 | * Constructor 20 | * Set options. 21 | * 22 | * @param array|object $options Optional. Options to set. 23 | */ 24 | public function __construct(array|object $options = []) 25 | { 26 | $this->setOptions($options); 27 | } 28 | 29 | /** 30 | * Handle response errors. 31 | * 32 | * @param string $body The raw, unparsed response body. 33 | * @param int $status The HTTP status code, passed along to any exceptions thrown. 34 | * 35 | * @throws SpotifyWebAPIException 36 | * @throws SpotifyWebAPIAuthException 37 | * 38 | * @return void 39 | */ 40 | protected function handleResponseError(string $body, int $status): void 41 | { 42 | $parsedBody = json_decode($body); 43 | $error = $parsedBody->error ?? null; 44 | 45 | if (isset($error->message) && isset($error->status)) { 46 | // It's an API call error 47 | $exception = new SpotifyWebAPIException($error->message, $error->status); 48 | 49 | if (isset($error->reason)) { 50 | $exception->setReason($error->reason); 51 | } 52 | 53 | throw $exception; 54 | } elseif (isset($parsedBody->error_description)) { 55 | // It's an auth call error 56 | throw new SpotifyWebAPIAuthException($parsedBody->error_description, $status); 57 | } elseif ($body) { 58 | // Something else went wrong, try to give at least some info 59 | throw new SpotifyWebAPIException($body, $status); 60 | } else { 61 | // Something went really wrong, we don't know what 62 | throw new SpotifyWebAPIException('An unknown error occurred.', $status); 63 | } 64 | } 65 | 66 | /** 67 | * Parse HTTP response body, taking the "return_assoc" option into account. 68 | */ 69 | protected function parseBody(string $body): mixed 70 | { 71 | return json_decode($body, $this->options['return_assoc']); 72 | } 73 | 74 | /** 75 | * Parse HTTP response headers and normalize names. 76 | * 77 | * @param string $headers The raw, unparsed response headers. 78 | * 79 | * @return array Headers as key–value pairs. 80 | */ 81 | protected function parseHeaders(string $headers): array 82 | { 83 | $headers = explode("\n", $headers); 84 | 85 | array_shift($headers); 86 | 87 | $parsedHeaders = []; 88 | foreach ($headers as $header) { 89 | [$key, $value] = explode(':', $header, 2); 90 | 91 | $key = strtolower($key); 92 | $parsedHeaders[$key] = trim($value); 93 | } 94 | 95 | return $parsedHeaders; 96 | } 97 | 98 | /** 99 | * Make a request to the "account" endpoint. 100 | * 101 | * @param string $method The HTTP method to use. 102 | * @param string $uri The URI to request. 103 | * @param string|array $parameters Optional. Query string parameters or HTTP body, depending on $method. 104 | * @param array $headers Optional. HTTP headers. 105 | * 106 | * @throws SpotifyWebAPIException 107 | * @throws SpotifyWebAPIAuthException 108 | * 109 | * @return array Response data. 110 | * - array|object body The response body. Type is controlled by the `return_assoc` option. 111 | * - array headers Response headers. 112 | * - int status HTTP status code. 113 | * - string url The requested URL. 114 | */ 115 | public function account(string $method, string $uri, string|array $parameters = [], array $headers = []): array 116 | { 117 | return $this->send($method, static::ACCOUNT_URL . $uri, $parameters, $headers); 118 | } 119 | 120 | /** 121 | * Make a request to the "api" endpoint. 122 | * 123 | * @param string $method The HTTP method to use. 124 | * @param string $uri The URI to request. 125 | * @param string|array $parameters Optional. Query string parameters or HTTP body, depending on $method. 126 | * @param array $headers Optional. HTTP headers. 127 | * 128 | * @throws SpotifyWebAPIException 129 | * @throws SpotifyWebAPIAuthException 130 | * 131 | * @return array Response data. 132 | * - array|object body The response body. Type is controlled by the `return_assoc` option. 133 | * - array headers Response headers. 134 | * - int status HTTP status code. 135 | * - string url The requested URL. 136 | */ 137 | public function api(string $method, string $uri, string|array $parameters = [], array $headers = []): array 138 | { 139 | return $this->send($method, static::API_URL . $uri, $parameters, $headers); 140 | } 141 | 142 | /** 143 | * Get the latest full response from the Spotify API. 144 | * 145 | * @return array Response data. 146 | * - array|object body The response body. Type is controlled by the `return_assoc` option. 147 | * - array headers Response headers. 148 | * - int status HTTP status code. 149 | * - string url The requested URL. 150 | */ 151 | public function getLastResponse(): array 152 | { 153 | return $this->lastResponse; 154 | } 155 | 156 | /** 157 | * Make a request to Spotify. 158 | * You'll probably want to use one of the convenience methods instead. 159 | * 160 | * @param string $method The HTTP method to use. 161 | * @param string $url The URL to request. 162 | * @param string|array|object $parameters Optional. Query string parameters or HTTP body, depending on $method. 163 | * @param array $headers Optional. HTTP headers. 164 | * 165 | * @throws SpotifyWebAPIException 166 | * @throws SpotifyWebAPIAuthException 167 | * 168 | * @return array Response data. 169 | * - array|object body The response body. Type is controlled by the `return_assoc` option. 170 | * - array headers Response headers. 171 | * - int status HTTP status code. 172 | * - string url The requested URL. 173 | */ 174 | public function send(string $method, string $url, string|array|object $parameters = [], array $headers = []): array 175 | { 176 | // Reset any old responses 177 | $this->lastResponse = []; 178 | 179 | // Sometimes a stringified JSON object is passed 180 | if (is_array($parameters) || is_object($parameters)) { 181 | $parameters = http_build_query($parameters, '', '&'); 182 | } 183 | 184 | $options = [ 185 | CURLOPT_CAINFO => __DIR__ . '/cacert.pem', 186 | CURLOPT_ENCODING => '', 187 | CURLOPT_HEADER => true, 188 | CURLOPT_HTTPHEADER => [], 189 | CURLOPT_RETURNTRANSFER => true, 190 | CURLOPT_URL => rtrim($url, '/'), 191 | ]; 192 | 193 | foreach ($headers as $key => $val) { 194 | $options[CURLOPT_HTTPHEADER][] = "$key: $val"; 195 | } 196 | 197 | $method = strtoupper($method); 198 | 199 | switch ($method) { 200 | case 'DELETE': // No break 201 | case 'PUT': 202 | $options[CURLOPT_CUSTOMREQUEST] = $method; 203 | $options[CURLOPT_POSTFIELDS] = $parameters; 204 | 205 | break; 206 | case 'POST': 207 | $options[CURLOPT_POST] = true; 208 | $options[CURLOPT_POSTFIELDS] = $parameters; 209 | 210 | break; 211 | default: 212 | $options[CURLOPT_CUSTOMREQUEST] = $method; 213 | 214 | if ($parameters) { 215 | $options[CURLOPT_URL] .= '/?' . $parameters; 216 | } 217 | 218 | break; 219 | } 220 | 221 | $ch = curl_init(); 222 | 223 | curl_setopt_array($ch, array_replace($options, $this->options['curl_options'])); 224 | 225 | $response = curl_exec($ch); 226 | 227 | if (curl_error($ch)) { 228 | $error = curl_error($ch); 229 | $errno = curl_errno($ch); 230 | curl_close($ch); 231 | 232 | throw new SpotifyWebAPIException('cURL transport error: ' . $errno . ' ' . $error); 233 | } 234 | 235 | [$headers, $body] = $this->splitResponse($response); 236 | 237 | $parsedBody = $this->parseBody($body); 238 | $parsedHeaders = $this->parseHeaders($headers); 239 | $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); 240 | 241 | $this->lastResponse = [ 242 | 'body' => $parsedBody, 243 | 'headers' => $parsedHeaders, 244 | 'status' => $status, 245 | 'url' => $url, 246 | ]; 247 | 248 | curl_close($ch); 249 | 250 | if ($status >= 400) { 251 | $this->handleResponseError($body, $status); 252 | } 253 | 254 | return $this->lastResponse; 255 | } 256 | 257 | /** 258 | * Set options 259 | * 260 | * @param array|object $options Options to set. 261 | * 262 | * @return self 263 | */ 264 | public function setOptions(array|object $options): self 265 | { 266 | $this->options = array_merge($this->options, (array) $options); 267 | 268 | return $this; 269 | } 270 | 271 | /** 272 | * Split response into headers and body, taking proxy response headers etc. into account. 273 | * 274 | * @param string $response The complete response. 275 | * 276 | * @return array An array consisting of two elements, headers and body. 277 | */ 278 | protected function splitResponse(string $response): array 279 | { 280 | $response = str_replace("\r\n", "\n", $response); 281 | $parts = explode("\n\n", $response, 3); 282 | 283 | // Skip first set of headers for proxied requests etc. 284 | if ( 285 | preg_match('/^HTTP\/1.\d 100 Continue/', $parts[0]) || 286 | preg_match('/^HTTP\/1.\d 200 Connection established/', $parts[0]) || 287 | preg_match('/^HTTP\/1.\d 200 Tunnel established/', $parts[0]) 288 | ) { 289 | return [ 290 | $parts[1], 291 | $parts[2], 292 | ]; 293 | } 294 | 295 | return [ 296 | $parts[0], 297 | $parts[1], 298 | ]; 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /src/SpotifyWebAPIAuthException.php: -------------------------------------------------------------------------------- 1 | getMessage(), [ 22 | self::INVALID_CLIENT, 23 | self::INVALID_CLIENT_SECRET, 24 | ]); 25 | } 26 | 27 | /** 28 | * Returns whether the exception was thrown because of an invalid refresh token. 29 | * 30 | * @return bool 31 | */ 32 | public function hasInvalidRefreshToken(): bool 33 | { 34 | return $this->getMessage() === self::INVALID_REFRESH_TOKEN; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/SpotifyWebAPIException.php: -------------------------------------------------------------------------------- 1 | reason; 27 | } 28 | 29 | /** 30 | * Returns whether the exception was thrown because of an expired access token. 31 | * 32 | * @return bool 33 | */ 34 | public function hasExpiredToken(): bool 35 | { 36 | return $this->getMessage() === self::TOKEN_EXPIRED; 37 | } 38 | 39 | /** 40 | * Returns whether the exception was thrown because of rate limiting. 41 | * 42 | * @return bool 43 | */ 44 | public function isRateLimited(): bool 45 | { 46 | return $this->getCode() === self::RATE_LIMIT_STATUS; 47 | } 48 | 49 | /** 50 | * Set the reason string. 51 | * 52 | * @param string $reason 53 | * 54 | * @return void 55 | */ 56 | public function setReason(string $reason): void 57 | { 58 | $this->reason = $reason; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/fixtures/access-token.json: -------------------------------------------------------------------------------- 1 | { 2 | "access_token": "75a25c2be83fdfa0bb221b04cf3a4525e9f1203a", 3 | "token_type": "Bearer", 4 | "expires_in": 3600, 5 | "refresh_token": "030b53252ef9f646d2981f9a2bc92353c32f5f22", 6 | "scope": "user-follow-read user-follow-modify user-library-read user-library-modify" 7 | } 8 | -------------------------------------------------------------------------------- /tests/fixtures/album-tracks.json: -------------------------------------------------------------------------------- 1 | { 2 | "href" : "https://api.spotify.com/v1/albums/1oR3KrPIp4CbagPa3PhtPp/tracks?offset=0&limit=5", 3 | "items" : [ { 4 | "artists" : [ { 5 | "external_urls" : { 6 | "spotify" : "https://open.spotify.com/artist/3qm84nBOXUEQ2vnTfUTTFC" 7 | }, 8 | "href" : "https://api.spotify.com/v1/artists/3qm84nBOXUEQ2vnTfUTTFC", 9 | "id" : "3qm84nBOXUEQ2vnTfUTTFC", 10 | "name" : "Guns N' Roses", 11 | "type" : "artist", 12 | "uri" : "spotify:artist:3qm84nBOXUEQ2vnTfUTTFC" 13 | } ], 14 | "available_markets" : [ "CR", "DO", "GT", "HN", "NI", "PA", "SV" ], 15 | "disc_number" : 1, 16 | "duration_ms" : 273600, 17 | "explicit" : false, 18 | "external_urls" : { 19 | "spotify" : "https://open.spotify.com/track/0oWHLtxWeMJhmwxtrxhNK0" 20 | }, 21 | "href" : "https://api.spotify.com/v1/tracks/0oWHLtxWeMJhmwxtrxhNK0", 22 | "id" : "0oWHLtxWeMJhmwxtrxhNK0", 23 | "name" : "Welcome To The Jungle", 24 | "preview_url" : "https://p.scdn.co/mp3-preview/6ac585f7791a2ec0a60c17e04846345ed7671d0b", 25 | "track_number" : 1, 26 | "type" : "track", 27 | "uri" : "spotify:track:0oWHLtxWeMJhmwxtrxhNK0" 28 | }, { 29 | "artists" : [ { 30 | "external_urls" : { 31 | "spotify" : "https://open.spotify.com/artist/3qm84nBOXUEQ2vnTfUTTFC" 32 | }, 33 | "href" : "https://api.spotify.com/v1/artists/3qm84nBOXUEQ2vnTfUTTFC", 34 | "id" : "3qm84nBOXUEQ2vnTfUTTFC", 35 | "name" : "Guns N' Roses", 36 | "type" : "artist", 37 | "uri" : "spotify:artist:3qm84nBOXUEQ2vnTfUTTFC" 38 | } ], 39 | "available_markets" : [ "CR", "DO", "GT", "HN", "NI", "PA", "SV" ], 40 | "disc_number" : 1, 41 | "duration_ms" : 202893, 42 | "explicit" : true, 43 | "external_urls" : { 44 | "spotify" : "https://open.spotify.com/track/0fu0HHl1yVC2iCUuyBT144" 45 | }, 46 | "href" : "https://api.spotify.com/v1/tracks/0fu0HHl1yVC2iCUuyBT144", 47 | "id" : "0fu0HHl1yVC2iCUuyBT144", 48 | "name" : "It's So Easy", 49 | "preview_url" : "https://p.scdn.co/mp3-preview/ee696163ab8f41d9bee0b44f6d2c49ab8b28e490", 50 | "track_number" : 2, 51 | "type" : "track", 52 | "uri" : "spotify:track:0fu0HHl1yVC2iCUuyBT144" 53 | }, { 54 | "artists" : [ { 55 | "external_urls" : { 56 | "spotify" : "https://open.spotify.com/artist/3qm84nBOXUEQ2vnTfUTTFC" 57 | }, 58 | "href" : "https://api.spotify.com/v1/artists/3qm84nBOXUEQ2vnTfUTTFC", 59 | "id" : "3qm84nBOXUEQ2vnTfUTTFC", 60 | "name" : "Guns N' Roses", 61 | "type" : "artist", 62 | "uri" : "spotify:artist:3qm84nBOXUEQ2vnTfUTTFC" 63 | } ], 64 | "available_markets" : [ "CR", "DO", "GT", "HN", "NI", "PA", "SV" ], 65 | "disc_number" : 1, 66 | "duration_ms" : 268506, 67 | "explicit" : false, 68 | "external_urls" : { 69 | "spotify" : "https://open.spotify.com/track/23ZdycsvnFHK9NF1X2V7bc" 70 | }, 71 | "href" : "https://api.spotify.com/v1/tracks/23ZdycsvnFHK9NF1X2V7bc", 72 | "id" : "23ZdycsvnFHK9NF1X2V7bc", 73 | "name" : "Nightrain", 74 | "preview_url" : "https://p.scdn.co/mp3-preview/30e5ca88cae78b02dea6a718b73b7db5e8ddcac6", 75 | "track_number" : 3, 76 | "type" : "track", 77 | "uri" : "spotify:track:23ZdycsvnFHK9NF1X2V7bc" 78 | }, { 79 | "artists" : [ { 80 | "external_urls" : { 81 | "spotify" : "https://open.spotify.com/artist/3qm84nBOXUEQ2vnTfUTTFC" 82 | }, 83 | "href" : "https://api.spotify.com/v1/artists/3qm84nBOXUEQ2vnTfUTTFC", 84 | "id" : "3qm84nBOXUEQ2vnTfUTTFC", 85 | "name" : "Guns N' Roses", 86 | "type" : "artist", 87 | "uri" : "spotify:artist:3qm84nBOXUEQ2vnTfUTTFC" 88 | } ], 89 | "available_markets" : [ "CR", "DO", "GT", "HN", "NI", "PA", "SV" ], 90 | "disc_number" : 1, 91 | "duration_ms" : 263866, 92 | "explicit" : true, 93 | "external_urls" : { 94 | "spotify" : "https://open.spotify.com/track/0DEGMFohkJUQprEG1wU4SN" 95 | }, 96 | "href" : "https://api.spotify.com/v1/tracks/0DEGMFohkJUQprEG1wU4SN", 97 | "id" : "0DEGMFohkJUQprEG1wU4SN", 98 | "name" : "Out Ta Get Me", 99 | "preview_url" : "https://p.scdn.co/mp3-preview/dc59755154bb63036390eba86ce774db660e1740", 100 | "track_number" : 4, 101 | "type" : "track", 102 | "uri" : "spotify:track:0DEGMFohkJUQprEG1wU4SN" 103 | }, { 104 | "artists" : [ { 105 | "external_urls" : { 106 | "spotify" : "https://open.spotify.com/artist/3qm84nBOXUEQ2vnTfUTTFC" 107 | }, 108 | "href" : "https://api.spotify.com/v1/artists/3qm84nBOXUEQ2vnTfUTTFC", 109 | "id" : "3qm84nBOXUEQ2vnTfUTTFC", 110 | "name" : "Guns N' Roses", 111 | "type" : "artist", 112 | "uri" : "spotify:artist:3qm84nBOXUEQ2vnTfUTTFC" 113 | } ], 114 | "available_markets" : [ "CR", "DO", "GT", "HN", "NI", "PA", "SV" ], 115 | "disc_number" : 1, 116 | "duration_ms" : 228893, 117 | "explicit" : true, 118 | "external_urls" : { 119 | "spotify" : "https://open.spotify.com/track/0waeZEK1KpCuUvXkXCTOM2" 120 | }, 121 | "href" : "https://api.spotify.com/v1/tracks/0waeZEK1KpCuUvXkXCTOM2", 122 | "id" : "0waeZEK1KpCuUvXkXCTOM2", 123 | "name" : "Mr. Brownstone", 124 | "preview_url" : "https://p.scdn.co/mp3-preview/7ef4ea55e9a9183f9ca504ee532b23ad90b944b3", 125 | "track_number" : 5, 126 | "type" : "track", 127 | "uri" : "spotify:track:0waeZEK1KpCuUvXkXCTOM2" 128 | } ], 129 | "limit" : 5, 130 | "next" : "https://api.spotify.com/v1/albums/1oR3KrPIp4CbagPa3PhtPp/tracks?offset=5&limit=5", 131 | "offset" : 0, 132 | "previous" : null, 133 | "total" : 12 134 | } 135 | -------------------------------------------------------------------------------- /tests/fixtures/artist-albums.json: -------------------------------------------------------------------------------- 1 | { 2 | "href" : "https://api.spotify.com/v1/artists/6v8FB84lnmJs434UJf2Mrm/albums?offset=0&limit=5&album_type=single,album,compilation,appears_on", 3 | "items" : [ { 4 | "album_type" : "album", 5 | "available_markets" : [ "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IT", "LT", "LU", "LV", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 6 | "external_urls" : { 7 | "spotify" : "https://open.spotify.com/album/361dgrFRURo9gwWnSQG6Uu" 8 | }, 9 | "href" : "https://api.spotify.com/v1/albums/361dgrFRURo9gwWnSQG6Uu", 10 | "id" : "361dgrFRURo9gwWnSQG6Uu", 11 | "images" : [ { 12 | "height" : 640, 13 | "url" : "https://i.scdn.co/image/e052cc9842df2738bf2c3368bafa49c43fad83a4", 14 | "width" : 640 15 | }, { 16 | "height" : 300, 17 | "url" : "https://i.scdn.co/image/4996a3b2db84b5d121b54e457d8b173d7162f917", 18 | "width" : 300 19 | }, { 20 | "height" : 64, 21 | "url" : "https://i.scdn.co/image/db0c983e1171f03186b61213ce9fe1f718b56ed1", 22 | "width" : 64 23 | } ], 24 | "name" : "Storytone", 25 | "type" : "album", 26 | "uri" : "spotify:album:361dgrFRURo9gwWnSQG6Uu" 27 | }, { 28 | "album_type" : "album", 29 | "available_markets" : [ "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IT", "LT", "LU", "LV", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 30 | "external_urls" : { 31 | "spotify" : "https://open.spotify.com/album/1HhdHx1asrL4Uf6rlQxCx1" 32 | }, 33 | "href" : "https://api.spotify.com/v1/albums/1HhdHx1asrL4Uf6rlQxCx1", 34 | "id" : "1HhdHx1asrL4Uf6rlQxCx1", 35 | "images" : [ { 36 | "height" : 640, 37 | "url" : "https://i.scdn.co/image/709702b800b5239e4bf345dbeb4fe78cd8e068b1", 38 | "width" : 640 39 | }, { 40 | "height" : 300, 41 | "url" : "https://i.scdn.co/image/637262e8c1c45457ec8773ecd08eef81f9cdb41d", 42 | "width" : 300 43 | }, { 44 | "height" : 64, 45 | "url" : "https://i.scdn.co/image/976b30553009b1866368a2afd01b336a2a36f180", 46 | "width" : 64 47 | } ], 48 | "name" : "Storytone (Deluxe Version)", 49 | "type" : "album", 50 | "uri" : "spotify:album:1HhdHx1asrL4Uf6rlQxCx1" 51 | }, { 52 | "album_type" : "album", 53 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 54 | "external_urls" : { 55 | "spotify" : "https://open.spotify.com/album/0qeBoQjqVd5L0T93Pf7Vwm" 56 | }, 57 | "href" : "https://api.spotify.com/v1/albums/0qeBoQjqVd5L0T93Pf7Vwm", 58 | "id" : "0qeBoQjqVd5L0T93Pf7Vwm", 59 | "images" : [ { 60 | "height" : 640, 61 | "url" : "https://i.scdn.co/image/aef94edcf57574157875dd9b7d496656b25d4aec", 62 | "width" : 640 63 | }, { 64 | "height" : 300, 65 | "url" : "https://i.scdn.co/image/6bac516642b884fb5661fa0c109bd3a5933d2b51", 66 | "width" : 300 67 | }, { 68 | "height" : 64, 69 | "url" : "https://i.scdn.co/image/2cc77e54f4519bfa72566fa293fbbd912c33d0bc", 70 | "width" : 64 71 | } ], 72 | "name" : "A Letter Home", 73 | "type" : "album", 74 | "uri" : "spotify:album:0qeBoQjqVd5L0T93Pf7Vwm" 75 | }, { 76 | "album_type" : "album", 77 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 78 | "external_urls" : { 79 | "spotify" : "https://open.spotify.com/album/2h2zv7tulv5460Ijs1tpxs" 80 | }, 81 | "href" : "https://api.spotify.com/v1/albums/2h2zv7tulv5460Ijs1tpxs", 82 | "id" : "2h2zv7tulv5460Ijs1tpxs", 83 | "images" : [ { 84 | "height" : 640, 85 | "url" : "https://i.scdn.co/image/a042e0e50696368635ba9df8c598f79d856e4b92", 86 | "width" : 640 87 | }, { 88 | "height" : 300, 89 | "url" : "https://i.scdn.co/image/adf120673a98a0dec42bb9cde4b8a437ec60ec97", 90 | "width" : 300 91 | }, { 92 | "height" : 64, 93 | "url" : "https://i.scdn.co/image/e2ee2d191ca0b30e417a1ae0d8889a0f365b8d37", 94 | "width" : 64 95 | } ], 96 | "name" : "Live At The Cellar Door", 97 | "type" : "album", 98 | "uri" : "spotify:album:2h2zv7tulv5460Ijs1tpxs" 99 | }, { 100 | "album_type" : "album", 101 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 102 | "external_urls" : { 103 | "spotify" : "https://open.spotify.com/album/4j9JVeEgEEiwYyYMiueTvG" 104 | }, 105 | "href" : "https://api.spotify.com/v1/albums/4j9JVeEgEEiwYyYMiueTvG", 106 | "id" : "4j9JVeEgEEiwYyYMiueTvG", 107 | "images" : [ { 108 | "height" : 640, 109 | "url" : "https://i.scdn.co/image/65d3f76280a7dd0344e26529792711c42c182d01", 110 | "width" : 640 111 | }, { 112 | "height" : 300, 113 | "url" : "https://i.scdn.co/image/48faa882bceddcf24bc9c867ec600ccb3a6f250f", 114 | "width" : 300 115 | }, { 116 | "height" : 64, 117 | "url" : "https://i.scdn.co/image/94f0b553fd359907deb913aa4ffdeaad9cf09148", 118 | "width" : 64 119 | } ], 120 | "name" : "Le Noise", 121 | "type" : "album", 122 | "uri" : "spotify:album:4j9JVeEgEEiwYyYMiueTvG" 123 | } ], 124 | "limit" : 5, 125 | "next" : "https://api.spotify.com/v1/artists/6v8FB84lnmJs434UJf2Mrm/albums?offset=5&limit=5&album_type=single,album,compilation,appears_on", 126 | "offset" : 0, 127 | "previous" : null, 128 | "total" : 113 129 | } 130 | -------------------------------------------------------------------------------- /tests/fixtures/artist.json: -------------------------------------------------------------------------------- 1 | { 2 | "external_urls" : { 3 | "spotify" : "https://open.spotify.com/artist/36QJpDe2go2KgaRleHCDTp" 4 | }, 5 | "followers" : { 6 | "href" : null, 7 | "total" : 499010 8 | }, 9 | "genres" : [ "album rock", "blues-rock", "classic rock", "rock" ], 10 | "href" : "https://api.spotify.com/v1/artists/36QJpDe2go2KgaRleHCDTp", 11 | "id" : "36QJpDe2go2KgaRleHCDTp", 12 | "images" : [ { 13 | "height" : 600, 14 | "url" : "https://i.scdn.co/image/f66bcaf9b7d00b6bd5dafa1d99586390a23f4935", 15 | "width" : 600 16 | }, { 17 | "height" : 200, 18 | "url" : "https://i.scdn.co/image/89f85b03f128056ea2d0ca941be999853bea3d7c", 19 | "width" : 200 20 | }, { 21 | "height" : 64, 22 | "url" : "https://i.scdn.co/image/f0959931aeb8aa174ef264d0b1ab7dd99cd22c03", 23 | "width" : 64 24 | } ], 25 | "name" : "Led Zeppelin", 26 | "popularity" : 56, 27 | "type" : "artist", 28 | "uri" : "spotify:artist:36QJpDe2go2KgaRleHCDTp" 29 | } 30 | -------------------------------------------------------------------------------- /tests/fixtures/artists.json: -------------------------------------------------------------------------------- 1 | { 2 | "artists" : [ { 3 | "external_urls" : { 4 | "spotify" : "https://open.spotify.com/artist/6v8FB84lnmJs434UJf2Mrm" 5 | }, 6 | "followers" : { 7 | "href" : null, 8 | "total" : 353848 9 | }, 10 | "genres" : [ "country rock", "folk rock", "roots rock", "singer-songwriter" ], 11 | "href" : "https://api.spotify.com/v1/artists/6v8FB84lnmJs434UJf2Mrm", 12 | "id" : "6v8FB84lnmJs434UJf2Mrm", 13 | "images" : [ { 14 | "height" : 667, 15 | "url" : "https://i.scdn.co/image/9cf3c4417d299cd974c156636aed25cf776cc588", 16 | "width" : 1000 17 | }, { 18 | "height" : 427, 19 | "url" : "https://i.scdn.co/image/28056039df6056d8cadc9f10a6bf407af316bf32", 20 | "width" : 640 21 | }, { 22 | "height" : 133, 23 | "url" : "https://i.scdn.co/image/fe3f3cc057d65f0a3425f1b95f589f0a894bac56", 24 | "width" : 200 25 | }, { 26 | "height" : 43, 27 | "url" : "https://i.scdn.co/image/fe257b86f46d004487856f413d4e1e8f505986de", 28 | "width" : 64 29 | } ], 30 | "name" : "Neil Young", 31 | "popularity" : 65, 32 | "type" : "artist", 33 | "uri" : "spotify:artist:6v8FB84lnmJs434UJf2Mrm" 34 | }, { 35 | "external_urls" : { 36 | "spotify" : "https://open.spotify.com/artist/6olE6TJLqED3rqDCT0FyPh" 37 | }, 38 | "followers" : { 39 | "href" : null, 40 | "total" : 962884 41 | }, 42 | "genres" : [ "alternative rock", "grunge", "permanent wave" ], 43 | "href" : "https://api.spotify.com/v1/artists/6olE6TJLqED3rqDCT0FyPh", 44 | "id" : "6olE6TJLqED3rqDCT0FyPh", 45 | "images" : [ { 46 | "height" : 1057, 47 | "url" : "https://i.scdn.co/image/5d66307bbf73337bb073bfb2bf242e099a47e219", 48 | "width" : 1000 49 | }, { 50 | "height" : 677, 51 | "url" : "https://i.scdn.co/image/695867a7c6a0df25c5bddf0b00cc2cfde35b3ffc", 52 | "width" : 640 53 | }, { 54 | "height" : 211, 55 | "url" : "https://i.scdn.co/image/3a9c12e86ad8e2bbecb0e919b80bb5ece6f1dbe3", 56 | "width" : 200 57 | }, { 58 | "height" : 68, 59 | "url" : "https://i.scdn.co/image/5ae6f10633adc53210cd18a4f216f27de330f19d", 60 | "width" : 64 61 | } ], 62 | "name" : "Nirvana", 63 | "popularity" : 67, 64 | "type" : "artist", 65 | "uri" : "spotify:artist:6olE6TJLqED3rqDCT0FyPh" 66 | } ] 67 | } 68 | -------------------------------------------------------------------------------- /tests/fixtures/audio-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "danceability": 0.808, 3 | "energy": 0.626, 4 | "key": 7, 5 | "loudness": -12.733, 6 | "mode": 1, 7 | "speechiness": 0.168, 8 | "acousticness": 0.00187, 9 | "instrumentalness": 0.159, 10 | "liveness": 0.376, 11 | "valence": 0.369, 12 | "tempo": 123.99, 13 | "type": "audio_features", 14 | "id": "0eGsygTp906u18L0Oimnem", 15 | "uri": "spotify:track:0eGsygTp906u18L0Oimnem", 16 | "track_href": "https://api.spotify.com/v1/tracks/0eGsygTp906u18L0Oimnem", 17 | "analysis_url": "http://echonest-analysis.s3.amazonaws.com/TR/WhpYUARk1kNJ_qP0AdKGcDDFKOQTTgsOoINrqyPQjkUnbteuuBiyj_u94iFCSGzdxGiwqQ6d77f4QLL_8=/3/full.json?AWSAccessKeyId=AKIAJRDFEY23UEVW42BQ&Expires=1458063189&Signature=JRE8SDZStpNOdUsPN/PoS49FMtQ%3D", 18 | "duration_ms": 535223, 19 | "time_signature": 4 20 | } 21 | -------------------------------------------------------------------------------- /tests/fixtures/available-genre-seeds.json: -------------------------------------------------------------------------------- 1 | { 2 | "genres" : [ "acoustic", "afrobeat", "alt-rock", "alternative", "ambient", "anime", "black-metal", "bluegrass", "blues", "bossanova", "brazil", "breakbeat", "british", "cantopop", "chicago-house", "children", "chill", "classical", "club", "comedy", "country", "dance", "dancehall", "death-metal", "deep-house", "detroit-techno", "disco", "disney", "drum-and-bass", "dub", "dubstep", "edm", "electro", "electronic", "emo", "folk", "forro", "french", "funk", "garage", "german", "gospel", "goth", "grindcore", "groove", "grunge", "guitar", "happy", "hard-rock", "hardcore", "hardstyle", "heavy-metal", "hip-hop", "holidays", "honky-tonk", "house", "idm", "indian", "indie", "indie-pop", "industrial", "iranian", "j-dance", "j-idol", "j-pop", "j-rock", "jazz", "k-pop", "kids", "latin", "latino", "malay", "mandopop", "metal", "metal-misc", "metalcore", "minimal-techno", "movies", "mpb", "new-age", "new-release", "opera", "pagode", "party", "philippines-opm", "piano", "pop", "pop-film", "post-dubstep", "power-pop", "progressive-house", "psych-rock", "punk", "punk-rock", "r-n-b", "rainy-day", "reggae", "reggaeton", "road-trip", "rock", "rock-n-roll", "rockabilly", "romance", "sad", "salsa", "samba", "sertanejo", "show-tunes", "singer-songwriter", "ska", "sleep", "songwriter", "soul", "soundtracks", "spanish", "study", "summer", "swedish", "synth-pop", "tango", "techno", "trance", "trip-hop", "turkish", "work-out", "world-music" ] 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/categories-list.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories" : { 3 | "href" : "https://api.spotify.com/v1/browse/categories?country=FR&offset=0&limit=20", 4 | "items" : [ { 5 | "href" : "https://api.spotify.com/v1/browse/categories/toplists", 6 | "icons" : [ { 7 | "height" : 275, 8 | "url" : "https://datsnxq1rwndn.cloudfront.net/media/derived/toplists_11160599e6a04ac5d6f2757f5511778f_0_0_275_275.jpg", 9 | "width" : 275 10 | } ], 11 | "id" : "toplists", 12 | "name" : "Top Lists" 13 | }, { 14 | "href" : "https://api.spotify.com/v1/browse/categories/mood", 15 | "icons" : [ { 16 | "height" : 274, 17 | "url" : "https://datsnxq1rwndn.cloudfront.net/media/original/mood-274x274_976986a31ac8c49794cbdc7246fd5ad7_274x274.jpg", 18 | "width" : 274 19 | } ], 20 | "id" : "mood", 21 | "name" : "Mood" 22 | }, { 23 | "href" : "https://api.spotify.com/v1/browse/categories/party", 24 | "icons" : [ { 25 | "height" : 274, 26 | "url" : "https://datsnxq1rwndn.cloudfront.net/media/derived/party-274x274_73d1907a7371c3bb96a288390a96ee27_0_0_274_274.jpg", 27 | "width" : 274 28 | } ], 29 | "id" : "party", 30 | "name" : "Party" 31 | }, { 32 | "href" : "https://api.spotify.com/v1/browse/categories/pop", 33 | "icons" : [ { 34 | "height" : 274, 35 | "url" : "https://datsnxq1rwndn.cloudfront.net/media/derived/pop-274x274_447148649685019f5e2a03a39e78ba52_0_0_274_274.jpg", 36 | "width" : 274 37 | } ], 38 | "id" : "pop", 39 | "name" : "Pop Culture" 40 | }, { 41 | "href" : "https://api.spotify.com/v1/browse/categories/workout", 42 | "icons" : [ { 43 | "height" : 275, 44 | "url" : "https://datsnxq1rwndn.cloudfront.net/media/derived/workout_856581c1c545a5305e49a3cd8be804a0_0_0_275_275.jpg", 45 | "width" : 275 46 | } ], 47 | "id" : "workout", 48 | "name" : "Workout" 49 | }, { 50 | "href" : "https://api.spotify.com/v1/browse/categories/focus", 51 | "icons" : [ { 52 | "height" : 274, 53 | "url" : "https://datsnxq1rwndn.cloudfront.net/media/original/genre-images-square-274x274_5e50d72b846a198fcd2ca9b3aef5f0c8_274x274.jpg", 54 | "width" : 274 55 | } ], 56 | "id" : "focus", 57 | "name" : "Focus" 58 | }, { 59 | "href" : "https://api.spotify.com/v1/browse/categories/rock", 60 | "icons" : [ { 61 | "height" : 274, 62 | "url" : "https://datsnxq1rwndn.cloudfront.net/media/derived/rock_9ce79e0a4ef901bbd10494f5b855d3cc_0_0_274_274.jpg", 63 | "width" : 274 64 | } ], 65 | "id" : "rock", 66 | "name" : "Rock" 67 | }, { 68 | "href" : "https://api.spotify.com/v1/browse/categories/indie_alt", 69 | "icons" : [ { 70 | "height" : 274, 71 | "url" : "https://datsnxq1rwndn.cloudfront.net/media/derived/indie-274x274_add35b2b767ff7f3897262ad86809bdb_0_0_274_274.jpg", 72 | "width" : 274 73 | } ], 74 | "id" : "indie_alt", 75 | "name" : "Indie/Alternative" 76 | }, { 77 | "href" : "https://api.spotify.com/v1/browse/categories/edm_dance", 78 | "icons" : [ { 79 | "height" : 274, 80 | "url" : "https://datsnxq1rwndn.cloudfront.net/media/derived/edm-274x274_0ef612604200a9c14995432994455a6d_0_0_274_274.jpg", 81 | "width" : 274 82 | } ], 83 | "id" : "edm_dance", 84 | "name" : "EDM/Dance" 85 | }, { 86 | "href" : "https://api.spotify.com/v1/browse/categories/chill", 87 | "icons" : [ { 88 | "height" : 274, 89 | "url" : "https://datsnxq1rwndn.cloudfront.net/media/derived/chill-274x274_4c46374f007813dd10b37e8d8fd35b4b_0_0_274_274.jpg", 90 | "width" : 274 91 | } ], 92 | "id" : "chill", 93 | "name" : "Chill" 94 | }, { 95 | "href" : "https://api.spotify.com/v1/browse/categories/dinner", 96 | "icons" : [ { 97 | "height" : 274, 98 | "url" : "https://datsnxq1rwndn.cloudfront.net/media/original/dinner_1b6506abba0ba52c54e6d695c8571078_274x274.jpg", 99 | "width" : 274 100 | } ], 101 | "id" : "dinner", 102 | "name" : "Dinner" 103 | }, { 104 | "href" : "https://api.spotify.com/v1/browse/categories/sleep", 105 | "icons" : [ { 106 | "height" : 274, 107 | "url" : "https://datsnxq1rwndn.cloudfront.net/media/derived/sleep-274x274_0d4f836af8fab7bf31526968073e671c_0_0_274_274.jpg", 108 | "width" : 274 109 | } ], 110 | "id" : "sleep", 111 | "name" : "Sleep" 112 | }, { 113 | "href" : "https://api.spotify.com/v1/browse/categories/hiphop", 114 | "icons" : [ { 115 | "height" : 274, 116 | "url" : "https://datsnxq1rwndn.cloudfront.net/media/original/hip-274_0a661854d61e29eace5fe63f73495e68_274x274.jpg", 117 | "width" : 274 118 | } ], 119 | "id" : "hiphop", 120 | "name" : "Hip Hop" 121 | }, { 122 | "href" : "https://api.spotify.com/v1/browse/categories/rnb", 123 | "icons" : [ { 124 | "height" : 274, 125 | "url" : "https://datsnxq1rwndn.cloudfront.net/media/derived/rb-274x274_a0e7a187f9449dd7722e1ddbd6191f48_0_0_274_274.jpg", 126 | "width" : 274 127 | } ], 128 | "id" : "rnb", 129 | "name" : "RnB" 130 | }, { 131 | "href" : "https://api.spotify.com/v1/browse/categories/country", 132 | "icons" : [ { 133 | "height" : 274, 134 | "url" : "https://datsnxq1rwndn.cloudfront.net/media/derived/country-folk_dd35e932222c37ea75623a5788a12935_0_0_274_274.jpg", 135 | "width" : 274 136 | } ], 137 | "id" : "country", 138 | "name" : "Country" 139 | }, { 140 | "href" : "https://api.spotify.com/v1/browse/categories/folk_americana", 141 | "icons" : [ { 142 | "height" : 274, 143 | "url" : "https://datsnxq1rwndn.cloudfront.net/media/derived/folk-icon_1682b3a3e59b9a351243e6b7b26129fb_0_0_274_274.jpg", 144 | "width" : 274 145 | } ], 146 | "id" : "folk_americana", 147 | "name" : "Folk & Americana" 148 | }, { 149 | "href" : "https://api.spotify.com/v1/browse/categories/metal", 150 | "icons" : [ { 151 | "height" : 274, 152 | "url" : "https://datsnxq1rwndn.cloudfront.net/media/original/metal_27c921443fd0a5ba95b1b2c2ae654b2b_274x274.jpg", 153 | "width" : 274 154 | } ], 155 | "id" : "metal", 156 | "name" : "Metal" 157 | }, { 158 | "href" : "https://api.spotify.com/v1/browse/categories/soul", 159 | "icons" : [ { 160 | "height" : 274, 161 | "url" : "https://datsnxq1rwndn.cloudfront.net/media/derived/soul-274x274_266bc900b35dda8956380cffc73a4d8c_0_0_274_274.jpg", 162 | "width" : 274 163 | } ], 164 | "id" : "soul", 165 | "name" : "Soul" 166 | }, { 167 | "href" : "https://api.spotify.com/v1/browse/categories/travel", 168 | "icons" : [ { 169 | "height" : 274, 170 | "url" : "https://datsnxq1rwndn.cloudfront.net/media/derived/travel-274x274_1e89cd5b42cf8bd2ff8fc4fb26f2e955_0_0_274_274.jpg", 171 | "width" : 274 172 | } ], 173 | "id" : "travel", 174 | "name" : "Travel" 175 | }, { 176 | "href" : "https://api.spotify.com/v1/browse/categories/decades", 177 | "icons" : [ { 178 | "height" : 274, 179 | "url" : "https://datsnxq1rwndn.cloudfront.net/media/derived/decades_9ad8e458242b2ac8b184e79ef336c455_0_0_274_274.jpg", 180 | "width" : 274 181 | } ], 182 | "id" : "decades", 183 | "name" : "Decades" 184 | } ], 185 | "limit" : 20, 186 | "next" : "https://api.spotify.com/v1/browse/categories?country=FR&offset=20&limit=20", 187 | "offset" : 0, 188 | "previous" : null, 189 | "total" : 31 190 | } 191 | } -------------------------------------------------------------------------------- /tests/fixtures/category.json: -------------------------------------------------------------------------------- 1 | { 2 | "href" : "https://api.spotify.com/v1/browse/categories/party", 3 | "icons" : [ { 4 | "height" : 274, 5 | "url" : "https://datsnxq1rwndn.cloudfront.net/media/derived/party-274x274_73d1907a7371c3bb96a288390a96ee27_0_0_274_274.jpg", 6 | "width" : 274 7 | } ], 8 | "id" : "party", 9 | "name" : "Party" 10 | } -------------------------------------------------------------------------------- /tests/fixtures/episode.json: -------------------------------------------------------------------------------- 1 | { 2 | "audio_preview_url": "https://p.scdn.co/mp3-preview/566fcc94708f39bcddc09e4ce84a8e5db8f07d4d", 3 | "description": "En ny tysk bok granskar för första gången Tredje rikets drogberoende, från Führerns knarkande till hans soldater på speed. Och kändisförfattaren Antony Beevor får nu kritik av en svensk kollega. Hitler var beroende av sin livläkare, som gav honom mängder av narkotiska preparat, och blitzkrigssoldaterna knaprade 35 miljoner speedtabletter under invasionen av Frankrike 1940. I den nyutkomna boken Der Totale Rausch, Det totala ruset, ger författaren Norman Ohler för första gången en samlad bild av knarkandet i Tredje riket. Mycket tyder på att Hitler var gravt drogpåverkad under flera avgörande beslut under kriget, säger han, och får medhåll av medicinhistorikern Peter Steinkamp som undersökt de tyska soldaternas intensiva användande av pervitin, en variant av crystal meth.Dessutom får nu den kände militärhistoriska författaren Antony Beevor kritik för att hans senaste bok om Ardenneroffensiven lutar sig alltför tungt mot amerikanska källor, och dessutom innehåller många felaktiga detaljer. Det menar författarkollegan Christer Bergström, som själv skrivit en bok om striderna i Ardennerna.Programledare är Tobias Svanelid.", 4 | "duration_ms": 1502795, 5 | "explicit": false, 6 | "external_urls": { 7 | "spotify": "https://open.spotify.com/episode/512ojhOuo1ktJprKbVcKyQ" 8 | }, 9 | "href": "https://api.spotify.com/v1/episodes/512ojhOuo1ktJprKbVcKyQ", 10 | "id": "512ojhOuo1ktJprKbVcKyQ", 11 | "images": [ 12 | { 13 | "height": 640, 14 | "url": "https://i.scdn.co/image/6bcff849a483dd3c2883b3f0272848b909f1bbce", 15 | "width": 640 16 | }, 17 | { 18 | "height": 300, 19 | "url": "https://i.scdn.co/image/66250bd121ee949ed5026decbfd97e255b25a5c8", 20 | "width": 300 21 | }, 22 | { 23 | "height": 64, 24 | "url": "https://i.scdn.co/image/e29c75799cad73927fad713011edad574868d8da", 25 | "width": 64 26 | } 27 | ], 28 | "is_externally_hosted": false, 29 | "is_playable": true, 30 | "language": "sv", 31 | "languages": [ 32 | "sv" 33 | ], 34 | "name": "Tredje rikets knarkande granskas", 35 | "release_date": "2015-10-01", 36 | "release_date_precision": "day", 37 | "show": { 38 | "available_markets": [ 39 | "AD", 40 | "AE", 41 | "AR", 42 | "AT", 43 | "AU", 44 | "BE", 45 | "BG", 46 | "BH", 47 | "BO", 48 | "BR", 49 | "CA", 50 | "CH", 51 | "CL", 52 | "CO", 53 | "CR", 54 | "CY", 55 | "CZ", 56 | "DE", 57 | "DK", 58 | "DO", 59 | "DZ", 60 | "EC", 61 | "EE", 62 | "ES", 63 | "FI", 64 | "FR", 65 | "GB", 66 | "GR", 67 | "GT", 68 | "HK", 69 | "HN", 70 | "HU", 71 | "ID", 72 | "IE", 73 | "IL", 74 | "IN", 75 | "IS", 76 | "IT", 77 | "JO", 78 | "JP", 79 | "KW", 80 | "LB", 81 | "LI", 82 | "LT", 83 | "LU", 84 | "LV", 85 | "MA", 86 | "MC", 87 | "MT", 88 | "MX", 89 | "MY", 90 | "NI", 91 | "NL", 92 | "NO", 93 | "NZ", 94 | "OM", 95 | "PA", 96 | "PE", 97 | "PH", 98 | "PL", 99 | "PS", 100 | "PT", 101 | "PY", 102 | "QA", 103 | "RO", 104 | "SE", 105 | "SG", 106 | "SK", 107 | "SV", 108 | "TH", 109 | "TN", 110 | "TR", 111 | "TW", 112 | "US", 113 | "UY", 114 | "VN", 115 | "ZA" 116 | ], 117 | "copyrights": [], 118 | "description": "Vi är där historien är. Ansvarig utgivare: Nina Glans", 119 | "explicit": false, 120 | "external_urls": { 121 | "spotify": "https://open.spotify.com/show/38bS44xjbVVZ3No3ByF1dJ" 122 | }, 123 | "href": "https://api.spotify.com/v1/shows/38bS44xjbVVZ3No3ByF1dJ", 124 | "id": "38bS44xjbVVZ3No3ByF1dJ", 125 | "images": [ 126 | { 127 | "height": 640, 128 | "url": "https://i.scdn.co/image/3c59a8b611000c8b10c8013013c3783dfb87a3bc", 129 | "width": 640 130 | }, 131 | { 132 | "height": 300, 133 | "url": "https://i.scdn.co/image/2d70c06ac70d8c6144c94cabf7f4abcf85c4b7e4", 134 | "width": 300 135 | }, 136 | { 137 | "height": 64, 138 | "url": "https://i.scdn.co/image/3dc007829bc0663c24089e46743a9f4ae15e65f8", 139 | "width": 64 140 | } 141 | ], 142 | "is_externally_hosted": false, 143 | "languages": [ 144 | "sv" 145 | ], 146 | "media_type": "audio", 147 | "name": "Vetenskapsradion Historia", 148 | "publisher": "Sveriges Radio", 149 | "type": "show", 150 | "uri": "spotify:show:38bS44xjbVVZ3No3ByF1dJ" 151 | }, 152 | "type": "episode", 153 | "uri": "spotify:episode:512ojhOuo1ktJprKbVcKyQ" 154 | } 155 | -------------------------------------------------------------------------------- /tests/fixtures/episodes.json: -------------------------------------------------------------------------------- 1 | { 2 | "episodes" : [ { 3 | "audio_preview_url" : "https://p.scdn.co/mp3-preview/7e8f7a00f1425d495bcb992bae48a19c31342490", 4 | "description" : "Följ med till Riddarhuset och hör om dråpliga motiv och billiga lösningar på husets drygt 2 300 vapensköldar som nu studerats. Och hör hur stormakten Sveriges krig finansierades av Frankrike. Skelögda ugglor och halshuggna troll är några av motiven på de drygt 2 300 vapensköldar som hänger i Riddarhuset i Stockholm. Den svenska adelns grafiska profiler har nu hamnat under luppen när heraldikern Magnus Bäckmark som förste forskare skärskådat detta bortglömda kulturarvs estetik och historia. Vetenskapsradion Historia följer med honom till Riddarhuset för att fascineras av både vackra och tokfula motiv. Dessutom om att den svenska stormaktstiden nu måste omvärderas efter att historikern Svante Norrhem undersökt de enorma summor som Sverige erhöll av Frankrike. Under närmare 170 år var Sverige närmast en klientstat till Frankrike, där närmare 20 procent av svensk ekonomi bestod av franska subsidier. Tobias Svanelid undersöker hur förhållandet påverkade länderna och hur mycket av den svenska stormaktstiden som egentligen var fransk.", 5 | "duration_ms" : 2685023, 6 | "explicit" : false, 7 | "external_urls" : { 8 | "spotify" : "https://open.spotify.com/episode/77o6BIVlYM3msb4MMIL1jH" 9 | }, 10 | "href" : "https://api.spotify.com/v1/episodes/77o6BIVlYM3msb4MMIL1jH", 11 | "id" : "77o6BIVlYM3msb4MMIL1jH", 12 | "images" : [ { 13 | "height" : 640, 14 | "url" : "https://i.scdn.co/image/8092469858486ff19eeefcea7ec5c17b72c9590a", 15 | "width" : 640 16 | }, { 17 | "height" : 300, 18 | "url" : "https://i.scdn.co/image/7e921e844f4deb5a8fbdacba7abb6210357237e5", 19 | "width" : 300 20 | }, { 21 | "height" : 64, 22 | "url" : "https://i.scdn.co/image/729df823ef7f9a6f8aaf57d532490c9aab43e0dc", 23 | "width" : 64 24 | } ], 25 | "is_externally_hosted" : false, 26 | "is_playable" : true, 27 | "language" : "sv", 28 | "name" : "Riddarnas vapensköldar under lupp", 29 | "release_date" : "2019-09-10", 30 | "release_date_precision" : "day", 31 | "show" : { 32 | "available_markets" : [ "AD", "AE", "AR", "AT", "AU", "BE", "BG", "BH", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "DZ", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "ID", "IE", "IL", "IN", "IS", "IT", "JO", "JP", "KW", "LB", "LI", "LT", "LU", "LV", "MA", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "OM", "PA", "PE", "PH", "PL", "PS", "PT", "PY", "QA", "RO", "SE", "SG", "SK", "SV", "TH", "TN", "TR", "TW", "US", "UY", "VN", "ZA" ], 33 | "copyrights" : [ ], 34 | "description" : "Vi är där historien är. Ansvarig utgivare: Nina Glans", 35 | "explicit" : false, 36 | "external_urls" : { 37 | "spotify" : "https://open.spotify.com/show/38bS44xjbVVZ3No3ByF1dJ" 38 | }, 39 | "href" : "https://api.spotify.com/v1/shows/38bS44xjbVVZ3No3ByF1dJ", 40 | "id" : "38bS44xjbVVZ3No3ByF1dJ", 41 | "images" : [ { 42 | "height" : 640, 43 | "url" : "https://i.scdn.co/image/3c59a8b611000c8b10c8013013c3783dfb87a3bc", 44 | "width" : 640 45 | }, { 46 | "height" : 300, 47 | "url" : "https://i.scdn.co/image/2d70c06ac70d8c6144c94cabf7f4abcf85c4b7e4", 48 | "width" : 300 49 | }, { 50 | "height" : 64, 51 | "url" : "https://i.scdn.co/image/3dc007829bc0663c24089e46743a9f4ae15e65f8", 52 | "width" : 64 53 | } ], 54 | "is_externally_hosted" : false, 55 | "languages" : [ "sv" ], 56 | "media_type" : "audio", 57 | "name" : "Vetenskapsradion Historia", 58 | "publisher" : "Sveriges Radio", 59 | "type" : "show", 60 | "uri" : "spotify:show:38bS44xjbVVZ3No3ByF1dJ" 61 | }, 62 | "type" : "episode", 63 | "uri" : "spotify:episode:77o6BIVlYM3msb4MMIL1jH" 64 | }, { 65 | "audio_preview_url" : "https://p.scdn.co/mp3-preview/83bc7f2d40e850582a4ca118b33c256358de06ff", 66 | "description" : "Följ med Tobias Svanelid till Sveriges äldsta tegelkyrka, till Edsleskog mitt i den dalsländska granskogen, där ett religiöst skrytbygge skulle resas över ett skändligt brott. I Edsleskog i Dalsland gräver arkeologerna nu ut vad som en gång verkar ha varit en av Sveriges största medeltidskyrkor, och kanske också den äldsta som byggts i tegel, 1200-talets high-tech-material. Tobias Svanelid reser dit för att höra historien om den märkliga och bortglömda kyrkan som grundlades på platsen för ett prästmord och dessutom kan ha varit Skarabiskopens försök att lägga beslag på det vilda Dalsland. Dessutom om sjudagarsveckan idag ett välkänt koncept runt hela världen, men hur gammal är egentligen veckans historia? Dick Harrison vet svaret.", 67 | "duration_ms" : 2685023, 68 | "explicit" : false, 69 | "external_urls" : { 70 | "spotify" : "https://open.spotify.com/episode/0Q86acNRm6V9GYx55SXKwf" 71 | }, 72 | "href" : "https://api.spotify.com/v1/episodes/0Q86acNRm6V9GYx55SXKwf", 73 | "id" : "0Q86acNRm6V9GYx55SXKwf", 74 | "images" : [ { 75 | "height" : 640, 76 | "url" : "https://i.scdn.co/image/b2398424d6158a21fe8677e2de5f6f3d1dc4a04f", 77 | "width" : 640 78 | }, { 79 | "height" : 300, 80 | "url" : "https://i.scdn.co/image/a52780a1d7e1bc42619413c3dea7042396c87f49", 81 | "width" : 300 82 | }, { 83 | "height" : 64, 84 | "url" : "https://i.scdn.co/image/88e21be860cf11f0b95ee8dfb47ddb08a13319a7", 85 | "width" : 64 86 | } ], 87 | "is_externally_hosted" : false, 88 | "is_playable" : true, 89 | "language" : "sv", 90 | "name" : "Okända katedralen i Dalsland", 91 | "release_date" : "2019-09-03", 92 | "release_date_precision" : "day", 93 | "show" : { 94 | "available_markets" : [ "AD", "AE", "AR", "AT", "AU", "BE", "BG", "BH", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "DZ", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "ID", "IE", "IL", "IN", "IS", "IT", "JO", "JP", "KW", "LB", "LI", "LT", "LU", "LV", "MA", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "OM", "PA", "PE", "PH", "PL", "PS", "PT", "PY", "QA", "RO", "SE", "SG", "SK", "SV", "TH", "TN", "TR", "TW", "US", "UY", "VN", "ZA" ], 95 | "copyrights" : [ ], 96 | "description" : "Vi är där historien är. Ansvarig utgivare: Nina Glans", 97 | "explicit" : false, 98 | "external_urls" : { 99 | "spotify" : "https://open.spotify.com/show/38bS44xjbVVZ3No3ByF1dJ" 100 | }, 101 | "href" : "https://api.spotify.com/v1/shows/38bS44xjbVVZ3No3ByF1dJ", 102 | "id" : "38bS44xjbVVZ3No3ByF1dJ", 103 | "images" : [ { 104 | "height" : 640, 105 | "url" : "https://i.scdn.co/image/3c59a8b611000c8b10c8013013c3783dfb87a3bc", 106 | "width" : 640 107 | }, { 108 | "height" : 300, 109 | "url" : "https://i.scdn.co/image/2d70c06ac70d8c6144c94cabf7f4abcf85c4b7e4", 110 | "width" : 300 111 | }, { 112 | "height" : 64, 113 | "url" : "https://i.scdn.co/image/3dc007829bc0663c24089e46743a9f4ae15e65f8", 114 | "width" : 64 115 | } ], 116 | "is_externally_hosted" : false, 117 | "languages" : [ "sv" ], 118 | "media_type" : "audio", 119 | "name" : "Vetenskapsradion Historia", 120 | "publisher" : "Sveriges Radio", 121 | "type" : "show", 122 | "uri" : "spotify:show:38bS44xjbVVZ3No3ByF1dJ" 123 | }, 124 | "type" : "episode", 125 | "uri" : "spotify:episode:0Q86acNRm6V9GYx55SXKwf" 126 | } ] 127 | } 128 | -------------------------------------------------------------------------------- /tests/fixtures/featured-playlists.json: -------------------------------------------------------------------------------- 1 | { 2 | "message" : "Your Saturday night just got so much better...", 3 | "playlists" : { 4 | "href" : "https://api.spotify.com/v1/browse/featured-playlists?country=SE&locale=sv_SE×tamp=2014-10-25T21:00:00&offset=0&limit=5", 5 | "items" : [ { 6 | "collaborative" : false, 7 | "external_urls" : { 8 | "spotify" : "http://open.spotify.com/user/spotify/playlist/5Oo5QuAOjjXMMXphFqC6eo" 9 | }, 10 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/5Oo5QuAOjjXMMXphFqC6eo", 11 | "id" : "5Oo5QuAOjjXMMXphFqC6eo", 12 | "images" : [ { 13 | "url" : "https://i.scdn.co/image/48294c9d3d171ac4c5b6400e0d8f2c357569ce75" 14 | } ], 15 | "name" : "Weekend Hangouts", 16 | "owner" : { 17 | "external_urls" : { 18 | "spotify" : "http://open.spotify.com/user/spotify" 19 | }, 20 | "href" : "https://api.spotify.com/v1/users/spotify", 21 | "id" : "spotify", 22 | "type" : "user", 23 | "uri" : "spotify:user:spotify" 24 | }, 25 | "public" : null, 26 | "tracks" : { 27 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/5Oo5QuAOjjXMMXphFqC6eo/tracks", 28 | "total" : 223 29 | }, 30 | "type" : "playlist", 31 | "uri" : "spotify:user:spotify:playlist:5Oo5QuAOjjXMMXphFqC6eo" 32 | }, { 33 | "collaborative" : false, 34 | "external_urls" : { 35 | "spotify" : "http://open.spotify.com/user/tv4se/playlist/6gI54Ois1dlCKZJqrLyVKN" 36 | }, 37 | "href" : "https://api.spotify.com/v1/users/tv4se/playlists/6gI54Ois1dlCKZJqrLyVKN", 38 | "id" : "6gI54Ois1dlCKZJqrLyVKN", 39 | "images" : [ { 40 | "url" : "https://i.scdn.co/image/3be6c5a5879dc630c7136a63e30ad61f55c64ec6" 41 | } ], 42 | "name" : "SÅ MYCKET BÄTTRE 2014", 43 | "owner" : { 44 | "external_urls" : { 45 | "spotify" : "http://open.spotify.com/user/tv4se" 46 | }, 47 | "href" : "https://api.spotify.com/v1/users/tv4se", 48 | "id" : "tv4se", 49 | "type" : "user", 50 | "uri" : "spotify:user:tv4se" 51 | }, 52 | "public" : null, 53 | "tracks" : { 54 | "href" : "https://api.spotify.com/v1/users/tv4se/playlists/6gI54Ois1dlCKZJqrLyVKN/tracks", 55 | "total" : 12 56 | }, 57 | "type" : "playlist", 58 | "uri" : "spotify:user:tv4se:playlist:6gI54Ois1dlCKZJqrLyVKN" 59 | }, { 60 | "collaborative" : false, 61 | "external_urls" : { 62 | "spotify" : "http://open.spotify.com/user/spotify/playlist/63UdjnaWDFp0VNrwQ28qUw" 63 | }, 64 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/63UdjnaWDFp0VNrwQ28qUw", 65 | "id" : "63UdjnaWDFp0VNrwQ28qUw", 66 | "images" : [ { 67 | "url" : "https://i.scdn.co/image/68dc8102daec4e47d501f5f83f0ed7c59e14ab23" 68 | } ], 69 | "name" : "Dinner Party", 70 | "owner" : { 71 | "external_urls" : { 72 | "spotify" : "http://open.spotify.com/user/spotify" 73 | }, 74 | "href" : "https://api.spotify.com/v1/users/spotify", 75 | "id" : "spotify", 76 | "type" : "user", 77 | "uri" : "spotify:user:spotify" 78 | }, 79 | "public" : null, 80 | "tracks" : { 81 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/63UdjnaWDFp0VNrwQ28qUw/tracks", 82 | "total" : 121 83 | }, 84 | "type" : "playlist", 85 | "uri" : "spotify:user:spotify:playlist:63UdjnaWDFp0VNrwQ28qUw" 86 | }, { 87 | "collaborative" : false, 88 | "external_urls" : { 89 | "spotify" : "http://open.spotify.com/user/spotify/playlist/5ILSWr90l2Bgk89xuhsysy" 90 | }, 91 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/5ILSWr90l2Bgk89xuhsysy", 92 | "id" : "5ILSWr90l2Bgk89xuhsysy", 93 | "images" : [ { 94 | "url" : "https://i.scdn.co/image/b15df0f45ccf5b0b4ac97674658de5b42c342df1" 95 | } ], 96 | "name" : "Pre-Party", 97 | "owner" : { 98 | "external_urls" : { 99 | "spotify" : "http://open.spotify.com/user/spotify" 100 | }, 101 | "href" : "https://api.spotify.com/v1/users/spotify", 102 | "id" : "spotify", 103 | "type" : "user", 104 | "uri" : "spotify:user:spotify" 105 | }, 106 | "public" : null, 107 | "tracks" : { 108 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/5ILSWr90l2Bgk89xuhsysy/tracks", 109 | "total" : 106 110 | }, 111 | "type" : "playlist", 112 | "uri" : "spotify:user:spotify:playlist:5ILSWr90l2Bgk89xuhsysy" 113 | }, { 114 | "collaborative" : false, 115 | "external_urls" : { 116 | "spotify" : "http://open.spotify.com/user/spotify/playlist/7BixMZxL4bhgULJQ5wPbUz" 117 | }, 118 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/7BixMZxL4bhgULJQ5wPbUz", 119 | "id" : "7BixMZxL4bhgULJQ5wPbUz", 120 | "images" : [ { 121 | "url" : "https://i.scdn.co/image/202df3b14174a06f1cd9541302d51c291eed8bee" 122 | } ], 123 | "name" : "Chillax", 124 | "owner" : { 125 | "external_urls" : { 126 | "spotify" : "http://open.spotify.com/user/spotify" 127 | }, 128 | "href" : "https://api.spotify.com/v1/users/spotify", 129 | "id" : "spotify", 130 | "type" : "user", 131 | "uri" : "spotify:user:spotify" 132 | }, 133 | "public" : null, 134 | "tracks" : { 135 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/7BixMZxL4bhgULJQ5wPbUz/tracks", 136 | "total" : 102 137 | }, 138 | "type" : "playlist", 139 | "uri" : "spotify:user:spotify:playlist:7BixMZxL4bhgULJQ5wPbUz" 140 | } ], 141 | "limit" : 5, 142 | "next" : "https://api.spotify.com/v1/browse/featured-playlists?country=SE&locale=sv_SE×tamp=2014-10-25T21:00:00&offset=5&limit=5", 143 | "offset" : 0, 144 | "previous" : null, 145 | "total" : 12 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /tests/fixtures/markets.json: -------------------------------------------------------------------------------- 1 | { 2 | "markets": ["AD","AE","AR","AT","AU","BE","BG","BH"] 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/multiple-audio-features.json: -------------------------------------------------------------------------------- 1 | { "audio_features": 2 | [ { "danceability": 0.808, 3 | "energy": 0.626, 4 | "key": 7, 5 | "loudness": -12.733, 6 | "mode": 1, 7 | "speechiness": 0.168, 8 | "acousticness": 0.00187, 9 | "instrumentalness": 0.159, 10 | "liveness": 0.376, 11 | "valence": 0.369, 12 | "tempo": 123.99, 13 | "type": "audio_features", 14 | "id": "0eGsygTp906u18L0Oimnem", 15 | "uri": "spotify:track:0eGsygTp906u18L0Oimnem", 16 | "track_href": "https://api.spotify.com/v1/tracks/0eGsygTp906u18L0Oimnem", 17 | "analysis_url": "http://echonest-analysis.s3.amazonaws.com/TR/WhpYUARk1kNJ_qP0AdKGcDDFKOQTTgsOoINrqyPQjkUnbteuuBiyj_u94iFCSGzdxGiwqQ6d77f4QLL_8=/3/full.json?AWSAccessKeyId=AKIAJRDFEY23UEVW42BQ&Expires=1458063189&Signature=JRE8SDZStpNOdUsPN/PoS49FMtQ%3D", 18 | "duration_ms": 535223, 19 | "time_signature": 4 20 | }, 21 | { "danceability": 0.457, 22 | "energy": 0.815, 23 | "key": 1, 24 | "loudness": -7.199, 25 | "mode": 1, 26 | "speechiness": 0.034, 27 | "acousticness": 0.102, 28 | "instrumentalness": 0.0319, 29 | "liveness": 0.103, 30 | "valence": 0.382, 31 | "tempo": 96.083, 32 | "type": "audio_features", 33 | "id": "1lDWb6b6ieDQ2xT7ewTC3G", 34 | "uri": "spotify:track:1lDWb6b6ieDQ2xT7ewTC3G", 35 | "track_href": "https://api.spotify.com/v1/tracks/1lDWb6b6ieDQ2xT7ewTC3G", 36 | "analysis_url": "http://echonest-analysis.s3.amazonaws.com/TR/WhuQhwPDhmEg5TO4JjbJu0my-awIhk3eaXkRd1ofoJ7tXogPnMtbxkTyLOeHXu5Jke0FCIt52saKJyfPM=/3/full.json?AWSAccessKeyId=AKIAJRDFEY23UEVW42BQ&Expires=1458063189&Signature=qfclum7FwTaR/7aQbnBNO0daCsM%3D", 37 | "duration_ms": 187800, 38 | "time_signature": 4 39 | } ] 40 | } 41 | -------------------------------------------------------------------------------- /tests/fixtures/my-playlists.json: -------------------------------------------------------------------------------- 1 | { 2 | "href": "https://api.spotify.com/v1/users/wizzler/playlists", 3 | "items": [ { 4 | "collaborative": false, 5 | "external_urls": { 6 | "spotify": "http://open.spotify.com/user/wizzler/playlists/53Y8wT46QIMz5H4WQ8O22c" 7 | }, 8 | "href": "https://api.spotify.com/v1/users/wizzler/playlists/53Y8wT46QIMz5H4WQ8O22c", 9 | "id": "53Y8wT46QIMz5H4WQ8O22c", 10 | "images" : [ ], 11 | "name": "Wizzlers Big Playlist", 12 | "owner": { 13 | "external_urls": { 14 | "spotify": "http://open.spotify.com/user/wizzler" 15 | }, 16 | "href": "https://api.spotify.com/v1/users/wizzler", 17 | "id": "wizzler", 18 | "type": "user", 19 | "uri": "spotify:user:wizzler" 20 | }, 21 | "public": true, 22 | "snapshot_id" : "bNLWdmhh+HDsbHzhckXeDC0uyKyg4FjPI/KEsKjAE526usnz2LxwgyBoMShVL+z+", 23 | "tracks": { 24 | "href": "https://api.spotify.com/v1/users/wizzler/playlists/53Y8wT46QIMz5H4WQ8O22c/tracks", 25 | "total": 30 26 | }, 27 | "type": "playlist", 28 | "uri": "spotify:user:wizzler:playlist:53Y8wT46QIMz5H4WQ8O22c" 29 | }, { 30 | "collaborative": false, 31 | "external_urls": { 32 | "spotify": "http://open.spotify.com/user/wizzlersmate/playlists/1AVZz0mBuGbCEoNRQdYQju" 33 | }, 34 | "href": "https://api.spotify.com/v1/users/wizzlersmate/playlists/1AVZz0mBuGbCEoNRQdYQju", 35 | "id": "1AVZz0mBuGbCEoNRQdYQju", 36 | "images" : [ ], 37 | "name": "Another Playlist", 38 | "owner": { 39 | "external_urls": { 40 | "spotify": "http://open.spotify.com/user/wizzlersmate" 41 | }, 42 | "href": "https://api.spotify.com/v1/users/wizzlersmate", 43 | "id": "wizzlersmate", 44 | "type": "user", 45 | "uri": "spotify:user:wizzlersmate" 46 | }, 47 | "public": true, 48 | "snapshot_id" : "Y0qg/IT5T02DKpw4uQKc/9RUrqQJ07hbTKyEeDRPOo9LU0g0icBrIXwVkHfQZ/aD", 49 | "tracks": { 50 | "href": "https://api.spotify.com/v1/users/wizzlersmate/playlists/1AVZz0mBuGbCEoNRQdYQju/tracks", 51 | "total": 58 52 | }, 53 | "type": "playlist", 54 | "uri": "spotify:user:wizzlersmate:playlist:1AVZz0mBuGbCEoNRQdYQju" 55 | } ], 56 | "limit": 9, 57 | "next": null, 58 | "offset": 0, 59 | "previous": null, 60 | "total": 9 61 | } 62 | -------------------------------------------------------------------------------- /tests/fixtures/my-queue.json: -------------------------------------------------------------------------------- 1 | { 2 | "currently_playing": { 3 | "album": { 4 | "album_type": "album", 5 | "artists": [ 6 | { 7 | "external_urls": { 8 | "spotify": "https://open.spotify.com/artist/3Mcii5XWf6E0lrY3Uky4cA" 9 | }, 10 | "href": "https://api.spotify.com/v1/artists/3Mcii5XWf6E0lrY3Uky4cA", 11 | "id": "3Mcii5XWf6E0lrY3Uky4cA", 12 | "name": "Ice Cube", 13 | "type": "artist", 14 | "uri": "spotify:artist:3Mcii5XWf6E0lrY3Uky4cA" 15 | } 16 | ], 17 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "UY" ], 18 | "external_urls": { 19 | "spotify": "https://open.spotify.com/album/3xbWu3mLkkWZvgHtExIaKZ" 20 | }, 21 | "href": "https://api.spotify.com/v1/albums/3xbWu3mLkkWZvgHtExIaKZ", 22 | "id": "3xbWu3mLkkWZvgHtExIaKZ", 23 | "images": [ 24 | { 25 | "height": 640, 26 | "url": "https://i.scdn.co/image/ab67616d0000b273e83d58253e2e7c6742bbf9e4", 27 | "width": 640 28 | }, 29 | { 30 | "height": 300, 31 | "url": "https://i.scdn.co/image/ab67616d00001e02e83d58253e2e7c6742bbf9e4", 32 | "width": 300 33 | }, 34 | { 35 | "height": 64, 36 | "url": "https://i.scdn.co/image/ab67616d00004851e83d58253e2e7c6742bbf9e4", 37 | "width": 64 38 | } 39 | ], 40 | "name": "Raw Footage", 41 | "release_date": "2008-01-01", 42 | "release_date_precision": "day", 43 | "total_tracks": 16, 44 | "type": "album", 45 | "uri": "spotify:album:3xbWu3mLkkWZvgHtExIaKZ" 46 | }, 47 | "artists": [ 48 | { 49 | "external_urls": { 50 | "spotify": "https://open.spotify.com/artist/3Mcii5XWf6E0lrY3Uky4cA" 51 | }, 52 | "href": "https://api.spotify.com/v1/artists/3Mcii5XWf6E0lrY3Uky4cA", 53 | "id": "3Mcii5XWf6E0lrY3Uky4cA", 54 | "name": "Ice Cube", 55 | "type": "artist", 56 | "uri": "spotify:artist:3Mcii5XWf6E0lrY3Uky4cA" 57 | } 58 | ], 59 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "UY" ], 60 | "disc_number": 1, 61 | "duration_ms": 247893, 62 | "explicit": true, 63 | "external_ids": { "isrc": "USNPD0800740" }, 64 | "external_urls": { 65 | "spotify": "https://open.spotify.com/track/5RAMj0e8KWDBYXZeWYmfEx" 66 | }, 67 | "href": "https://api.spotify.com/v1/tracks/5RAMj0e8KWDBYXZeWYmfEx", 68 | "id": "5RAMj0e8KWDBYXZeWYmfEx", 69 | "is_local": false, 70 | "name": "Get Money, Spend Money, No Money", 71 | "popularity": 29, 72 | "preview_url": "https://p.scdn.co/mp3-preview/9ed43baa5752713105962f8bcaa8ea092cb17764?cid=ab12ee273d0b40b38b78f557aaccc0fe", 73 | "track_number": 12, 74 | "type": "track", 75 | "uri": "spotify:track:5RAMj0e8KWDBYXZeWYmfEx" 76 | }, 77 | "queue": [ 78 | { 79 | "album": { 80 | "album_type": "album", 81 | "artists": [ 82 | { 83 | "external_urls": { 84 | "spotify": "https://open.spotify.com/artist/6NyJIFHAePjHR1pFxwisqz" 85 | }, 86 | "href": "https://api.spotify.com/v1/artists/6NyJIFHAePjHR1pFxwisqz", 87 | "id": "6NyJIFHAePjHR1pFxwisqz", 88 | "name": "Kurupt", 89 | "type": "artist", 90 | "uri": "spotify:artist:6NyJIFHAePjHR1pFxwisqz" 91 | } 92 | ], 93 | "available_markets": [ "AE", "BH", "CA", "DZ", "EG", "IQ", "JO", "KW", "LB", "LY", "MA", "MX", "OM", "QA", "SA", "TN", "US" ], 94 | "external_urls": { 95 | "spotify": "https://open.spotify.com/album/4dSH1oNZoziNjKAanUimWd" 96 | }, 97 | "href": "https://api.spotify.com/v1/albums/4dSH1oNZoziNjKAanUimWd", 98 | "id": "4dSH1oNZoziNjKAanUimWd", 99 | "images": [ 100 | { 101 | "height": 640, 102 | "url": "https://i.scdn.co/image/ab67616d0000b2730c46f1a7e2cb5ebfd4714b28", 103 | "width": 640 104 | }, 105 | { 106 | "height": 300, 107 | "url": "https://i.scdn.co/image/ab67616d00001e020c46f1a7e2cb5ebfd4714b28", 108 | "width": 300 109 | }, 110 | { 111 | "height": 64, 112 | "url": "https://i.scdn.co/image/ab67616d000048510c46f1a7e2cb5ebfd4714b28", 113 | "width": 64 114 | } 115 | ], 116 | "name": "Kuruption!", 117 | "release_date": "1998-10-06", 118 | "release_date_precision": "day", 119 | "total_tracks": 23, 120 | "type": "album", 121 | "uri": "spotify:album:4dSH1oNZoziNjKAanUimWd" 122 | }, 123 | "artists": [ 124 | { 125 | "external_urls": { 126 | "spotify": "https://open.spotify.com/artist/6NyJIFHAePjHR1pFxwisqz" 127 | }, 128 | "href": "https://api.spotify.com/v1/artists/6NyJIFHAePjHR1pFxwisqz", 129 | "id": "6NyJIFHAePjHR1pFxwisqz", 130 | "name": "Kurupt", 131 | "type": "artist", 132 | "uri": "spotify:artist:6NyJIFHAePjHR1pFxwisqz" 133 | }, 134 | { 135 | "external_urls": { 136 | "spotify": "https://open.spotify.com/artist/0bqBpcIABLyrGD6e6llQ1S" 137 | }, 138 | "href": "https://api.spotify.com/v1/artists/0bqBpcIABLyrGD6e6llQ1S", 139 | "id": "0bqBpcIABLyrGD6e6llQ1S", 140 | "name": "Tray Dee", 141 | "type": "artist", 142 | "uri": "spotify:artist:0bqBpcIABLyrGD6e6llQ1S" 143 | }, 144 | { 145 | "external_urls": { 146 | "spotify": "https://open.spotify.com/artist/197MEJ6lXwJXav4BstjTxl" 147 | }, 148 | "href": "https://api.spotify.com/v1/artists/197MEJ6lXwJXav4BstjTxl", 149 | "id": "197MEJ6lXwJXav4BstjTxl", 150 | "name": "Slip Capone", 151 | "type": "artist", 152 | "uri": "spotify:artist:197MEJ6lXwJXav4BstjTxl" 153 | } 154 | ], 155 | "available_markets": [ "AE", "BH", "CA", "DZ", "EG", "IQ", "JO", "KW", "LB", "LY", "MA", "MX", "OM", "QA", "SA", "TN", "US" ], 156 | "disc_number": 1, 157 | "duration_ms": 311666, 158 | "explicit": true, 159 | "external_ids": { "isrc": "USAM19800243" }, 160 | "external_urls": { 161 | "spotify": "https://open.spotify.com/track/6S1oHrwjeF66VRUl1b76P6" 162 | }, 163 | "href": "https://api.spotify.com/v1/tracks/6S1oHrwjeF66VRUl1b76P6", 164 | "id": "6S1oHrwjeF66VRUl1b76P6", 165 | "is_local": false, 166 | "name": "C-Walk", 167 | "popularity": 55, 168 | "preview_url": "https://p.scdn.co/mp3-preview/34ba7cde30c9ece93e927db9f90d29484a2aa712?cid=ab12ee273d0b40b38b78f557aaccc0fe", 169 | "track_number": 7, 170 | "type": "track", 171 | "uri": "spotify:track:6S1oHrwjeF66VRUl1b76P6" 172 | }, 173 | { 174 | "album": { 175 | "album_type": "single", 176 | "artists": [ 177 | { 178 | "external_urls": { 179 | "spotify": "https://open.spotify.com/artist/7B4hKK0S9QYnaoqa9OuwgX" 180 | }, 181 | "href": "https://api.spotify.com/v1/artists/7B4hKK0S9QYnaoqa9OuwgX", 182 | "id": "7B4hKK0S9QYnaoqa9OuwgX", 183 | "name": "Eazy-E", 184 | "type": "artist", 185 | "uri": "spotify:artist:7B4hKK0S9QYnaoqa9OuwgX" 186 | } 187 | ], 188 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "UY" ], 189 | "external_urls": { 190 | "spotify": "https://open.spotify.com/album/65bIyEn06DYO7oFkEYKOdl" 191 | }, 192 | "href": "https://api.spotify.com/v1/albums/65bIyEn06DYO7oFkEYKOdl", 193 | "id": "65bIyEn06DYO7oFkEYKOdl", 194 | "images": [ 195 | { 196 | "height": 640, 197 | "url": "https://i.scdn.co/image/ab67616d0000b273edfb9175857fb59639e148e0", 198 | "width": 640 199 | }, 200 | { 201 | "height": 300, 202 | "url": "https://i.scdn.co/image/ab67616d00001e02edfb9175857fb59639e148e0", 203 | "width": 300 204 | }, 205 | { 206 | "height": 64, 207 | "url": "https://i.scdn.co/image/ab67616d00004851edfb9175857fb59639e148e0", 208 | "width": 64 209 | } 210 | ], 211 | "name": "5150 Home 4 Tha Sick", 212 | "release_date": "1992-12-10", 213 | "release_date_precision": "day", 214 | "total_tracks": 5, 215 | "type": "album", 216 | "uri": "spotify:album:65bIyEn06DYO7oFkEYKOdl" 217 | }, 218 | "artists": [ 219 | { 220 | "external_urls": { 221 | "spotify": "https://open.spotify.com/artist/7B4hKK0S9QYnaoqa9OuwgX" 222 | }, 223 | "href": "https://api.spotify.com/v1/artists/7B4hKK0S9QYnaoqa9OuwgX", 224 | "id": "7B4hKK0S9QYnaoqa9OuwgX", 225 | "name": "Eazy-E", 226 | "type": "artist", 227 | "uri": "spotify:artist:7B4hKK0S9QYnaoqa9OuwgX" 228 | } 229 | ], 230 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "UY" ], 231 | "disc_number": 1, 232 | "duration_ms": 183333, 233 | "explicit": true, 234 | "external_ids": { "isrc": "USPO19200009" }, 235 | "external_urls": { 236 | "spotify": "https://open.spotify.com/track/217m5TxpXyqbpR7WWmoSqO" 237 | }, 238 | "href": "https://api.spotify.com/v1/tracks/217m5TxpXyqbpR7WWmoSqO", 239 | "id": "217m5TxpXyqbpR7WWmoSqO", 240 | "is_local": false, 241 | "name": "Only If You Want It", 242 | "popularity": 50, 243 | "preview_url": "https://p.scdn.co/mp3-preview/7d638c71412ab30df978e5684e04d86c50b84a7e?cid=ab12ee273d0b40b38b78f557aaccc0fe", 244 | "track_number": 2, 245 | "type": "track", 246 | "uri": "spotify:track:217m5TxpXyqbpR7WWmoSqO" 247 | } 248 | ] 249 | } 250 | -------------------------------------------------------------------------------- /tests/fixtures/new-releases.json: -------------------------------------------------------------------------------- 1 | { 2 | "albums" : { 3 | "href" : "https://api.spotify.com/v1/browse/new-releases?country=SE&offset=0&limit=5", 4 | "items" : [ { 5 | "album_type" : "album", 6 | "available_markets" : [ "AT", "AU", "CH", "CZ", "DE", "FI", "IE", "NL", "NZ", "SI", "SK", "HK", "MY", "PH", "SG", "TW", "BG", "CY", "EE", "GR", "LT", "LV", "RO", "TR", "AD", "BE", "DK", "ES", "FR", "HU", "IT", "LI", "LU", "MC", "MT", "NO", "PL", "SE", "GB", "PT", "IS", "UY", "AR", "CL", "PY", "BO", "BR", "DO", "CO", "EC", "PA", "PE", "CR", "GT", "HN", "NI", "SV" ], 7 | "external_urls" : { 8 | "spotify" : "https://open.spotify.com/album/59Mjf7KcSU7niudeL4JsmQ" 9 | }, 10 | "href" : "https://api.spotify.com/v1/albums/59Mjf7KcSU7niudeL4JsmQ", 11 | "id" : "59Mjf7KcSU7niudeL4JsmQ", 12 | "images" : [ { 13 | "height" : 640, 14 | "url" : "https://i.scdn.co/image/34ad44065027a8e99b5fc071b09b8109928a1e59", 15 | "width" : 640 16 | }, { 17 | "height" : 300, 18 | "url" : "https://i.scdn.co/image/b9fc808bcf4a9e1d9576489e3f90ec6bfbf762df", 19 | "width" : 300 20 | }, { 21 | "height" : 64, 22 | "url" : "https://i.scdn.co/image/7319c88ba4e6c9a091dd62a1bfbc1f72fe68e779", 23 | "width" : 64 24 | } ], 25 | "name" : "Feels So Good", 26 | "type" : "album", 27 | "uri" : "spotify:album:59Mjf7KcSU7niudeL4JsmQ" 28 | }, { 29 | "album_type" : "album", 30 | "available_markets" : [ "AT", "BE", "CH", "CZ", "DE", "EE", "FI", "IE", "LI", "LT", "LU", "LV", "NL", "SE", "SK", "TR" ], 31 | "external_urls" : { 32 | "spotify" : "https://open.spotify.com/album/4KyyjcX6N1xcxTqdOTOghN" 33 | }, 34 | "href" : "https://api.spotify.com/v1/albums/4KyyjcX6N1xcxTqdOTOghN", 35 | "id" : "4KyyjcX6N1xcxTqdOTOghN", 36 | "images" : [ { 37 | "height" : 640, 38 | "url" : "https://i.scdn.co/image/4b12129e2b6630cab568c2941835d0e28f85840b", 39 | "width" : 640 40 | }, { 41 | "height" : 300, 42 | "url" : "https://i.scdn.co/image/c9ed2290296aa4038139f708ff7b6cefa65b98af", 43 | "width" : 300 44 | }, { 45 | "height" : 64, 46 | "url" : "https://i.scdn.co/image/e8dd9b7e27363122b27eb99e63bf274a0849bd5f", 47 | "width" : 64 48 | } ], 49 | "name" : "Back to Earth", 50 | "type" : "album", 51 | "uri" : "spotify:album:4KyyjcX6N1xcxTqdOTOghN" 52 | }, { 53 | "album_type" : "album", 54 | "available_markets" : [ "AT", "CH", "CZ", "DE", "FI", "NL", "NZ", "SE", "SI", "SK" ], 55 | "external_urls" : { 56 | "spotify" : "https://open.spotify.com/album/7yYva2MJYef1aOFVBtlA8I" 57 | }, 58 | "href" : "https://api.spotify.com/v1/albums/7yYva2MJYef1aOFVBtlA8I", 59 | "id" : "7yYva2MJYef1aOFVBtlA8I", 60 | "images" : [ { 61 | "height" : 640, 62 | "url" : "https://i.scdn.co/image/a904eaf3c6440542493b10ef552cd955a8b1515a", 63 | "width" : 640 64 | }, { 65 | "height" : 300, 66 | "url" : "https://i.scdn.co/image/c9bbdc3adc7a8a2f159f31da13a989457572353d", 67 | "width" : 300 68 | }, { 69 | "height" : 64, 70 | "url" : "https://i.scdn.co/image/fdaa69c01adb12a603a7af8f4bfbce39a413ef21", 71 | "width" : 64 72 | } ], 73 | "name" : "Nostalgia", 74 | "type" : "album", 75 | "uri" : "spotify:album:7yYva2MJYef1aOFVBtlA8I" 76 | }, { 77 | "album_type" : "album", 78 | "available_markets" : [ "SE" ], 79 | "external_urls" : { 80 | "spotify" : "https://open.spotify.com/album/517pAxOuxIz2HyltDuBWYf" 81 | }, 82 | "href" : "https://api.spotify.com/v1/albums/517pAxOuxIz2HyltDuBWYf", 83 | "id" : "517pAxOuxIz2HyltDuBWYf", 84 | "images" : [ { 85 | "height" : 640, 86 | "url" : "https://i.scdn.co/image/8db1fb7e5589d4d98022f3cc4ec0175a8bbc9b0e", 87 | "width" : 640 88 | }, { 89 | "height" : 300, 90 | "url" : "https://i.scdn.co/image/1342d206b6f1191c7b900878e1b2d1f147215d9d", 91 | "width" : 300 92 | }, { 93 | "height" : 64, 94 | "url" : "https://i.scdn.co/image/091236fc857a1c13a84319e614a0611d6829324f", 95 | "width" : 64 96 | } ], 97 | "name" : "Så mycket bättre 5 - Orups dag", 98 | "type" : "album", 99 | "uri" : "spotify:album:517pAxOuxIz2HyltDuBWYf" 100 | }, { 101 | "album_type" : "single", 102 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "UY" ], 103 | "external_urls" : { 104 | "spotify" : "https://open.spotify.com/album/50nrlTw0DWxBBvjNh1aiaq" 105 | }, 106 | "href" : "https://api.spotify.com/v1/albums/50nrlTw0DWxBBvjNh1aiaq", 107 | "id" : "50nrlTw0DWxBBvjNh1aiaq", 108 | "images" : [ { 109 | "height" : 640, 110 | "url" : "https://i.scdn.co/image/e351bd24bd85140f3558022a101363bfc61c7e30", 111 | "width" : 640 112 | }, { 113 | "height" : 300, 114 | "url" : "https://i.scdn.co/image/159230ff048626f793c86c7763c2dc782430d95e", 115 | "width" : 300 116 | }, { 117 | "height" : 64, 118 | "url" : "https://i.scdn.co/image/8118a38f53efeb1dc9049d673851ed0ae3b6ac08", 119 | "width" : 64 120 | } ], 121 | "name" : "Skuggor från Karelen", 122 | "type" : "album", 123 | "uri" : "spotify:album:50nrlTw0DWxBBvjNh1aiaq" 124 | } ], 125 | "limit" : 5, 126 | "next" : "https://api.spotify.com/v1/browse/new-releases?country=SE&offset=5&limit=5", 127 | "offset" : 0, 128 | "previous" : null, 129 | "total" : 500 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /tests/fixtures/playlist-cover-image.json: -------------------------------------------------------------------------------- 1 | { 2 | "height" : null, 3 | "url" : "https://i.scdn.co/image/ab67706c0000bebb8d0ce13d55f634e290f744ba", 4 | "width" : null 5 | } -------------------------------------------------------------------------------- /tests/fixtures/recently-played.json: -------------------------------------------------------------------------------- 1 | { 2 | "items": [ 3 | { 4 | "track": { 5 | "artists": [ 6 | { 7 | "external_urls": { 8 | "spotify": "https://open.spotify.com/artist/5EGOerlVYxwqxaTLEWumBR" 9 | }, 10 | "href": "https://api.spotify.com/v1/artists/5EGOerlVYxwqxaTLEWumBR", 11 | "id": "5EGOerlVYxwqxaTLEWumBR", 12 | "name": "Synapson", 13 | "type": "artist", 14 | "uri": "spotify:artist:5EGOerlVYxwqxaTLEWumBR" 15 | }, 16 | { 17 | "external_urls": { 18 | "spotify": "https://open.spotify.com/artist/1FCJ4zmRfkGUOtY65Jettg" 19 | }, 20 | "href": "https://api.spotify.com/v1/artists/1FCJ4zmRfkGUOtY65Jettg", 21 | "id": "1FCJ4zmRfkGUOtY65Jettg", 22 | "name": "Victor D\u00e9m\u00e9", 23 | "type": "artist", 24 | "uri": "spotify:artist:1FCJ4zmRfkGUOtY65Jettg" 25 | } 26 | ], 27 | "available_markets": [ 28 | "AD", 29 | "BE", 30 | "CH", 31 | "FR", 32 | "MC" 33 | ], 34 | "disc_number": 1, 35 | "duration_ms": 194706, 36 | "explicit": false, 37 | "external_urls": { 38 | "spotify": "https://open.spotify.com/track/3hxRKXzZS0XRYGZ123JDNH" 39 | }, 40 | "href": "https://api.spotify.com/v1/tracks/3hxRKXzZS0XRYGZ123JDNH", 41 | "id": "3hxRKXzZS0XRYGZ123JDNH", 42 | "name": "Djon Maya Ma\u00ef - feat. Victor D\u00e9m\u00e9 [Original Mix]", 43 | "preview_url": "https://p.scdn.co/mp3-preview/000f50cee43b1dad8ba39983b519615f2db52265?cid=63c1bb278a88426aa745cb4d887e4054", 44 | "track_number": 1, 45 | "type": "track", 46 | "uri": "spotify:track:3hxRKXzZS0XRYGZ123JDNH" 47 | }, 48 | "context": { 49 | "uri": "spotify:user:spotify_france:playlist:2sxfgEZ1yRhGo0mF3v8YSM", 50 | "external_urls": { 51 | "spotify": "https://open.spotify.com/user/spotify_france/playlist/2sxfgEZ1yRhGo0mF3v8YSM" 52 | }, 53 | "href": "https://api.spotify.com/v1/users/spotify_france/playlists/2sxfgEZ1yRhGo0mF3v8YSM", 54 | "type": "playlist" 55 | }, 56 | "played_at": "2017-03-05T10:07:38.946Z" 57 | }, 58 | { 59 | "track": { 60 | "artists": [ 61 | { 62 | "external_urls": { 63 | "spotify": "https://open.spotify.com/artist/1BNQnTVxfQqeMxr6xBi8X6" 64 | }, 65 | "href": "https://api.spotify.com/v1/artists/1BNQnTVxfQqeMxr6xBi8X6", 66 | "id": "1BNQnTVxfQqeMxr6xBi8X6", 67 | "name": "Puggy", 68 | "type": "artist", 69 | "uri": "spotify:artist:1BNQnTVxfQqeMxr6xBi8X6" 70 | } 71 | ], 72 | "available_markets": [ 73 | "AD", 74 | "AR", 75 | "AT", 76 | "AU", 77 | "BE", 78 | "BG", 79 | "BO", 80 | "BR", 81 | "CH", 82 | "CL", 83 | "CO", 84 | "CR", 85 | "CY", 86 | "CZ", 87 | "DE", 88 | "DK", 89 | "DO", 90 | "EC", 91 | "EE", 92 | "ES", 93 | "FI", 94 | "FR", 95 | "GB", 96 | "GR", 97 | "GT", 98 | "HK", 99 | "HN", 100 | "HU", 101 | "ID", 102 | "IE", 103 | "IS", 104 | "IT", 105 | "LI", 106 | "LT", 107 | "LU", 108 | "LV", 109 | "MC", 110 | "MT", 111 | "MY", 112 | "NI", 113 | "NO", 114 | "NZ", 115 | "PA", 116 | "PE", 117 | "PH", 118 | "PL", 119 | "PT", 120 | "PY", 121 | "SE", 122 | "SG", 123 | "SK", 124 | "SV", 125 | "TR", 126 | "TW", 127 | "UY" 128 | ], 129 | "disc_number": 1, 130 | "duration_ms": 284026, 131 | "explicit": false, 132 | "external_urls": { 133 | "spotify": "https://open.spotify.com/track/5ebiH7S9MKr4xRGXm7Wc8o" 134 | }, 135 | "href": "https://api.spotify.com/v1/tracks/5ebiH7S9MKr4xRGXm7Wc8o", 136 | "id": "5ebiH7S9MKr4xRGXm7Wc8o", 137 | "name": "Lonely Town", 138 | "preview_url": "https://p.scdn.co/mp3-preview/e18ca6560f3bb74e661677e57741d527a9ae5747?cid=63c1bb278a88426aa745cb4d887e4054", 139 | "track_number": 4, 140 | "type": "track", 141 | "uri": "spotify:track:5ebiH7S9MKr4xRGXm7Wc8o" 142 | }, 143 | "context": { 144 | "uri": "spotify:user:spotify_france:playlist:2sxfgEZ1yRhGo0mF3v8YSM", 145 | "external_urls": { 146 | "spotify": "https://open.spotify.com/user/spotify_france/playlist/2sxfgEZ1yRhGo0mF3v8YSM" 147 | }, 148 | "href": "https://api.spotify.com/v1/users/spotify_france/playlists/2sxfgEZ1yRhGo0mF3v8YSM", 149 | "type": "playlist" 150 | }, 151 | "played_at": "2017-03-05T09:46:23.542Z" 152 | } 153 | ], 154 | "next": "https://api.spotify.com/v1/me/player/recently-played?before=1488707183542&limit=2", 155 | "cursors": { 156 | "after": "1488708458946", 157 | "before": "1488707183542" 158 | }, 159 | "limit": 2, 160 | "href": "https://api.spotify.com/v1/me/player/recently-played?before=1488708498755&limit=2" 161 | } 162 | -------------------------------------------------------------------------------- /tests/fixtures/recommendations.json: -------------------------------------------------------------------------------- 1 | { 2 | "tracks": [ 3 | { 4 | "artists" : [ { 5 | "external_urls" : { 6 | "spotify" : "https://open.spotify.com/artist/134GdR5tUtxJrf8cpsfpyY" 7 | }, 8 | "href" : "https://api.spotify.com/v1/artists/134GdR5tUtxJrf8cpsfpyY", 9 | "id" : "134GdR5tUtxJrf8cpsfpyY", 10 | "name" : "Elliphant", 11 | "type" : "artist", 12 | "uri" : "spotify:artist:134GdR5tUtxJrf8cpsfpyY" 13 | }, { 14 | "external_urls" : { 15 | "spotify" : "https://open.spotify.com/artist/1D2oK3cJRq97OXDzu77BFR" 16 | }, 17 | "href" : "https://api.spotify.com/v1/artists/1D2oK3cJRq97OXDzu77BFR", 18 | "id" : "1D2oK3cJRq97OXDzu77BFR", 19 | "name" : "Ras Fraser Jr.", 20 | "type" : "artist", 21 | "uri" : "spotify:artist:1D2oK3cJRq97OXDzu77BFR" 22 | } ], 23 | "disc_number" : 1, 24 | "duration_ms" : 199133, 25 | "explicit" : false, 26 | "external_urls" : { 27 | "spotify" : "https://open.spotify.com/track/1TKYPzH66GwsqyJFKFkBHQ" 28 | }, 29 | "href" : "https://api.spotify.com/v1/tracks/1TKYPzH66GwsqyJFKFkBHQ", 30 | "id" : "1TKYPzH66GwsqyJFKFkBHQ", 31 | "is_playable" : true, 32 | "name" : "Music Is Life", 33 | "preview_url" : "https://p.scdn.co/mp3-preview/546099103387186dfe16743a33edd77e52cec738", 34 | "track_number" : 1, 35 | "type" : "track", 36 | "uri" : "spotify:track:1TKYPzH66GwsqyJFKFkBHQ" 37 | }, { 38 | "artists" : [ { 39 | "external_urls" : { 40 | "spotify" : "https://open.spotify.com/artist/1VBflYyxBhnDc9uVib98rw" 41 | }, 42 | "href" : "https://api.spotify.com/v1/artists/1VBflYyxBhnDc9uVib98rw", 43 | "id" : "1VBflYyxBhnDc9uVib98rw", 44 | "name" : "Icona Pop", 45 | "type" : "artist", 46 | "uri" : "spotify:artist:1VBflYyxBhnDc9uVib98rw" 47 | } ], 48 | "disc_number" : 1, 49 | "duration_ms" : 187026, 50 | "explicit" : false, 51 | "external_urls" : { 52 | "spotify" : "https://open.spotify.com/track/15iosIuxC3C53BgsM5Uggs" 53 | }, 54 | "href" : "https://api.spotify.com/v1/tracks/15iosIuxC3C53BgsM5Uggs", 55 | "id" : "15iosIuxC3C53BgsM5Uggs", 56 | "is_playable" : true, 57 | "name" : "All Night", 58 | "preview_url" : "https://p.scdn.co/mp3-preview/9ee589fa7fe4e96bad3483c20b3405fb59776424", 59 | "track_number" : 2, 60 | "type" : "track", 61 | "uri" : "spotify:track:15iosIuxC3C53BgsM5Uggs" 62 | } 63 | ], 64 | "seeds": [ 65 | { 66 | "initial_pool_size": 500, 67 | "after_filtering_size": 380, 68 | "after_relinking_size": 365, 69 | "href": "https://api.spotify.com/v1/artists/4NHQUGzhtTLFvgF5SZesLK", 70 | "id": "4NHQUGzhtTLFvgF5SZesLK", 71 | "type": "artist" 72 | }, { 73 | "initial_pool_size": 250, 74 | "after_filtering_size": 172, 75 | "after_relinking_size": 144, 76 | "href": "https://api.spotify.com/v1/tracks/0c6xIDDpzE81m2q797ordA", 77 | "id": "0c6xIDDpzE81m2q797ordA", 78 | "type": "track" 79 | } 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /tests/fixtures/refresh-token-no-refresh-token.json: -------------------------------------------------------------------------------- 1 | { 2 | "access_token": "64b2b6d12bfe4baae7dad3d018f8cbf6b0e7a044", 3 | "token_type": "Bearer", 4 | "expires_in": 3600, 5 | "scope": "user-follow-read user-follow-modify" 6 | } 7 | -------------------------------------------------------------------------------- /tests/fixtures/refresh-token.json: -------------------------------------------------------------------------------- 1 | { 2 | "access_token": "64b2b6d12bfe4baae7dad3d018f8cbf6b0e7a044", 3 | "token_type": "Bearer", 4 | "expires_in": 3600, 5 | "scope": "user-follow-read user-follow-modify", 6 | "refresh_token": "4c82f23d91a75961f4d08134fc5ad0dfe6a4c36a" 7 | } 8 | -------------------------------------------------------------------------------- /tests/fixtures/show-episodes.json: -------------------------------------------------------------------------------- 1 | { 2 | "href" : "https://api.spotify.com/v1/shows/38bS44xjbVVZ3No3ByF1dJ/episodes?offset=1&limit=2", 3 | "items" : [ { 4 | "audio_preview_url" : "https://p.scdn.co/mp3-preview/83bc7f2d40e850582a4ca118b33c256358de06ff", 5 | "description" : "Följ med Tobias Svanelid till Sveriges äldsta tegelkyrka, till Edsleskog mitt i den dalsländska granskogen, där ett religiöst skrytbygge skulle resas över ett skändligt brott. I Edsleskog i Dalsland gräver arkeologerna nu ut vad som en gång verkar ha varit en av Sveriges största medeltidskyrkor, och kanske också den äldsta som byggts i tegel, 1200-talets high-tech-material. Tobias Svanelid reser dit för att höra historien om den märkliga och bortglömda kyrkan som grundlades på platsen för ett prästmord och dessutom kan ha varit Skarabiskopens försök att lägga beslag på det vilda Dalsland. Dessutom om sjudagarsveckan idag ett välkänt koncept runt hela världen, men hur gammal är egentligen veckans historia? Dick Harrison vet svaret.", 6 | "duration_ms" : 2685023, 7 | "explicit" : false, 8 | "external_urls" : { 9 | "spotify" : "https://open.spotify.com/episode/0Q86acNRm6V9GYx55SXKwf" 10 | }, 11 | "href" : "https://api.spotify.com/v1/episodes/0Q86acNRm6V9GYx55SXKwf", 12 | "id" : "0Q86acNRm6V9GYx55SXKwf", 13 | "images" : [ { 14 | "height" : 640, 15 | "url" : "https://i.scdn.co/image/b2398424d6158a21fe8677e2de5f6f3d1dc4a04f", 16 | "width" : 640 17 | }, { 18 | "height" : 300, 19 | "url" : "https://i.scdn.co/image/a52780a1d7e1bc42619413c3dea7042396c87f49", 20 | "width" : 300 21 | }, { 22 | "height" : 64, 23 | "url" : "https://i.scdn.co/image/88e21be860cf11f0b95ee8dfb47ddb08a13319a7", 24 | "width" : 64 25 | } ], 26 | "is_externally_hosted" : false, 27 | "is_playable" : true, 28 | "language" : "sv", 29 | "languages" : [ "sv" ], 30 | "name" : "Okända katedralen i Dalsland", 31 | "release_date" : "2019-09-03", 32 | "release_date_precision" : "day", 33 | "type" : "episode", 34 | "uri" : "spotify:episode:0Q86acNRm6V9GYx55SXKwf" 35 | }, { 36 | "audio_preview_url" : "https://p.scdn.co/mp3-preview/a712dea885b8d4090a61f0a903094181bd7d2005", 37 | "description" : "Electrolux firar hundra år av att ha dammsugit folkhemmet rent. Följ med Tobias Svanelid genom företagets historia och hör om dess grundare Axel Wenner-Gren - folkhemmets Elon Musk. 1919 drog Axel Wenner-Gren igång företaget som skulle komma att städa rent i folkhemmet Sverige och samtidigt göra sin grundare ofantligt rik. Författaren Ronald Fagerfjäll har färdigställt Electrolux jubileumsbok och guidar runt bland hundraåriga dammsugare och kylskåp. Men Wenner-grens sista projekt skulle bli ett fiasko, i Wennergrenland i British Columbia krossades de flesta av entreprenörens drömmar berättar den kanadensiske historikern Frank Leonard. Dessutom reder Dick Harrison ut huruvida Julius Caesar fått gå på plankan.", 38 | "duration_ms" : 2685023, 39 | "explicit" : false, 40 | "external_urls" : { 41 | "spotify" : "https://open.spotify.com/episode/1spUiev4ggXPq95a7KKHjW" 42 | }, 43 | "href" : "https://api.spotify.com/v1/episodes/1spUiev4ggXPq95a7KKHjW", 44 | "id" : "1spUiev4ggXPq95a7KKHjW", 45 | "images" : [ { 46 | "height" : 640, 47 | "url" : "https://i.scdn.co/image/0bcb5e368982156f9da093d1e471b188af184559", 48 | "width" : 640 49 | }, { 50 | "height" : 300, 51 | "url" : "https://i.scdn.co/image/2023d6f3dc774a89e26b607e25c3b08ecaede0e9", 52 | "width" : 300 53 | }, { 54 | "height" : 64, 55 | "url" : "https://i.scdn.co/image/a2fa6895a86e61c149083adce7aa5f0404a19d8c", 56 | "width" : 64 57 | } ], 58 | "is_externally_hosted" : false, 59 | "is_playable" : true, 60 | "language" : "sv", 61 | "name" : "Electrolux och folkhemmets Elon Musk", 62 | "release_date" : "2019-08-27", 63 | "release_date_precision" : "day", 64 | "type" : "episode", 65 | "uri" : "spotify:episode:1spUiev4ggXPq95a7KKHjW" 66 | } ], 67 | "limit" : 2, 68 | "next" : "https://api.spotify.com/v1/shows/38bS44xjbVVZ3No3ByF1dJ/episodes?offset=3&limit=2", 69 | "offset" : 1, 70 | "previous" : "https://api.spotify.com/v1/shows/38bS44xjbVVZ3No3ByF1dJ/episodes?offset=0&limit=2", 71 | "total" : 499 72 | } 73 | -------------------------------------------------------------------------------- /tests/fixtures/show.json: -------------------------------------------------------------------------------- 1 | { 2 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "ID", "IE", "IL", "IS", "IT", "JP", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SK", "SV", "TH", "TR", "TW", "US", "UY", "VN", "ZA" ], 3 | "copyrights" : [ ], 4 | "description" : "Vi är där historien är. Ansvarig utgivare: Nina Glans", 5 | "episodes" : { 6 | "href" : "https://api.spotify.com/v1/shows/38bS44xjbVVZ3No3ByF1dJ/episodes?offset=0&limit=50", 7 | "items" : [ { 8 | "audio_preview_url" : "https://p.scdn.co/mp3-preview/7a785904a33e34b0b2bd382c82fca16be7060c36", 9 | "description" : "Hör Tobias Svanelid och hans bok- och spelpaneler tipsa om de bästa historiska böckerna och spelen att njuta av i sommar! Hör Vetenskapsradion Historias Tobias Svanelid tipsa tillsammans med Kristina Ekero Eriksson och Urban Björstadius om de bästa böckerna att ta med till hängmattan i sommar. Något för alla utlovas såsom stormande 1700-talskärlek, seglivade småfolk och kvinnliga bågskyttar under korstågen. Dessutom tipsas om två historiska brädspel, där spelarna i det ena förflyttas till medeltidens Orléans, i det andra ska överleva på Robinson Kruses öde ö, bland annat genom att tillaga tigerstek! De böcker som nämns i programmet är: Völvor, krigare och vanligt folk av Kent Andersson Kvinnliga krigare av Stefan Högberg De små folkens historia av Ingmar Karlsson Jugoslaviens undergång av Sanimir Resic Venedig: En vägvisare i tid och rum av Carin Norberg och Carl Tham Samlare, jägare och andra fågelskådare av Susanne Nylund Skog Ebba Hochschild: Att leva efter döden av Caroline Ranby 68 av Henrik Berggren Förnuft eller känsla av Brita Planck Finanskrascher av Lars Magnusson Spelen som testas är: Orléans av Reiner Stockhausen Robinson Crusoe - Adventures on the Cursed Island av Ignazy Trzewiczek", 10 | "duration_ms" : 2677448, 11 | "external_urls" : { 12 | "spotify" : "https://open.spotify.com/episode/4d237GqKH4NP1jtgwy6bP3" 13 | }, 14 | "href" : "https://api.spotify.com/v1/episodes/4d237GqKH4NP1jtgwy6bP3", 15 | "id" : "4d237GqKH4NP1jtgwy6bP3", 16 | "images" : [ { 17 | "height" : 640, 18 | "url" : "https://i.scdn.co/image/606eba074860660c91819636bcb5e141ddf4e23d", 19 | "width" : 640 20 | }, { 21 | "height" : 300, 22 | "url" : "https://i.scdn.co/image/245062bc785bb8672cd002cb96b518a4f50e9067", 23 | "width" : 300 24 | }, { 25 | "height" : 64, 26 | "url" : "https://i.scdn.co/image/cc0797a99e21733caf0f4e23685a173033fdaa49", 27 | "width" : 64 28 | } ], 29 | "is_externally_hosted" : false, 30 | "language" : "sv", 31 | "name" : "Pyttefolk och tigerstekar i hängmattan", 32 | "release_date" : "2018-06-19", 33 | "release_date_precision" : "day", 34 | "type" : "episode", 35 | "uri" : "spotify:episode:4d237GqKH4NP1jtgwy6bP3" 36 | } ], 37 | "limit" : 50, 38 | "next" : "https://api.spotify.com/v1/shows/38bS44xjbVVZ3No3ByF1dJ/episodes?offset=50&limit=50", 39 | "offset" : 0, 40 | "previous" : null, 41 | "total" : 520 42 | }, 43 | "explicit" : false, 44 | "external_urls" : { 45 | "spotify" : "https://open.spotify.com/show/38bS44xjbVVZ3No3ByF1dJ" 46 | }, 47 | "href" : "https://api.spotify.com/v1/shows/38bS44xjbVVZ3No3ByF1dJ", 48 | "id" : "38bS44xjbVVZ3No3ByF1dJ", 49 | "images" : [ { 50 | "height" : 640, 51 | "url" : "https://i.scdn.co/image/3c59a8b611000c8b10c8013013c3783dfb87a3bc", 52 | "width" : 640 53 | }, { 54 | "height" : 300, 55 | "url" : "https://i.scdn.co/image/2d70c06ac70d8c6144c94cabf7f4abcf85c4b7e4", 56 | "width" : 300 57 | }, { 58 | "height" : 64, 59 | "url" : "https://i.scdn.co/image/3dc007829bc0663c24089e46743a9f4ae15e65f8", 60 | "width" : 64 61 | } ], 62 | "is_externally_hosted" : false, 63 | "languages" : [ "sv" ], 64 | "media_type" : "audio", 65 | "name" : "Vetenskapsradion Historia", 66 | "publisher" : "Sveriges Radio", 67 | "type" : "show", 68 | "uri" : "spotify:show:38bS44xjbVVZ3No3ByF1dJ" 69 | } 70 | -------------------------------------------------------------------------------- /tests/fixtures/shows.json: -------------------------------------------------------------------------------- 1 | { 2 | "shows" : [ { 3 | "available_markets" : [ "AD", "AE", "AR", "AT", "AU", "BE", "BG", "BH", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "DZ", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "ID", "IE", "IL", "IN", "IS", "IT", "JO", "JP", "KW", "LB", "LI", "LT", "LU", "LV", "MA", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "OM", "PA", "PE", "PH", "PL", "PS", "PT", "PY", "QA", "RO", "SE", "SG", "SK", "SV", "TH", "TN", "TR", "TW", "US", "UY", "VN", "ZA" ], 4 | "copyrights" : [ ], 5 | "description" : "Candid conversations with entrepreneurs, artists, athletes, visionaries of all kinds—about their successes, and their failures, and what they learned from both. Hosted by Alex Blumberg, from Gimlet Media.", 6 | "explicit" : true, 7 | "external_urls" : { 8 | "spotify" : "https://open.spotify.com/show/5CfCWKI5pZ28U0uOzXkDHe" 9 | }, 10 | "href" : "https://api.spotify.com/v1/shows/5CfCWKI5pZ28U0uOzXkDHe", 11 | "id" : "5CfCWKI5pZ28U0uOzXkDHe", 12 | "images" : [ { 13 | "height" : 640, 14 | "url" : "https://i.scdn.co/image/12903409b9e5dd26f2a41e401cd7fcabd5164ed4", 15 | "width" : 640 16 | }, { 17 | "height" : 300, 18 | "url" : "https://i.scdn.co/image/4f19eb7986a7c2246d713dcc46684e2187ccea4f", 19 | "width" : 300 20 | }, { 21 | "height" : 64, 22 | "url" : "https://i.scdn.co/image/c0b072976a28792a4b451dfc7011a2176ec8cd34", 23 | "width" : 64 24 | } ], 25 | "is_externally_hosted" : false, 26 | "languages" : [ "en" ], 27 | "media_type" : "audio", 28 | "name" : "Without Fail", 29 | "publisher" : "Gimlet", 30 | "type" : "show", 31 | "uri" : "spotify:show:5CfCWKI5pZ28U0uOzXkDHe" 32 | }, { 33 | "available_markets" : [ "AD", "AE", "AR", "AT", "AU", "BE", "BG", "BH", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "DZ", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "ID", "IE", "IL", "IN", "IS", "IT", "JO", "JP", "KW", "LB", "LI", "LT", "LU", "LV", "MA", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "OM", "PA", "PE", "PH", "PL", "PS", "PT", "PY", "QA", "RO", "SE", "SG", "SK", "SV", "TH", "TN", "TR", "TW", "US", "UY", "VN", "ZA" ], 34 | "copyrights" : [ ], 35 | "description" : "Giant Bomb discusses the latest video game news and new releases, taste-test questionable beverages, and get wildly off-topic in this weekly podcast.", 36 | "explicit" : false, 37 | "external_urls" : { 38 | "spotify" : "https://open.spotify.com/show/5as3aKmN2k11yfDDDSrvaZ" 39 | }, 40 | "href" : "https://api.spotify.com/v1/shows/5as3aKmN2k11yfDDDSrvaZ", 41 | "id" : "5as3aKmN2k11yfDDDSrvaZ", 42 | "images" : [ { 43 | "height" : 640, 44 | "url" : "https://i.scdn.co/image/9bd9b3be1111810a91cd768115a57ee5a08c7145", 45 | "width" : 640 46 | }, { 47 | "height" : 300, 48 | "url" : "https://i.scdn.co/image/1f5c122086aa4602742ba2301302f2f9bc1f0345", 49 | "width" : 300 50 | }, { 51 | "height" : 64, 52 | "url" : "https://i.scdn.co/image/b97f288023e547f40862976c89a5c342eacaaac1", 53 | "width" : 64 54 | } ], 55 | "is_externally_hosted" : false, 56 | "languages" : [ "en-US" ], 57 | "media_type" : "audio", 58 | "name" : "Giant Bombcast", 59 | "publisher" : "Giant Bomb", 60 | "type" : "show", 61 | "uri" : "spotify:show:5as3aKmN2k11yfDDDSrvaZ" 62 | } ] 63 | } 64 | -------------------------------------------------------------------------------- /tests/fixtures/snapshot-id.json: -------------------------------------------------------------------------------- 1 | { "snapshot_id" : "JbtmHBDBAYu3/bt8BOXKjzKx3i0b6LCa/wVjyl6qQ2Yf6nFXkbmzuEa+ZI/U1yF+" } 2 | -------------------------------------------------------------------------------- /tests/fixtures/top-artists-and-tracks.json: -------------------------------------------------------------------------------- 1 | { 2 | "items" : [ { 3 | "external_urls" : { 4 | "spotify" : "https://open.spotify.com/artist/0I2XqVXqHScXjHhk6AYYRe" 5 | }, 6 | "followers" : { 7 | "href" : null, 8 | "total" : 7753 9 | }, 10 | "genres" : [ "swedish hip hop" ], 11 | "href" : "https://api.spotify.com/v1/artists/0I2XqVXqHScXjHhk6AYYRe", 12 | "id" : "0I2XqVXqHScXjHhk6AYYRe", 13 | "images" : [ { 14 | "height" : 640, 15 | "url" : "https://i.scdn.co/image/2c8c0cea05bf3d3c070b7498d8d0b957c4cdec20", 16 | "width" : 640 17 | }, { 18 | "height" : 300, 19 | "url" : "https://i.scdn.co/image/394302b42c4b894786943e028cdd46d7baaa29b7", 20 | "width" : 300 21 | }, { 22 | "height" : 64, 23 | "url" : "https://i.scdn.co/image/ca9df7225ade6e5dfc62e7076709ca3409a7cbbf", 24 | "width" : 64 25 | } ], 26 | "name" : "Afasi & Filthy", 27 | "popularity" : 54, 28 | "type" : "artist", 29 | "uri" : "spotify:artist:0I2XqVXqHScXjHhk6AYYRe" 30 | }], 31 | "next" : "https://api.spotify.com/v1/me/top/artists?offset=20", 32 | "previous" : null, 33 | "total" : 50, 34 | "limit" : 20, 35 | "href" : "https://api.spotify.com/v1/me/top/artists" 36 | } 37 | -------------------------------------------------------------------------------- /tests/fixtures/track.json: -------------------------------------------------------------------------------- 1 | { 2 | "album" : { 3 | "album_type" : "album", 4 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 5 | "external_urls" : { 6 | "spotify" : "https://open.spotify.com/album/3tpJtzZm4Urb0n2ITN5mwF" 7 | }, 8 | "href" : "https://api.spotify.com/v1/albums/3tpJtzZm4Urb0n2ITN5mwF", 9 | "id" : "3tpJtzZm4Urb0n2ITN5mwF", 10 | "images" : [ { 11 | "height" : 640, 12 | "url" : "https://i.scdn.co/image/42f1407919f09c53af5cac7290f5224cc098b6cc", 13 | "width" : 640 14 | }, { 15 | "height" : 300, 16 | "url" : "https://i.scdn.co/image/1844e9ff8ea90e78e62ca892900c1c36e98a58ee", 17 | "width" : 300 18 | }, { 19 | "height" : 64, 20 | "url" : "https://i.scdn.co/image/7c5288b11adf33302a5eef5a722372494335114a", 21 | "width" : 64 22 | } ], 23 | "name" : "Original Motion Picture Soundtrack - Full Metal Jacket", 24 | "type" : "album", 25 | "uri" : "spotify:album:3tpJtzZm4Urb0n2ITN5mwF" 26 | }, 27 | "artists" : [ { 28 | "external_urls" : { 29 | "spotify" : "https://open.spotify.com/artist/5QEA3sofVt5QckQA6QX2nN" 30 | }, 31 | "href" : "https://api.spotify.com/v1/artists/5QEA3sofVt5QckQA6QX2nN", 32 | "id" : "5QEA3sofVt5QckQA6QX2nN", 33 | "name" : "The Trashmen", 34 | "type" : "artist", 35 | "uri" : "spotify:artist:5QEA3sofVt5QckQA6QX2nN" 36 | } ], 37 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 38 | "disc_number" : 1, 39 | "duration_ms" : 137133, 40 | "explicit" : false, 41 | "external_ids" : { 42 | "isrc" : "USWB10102873" 43 | }, 44 | "external_urls" : { 45 | "spotify" : "https://open.spotify.com/track/7EjyzZcbLxW7PaaLua9Ksb" 46 | }, 47 | "href" : "https://api.spotify.com/v1/tracks/7EjyzZcbLxW7PaaLua9Ksb", 48 | "id" : "7EjyzZcbLxW7PaaLua9Ksb", 49 | "name" : "Surfin' Bird", 50 | "popularity" : 53, 51 | "preview_url" : "https://p.scdn.co/mp3-preview/c1a3f50a46ad126ec97442119bf912fc4017c2a5", 52 | "track_number" : 7, 53 | "type" : "track", 54 | "uri" : "spotify:track:7EjyzZcbLxW7PaaLua9Ksb" 55 | } 56 | -------------------------------------------------------------------------------- /tests/fixtures/tracks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tracks" : [ { 3 | "album" : { 4 | "album_type" : "album", 5 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "UY" ], 6 | "external_urls" : { 7 | "spotify" : "https://open.spotify.com/album/6TJmQnO44YE5BtTxH8pop1" 8 | }, 9 | "href" : "https://api.spotify.com/v1/albums/6TJmQnO44YE5BtTxH8pop1", 10 | "id" : "6TJmQnO44YE5BtTxH8pop1", 11 | "images" : [ { 12 | "height" : 640, 13 | "url" : "https://i.scdn.co/image/8e13218039f81b000553e25522a7f0d7a0600f2e", 14 | "width" : 629 15 | }, { 16 | "height" : 300, 17 | "url" : "https://i.scdn.co/image/8c1e066b5d1045038437d92815d49987f519e44f", 18 | "width" : 295 19 | }, { 20 | "height" : 64, 21 | "url" : "https://i.scdn.co/image/d49268a8fc0768084f4750cf1647709e89a27172", 22 | "width" : 63 23 | } ], 24 | "name" : "Hot Fuss", 25 | "type" : "album", 26 | "uri" : "spotify:album:6TJmQnO44YE5BtTxH8pop1" 27 | }, 28 | "artists" : [ { 29 | "external_urls" : { 30 | "spotify" : "https://open.spotify.com/artist/0C0XlULifJtAgn6ZNCW2eu" 31 | }, 32 | "href" : "https://api.spotify.com/v1/artists/0C0XlULifJtAgn6ZNCW2eu", 33 | "id" : "0C0XlULifJtAgn6ZNCW2eu", 34 | "name" : "The Killers", 35 | "type" : "artist", 36 | "uri" : "spotify:artist:0C0XlULifJtAgn6ZNCW2eu" 37 | } ], 38 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "UY" ], 39 | "disc_number" : 1, 40 | "duration_ms" : 222075, 41 | "explicit" : false, 42 | "external_ids" : { 43 | "isrc" : "USIR20400274" 44 | }, 45 | "external_urls" : { 46 | "spotify" : "https://open.spotify.com/track/0eGsygTp906u18L0Oimnem" 47 | }, 48 | "href" : "https://api.spotify.com/v1/tracks/0eGsygTp906u18L0Oimnem", 49 | "id" : "0eGsygTp906u18L0Oimnem", 50 | "name" : "Mr. Brightside", 51 | "popularity" : 75, 52 | "preview_url" : "https://p.scdn.co/mp3-preview/f454c8224828e21fa146af84916fd22cb89cedc6", 53 | "track_number" : 2, 54 | "type" : "track", 55 | "uri" : "spotify:track:0eGsygTp906u18L0Oimnem" 56 | }, { 57 | "album" : { 58 | "album_type" : "album", 59 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "UY" ], 60 | "external_urls" : { 61 | "spotify" : "https://open.spotify.com/album/6TJmQnO44YE5BtTxH8pop1" 62 | }, 63 | "href" : "https://api.spotify.com/v1/albums/6TJmQnO44YE5BtTxH8pop1", 64 | "id" : "6TJmQnO44YE5BtTxH8pop1", 65 | "images" : [ { 66 | "height" : 640, 67 | "url" : "https://i.scdn.co/image/8e13218039f81b000553e25522a7f0d7a0600f2e", 68 | "width" : 629 69 | }, { 70 | "height" : 300, 71 | "url" : "https://i.scdn.co/image/8c1e066b5d1045038437d92815d49987f519e44f", 72 | "width" : 295 73 | }, { 74 | "height" : 64, 75 | "url" : "https://i.scdn.co/image/d49268a8fc0768084f4750cf1647709e89a27172", 76 | "width" : 63 77 | } ], 78 | "name" : "Hot Fuss", 79 | "type" : "album", 80 | "uri" : "spotify:album:6TJmQnO44YE5BtTxH8pop1" 81 | }, 82 | "artists" : [ { 83 | "external_urls" : { 84 | "spotify" : "https://open.spotify.com/artist/0C0XlULifJtAgn6ZNCW2eu" 85 | }, 86 | "href" : "https://api.spotify.com/v1/artists/0C0XlULifJtAgn6ZNCW2eu", 87 | "id" : "0C0XlULifJtAgn6ZNCW2eu", 88 | "name" : "The Killers", 89 | "type" : "artist", 90 | "uri" : "spotify:artist:0C0XlULifJtAgn6ZNCW2eu" 91 | } ], 92 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "UY" ], 93 | "disc_number" : 1, 94 | "duration_ms" : 197160, 95 | "explicit" : false, 96 | "external_ids" : { 97 | "isrc" : "USIR20400195" 98 | }, 99 | "external_urls" : { 100 | "spotify" : "https://open.spotify.com/track/1lDWb6b6ieDQ2xT7ewTC3G" 101 | }, 102 | "href" : "https://api.spotify.com/v1/tracks/1lDWb6b6ieDQ2xT7ewTC3G", 103 | "id" : "1lDWb6b6ieDQ2xT7ewTC3G", 104 | "name" : "Somebody Told Me", 105 | "popularity" : 71, 106 | "preview_url" : "https://p.scdn.co/mp3-preview/4c63a3d4eaf7f8f86cfdb8bf46ef3974f4092357", 107 | "track_number" : 4, 108 | "type" : "track", 109 | "uri" : "spotify:track:1lDWb6b6ieDQ2xT7ewTC3G" 110 | } ] 111 | } 112 | -------------------------------------------------------------------------------- /tests/fixtures/user-albums-contains.json: -------------------------------------------------------------------------------- 1 | [true, true] 2 | -------------------------------------------------------------------------------- /tests/fixtures/user-albums.json: -------------------------------------------------------------------------------- 1 | { 2 | "href": "https://api.spotify.com/v1/me/albums?offset=0&limit=20", 3 | "items": [ 4 | { 5 | "added_at": "2014-07-08T18:18:33Z", 6 | "album": { 7 | "album_type": "album", 8 | "artists": [ 9 | { 10 | "external_urls": { 11 | "spotify": "https://open.spotify.com/artist/44gRHbEm4Uqa0ykW0rDTNk" 12 | }, 13 | "href": "https://api.spotify.com/v1/artists/44gRHbEm4Uqa0ykW0rDTNk", 14 | "id": "44gRHbEm4Uqa0ykW0rDTNk", 15 | "name": "Ben Folds Five", 16 | "type": "artist", 17 | "uri": "spotify:artist:44gRHbEm4Uqa0ykW0rDTNk" 18 | } 19 | ], 20 | "available_markets": [ 21 | "AD", 22 | "AR", 23 | "AT", 24 | "TW", 25 | "US", 26 | "UY" 27 | ], 28 | "external_ids": { 29 | "upc": "074646776223" 30 | }, 31 | "external_urls": { 32 | "spotify": "https://open.spotify.com/album/7ggEMmVTUloqdMw18rv0ve" 33 | }, 34 | "genres": [ 35 | "Adult Alternative Pop/Rock", 36 | "Alternative Pop/Rock", 37 | "Alternative/Indie Rock", 38 | "Contemporary Pop/Rock", 39 | "Pop/Rock" 40 | ], 41 | "href": "https://api.spotify.com/v1/albums/7ggEMmVTUloqdMw18rv0ve", 42 | "id": "7ggEMmVTUloqdMw18rv0ve", 43 | "images": [ 44 | { 45 | "height": 640, 46 | "url": "https://i.scdn.co/image/8b5b036272caf598e4d708e4db4a418031486cca", 47 | "width": 640 48 | }, 49 | { 50 | "height": 300, 51 | "url": "https://i.scdn.co/image/47030c6e70229ba53fcb752d6114813e893d8b40", 52 | "width": 300 53 | }, 54 | { 55 | "height": 64, 56 | "url": "https://i.scdn.co/image/2e8be5564680cccbe6888e57ba2f00521381c8f5", 57 | "width": 64 58 | } 59 | ], 60 | "name": "Whatever and Ever Amen", 61 | "popularity": 53, 62 | "release_date": "1997-02-10", 63 | "release_date_precision": "day", 64 | "tracks": { 65 | "href": "https://api.spotify.com/v1/albums/7ggEMmVTUloqdMw18rv0ve/tracks?offset=0&limit=50", 66 | "items": [ 67 | { 68 | "artists": [ 69 | { 70 | "external_urls": { 71 | "spotify": "https://open.spotify.com/artist/44gRHbEm4Uqa0ykW0rDTNk" 72 | }, 73 | "href": "https://api.spotify.com/v1/artists/44gRHbEm4Uqa0ykW0rDTNk", 74 | "id": "44gRHbEm4Uqa0ykW0rDTNk", 75 | "name": "Ben Folds Five", 76 | "type": "artist", 77 | "uri": "spotify:artist:44gRHbEm4Uqa0ykW0rDTNk" 78 | } 79 | ], 80 | "disc_number": 1, 81 | "duration_ms": 232293, 82 | "explicit": false, 83 | "external_urls": { 84 | "spotify": "https://open.spotify.com/track/4rOPzC3EoK0v3EDQW3V4oD" 85 | }, 86 | "href": "https://api.spotify.com/v1/tracks/4rOPzC3EoK0v3EDQW3V4oD", 87 | "id": "4rOPzC3EoK0v3EDQW3V4oD", 88 | "name": "One Angry Dwarf and 200 Solemn Faces", 89 | "preview_url": "https://p.scdn.co/mp3-preview/921ae28a02ddb95da1f34606ab0b18a92f29c253", 90 | "track_number": 1, 91 | "type": "track", 92 | "uri": "spotify:track:4rOPzC3EoK0v3EDQW3V4oD" 93 | } 94 | ], 95 | "limit": 50, 96 | "next": null, 97 | "offset": 0, 98 | "previous": null, 99 | "total": 12 100 | }, 101 | "type": "album", 102 | "uri": "spotify:album:7ggEMmVTUloqdMw18rv0ve" 103 | } 104 | } 105 | ], 106 | "limit": 20, 107 | "next": null, 108 | "offset": 0, 109 | "previous": null, 110 | "total": 9 111 | } 112 | -------------------------------------------------------------------------------- /tests/fixtures/user-current-playback-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": 1490252122574, 3 | "device": { 4 | "id": "3f228e06c8562e2f439e22932da6c3231715ed53", 5 | "is_active": false, 6 | "is_restricted2": false, 7 | "name": "Xperia Z5 Compact", 8 | "type": "Smartphone", 9 | "volume_percent": 54 10 | }, 11 | "progress_ms": "44272", 12 | "is_playing": true, 13 | "item": { 14 | }, 15 | "shuffle_state": false, 16 | "repeat_state": "off", 17 | "context": { 18 | "external_urls" : { 19 | "spotify" : "http://open.spotify.com/user/spotify/playlist/49znshcYJROspEqBoHg3Sv" 20 | }, 21 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/49znshcYJROspEqBoHg3Sv", 22 | "type" : "playlist", 23 | "uri" : "spotify:user:spotify:playlist:49znshcYJROspEqBoHg3Sv" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/fixtures/user-current-track.json: -------------------------------------------------------------------------------- 1 | { 2 | "context": { 3 | "external_urls" : { 4 | "spotify" : "http://open.spotify.com/user/spotify/playlist/49znshcYJROspEqBoHg3Sv" 5 | }, 6 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/49znshcYJROspEqBoHg3Sv", 7 | "type" : "playlist", 8 | "uri" : "spotify:user:spotify:playlist:49znshcYJROspEqBoHg3Sv" 9 | }, 10 | "timestamp": 1490252122574, 11 | "progress_ms": 44272, 12 | "is_playing": true, 13 | "item": { 14 | "album": { 15 | "album_type": "album", 16 | "external_urls": { 17 | "spotify": "https://open.spotify.com/album/6TJmQnO44YE5BtTxH8pop1" 18 | }, 19 | "href": "https://api.spotify.com/v1/albums/6TJmQnO44YE5BtTxH8pop1", 20 | "id": "6TJmQnO44YE5BtTxH8pop1", 21 | "images": [ 22 | { 23 | "height": 640, 24 | "url": "https://i.scdn.co/image/8e13218039f81b000553e25522a7f0d7a0600f2e", 25 | "width": 629 26 | }, 27 | { 28 | "height": 300, 29 | "url": "https://i.scdn.co/image/8c1e066b5d1045038437d92815d49987f519e44f", 30 | "width": 295 31 | }, 32 | { 33 | "height": 64, 34 | "url": "https://i.scdn.co/image/d49268a8fc0768084f4750cf1647709e89a27172", 35 | "width": 63 36 | } 37 | ], 38 | "name": "Hot Fuss", 39 | "type": "album", 40 | "uri": "spotify:album:6TJmQnO44YE5BtTxH8pop1" 41 | }, 42 | "artists": [ 43 | { 44 | "external_urls": { 45 | "spotify": "https://open.spotify.com/artist/0C0XlULifJtAgn6ZNCW2eu" 46 | }, 47 | "href": "https://api.spotify.com/v1/artists/0C0XlULifJtAgn6ZNCW2eu", 48 | "id": "0C0XlULifJtAgn6ZNCW2eu", 49 | "name": "The Killers", 50 | "type": "artist", 51 | "uri": "spotify:artist:0C0XlULifJtAgn6ZNCW2eu" 52 | } 53 | ], 54 | "available_markets": [ 55 | "AD", 56 | "AR", 57 | "TW", 58 | "UY" 59 | ], 60 | "disc_number": 1, 61 | "duration_ms": 222075, 62 | "explicit": false, 63 | "external_ids": { 64 | "isrc": "USIR20400274" 65 | }, 66 | "external_urls": { 67 | "spotify": "https://open.spotify.com/track/0eGsygTp906u18L0Oimnem" 68 | }, 69 | "href": "https://api.spotify.com/v1/tracks/0eGsygTp906u18L0Oimnem", 70 | "id": "0eGsygTp906u18L0Oimnem", 71 | "name": "Mr. Brightside", 72 | "popularity": 0, 73 | "preview_url": "http://d318706lgtcm8e.cloudfront.net/mp3-preview/f454c8224828e21fa146af84916fd22cb89cedc6", 74 | "track_number": 2, 75 | "type": "track", 76 | "uri": "spotify:track:0eGsygTp906u18L0Oimnem" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/fixtures/user-devices.json: -------------------------------------------------------------------------------- 1 | { 2 | "devices" : [ { 3 | "id" : "5fbb3ba6aa454b5534c4ba43a8c7e8e45a63ad0e", 4 | "is_active" : false, 5 | "is_restricted" : false, 6 | "name" : "My fridge", 7 | "type" : "Computer", 8 | "volume_percent" : 100 9 | } ] 10 | } 11 | -------------------------------------------------------------------------------- /tests/fixtures/user-episodes-contains.json: -------------------------------------------------------------------------------- 1 | [true, true, false] 2 | -------------------------------------------------------------------------------- /tests/fixtures/user-episodes.json: -------------------------------------------------------------------------------- 1 | { 2 | "href": "https://api.spotify.com/v1/me/episodes?offset=0&limit=1&market=SE", 3 | "items": [ 4 | { 5 | "added_at": "2021-02-24T14:13:23Z", 6 | "episode": { 7 | "audio_preview_url": "https://p.scdn.co/mp3-preview/499805f296742a8b639537dd93dac043afa9beae", 8 | "description": "Have you ever wondered how Spotify enters new listening markets? Kossy walks us through her experience launching new markets all across Asia. She explains some of the most challenging parts and also reveals the most rewarding aspects of bringing music to different people all over the world. ", 9 | "duration_ms": 682512, 10 | "explicit": false, 11 | "external_urls": { 12 | "spotify": "https://open.spotify.com/episode/0nrJxkuCUzLgYsXfD4s8Um" 13 | }, 14 | "href": "https://api.spotify.com/v1/episodes/0nrJxkuCUzLgYsXfD4s8Um", 15 | "id": "0nrJxkuCUzLgYsXfD4s8Um", 16 | "images": [ 17 | { 18 | "height": 640, 19 | "url": "https://i.scdn.co/image/b033e84a9136601e63a7a0713355b1365330d29f", 20 | "width": 640 21 | }, 22 | { 23 | "height": 300, 24 | "url": "https://i.scdn.co/image/11ee74114995e9ad711fe6220c366cc2e8ea9af3", 25 | "width": 300 26 | }, 27 | { 28 | "height": 64, 29 | "url": "https://i.scdn.co/image/14af1d17732a4ea525ff7878d1592f13045fecc0", 30 | "width": 64 31 | } 32 | ], 33 | "is_externally_hosted": false, 34 | "is_playable": true, 35 | "language": "en", 36 | "languages": [ 37 | "en" 38 | ], 39 | "name": "S2E8: Kossy Ng & Launching New Markets", 40 | "release_date": "2020-12-01", 41 | "release_date_precision": "day", 42 | "resume_point": { 43 | "fully_played": false, 44 | "resume_position_ms": 0 45 | }, 46 | "show": { 47 | "available_markets": [ 48 | "AD", 49 | "AE", 50 | "AG", 51 | "AL", 52 | "AM", 53 | "AR", 54 | "AT", 55 | "AU", 56 | "BA", 57 | "BB", 58 | "BE", 59 | "BF", 60 | "BG", 61 | "BH", 62 | "BO", 63 | "BR", 64 | "BS", 65 | "BT", 66 | "BW", 67 | "BZ", 68 | "CA", 69 | "CH", 70 | "CL", 71 | "CO", 72 | "CR", 73 | "CV", 74 | "CW", 75 | "CY", 76 | "CZ", 77 | "DE", 78 | "DK", 79 | "DM", 80 | "DO", 81 | "DZ", 82 | "EC", 83 | "EE", 84 | "ES", 85 | "FI", 86 | "FJ", 87 | "FM", 88 | "FR", 89 | "GB", 90 | "GD", 91 | "GE", 92 | "GH", 93 | "GM", 94 | "GR", 95 | "GT", 96 | "GW", 97 | "GY", 98 | "HK", 99 | "HN", 100 | "HR", 101 | "HT", 102 | "HU", 103 | "ID", 104 | "IE", 105 | "IL", 106 | "IN", 107 | "IS", 108 | "IT", 109 | "JM", 110 | "JO", 111 | "JP", 112 | "KE", 113 | "KI", 114 | "KN", 115 | "KW", 116 | "LB", 117 | "LC", 118 | "LI", 119 | "LR", 120 | "LS", 121 | "LT", 122 | "LU", 123 | "LV", 124 | "MA", 125 | "MC", 126 | "ME", 127 | "MH", 128 | "MK", 129 | "ML", 130 | "MT", 131 | "MV", 132 | "MW", 133 | "MX", 134 | "MY", 135 | "NA", 136 | "NE", 137 | "NG", 138 | "NI", 139 | "NL", 140 | "NO", 141 | "NR", 142 | "NZ", 143 | "OM", 144 | "PA", 145 | "PE", 146 | "PG", 147 | "PH", 148 | "PL", 149 | "PS", 150 | "PT", 151 | "PW", 152 | "PY", 153 | "QA", 154 | "RO", 155 | "RS", 156 | "SB", 157 | "SC", 158 | "SE", 159 | "SG", 160 | "SI", 161 | "SK", 162 | "SL", 163 | "SM", 164 | "SN", 165 | "SR", 166 | "ST", 167 | "SV", 168 | "TH", 169 | "TL", 170 | "TN", 171 | "TO", 172 | "TR", 173 | "TT", 174 | "TV", 175 | "TW", 176 | "US", 177 | "UY", 178 | "VC", 179 | "VN", 180 | "VU", 181 | "WS", 182 | "XK", 183 | "ZA" 184 | ], 185 | "copyrights": [], 186 | "description": "At Spotify, we like to think of ourselves as a really big band! On the GreenRoom podcast, we give you a behind the scenes look into what being a band member is all about. Our mission is to showcase Spotifers and the unique things that drive our culture and help us all grow. Join Spotifiers, Mal & Gus, as they host an array of other band members from around the globe to chat about #lifeatspotify · Follow along on Instagram: @spotifyjobs · Check out our career page for open roles: spotifyjobs.com · Join the convo on Twitter: @spotifyjobs ", 187 | "explicit": false, 188 | "external_urls": { 189 | "spotify": "https://open.spotify.com/show/2bzjLBEWRldARaf1IytFFS" 190 | }, 191 | "href": "https://api.spotify.com/v1/shows/2bzjLBEWRldARaf1IytFFS", 192 | "id": "2bzjLBEWRldARaf1IytFFS", 193 | "images": [ 194 | { 195 | "height": 640, 196 | "url": "https://i.scdn.co/image/b033e84a9136601e63a7a0713355b1365330d29f", 197 | "width": 640 198 | }, 199 | { 200 | "height": 300, 201 | "url": "https://i.scdn.co/image/11ee74114995e9ad711fe6220c366cc2e8ea9af3", 202 | "width": 300 203 | }, 204 | { 205 | "height": 64, 206 | "url": "https://i.scdn.co/image/14af1d17732a4ea525ff7878d1592f13045fecc0", 207 | "width": 64 208 | } 209 | ], 210 | "is_externally_hosted": false, 211 | "languages": [ 212 | "en" 213 | ], 214 | "media_type": "audio", 215 | "name": "Spotify: The GreenRoom", 216 | "publisher": "Spotify Jobs", 217 | "total_episodes": 15, 218 | "type": "show", 219 | "uri": "spotify:show:2bzjLBEWRldARaf1IytFFS" 220 | }, 221 | "type": "episode", 222 | "uri": "spotify:episode:0nrJxkuCUzLgYsXfD4s8Um" 223 | } 224 | } 225 | ], 226 | "limit": 1, 227 | "next": null, 228 | "offset": 0, 229 | "previous": null, 230 | "total": 1 231 | } 232 | -------------------------------------------------------------------------------- /tests/fixtures/user-followed-artists.json: -------------------------------------------------------------------------------- 1 | { 2 | "artists" : { 3 | "items" : [ { 4 | "external_urls" : { 5 | "spotify" : "https://open.spotify.com/artist/4tZwfgrHOc3mvqYlEYSvVi" 6 | }, 7 | "followers" : { 8 | "href" : null, 9 | "total" : 2371485 10 | }, 11 | "genres" : [ "house" ], 12 | "href" : "https://api.spotify.com/v1/artists/4tZwfgrHOc3mvqYlEYSvVi", 13 | "id" : "4tZwfgrHOc3mvqYlEYSvVi", 14 | "images" : [ { 15 | "height" : 751, 16 | "url" : "https://i.scdn.co/image/e52651f03da8c9bf264f75cdabf39cf039606ddc", 17 | "width" : 999 18 | }, { 19 | "height" : 481, 20 | "url" : "https://i.scdn.co/image/b96d08f790bf2be50fee0aa490a7d06d40ba36bb", 21 | "width" : 640 22 | }, { 23 | "height" : 150, 24 | "url" : "https://i.scdn.co/image/5bafab32f2eb71e881de73ec4b088e106feb9e3f", 25 | "width" : 200 26 | }, { 27 | "height" : 48, 28 | "url" : "https://i.scdn.co/image/809d6b89f4bb3f0f42f86c953a8b312234a31f31", 29 | "width" : 64 30 | } ], 31 | "name" : "Daft Punk", 32 | "popularity" : 85, 33 | "type" : "artist", 34 | "uri" : "spotify:artist:4tZwfgrHOc3mvqYlEYSvVi" 35 | } ], 36 | "next" : null, 37 | "total" : 1, 38 | "cursors" : { 39 | "after" : null 40 | }, 41 | "limit" : 20, 42 | "href" : "https://api.spotify.com/v1/users/mcgurk/following?type=artist&limit=20" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/fixtures/user-follows-playlist.json: -------------------------------------------------------------------------------- 1 | [true] 2 | -------------------------------------------------------------------------------- /tests/fixtures/user-follows.json: -------------------------------------------------------------------------------- 1 | [true, true] 2 | -------------------------------------------------------------------------------- /tests/fixtures/user-playlists.json: -------------------------------------------------------------------------------- 1 | { 2 | "href" : "https://api.spotify.com/v1/users/mcgurk/playlists?offset=0&limit=5", 3 | "items" : [ { 4 | "collaborative" : false, 5 | "external_urls" : { 6 | "spotify" : "http://open.spotify.com/user/mcgurk/playlist/4YOPp7RlelOVGJ1d4CZBqW" 7 | }, 8 | "href" : "https://api.spotify.com/v1/users/mcgurk/playlists/4YOPp7RlelOVGJ1d4CZBqW", 9 | "id" : "4YOPp7RlelOVGJ1d4CZBqW", 10 | "images" : [ ], 11 | "name" : "Soft chill", 12 | "owner" : { 13 | "external_urls" : { 14 | "spotify" : "http://open.spotify.com/user/mcgurk" 15 | }, 16 | "href" : "https://api.spotify.com/v1/users/mcgurk", 17 | "id" : "mcgurk", 18 | "type" : "user", 19 | "uri" : "spotify:user:mcgurk" 20 | }, 21 | "public" : false, 22 | "tracks" : { 23 | "href" : "https://api.spotify.com/v1/users/mcgurk/playlists/4YOPp7RlelOVGJ1d4CZBqW/tracks", 24 | "total" : 7 25 | }, 26 | "type" : "playlist", 27 | "uri" : "spotify:user:mcgurk:playlist:4YOPp7RlelOVGJ1d4CZBqW" 28 | }, { 29 | "collaborative" : false, 30 | "external_urls" : { 31 | "spotify" : "http://open.spotify.com/user/mcgurk/playlist/2zNRHPRBynwGI2qIZZ2EGp" 32 | }, 33 | "href" : "https://api.spotify.com/v1/users/mcgurk/playlists/2zNRHPRBynwGI2qIZZ2EGp", 34 | "id" : "2zNRHPRBynwGI2qIZZ2EGp", 35 | "images" : [ ], 36 | "name" : "Liked from Radio", 37 | "owner" : { 38 | "external_urls" : { 39 | "spotify" : "http://open.spotify.com/user/mcgurk" 40 | }, 41 | "href" : "https://api.spotify.com/v1/users/mcgurk", 42 | "id" : "mcgurk", 43 | "type" : "user", 44 | "uri" : "spotify:user:mcgurk" 45 | }, 46 | "public" : false, 47 | "tracks" : { 48 | "href" : "https://api.spotify.com/v1/users/mcgurk/playlists/2zNRHPRBynwGI2qIZZ2EGp/tracks", 49 | "total" : 66 50 | }, 51 | "type" : "playlist", 52 | "uri" : "spotify:user:mcgurk:playlist:2zNRHPRBynwGI2qIZZ2EGp" 53 | }, { 54 | "collaborative" : false, 55 | "external_urls" : { 56 | "spotify" : "http://open.spotify.com/user/mcgurk/playlist/0CQyRJcgAUtu3HXedHTukP" 57 | }, 58 | "href" : "https://api.spotify.com/v1/users/mcgurk/playlists/0CQyRJcgAUtu3HXedHTukP", 59 | "id" : "0CQyRJcgAUtu3HXedHTukP", 60 | "images" : [ ], 61 | "name" : "Musica", 62 | "owner" : { 63 | "external_urls" : { 64 | "spotify" : "http://open.spotify.com/user/mcgurk" 65 | }, 66 | "href" : "https://api.spotify.com/v1/users/mcgurk", 67 | "id" : "mcgurk", 68 | "type" : "user", 69 | "uri" : "spotify:user:mcgurk" 70 | }, 71 | "public" : false, 72 | "tracks" : { 73 | "href" : "https://api.spotify.com/v1/users/mcgurk/playlists/0CQyRJcgAUtu3HXedHTukP/tracks", 74 | "total" : 774 75 | }, 76 | "type" : "playlist", 77 | "uri" : "spotify:user:mcgurk:playlist:0CQyRJcgAUtu3HXedHTukP" 78 | }, { 79 | "collaborative" : false, 80 | "external_urls" : { 81 | "spotify" : "http://open.spotify.com/user/mcgurk/playlist/0dWICxwZcXNTxavch2ViJl" 82 | }, 83 | "href" : "https://api.spotify.com/v1/users/mcgurk/playlists/0dWICxwZcXNTxavch2ViJl", 84 | "id" : "0dWICxwZcXNTxavch2ViJl", 85 | "images" : [ ], 86 | "name" : "Mmmh marabou", 87 | "owner" : { 88 | "external_urls" : { 89 | "spotify" : "http://open.spotify.com/user/mcgurk" 90 | }, 91 | "href" : "https://api.spotify.com/v1/users/mcgurk", 92 | "id" : "mcgurk", 93 | "type" : "user", 94 | "uri" : "spotify:user:mcgurk" 95 | }, 96 | "public" : false, 97 | "tracks" : { 98 | "href" : "https://api.spotify.com/v1/users/mcgurk/playlists/0dWICxwZcXNTxavch2ViJl/tracks", 99 | "total" : 58 100 | }, 101 | "type" : "playlist", 102 | "uri" : "spotify:user:mcgurk:playlist:0dWICxwZcXNTxavch2ViJl" 103 | }, { 104 | "collaborative" : false, 105 | "external_urls" : { 106 | "spotify" : "http://open.spotify.com/user/mcgurk/playlist/1f1zO9SrQXk9X0bY59HS5r" 107 | }, 108 | "href" : "https://api.spotify.com/v1/users/mcgurk/playlists/1f1zO9SrQXk9X0bY59HS5r", 109 | "id" : "1f1zO9SrQXk9X0bY59HS5r", 110 | "images" : [ ], 111 | "name" : "Mys", 112 | "owner" : { 113 | "external_urls" : { 114 | "spotify" : "http://open.spotify.com/user/mcgurk" 115 | }, 116 | "href" : "https://api.spotify.com/v1/users/mcgurk", 117 | "id" : "mcgurk", 118 | "type" : "user", 119 | "uri" : "spotify:user:mcgurk" 120 | }, 121 | "public" : false, 122 | "tracks" : { 123 | "href" : "https://api.spotify.com/v1/users/mcgurk/playlists/1f1zO9SrQXk9X0bY59HS5r/tracks", 124 | "total" : 4 125 | }, 126 | "type" : "playlist", 127 | "uri" : "spotify:user:mcgurk:playlist:1f1zO9SrQXk9X0bY59HS5r" 128 | } ], 129 | "limit" : 5, 130 | "next" : "https://api.spotify.com/v1/users/mcgurk/playlists?offset=5&limit=5", 131 | "offset" : 0, 132 | "previous" : null, 133 | "total" : 8 134 | } 135 | -------------------------------------------------------------------------------- /tests/fixtures/user-shows-contains.json: -------------------------------------------------------------------------------- 1 | [true, false, true] 2 | -------------------------------------------------------------------------------- /tests/fixtures/user-shows.json: -------------------------------------------------------------------------------- 1 | { 2 | "href": "https://api.spotify.com/v1/me/shows?offset=0&limit=20", 3 | "items": [ 4 | { 5 | "added_at": "2019-12-08T21:14:30Z", 6 | "show": { 7 | "available_markets": [ 8 | "AD", 9 | "AE" 10 | ], 11 | "copyrights": [], 12 | "description": "Explore the dark side of the Internet with host Jack Rhysider as he takes you on a journey through the chilling world of privacy hacks, data breaches, and cyber crime. The masterful criminal hackers who dwell on the dark side show us just how vulnerable we all are.", 13 | "explicit": false, 14 | "external_urls": { 15 | "spotify": "https://open.spotify.com/show/4XPl3uEEL9hvqMkoZrzbx5" 16 | }, 17 | "href": "https://api.spotify.com/v1/shows/4XPl3uEEL9hvqMkoZrzbx5", 18 | "id": "4XPl3uEEL9hvqMkoZrzbx5", 19 | "images": [ 20 | { 21 | "height": 640, 22 | "url": "https://i.scdn.co/image/53ba2adaaf2d3e47898aed9edb64026145032e7b", 23 | "width": 640 24 | }, 25 | { 26 | "height": 300, 27 | "url": "https://i.scdn.co/image/5f4726afb1e227c80f228b6b1ea7a6a1209ebe97", 28 | "width": 300 29 | }, 30 | { 31 | "height": 64, 32 | "url": "https://i.scdn.co/image/33cf2b6fea8d62ab730f902b52b0dc1f676cf015", 33 | "width": 64 34 | } 35 | ], 36 | "is_externally_hosted": false, 37 | "languages": [ 38 | "en" 39 | ], 40 | "media_type": "audio", 41 | "name": "Darknet Diaries", 42 | "publisher": "Jack Rhysider", 43 | "type": "show", 44 | "uri": "spotify:show:4XPl3uEEL9hvqMkoZrzbx5" 45 | } 46 | }, 47 | { 48 | "added_at": "2019-11-22T11:08:10Z", 49 | "show": { 50 | "available_markets": [ 51 | "AD", 52 | "AE" 53 | ], 54 | "copyrights": [], 55 | "description": "Fest & Flauschig mit Jan Böhmermann und Olli Schulz. Der preisgekrönte, verblüffend fabelhafte, grenzenlos fantastische Podcast für sie, ihn und es.", 56 | "explicit": false, 57 | "external_urls": { 58 | "spotify": "https://open.spotify.com/show/1OLcQdw2PFDPG1jo3s0wbp" 59 | }, 60 | "href": "https://api.spotify.com/v1/shows/1OLcQdw2PFDPG1jo3s0wbp", 61 | "id": "1OLcQdw2PFDPG1jo3s0wbp", 62 | "images": [ 63 | { 64 | "height": 640, 65 | "url": "https://i.scdn.co/image/79364dab39c9d3757838940fc7cb133c75fdaad2", 66 | "width": 640 67 | }, 68 | { 69 | "height": 300, 70 | "url": "https://i.scdn.co/image/eaf33726dff2bcafeff475813f5bd18ddf51b89d", 71 | "width": 300 72 | }, 73 | { 74 | "height": 64, 75 | "url": "https://i.scdn.co/image/a6514cfa222d1ee22ece832500334903154ffa83", 76 | "width": 64 77 | } 78 | ], 79 | "is_externally_hosted": false, 80 | "languages": [ 81 | "de" 82 | ], 83 | "media_type": "audio", 84 | "name": "Fest & Flauschig", 85 | "publisher": "Jan Böhmermann & Olli Schulz", 86 | "type": "show", 87 | "uri": "spotify:show:1OLcQdw2PFDPG1jo3s0wbp" 88 | } 89 | }, 90 | { 91 | "added_at": "2019-10-19T10:57:38Z", 92 | "show": { 93 | "available_markets": [ 94 | "AD", 95 | "AE" 96 | ], 97 | "copyrights": [], 98 | "description": "A series about what it's really like to start a business.", 99 | "explicit": false, 100 | "external_urls": { 101 | "spotify": "https://open.spotify.com/show/5CnDmMUG0S5bSSw612fs8C" 102 | }, 103 | "href": "https://api.spotify.com/v1/shows/5CnDmMUG0S5bSSw612fs8C", 104 | "id": "5CnDmMUG0S5bSSw612fs8C", 105 | "images": [ 106 | { 107 | "height": 640, 108 | "url": "https://i.scdn.co/image/6fe88d8c175bdec76c7f9f204c60f4331ce89bdc", 109 | "width": 640 110 | }, 111 | { 112 | "height": 300, 113 | "url": "https://i.scdn.co/image/00511e028a3b993efaf5e2e12b552cda1e979206", 114 | "width": 300 115 | }, 116 | { 117 | "height": 64, 118 | "url": "https://i.scdn.co/image/aa1dbf8c6c545c623d088d5e432afdf8dee3029d", 119 | "width": 64 120 | } 121 | ], 122 | "is_externally_hosted": false, 123 | "languages": [ 124 | "en" 125 | ], 126 | "media_type": "audio", 127 | "name": "StartUp Podcast", 128 | "publisher": "Gimlet", 129 | "type": "show", 130 | "uri": "spotify:show:5CnDmMUG0S5bSSw612fs8C" 131 | } 132 | } 133 | ], 134 | "limit": 20, 135 | "next": null, 136 | "offset": 0, 137 | "previous": null, 138 | "total": 3 139 | } 140 | -------------------------------------------------------------------------------- /tests/fixtures/user-tracks-contains.json: -------------------------------------------------------------------------------- 1 | [true, true] 2 | -------------------------------------------------------------------------------- /tests/fixtures/user-tracks.json: -------------------------------------------------------------------------------- 1 | { 2 | "href" : "https://api.spotify.com/v1/me/tracks?offset=0&limit=5", 3 | "items" : [ { 4 | "added_at" : "2014-10-26T14:22:34Z", 5 | "track" : { 6 | "album" : { 7 | "album_type" : "album", 8 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 9 | "external_urls" : { 10 | "spotify" : "https://open.spotify.com/album/4m2880jivSbbyEGAKfITCa" 11 | }, 12 | "href" : "https://api.spotify.com/v1/albums/4m2880jivSbbyEGAKfITCa", 13 | "id" : "4m2880jivSbbyEGAKfITCa", 14 | "images" : [ { 15 | "height" : 636, 16 | "url" : "https://i.scdn.co/image/6710552422788861893233437ad9f0830b95c07c", 17 | "width" : 640 18 | }, { 19 | "height" : 298, 20 | "url" : "https://i.scdn.co/image/4df66c8b705efa6dd3be4e884fe2ca8779fd4fc9", 21 | "width" : 300 22 | }, { 23 | "height" : 64, 24 | "url" : "https://i.scdn.co/image/a3d945a1c9de931ed6eeb3dbca90579f6a6388c8", 25 | "width" : 64 26 | } ], 27 | "name" : "Random Access Memories", 28 | "type" : "album", 29 | "uri" : "spotify:album:4m2880jivSbbyEGAKfITCa" 30 | }, 31 | "artists" : [ { 32 | "external_urls" : { 33 | "spotify" : "https://open.spotify.com/artist/4tZwfgrHOc3mvqYlEYSvVi" 34 | }, 35 | "href" : "https://api.spotify.com/v1/artists/4tZwfgrHOc3mvqYlEYSvVi", 36 | "id" : "4tZwfgrHOc3mvqYlEYSvVi", 37 | "name" : "Daft Punk", 38 | "type" : "artist", 39 | "uri" : "spotify:artist:4tZwfgrHOc3mvqYlEYSvVi" 40 | } ], 41 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 42 | "disc_number" : 1, 43 | "duration_ms" : 544626, 44 | "explicit" : false, 45 | "external_ids" : { 46 | "isrc" : "USQX91300103" 47 | }, 48 | "external_urls" : { 49 | "spotify" : "https://open.spotify.com/track/0oks4FnzhNp5QPTZtoet7c" 50 | }, 51 | "href" : "https://api.spotify.com/v1/tracks/0oks4FnzhNp5QPTZtoet7c", 52 | "id" : "0oks4FnzhNp5QPTZtoet7c", 53 | "name" : "Giorgio by Moroder", 54 | "popularity" : 68, 55 | "preview_url" : "https://p.scdn.co/mp3-preview/9f30c1ab377fab3d5fba3c376ce32174f70d5546", 56 | "track_number" : 3, 57 | "type" : "track", 58 | "uri" : "spotify:track:0oks4FnzhNp5QPTZtoet7c" 59 | } 60 | }, { 61 | "added_at" : "2014-10-26T14:22:29Z", 62 | "track" : { 63 | "album" : { 64 | "album_type" : "album", 65 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 66 | "external_urls" : { 67 | "spotify" : "https://open.spotify.com/album/4m2880jivSbbyEGAKfITCa" 68 | }, 69 | "href" : "https://api.spotify.com/v1/albums/4m2880jivSbbyEGAKfITCa", 70 | "id" : "4m2880jivSbbyEGAKfITCa", 71 | "images" : [ { 72 | "height" : 636, 73 | "url" : "https://i.scdn.co/image/6710552422788861893233437ad9f0830b95c07c", 74 | "width" : 640 75 | }, { 76 | "height" : 298, 77 | "url" : "https://i.scdn.co/image/4df66c8b705efa6dd3be4e884fe2ca8779fd4fc9", 78 | "width" : 300 79 | }, { 80 | "height" : 64, 81 | "url" : "https://i.scdn.co/image/a3d945a1c9de931ed6eeb3dbca90579f6a6388c8", 82 | "width" : 64 83 | } ], 84 | "name" : "Random Access Memories", 85 | "type" : "album", 86 | "uri" : "spotify:album:4m2880jivSbbyEGAKfITCa" 87 | }, 88 | "artists" : [ { 89 | "external_urls" : { 90 | "spotify" : "https://open.spotify.com/artist/4tZwfgrHOc3mvqYlEYSvVi" 91 | }, 92 | "href" : "https://api.spotify.com/v1/artists/4tZwfgrHOc3mvqYlEYSvVi", 93 | "id" : "4tZwfgrHOc3mvqYlEYSvVi", 94 | "name" : "Daft Punk", 95 | "type" : "artist", 96 | "uri" : "spotify:artist:4tZwfgrHOc3mvqYlEYSvVi" 97 | }, { 98 | "external_urls" : { 99 | "spotify" : "https://open.spotify.com/artist/2RdwBSPQiwcmiDo9kixcl8" 100 | }, 101 | "href" : "https://api.spotify.com/v1/artists/2RdwBSPQiwcmiDo9kixcl8", 102 | "id" : "2RdwBSPQiwcmiDo9kixcl8", 103 | "name" : "Pharrell Williams", 104 | "type" : "artist", 105 | "uri" : "spotify:artist:2RdwBSPQiwcmiDo9kixcl8" 106 | }, { 107 | "external_urls" : { 108 | "spotify" : "https://open.spotify.com/artist/3yDIp0kaq9EFKe07X1X2rz" 109 | }, 110 | "href" : "https://api.spotify.com/v1/artists/3yDIp0kaq9EFKe07X1X2rz", 111 | "id" : "3yDIp0kaq9EFKe07X1X2rz", 112 | "name" : "Nile Rodgers", 113 | "type" : "artist", 114 | "uri" : "spotify:artist:3yDIp0kaq9EFKe07X1X2rz" 115 | } ], 116 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 117 | "disc_number" : 1, 118 | "duration_ms" : 369626, 119 | "explicit" : false, 120 | "external_ids" : { 121 | "isrc" : "USQX91300108" 122 | }, 123 | "external_urls" : { 124 | "spotify" : "https://open.spotify.com/track/69kOkLUCkxIZYexIgSG8rq" 125 | }, 126 | "href" : "https://api.spotify.com/v1/tracks/69kOkLUCkxIZYexIgSG8rq", 127 | "id" : "69kOkLUCkxIZYexIgSG8rq", 128 | "name" : "Get Lucky", 129 | "popularity" : 78, 130 | "preview_url" : "https://p.scdn.co/mp3-preview/31208a018b7ab55969b945200901b7ef371c3a72", 131 | "track_number" : 8, 132 | "type" : "track", 133 | "uri" : "spotify:track:69kOkLUCkxIZYexIgSG8rq" 134 | } 135 | } ], 136 | "limit" : 5, 137 | "next" : null, 138 | "offset" : 0, 139 | "previous" : null, 140 | "total" : 2 141 | } 142 | -------------------------------------------------------------------------------- /tests/fixtures/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "display_name" : null, 3 | "external_urls" : { 4 | "spotify" : "https://open.spotify.com/user/mcgurk" 5 | }, 6 | "followers" : { 7 | "href" : null, 8 | "total" : 1 9 | }, 10 | "href" : "https://api.spotify.com/v1/users/mcgurk", 11 | "id" : "mcgurk", 12 | "images" : [ ], 13 | "type" : "user", 14 | "uri" : "spotify:user:mcgurk" 15 | } 16 | -------------------------------------------------------------------------------- /tests/fixtures/users-follows-playlist.json: -------------------------------------------------------------------------------- 1 | [true, true] 2 | --------------------------------------------------------------------------------