├── .coveralls.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __mocks__ └── superagent.js ├── __tests__ ├── authentication-request.js ├── base-request.js ├── http-manager.js ├── response-error.js ├── spotify-web-api.js └── webapi-request.js ├── examples ├── access-token-refresh.js ├── access-token-using-client-credentials.js ├── add-remove-replace-tracks-in-a-playlist.js ├── add-tracks-to-a-playlist.js ├── client-credentials.js ├── get-info-about-current-user.js ├── get-related-artists.js ├── get-top-tracks-for-artist.js ├── search-for-tracks.js └── tutorial │ ├── 00-get-access-token.js │ ├── 01-basics │ └── 01-get-info-about-current-user.js │ └── README.md ├── package-lock.json ├── package.json └── src ├── authentication-request.js ├── base-request.js ├── client.js ├── http-manager.js ├── response-error.js ├── server-methods.js ├── server.js ├── spotify-web-api.js └── webapi-request.js /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | repo_token: vuiBI2GvUrtb2lU2yChUBCOPggEAxIdRs -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .idea 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | __mocks__ 2 | __tests__ 3 | node_modules 4 | .coveralls.yml 5 | .npmignore 6 | *.log 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 14 4 | after_success: 5 | - npm run travis 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Change log 2 | 3 | #### 5.0.2 (Jan 2021) 4 | 5 | * Fix: Make `transferMyPlayback` not require the `options` object, since it should be optional. Thanks for the heads-up [@Simber1](https://github.com/Simber1)! 6 | 7 | #### 5.0.1 (Jan 2021) 8 | 9 | * Fix error handling in the HTTP client. Thanks [@yamadapc](https://github.com/yamadapc)! 10 | * This package can currently not be built on **Node 15 on Linux**, due to a dependency not being available yet. Issue can be followed on the [node-canvas](https://github.com/Automattic/node-canvas/issues/1688) issue tracker. In the mean time, Travis CI will run on earlier versions of Node. 11 | 12 | #### 5.0.0 (Oct 2020) 13 | 14 | * **BREAKING CHANGES**. 15 | * Arguments for some API methods have changed, causing incorrect behaviour using argument order from version 4.x. See the `README.md` for examples of how the methods can be used. 16 | * Create Playlist (`createPlaylist`) method no longer accepts a `userId` string as its first argument. 17 | * Transfer A User's Playback (`transferMyPlayback`) method takes a `deviceIds` array as its first argument. 18 | * Skip to Previous (`skipToPrevious`) method takes an `options` object as its first argument. 19 | * Skip to Next (`skipToNext`) method takes an `options` object as its first argument. 20 | * Set Repeat Mode on the Current User's Playback (`setRepeat`) method takes a `state` string as its first argument. 21 | * Set Shuffle Mode on the Current User's Playback (`setShuffle`) method takes a `state` string as its first argument. 22 | 23 | Cheers [@marinacrachi](https://github.com/marinacrachi) for the createPlaylist update. 24 | * Removed legacy support for not passing an `options` object while providing a callback method. This was only supported on a few of the older endpoints, and could lead to tricky bugs. The affected endpoints are `getTrack`, `getTracks`, `getAlbum`, `getAlbums`, and `createPlaylist`. Again, check the `README.md` for examples on how these methods can be used if needed. 25 | * Removed `options` argument for retrieving an access token using the Client Credentials flow, `clientCredentialsGrant`. 26 | * API errors come in five different flavours. 27 | * WebapiRegularError - For errors returned by most API endpoints. 28 | * WebapiPlayerError - For errors returned by the Player API. These contain a bit more information. 29 | * WebapiAuthenticationError - For errors related to authentication. 30 | * WebapiError - For errors that come from the Web API that didn't fit into one of the above. 31 | * TimeoutError - For network timeout errors. 32 | 33 | More importantly, errors now contain the response body, headers, and status code. One side-effect of this is that rate limited requests can be handled by checking the `Retry-After` header. Thanks for the PRs [@kauffecup](https://github.com/kauffecup), [@lantelyes](https://github.com/lantelyes), [@dkliemsch](https://github.com/dkliemsch), and [@erezny](https://github.com/erezny). 34 | 35 | Much appreciated [@konstantinjdobler](https://github.com/konstantinjdobler) for updates to the Player API errors. 36 | * Added support for [Implicit Grant flow](https://developer.spotify.com/documentation/general/guides/authorization-guide/#implicit-grant-flow) - Thanks [@gaganza](https://github.com/gaganza), [@reblws](https://github.com/reblws) and [@noahp78](https://github.com/noahp78)! 37 | * Starts or Resumes the Current User's Playback (`play`) method now supports the `position_ms` option. Thanks [@alqubo](https://github.com/alqubo), [@koflin](https://github.com/koflin), [@DoctorFishy](https://github.com/DoctorFishy). Thanks [@carmilso](https://github.com/carmilso) for general improvements to the Player API methods. 38 | * Binding for [Add an Item to the User's Playback Queue](https://developer.spotify.com/documentation/web-api/reference/player/add-to-queue/) endpoint added. Thanks [@thattomperson](https://github.com/thattomperson) and [@AriciducaZagaria](https://github.com/AriciducaZagaria)! 39 | * Binding for all [Shows and Episodes endpoints](https://developer.spotify.com/console/shows/). Thanks a _lot_ [@andyruwruw](https://github.com/andyruwruw)! 40 | * Documentation updates to keep up to date with ES6, thanks [@dandv](https://github.com/dandv)! Other documentation improvements by [@terensu-desu](https://github.com/terensu-desu), and examples by [@dersimn](https://github.com/dersimn). Thanks! 41 | * Bumped dependencies to resolve critical security issues. 42 | * Finally, hat off to [@dersimn](https://github.com/dersimn). Thanks for collecting all of the lingering PRs and merging them into a working and up-to-date fork. You really stepped up. 43 | 44 | Likely more changes coming before release to npm, which will happen shortly. 45 | 46 | #### 4.0.0 (14 Sep 2018) 47 | 48 | * Modified functions that operate on playlists to drop the user id parameter. This is a breaking change. [PR](https://github.com/thelinmichael/spotify-web-api-node/pull/243) 49 | * Updated superagent to fix a security warning [PR](https://github.com/thelinmichael/spotify-web-api-node/pull/211) 50 | * Fixed a bug by which an empty user was not handled properly in getUserPlaylists(). [PR](https://github.com/thelinmichael/spotify-web-api-node/pull/244) 51 | 52 | #### 3.1.1 (29 Apr 2018) 53 | 54 | * Modernized stack for a better developer experience. Integrated [prettier](https://github.com/thelinmichael/spotify-web-api-node/pull/205) and [jest](https://github.com/thelinmichael/spotify-web-api-node/pull/206). This simplifies the amount of dev dependencies. 55 | * Improved calls to save and remove saved tracks by adding a key as specified in the Spotify docs (See [PR](https://github.com/thelinmichael/spotify-web-api-node/pull/207)). Thanks to [@yanniz0r](https://github.com/yanniz0r) and [@adcar](https://github.com/adcar) for bringing it up. 56 | 57 | #### 3.1.0 (26 Apr 2018) 58 | 59 | * Added support for seeking and setting volume. Thanks to [@isokar](https://github.com/isokar), [@jamesemwallis](https://github.com/jamesemwallis), [@ashthespy](https://github.com/ashthespy), and [@vanderlin](https://github.com/vanderlin) for your PRs. 60 | 61 | #### 3.0.0 (8 Mar 2018) 62 | 63 | * @DalerAsrorov added support for uploading a custom image to a playlist in [this PR](https://github.com/thelinmichael/spotify-web-api-node/pull/169). 64 | * You can now pass a `device_id` when playing and pausing playback. @pfftdammitchris started [a PR to add device_id to the play() method](https://github.com/thelinmichael/spotify-web-api-node/pull/185). The changes served to another PR where we included the functionality. Thanks! 65 | * Added documentation in the README for `getMyCurrentPlaybackState()`. Thanks @PanMan for [your PR](https://github.com/thelinmichael/spotify-web-api-node/pull/160)! 66 | * @brodin realized we there was a lot of duplicated code and refactored it in a [great PR](https://github.com/thelinmichael/spotify-web-api-node/pull/123). 67 | 68 | #### 2.5.0 (4 Sep 2017) 69 | 70 | * Change README to reflect new authorization. Thanks [@arirawr](https://github.com/arirawr) for the [PR](https://github.com/thelinmichael/spotify-web-api-node/pull/146). 71 | * Add support for 'show_dialog' parameter when creating authorization url. Thanks [@ajhaupt7](https://github.com/ajhaupt7) for [the PR](https://github.com/thelinmichael/spotify-web-api-node/pull/101). 72 | * Add support for playback control (play, pause, prev, next), shuffle and repeat. Thanks [@JoseMCO](https://github.com/JoseMCO) for [the PR](https://github.com/thelinmichael/spotify-web-api-node/pull/150). 73 | * Add support for currently playing. Thanks [@dustinblackman](https://github.com/dustinblackman) for [the PR](https://github.com/thelinmichael/spotify-web-api-node/pull/145). 74 | * Fix to remove unnecessary deviceIds parameter from request to transfer playback. Thanks [@philnash](https://github.com/philnash) for [the PR](https://github.com/thelinmichael/spotify-web-api-node/pull/154). 75 | 76 | #### 2.4.0 (2 May 2017) 77 | 78 | * Change `addTracksToPlaylist` to pass the data in the body, preventing an issue with a long URL when passing many tracks. Thanks [@dolcalmi](https://github.com/dolcalmi) for [the PR](https://github.com/thelinmichael/spotify-web-api-node/pull/117) 79 | * Add support for fetching [recently played tracks](https://developer.spotify.com/web-api/console/get-recently-played/). Thanks [@jeremyboles](https://github.com/jeremyboles) for [the PR](https://github.com/thelinmichael/spotify-web-api-node/pull/111). 80 | 81 | #### 2.3.6 (15 October 2016) 82 | 83 | * Add language bindings for the **[Get Audio Analysis for a Track](https://developer.spotify.com/web-api/get-audio-analysis/)** endpoint. 84 | 85 | #### 2.3.5 (20 July 2016) 86 | 87 | * Use `encodeURIComponent` instead of `encodeURI` to encode the user's id. 'encodeURI' wasn't encoding characters like `/` or `#` that were generating an invalid endpoint url. Thanks [@jgranstrom](https://github.com/jgranstrom) for the PR. 88 | 89 | #### 2.3.4 (18 July 2016) 90 | 91 | * Fixed a bug in `clientCredentialsGrant()`. 92 | 93 | #### 2.3.3 (18 July 2016) 94 | 95 | * Migrated to the `superagent` request library to support Node.JS and browsers. Thanks [@SomeoneWeird](https://github.com/SomeoneWeird) for the PR to add it, and [@erezny](https://github.com/erezny) for reporting bugs. 96 | 97 | #### 2.3.2 (10 July 2016) 98 | 99 | * Add language bindings for **[Get a List of Current User's Playlists](https://developer.spotify.com/web-api/get-a-list-of-current-users-playlists/)**. Thanks [@JMPerez](https://github.com/JMPerez) and [@vinialbano](https://github.com/vinialbano). 100 | 101 | #### 2.3.1 (3 July 2016) 102 | 103 | * Fix for `getRecomendations` method causing client error response from the API when making the request. Thanks [@kyv](https://github.com/kyv) for reporting, and [@Boberober](https://github.com/Boberober) and [@JMPerez](https://github.com/JMPerez) for providing fixes. 104 | 105 | #### 2.3.0 (2 April 2016) 106 | 107 | * Add language bindings for **[Get Recommendations Based on Seeds](https://developer.spotify.com/web-api/get-recommendations/)**, **[Get a User's Top Artists and Tracks](https://developer.spotify.com/web-api/get-users-top-artists-and-tracks/)**, **[Get Audio Features for a Track](https://developer.spotify.com/web-api/get-audio-features/)**, and **[Get Audio Features for Several Tracks](https://developer.spotify.com/web-api/get-several-audio-features/)**. Read more about the endpoints in the links above or in this [blog post](https://developer.spotify.com/news-stories/2016/03/29/api-improvements-update/). 108 | * Add generic search method enabling searches for several types at once, e.g. search for both tracks and albums in a single request, instead of one request for track results and one request for album results. 109 | 110 | #### 2.2.0 (23 November 2015) 111 | 112 | * Add language bindings for **[Get User's Saved Albums](https://developer.spotify.com/web-api/get-users-saved-albums/)** and other endpoints related to the user's saved albums. 113 | 114 | #### 2.1.1 (23 November 2015) 115 | 116 | * Username encoding bugfix. 117 | 118 | #### 2.1.0 (16 July 2015) 119 | 120 | * Add language binding for **[Get Followed Artists](https://developer.spotify.com/web-api/get-followed-artists/)** 121 | 122 | #### 2.0.2 (11 May 2015) 123 | 124 | * Bugfix for retrieving an access token through the Client Credentials flow. (Thanks [Nate Wilkins](https://github.com/Nate-Wilkins)!) 125 | * Add test coverage and Travis CI. 126 | 127 | #### 2.0.1 (2 Mar 2015) 128 | 129 | * Return WebApiError objects if error occurs during authentication. 130 | 131 | #### 2.0.0 (27 Feb 2015) 132 | 133 | * **Breaking change**: Response object changed. Add headers and status code to all responses to enable users to implement caching. 134 | 135 | #### 1.3.13 (26 Feb 2015) 136 | 137 | * Add language binding for **[Reorder tracks in a Playlist](https://developer.spotify.com/web-api/reorder-playlists-tracks/)** 138 | 139 | #### 1.3.12 (22 Feb 2015) 140 | 141 | * Add language binding for **[Remove tracks in a Playlist by Position](https://developer.spotify.com/web-api/remove-tracks-playlist/)** 142 | 143 | #### 1.3.11 144 | 145 | * Add **[Search for Playlists](https://developer.spotify.com/web-api/search-item/)** endpoint. 146 | 147 | #### 1.3.10 148 | 149 | * Add market parameter to endpoints supporting **[Track Relinking](https://developer.spotify.com/web-api/track-relinking-guide/)**. 150 | * Improve SEO by adding keywords to the package.json file. ;-) 151 | 152 | #### 1.3.8 153 | 154 | * Add **[Get a List of Categories](https://developer.spotify.com/web-api/get-list-categories/)**, **[Get a Category](https://developer.spotify.com/web-api/get-category/)**, and **[Get A Category's Playlists](https://developer.spotify.com/web-api/get-categorys-playlists/)** endpoints. 155 | 156 | #### 1.3.7 157 | 158 | * Add **[Check if Users are Following Playlist](https://developer.spotify.com/web-api/check-user-following-playlist/)** endpoint. 159 | 160 | #### 1.3.5 161 | 162 | * Add missing options parameter in createPlaylist (issue #19). Thanks for raising this [allinallin](https://github.com/allinallin). 163 | 164 | #### 1.3.4 165 | 166 | * Add **[Follow Playlist](https://developer.spotify.com/web-api/follow-playlist/)** and **[Unfollow Playlist](https://developer.spotify.com/web-api/unfollow-playlist/)** endpoints. 167 | 168 | #### 1.3.3 169 | 170 | * [Fix](https://github.com/thelinmichael/spotify-web-api-node/pull/18) error format. Thanks [extrakt](https://github.com/extrakt). 171 | 172 | #### 1.3.2 173 | 174 | * Add ability to use callback methods instead of promise. 175 | 176 | #### 1.2.2 177 | 178 | * Bugfix. api.addTracksToPlaylist tracks parameter can be a string or an array. Thanks [ofagbemi](https://github.com/ofagbemi)! 179 | 180 | #### 1.2.1 181 | 182 | * Add **[Follow endpoints](https://developer.spotify.com/web-api/web-api-follow-endpoints/)**. Great work [JMPerez](https://github.com/JMPerez). 183 | 184 | #### 1.1.0 185 | 186 | * Add **[Browse endpoints](https://developer.spotify.com/web-api/browse-endpoints/)**. Thanks [fsahin](https://github.com/fsahin). 187 | 188 | #### 1.0.2 189 | 190 | * Specify module's git repository. Thanks [vincentorback](https://github.com/vincentorback). 191 | 192 | #### 1.0.1 193 | 194 | * Allow options to be set when retrieving a user's playlists. Thanks [EaterOfCode](https://github.com/EaterOfCode). 195 | 196 | #### 1.0.0 197 | 198 | * Add **[Replace tracks in a Playlist](https://developer.spotify.com/web-api/replace-playlists-tracks/)** endpoint 199 | * Add **[Remove tracks in a Playlist](https://developer.spotify.com/web-api/remove-tracks-playlist/)** endpoint 200 | * Return errors as Error objects instead of unparsed JSON. Thanks [niftylettuce](https://github.com/niftylettuce). 201 | 202 | #### 0.0.11 203 | 204 | * Add **[Change Playlist details](https://developer.spotify.com/web-api/change-playlist-details/)** endpoint (change published status and name). Gracias [JMPerez](https://github.com/JMPerez). 205 | 206 | #### 0.0.10 207 | 208 | * Add Your Music Endpoints (**[Add tracks](https://developer.spotify.com/web-api/save-tracks-user/)**, **[Remove tracks](https://developer.spotify.com/web-api/remove-tracks-user/)**, **[Contains tracks](https://developer.spotify.com/web-api/check-users-saved-tracks/)**, **[Get tracks](https://developer.spotify.com/web-api/get-users-saved-tracks/)**). 209 | * Documentation updates (change scope name of playlist-modify to playlist-modify-public, and a fix to a parameter type). Thanks [JMPerez](https://github.com/JMPerez) and [matiassingers](https://github.com/matiassingers). 210 | 211 | #### 0.0.9 212 | 213 | * Add **[Related artists](https://developer.spotify.com/web-api/get-related-artists/)** endpoint 214 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2021 Michael Thelin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spotify Web API Node 2 | 3 | [![Tests](https://travis-ci.org/thelinmichael/spotify-web-api-node.svg?branch=master)](https://travis-ci.org/thelinmichael/spotify-web-api-node) 4 | [![Coverage Status](https://coveralls.io/repos/thelinmichael/spotify-web-api-node/badge.svg)](https://coveralls.io/r/thelinmichael/spotify-web-api-node) 5 | [![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/spotify-web-api-node.svg)](https://bundlephobia.com/result?p=spotify-web-api-node) 6 | 7 | This is a universal wrapper/client for the [Spotify Web API](https://developer.spotify.com/web-api/) that runs on Node.JS and the browser, using [browserify](http://browserify.org/)/[webpack](https://webpack.github.io/)/[rollup](http://rollupjs.org/). A list of selected wrappers for different languages and environments is available at the Developer site's [Libraries page](https://developer.spotify.com/documentation/web-api/libraries/). 8 | 9 | Project owners are [thelinmichael](https://github.com/thelinmichael) and [JMPerez](https://github.com/JMPerez), with help from [a lot of awesome contributors](https://github.com/thelinmichael/spotify-web-api-node/network/members). 10 | 11 | ## Version 5 12 | 13 | > :warning: Since the last release (4.0.0, released over year ago) a lot of new functionality has been added by a lot of different contributors. **Thank you.** In order to implement some of the feature requests, some **breaking changes** had to be made. A list of them, along with a list of the new functionality, can be found in the [CHANGELOG](https://github.com/thelinmichael/spotify-web-api-node/blob/master/CHANGELOG.md). 14 | 15 | ## Table of contents 16 | 17 | * [Features](#features) 18 | * [Installation](#installation) 19 | * [Usage](#usage) 20 | * [Development](#development) 21 | 22 | ## Features 23 | 24 | The library includes helper functions to do the following: 25 | 26 | #### Fetch music metadata 27 | 28 | * Albums, artists, and tracks 29 | * Audio features and analysis for tracks 30 | * Albums for a specific artist 31 | * Top tracks for a specific artist 32 | * Artists similar to a specific artist 33 | 34 | #### Profiles 35 | 36 | * User's emails, product type, display name, birthdate, image 37 | 38 | #### Search 39 | 40 | * Albums, artists, tracks, and playlists 41 | 42 | #### Playlist manipulation 43 | 44 | * Get a user's playlists 45 | * Create playlists 46 | * Change playlist details 47 | * Add tracks to a playlist 48 | * Remove tracks from a playlist 49 | * Replace tracks in a playlist 50 | * Reorder tracks in a playlist 51 | 52 | #### Your Music library 53 | 54 | * Add, remove, and get tracks and albums that are in the signed in user's Your Music library 55 | * Check if a track or album is in the signed in user's Your Music library 56 | 57 | #### Personalization 58 | 59 | * Get a user’s top artists and tracks based on calculated affinity 60 | 61 | #### Browse 62 | 63 | * Get New Releases 64 | * Get Featured Playlists 65 | * Get a List of Categories 66 | * Get a Category 67 | * Get a Category's Playlists 68 | * Get recommendations based on seeds 69 | * Get available genre seeds 70 | 71 | #### Player 72 | 73 | * Get a User's Available Devices 74 | * Get Information About The User's Current Playback State 75 | * Get Current User's Recently Played Tracks 76 | * Get the User's Currently Playing Track 77 | * Pause a User's Playback 78 | * Seek To Position In Currently Playing Track 79 | * Set Repeat Mode On User’s Playback 80 | * Set Volume For User's Playback 81 | * Skip User’s Playback To Next Track 82 | * Skip User’s Playback To Previous Track 83 | * Start/Resume a User's Playback 84 | * Toggle Shuffle For User’s Playback 85 | * Transfer a User's Playback 86 | 87 | #### Follow 88 | 89 | * Follow and unfollow users 90 | * Follow and unfollow artists 91 | * Check if the logged in user follows a user or artist 92 | * Follow a playlist 93 | * Unfollow a playlist 94 | * Get followed artists 95 | * Check if users are following a Playlist 96 | 97 | #### Player 98 | 99 | * Add an Item to the User's Playback Queue 100 | * Get a user's available devices 101 | * Get information about the user's current playback 102 | * Get current user’s recently played tracks 103 | * Transfer a user's playback 104 | * Resume a user's playback 105 | * Skip a user's playback to next track 106 | * Skip a user's playback to previous track 107 | * Set a user's shuffle mode 108 | * Set a user's repeat mode 109 | * Set volume 110 | * Seek playback to a given position 111 | 112 | #### Shows 113 | 114 | * [Get a Show](https://developer.spotify.com/documentation/web-api/reference/shows/get-a-show/) 115 | 116 | ### Authentication 117 | 118 | All methods require authentication, which can be done using these flows: 119 | 120 | * [Client credentials flow](http://tools.ietf.org/html/rfc6749#section-4.4) (Application-only authentication) 121 | * [Authorization code grant](http://tools.ietf.org/html/rfc6749#section-4.1) (Signed by user) 122 | * [Implicit Grant Flow](https://tools.ietf.org/html/rfc6749#section-4.2) (Client-side Authentication) 123 | 124 | ##### Dependencies 125 | 126 | This project depends on [superagent](https://github.com/visionmedia/superagent) to make HTTP requests. 127 | 128 | ## Installation 129 | 130 | $ npm install spotify-web-api-node --save 131 | 132 | ## Usage 133 | 134 | First, instantiate the wrapper. 135 | 136 | ```javascript 137 | var SpotifyWebApi = require('spotify-web-api-node'); 138 | 139 | // credentials are optional 140 | var spotifyApi = new SpotifyWebApi({ 141 | clientId: 'fcecfc72172e4cd267473117a17cbd4d', 142 | clientSecret: 'a6338157c9bb5ac9c71924cb2940e1a7', 143 | redirectUri: 'http://www.example.com/callback' 144 | }); 145 | ``` 146 | 147 | If you've got an access token and want to use it for all calls, simply use the API object's set method. Handling credentials is described in detail in the Authorization section. 148 | 149 | ```javascript 150 | spotifyApi.setAccessToken(''); 151 | ``` 152 | 153 | Lastly, use the wrapper's helper methods to make the request to Spotify's Web API. The wrapper uses promises, so you need to provide a success callback as well as an error callback. 154 | 155 | ```javascript 156 | // Get Elvis' albums 157 | spotifyApi.getArtistAlbums('43ZHCT0cAZBISjO8DG9PnE').then( 158 | function(data) { 159 | console.log('Artist albums', data.body); 160 | }, 161 | function(err) { 162 | console.error(err); 163 | } 164 | ); 165 | ``` 166 | 167 | If you dont wan't to use promises, you can provide a callback method instead. 168 | 169 | ```javascript 170 | // Get Elvis' albums 171 | spotifyApi.getArtistAlbums( 172 | '43ZHCT0cAZBISjO8DG9PnE', 173 | { limit: 10, offset: 20 }, 174 | function(err, data) { 175 | if (err) { 176 | console.error('Something went wrong!'); 177 | } else { 178 | console.log(data.body); 179 | } 180 | } 181 | ); 182 | ``` 183 | 184 | The functions that fetch data from the API also accept a JSON object with a set of options. For example, `limit` and `offset` can be used in functions that returns paginated results, such as search and retrieving an artist's albums. 185 | 186 | Note that the **options** parameter is **required if you're using a callback method.**, even if it's empty. 187 | 188 | ```javascript 189 | // Passing a callback - get Elvis' albums in range [20...29] 190 | spotifyApi 191 | .getArtistAlbums('43ZHCT0cAZBISjO8DG9PnE', { limit: 10, offset: 20 }) 192 | .then( 193 | function(data) { 194 | console.log('Album information', data.body); 195 | }, 196 | function(err) { 197 | console.error(err); 198 | } 199 | ); 200 | ``` 201 | 202 | ### Responses and errors 203 | 204 | This exposes the response headers, status code and body. 205 | 206 | ```json 207 | { 208 | "body" : { 209 | 210 | }, 211 | "headers" : { 212 | 213 | }, 214 | "statusCode" : 215 | } 216 | ``` 217 | 218 | Errors have same fields, as well as a human readable `message`. This is especially useful since 219 | Spotify's Web API returns different types of error objects depending on the endpoint being called. 220 | 221 | #### Example of a response 222 | 223 | Retrieving a track's metadata in `spotify-web-api-node` version 1.4.0 and later: 224 | 225 | ```json 226 | { 227 | "body": { 228 | "name": "Golpe Maestro", 229 | "popularity": 42, 230 | "preview_url": 231 | "https://p.scdn.co/mp3-preview/4ac44a56e3a4b7b354c1273d7550bbad38c51f5d", 232 | "track_number": 1, 233 | "type": "track", 234 | "uri": "spotify:track:3Qm86XLflmIXVm1wcwkgDK" 235 | }, 236 | "headers": { 237 | "date": "Fri, 27 Feb 2015 09:25:48 GMT", 238 | "content-type": "application/json; charset=utf-8", 239 | "cache-control": "public, max-age=7200" 240 | }, 241 | "statusCode": 200 242 | } 243 | ``` 244 | 245 | ### More examples 246 | 247 | Below are examples for all helper functions. Longer examples of some requests can be found in the [examples folder](examples/). 248 | 249 | ```javascript 250 | var SpotifyWebApi = require('spotify-web-api-node'); 251 | 252 | var spotifyApi = new SpotifyWebApi(); 253 | 254 | /** 255 | * Get metadata of tracks, albums, artists, shows, and episodes 256 | */ 257 | 258 | // Get album 259 | spotifyApi.getAlbum('5U4W9E5WsYb2jUQWePT8Xm') 260 | .then(function(data) { 261 | console.log('Album information', data.body); 262 | }, function(err) { 263 | console.error(err); 264 | }); 265 | 266 | // Get multiple albums 267 | spotifyApi.getAlbums(['5U4W9E5WsYb2jUQWePT8Xm', '3KyVcddATClQKIdtaap4bV']) 268 | .then(function(data) { 269 | console.log('Albums information', data.body); 270 | }, function(err) { 271 | console.error(err); 272 | }); 273 | 274 | // Get an artist 275 | spotifyApi.getArtist('2hazSY4Ef3aB9ATXW7F5w3') 276 | .then(function(data) { 277 | console.log('Artist information', data.body); 278 | }, function(err) { 279 | console.error(err); 280 | }); 281 | 282 | // Get multiple artists 283 | spotifyApi.getArtists(['2hazSY4Ef3aB9ATXW7F5w3', '6J6yx1t3nwIDyPXk5xa7O8']) 284 | .then(function(data) { 285 | console.log('Artists information', data.body); 286 | }, function(err) { 287 | console.error(err); 288 | }); 289 | 290 | // Get albums by a certain artist 291 | spotifyApi.getArtistAlbums('43ZHCT0cAZBISjO8DG9PnE') 292 | .then(function(data) { 293 | console.log('Artist albums', data.body); 294 | }, function(err) { 295 | console.error(err); 296 | }); 297 | 298 | // Search tracks whose name, album or artist contains 'Love' 299 | spotifyApi.searchTracks('Love') 300 | .then(function(data) { 301 | console.log('Search by "Love"', data.body); 302 | }, function(err) { 303 | console.error(err); 304 | }); 305 | 306 | // Search artists whose name contains 'Love' 307 | spotifyApi.searchArtists('Love') 308 | .then(function(data) { 309 | console.log('Search artists by "Love"', data.body); 310 | }, function(err) { 311 | console.error(err); 312 | }); 313 | 314 | // Search tracks whose artist's name contains 'Love' 315 | spotifyApi.searchTracks('artist:Love') 316 | .then(function(data) { 317 | console.log('Search tracks by "Love" in the artist name', data.body); 318 | }, function(err) { 319 | console.log('Something went wrong!', err); 320 | }); 321 | 322 | // Search tracks whose artist's name contains 'Kendrick Lamar', and track name contains 'Alright' 323 | spotifyApi.searchTracks('track:Alright artist:Kendrick Lamar') 324 | .then(function(data) { 325 | console.log('Search tracks by "Alright" in the track name and "Kendrick Lamar" in the artist name', data.body); 326 | }, function(err) { 327 | console.log('Something went wrong!', err); 328 | }); 329 | 330 | 331 | // Search playlists whose name or description contains 'workout' 332 | spotifyApi.searchPlaylists('workout') 333 | .then(function(data) { 334 | console.log('Found playlists are', data.body); 335 | }, function(err) { 336 | console.log('Something went wrong!', err); 337 | }); 338 | 339 | // Get tracks in an album 340 | spotifyApi.getAlbumTracks('41MnTivkwTO3UUJ8DrqEJJ', { limit : 5, offset : 1 }) 341 | .then(function(data) { 342 | console.log(data.body); 343 | }, function(err) { 344 | console.log('Something went wrong!', err); 345 | }); 346 | 347 | // Get an artist's top tracks 348 | spotifyApi.getArtistTopTracks('0oSGxfWSnnOXhD2fKuz2Gy', 'GB') 349 | .then(function(data) { 350 | console.log(data.body); 351 | }, function(err) { 352 | console.log('Something went wrong!', err); 353 | }); 354 | 355 | // Get artists related to an artist 356 | spotifyApi.getArtistRelatedArtists('0qeei9KQnptjwb8MgkqEoy') 357 | .then(function(data) { 358 | console.log(data.body); 359 | }, function(err) { 360 | done(err); 361 | }); 362 | 363 | /* Get Audio Features for a Track */ 364 | spotifyApi.getAudioFeaturesForTrack('3Qm86XLflmIXVm1wcwkgDK') 365 | .then(function(data) { 366 | console.log(data.body); 367 | }, function(err) { 368 | done(err); 369 | }); 370 | 371 | /* Get Audio Analysis for a Track */ 372 | spotifyApi.getAudioAnalysisForTrack('3Qm86XLflmIXVm1wcwkgDK') 373 | .then(function(data) { 374 | console.log(data.body); 375 | }, function(err) { 376 | done(err); 377 | }); 378 | 379 | /* Get Audio Features for several tracks */ 380 | spotifyApi.getAudioFeaturesForTracks(['4iV5W9uYEdYUVa79Axb7Rh', '3Qm86XLflmIXVm1wcwkgDK']) 381 | .then(function(data) { 382 | console.log(data.body); 383 | }, function(err) { 384 | done(err); 385 | }); 386 | 387 | 388 | /* 389 | * User methods 390 | */ 391 | 392 | // Get a user 393 | spotifyApi.getUser('petteralexis') 394 | .then(function(data) { 395 | console.log('Some information about this user', data.body); 396 | }, function(err) { 397 | console.log('Something went wrong!', err); 398 | }); 399 | 400 | // Get the authenticated user 401 | spotifyApi.getMe() 402 | .then(function(data) { 403 | console.log('Some information about the authenticated user', data.body); 404 | }, function(err) { 405 | console.log('Something went wrong!', err); 406 | }); 407 | 408 | /* 409 | * Playlist methods 410 | */ 411 | 412 | // Get a playlist 413 | spotifyApi.getPlaylist('5ieJqeLJjjI8iJWaxeBLuK') 414 | .then(function(data) { 415 | console.log('Some information about this playlist', data.body); 416 | }, function(err) { 417 | console.log('Something went wrong!', err); 418 | }); 419 | 420 | // Get a user's playlists 421 | spotifyApi.getUserPlaylists('thelinmichael') 422 | .then(function(data) { 423 | console.log('Retrieved playlists', data.body); 424 | },function(err) { 425 | console.log('Something went wrong!', err); 426 | }); 427 | 428 | // Create a private playlist 429 | spotifyApi.createPlaylist('My playlist', { 'description': 'My description', 'public': true }) 430 | .then(function(data) { 431 | console.log('Created playlist!'); 432 | }, function(err) { 433 | console.log('Something went wrong!', err); 434 | }); 435 | 436 | // Add tracks to a playlist 437 | spotifyApi.addTracksToPlaylist('5ieJqeLJjjI8iJWaxeBLuK', ["spotify:track:4iV5W9uYEdYUVa79Axb7Rh", "spotify:track:1301WleyT98MSxVHPZCA6M"]) 438 | .then(function(data) { 439 | console.log('Added tracks to playlist!'); 440 | }, function(err) { 441 | console.log('Something went wrong!', err); 442 | }); 443 | 444 | // Add tracks to a specific position in a playlist 445 | spotifyApi.addTracksToPlaylist('5ieJqeLJjjI8iJWaxeBLuK', ["spotify:track:4iV5W9uYEdYUVa79Axb7Rh", "spotify:track:1301WleyT98MSxVHPZCA6M"], 446 | { 447 | position : 5 448 | }) 449 | .then(function(data) { 450 | console.log('Added tracks to playlist!'); 451 | }, function(err) { 452 | console.log('Something went wrong!', err); 453 | }); 454 | 455 | // Remove tracks from a playlist at a specific position 456 | spotifyApi.removeTracksFromPlaylistByPosition('5ieJqeLJjjI8iJWaxeBLuK', [0, 2, 130], "0wD+DKCUxiSR/WY8lF3fiCTb7Z8X4ifTUtqn8rO82O4Mvi5wsX8BsLj7IbIpLVM9") 457 | .then(function(data) { 458 | console.log('Tracks removed from playlist!'); 459 | }, function(err) { 460 | console.log('Something went wrong!', err); 461 | }); 462 | 463 | // Remove all occurrence of a track 464 | var tracks = [{ uri : "spotify:track:4iV5W9uYEdYUVa79Axb7Rh" }]; 465 | var playlistId = '5ieJqeLJjjI8iJWaxeBLuK'; 466 | var options = { snapshot_id : "0wD+DKCUxiSR/WY8lF3fiCTb7Z8X4ifTUtqn8rO82O4Mvi5wsX8BsLj7IbIpLVM9" }; 467 | spotifyApi.removeTracksFromPlaylist(playlistId, tracks, options) 468 | .then(function(data) { 469 | console.log('Tracks removed from playlist!'); 470 | }, function(err) { 471 | console.log('Something went wrong!', err); 472 | }); 473 | 474 | // Reorder the first two tracks in a playlist to the place before the track at the 10th position 475 | var options = { "range_length" : 2 }; 476 | spotifyApi.reorderTracksInPlaylist('5ieJqeLJjjI8iJWaxeBLuK', 0, 10, options) 477 | .then(function(data) { 478 | console.log('Tracks reordered in playlist!'); 479 | }, function(err) { 480 | console.log('Something went wrong!', err); 481 | }); 482 | 483 | // Change playlist details 484 | spotifyApi.changePlaylistDetails('5ieJqeLJjjI8iJWaxeBLuK', 485 | { 486 | name: 'This is a new name for my Cool Playlist, and will become private', 487 | 'public' : false 488 | }).then(function(data) { 489 | console.log('Playlist is now private!'); 490 | }, function(err) { 491 | console.log('Something went wrong!', err); 492 | }); 493 | 494 | // Upload a custom playlist cover image 495 | spotifyApi.uploadCustomPlaylistCoverImage('5ieJqeLJjjI8iJWaxeBLuK','longbase64uri') 496 | .then(function(data) { 497 | console.log('Playlsit cover image uploaded!'); 498 | }, function(err) { 499 | console.log('Something went wrong!', err); 500 | }); 501 | 502 | // Follow a playlist (privately) 503 | spotifyApi.followPlaylist('5ieJqeLJjjI8iJWaxeBLuK', 504 | { 505 | 'public' : false 506 | }).then(function(data) { 507 | console.log('Playlist successfully followed privately!'); 508 | }, function(err) { 509 | console.log('Something went wrong!', err); 510 | }); 511 | 512 | // Unfollow a playlist 513 | spotifyApi.unfollowPlaylist('5ieJqeLJjjI8iJWaxeBLuK') 514 | .then(function(data) { 515 | console.log('Playlist successfully unfollowed!'); 516 | }, function(err) { 517 | console.log('Something went wrong!', err); 518 | }); 519 | 520 | // Check if Users are following a Playlist 521 | spotifyApi.areFollowingPlaylist('5ieJqeLJjjI8iJWaxeBLuK', ['thelinmichael', 'ella']) 522 | .then(function(data) { 523 | data.body.forEach(function(isFollowing) { 524 | console.log("User is following: " + isFollowing); 525 | }); 526 | }, function(err) { 527 | console.log('Something went wrong!', err); 528 | }); 529 | 530 | /* 531 | * Following Users and Artists methods 532 | */ 533 | 534 | /* Get followed artists */ 535 | spotifyApi.getFollowedArtists({ limit : 1 }) 536 | .then(function(data) { 537 | // 'This user is following 1051 artists!' 538 | console.log('This user is following ', data.body.artists.total, ' artists!'); 539 | }, function(err) { 540 | console.log('Something went wrong!', err); 541 | }); 542 | 543 | /* Follow a user */ 544 | spotifyApi.followUsers(['thelinmichael']) 545 | .then(function(data) { 546 | console.log(data); 547 | }, function(err) { 548 | console.log('Something went wrong!', err); 549 | }); 550 | 551 | /* Follow an artist */ 552 | spotifyApi.followArtists(['2hazSY4Ef3aB9ATXW7F5w3', '6J6yx1t3nwIDyPXk5xa7O8']) 553 | .then(function(data) { 554 | console.log(data); 555 | }, function(err) { 556 | console.log('Something went wrong!', err); 557 | }); 558 | 559 | /* Unfollow a user */ 560 | spotifyApi.unfollowUsers(['thelinmichael']) 561 | .then(function(data) { 562 | console.log(data); 563 | }, function(err) { 564 | console.log('Something went wrong!', err); 565 | }); 566 | 567 | /* Unfollow an artist */ 568 | spotifyApi.unfollowArtists(['2hazSY4Ef3aB9ATXW7F5w3', '6J6yx1t3nwIDyPXk5xa7O8']) 569 | .then(function(data) { 570 | console.log(data); 571 | }, function(err) { 572 | console.log('Something went wrong!', err); 573 | }); 574 | 575 | /* Check if a user is following a user */ 576 | let usersId = ['thelinmichael']; 577 | 578 | spotifyApi.isFollowingUsers(usersId) 579 | .then(function(data) { 580 | let isFollowing = data.body; 581 | 582 | for (let index = 0; index < usersId.length; index++) { 583 | console.log(usersId[index] + ':' + isFollowing[index]) 584 | } 585 | }, function(err) { 586 | console.log('Something went wrong!', err); 587 | }); 588 | 589 | /* Check if a user is following an artist */ 590 | let artistsId = ['6mfK6Q2tzLMEchAr0e9Uzu', '4DYFVNKZ1uixa6SQTvzQwJ']; 591 | 592 | spotifyApi.isFollowingArtists(artistsId) 593 | .then(function(data) { 594 | let isFollowing = data.body; 595 | 596 | for (let index = 0; index < artistsId.length; index++) { 597 | console.log(artistsId[index] + ':' + isFollowing[index]) 598 | } 599 | }, function(err) { 600 | console.log('Something went wrong!', err); 601 | }); 602 | 603 | /* 604 | * Your Music library methods 605 | */ 606 | 607 | /* Tracks */ 608 | 609 | // Get tracks in the signed in user's Your Music library 610 | spotifyApi.getMySavedTracks({ 611 | limit : 2, 612 | offset: 1 613 | }) 614 | .then(function(data) { 615 | console.log('Done!'); 616 | }, function(err) { 617 | console.log('Something went wrong!', err); 618 | }); 619 | 620 | 621 | // Check if tracks are in the signed in user's Your Music library 622 | spotifyApi.containsMySavedTracks(["5ybJm6GczjQOgTqmJ0BomP"]) 623 | .then(function(data) { 624 | 625 | // An array is returned, where the first element corresponds to the first track ID in the query 626 | var trackIsInYourMusic = data.body[0]; 627 | 628 | if (trackIsInYourMusic) { 629 | console.log('Track was found in the user\'s Your Music library'); 630 | } else { 631 | console.log('Track was not found.'); 632 | } 633 | }, function(err) { 634 | console.log('Something went wrong!', err); 635 | }); 636 | 637 | // Remove tracks from the signed in user's Your Music library 638 | spotifyApi.removeFromMySavedTracks(["3VNWq8rTnQG6fM1eldSpZ0"]) 639 | .then(function(data) { 640 | console.log('Removed!'); 641 | }, function(err) { 642 | console.log('Something went wrong!', err); 643 | }); 644 | }); 645 | 646 | // Add tracks to the signed in user's Your Music library 647 | spotifyApi.addToMySavedTracks(["3VNWq8rTnQG6fM1eldSpZ0"]) 648 | .then(function(data) { 649 | console.log('Added track!'); 650 | }, function(err) { 651 | console.log('Something went wrong!', err); 652 | }); 653 | }); 654 | 655 | /* Albums */ 656 | 657 | // Get albums in the signed in user's Your Music library 658 | spotifyApi.getMySavedAlbums({ 659 | limit : 1, 660 | offset: 0 661 | }) 662 | .then(function(data) { 663 | // Output items 664 | console.log(data.body.items); 665 | }, function(err) { 666 | console.log('Something went wrong!', err); 667 | }); 668 | 669 | 670 | // Check if albums are in the signed in user's Your Music library 671 | spotifyApi.containsMySavedAlbums(["1H8AHEB8VSE8irHViGOIrF"]) 672 | .then(function(data) { 673 | 674 | // An array is returned, where the first element corresponds to the first album ID in the query 675 | var albumIsInYourMusic = data.body[0]; 676 | 677 | if (albumIsInYourMusic) { 678 | console.log('Album was found in the user\'s Your Music library'); 679 | } else { 680 | console.log('Album was not found.'); 681 | } 682 | }, function(err) { 683 | console.log('Something went wrong!', err); 684 | }); 685 | 686 | // Remove albums from the signed in user's Your Music library 687 | spotifyApi.removeFromMySavedAlbums(["1H8AHEB8VSE8irHViGOIrF"]) 688 | .then(function(data) { 689 | console.log('Removed!'); 690 | }, function(err) { 691 | console.log('Something went wrong!', err); 692 | }); 693 | }); 694 | 695 | // Add albums to the signed in user's Your Music library 696 | spotifyApi.addToMySavedAlbums(["1H8AHEB8VSE8irHViGOIrF"]) 697 | .then(function(data) { 698 | console.log('Added album!'); 699 | }, function(err) { 700 | console.log('Something went wrong!', err); 701 | }); 702 | }); 703 | 704 | 705 | /* 706 | * Browse methods 707 | */ 708 | 709 | // Retrieve new releases 710 | spotifyApi.getNewReleases({ limit : 5, offset: 0, country: 'SE' }) 711 | .then(function(data) { 712 | console.log(data.body); 713 | done(); 714 | }, function(err) { 715 | console.log("Something went wrong!", err); 716 | }); 717 | }); 718 | 719 | // Retrieve featured playlists 720 | spotifyApi.getFeaturedPlaylists({ limit : 3, offset: 1, country: 'SE', locale: 'sv_SE', timestamp:'2014-10-23T09:00:00' }) 721 | .then(function(data) { 722 | console.log(data.body); 723 | }, function(err) { 724 | console.log("Something went wrong!", err); 725 | }); 726 | 727 | // Get a List of Categories 728 | spotifyApi.getCategories({ 729 | limit : 5, 730 | offset: 0, 731 | country: 'SE', 732 | locale: 'sv_SE' 733 | }) 734 | .then(function(data) { 735 | console.log(data.body); 736 | }, function(err) { 737 | console.log("Something went wrong!", err); 738 | }); 739 | 740 | // Get a Category (in Sweden) 741 | spotifyApi.getCategory('party', { 742 | country: 'SE', 743 | locale: 'sv_SE' 744 | }) 745 | .then(function(data) { 746 | console.log(data.body); 747 | }, function(err) { 748 | console.log("Something went wrong!", err); 749 | }); 750 | 751 | // Get Playlists for a Category (Party in Brazil) 752 | spotifyApi.getPlaylistsForCategory('party', { 753 | country: 'BR', 754 | limit : 2, 755 | offset : 0 756 | }) 757 | .then(function(data) { 758 | console.log(data.body); 759 | }, function(err) { 760 | console.log("Something went wrong!", err); 761 | }); 762 | 763 | // Get Recommendations Based on Seeds 764 | spotifyApi.getRecommendations({ 765 | min_energy: 0.4, 766 | seed_artists: ['6mfK6Q2tzLMEchAr0e9Uzu', '4DYFVNKZ1uixa6SQTvzQwJ'], 767 | min_popularity: 50 768 | }) 769 | .then(function(data) { 770 | let recommendations = data.body; 771 | console.log(recommendations); 772 | }, function(err) { 773 | console.log("Something went wrong!", err); 774 | }); 775 | 776 | // Get available genre seeds 777 | spotifyApi.getAvailableGenreSeeds() 778 | .then(function(data) { 779 | let genreSeeds = data.body; 780 | console.log(genreSeeds); 781 | }, function(err) { 782 | console.log('Something went wrong!', err); 783 | }); 784 | 785 | /* Player */ 786 | 787 | // Add an Item to the User's Playback Queue 788 | // TBD 789 | 790 | // Get a User's Available Devices 791 | spotifyApi.getMyDevices() 792 | .then(function(data) { 793 | let availableDevices = data.body.devices; 794 | console.log(availableDevices); 795 | }, function(err) { 796 | console.log('Something went wrong!', err); 797 | }); 798 | 799 | // Get Information About The User's Current Playback State 800 | spotifyApi.getMyCurrentPlaybackState() 801 | .then(function(data) { 802 | // Output items 803 | if (data.body && data.body.is_playing) { 804 | console.log("User is currently playing something!"); 805 | } else { 806 | console.log("User is not playing anything, or doing so in private."); 807 | } 808 | }, function(err) { 809 | console.log('Something went wrong!', err); 810 | }); 811 | 812 | // Get Current User's Recently Played Tracks 813 | spotifyApi.getMyRecentlyPlayedTracks({ 814 | limit : 20 815 | }).then(function(data) { 816 | // Output items 817 | console.log("Your 20 most recently played tracks are:"); 818 | data.body.items.forEach(item => console.log(item.track)); 819 | }, function(err) { 820 | console.log('Something went wrong!', err); 821 | }); 822 | 823 | // Get the User's Currently Playing Track 824 | spotifyApi.getMyCurrentPlayingTrack() 825 | .then(function(data) { 826 | console.log('Now playing: ' + data.body.item.name); 827 | }, function(err) { 828 | console.log('Something went wrong!', err); 829 | }); 830 | 831 | // Pause a User's Playback 832 | spotifyApi.pause() 833 | .then(function() { 834 | console.log('Playback paused'); 835 | }, function(err) { 836 | //if the user making the request is non-premium, a 403 FORBIDDEN response code will be returned 837 | console.log('Something went wrong!', err); 838 | }); 839 | 840 | // Seek To Position In Currently Playing Track 841 | spotifyApi.seek(positionMs) 842 | .then(function() { 843 | console.log('Seek to ' + positionMs); 844 | }, function(err) { 845 | //if the user making the request is non-premium, a 403 FORBIDDEN response code will be returned 846 | console.log('Something went wrong!', err); 847 | }); 848 | 849 | // Set Repeat Mode On User’s Playback 850 | spotifyApi.setRepeat('track') 851 | .then(function () { 852 | console.log('Repeat track.'); 853 | }, function(err) { 854 | //if the user making the request is non-premium, a 403 FORBIDDEN response code will be returned 855 | console.log('Something went wrong!', err); 856 | }); 857 | 858 | // Set Volume For User's Playback 859 | spotifyApi.setVolume(50) 860 | .then(function () { 861 | console.log('Setting volume to 50.'); 862 | }, function(err) { 863 | //if the user making the request is non-premium, a 403 FORBIDDEN response code will be returned 864 | console.log('Something went wrong!', err); 865 | }); 866 | 867 | // Skip User’s Playback To Next Track 868 | spotifyApi.skipToNext() 869 | .then(function() { 870 | console.log('Skip to next'); 871 | }, function(err) { 872 | //if the user making the request is non-premium, a 403 FORBIDDEN response code will be returned 873 | console.log('Something went wrong!', err); 874 | }); 875 | 876 | // Skip User’s Playback To Previous Track 877 | spotifyApi.skipToPrevious() 878 | .then(function() { 879 | console.log('Skip to previous'); 880 | }, function(err) { 881 | //if the user making the request is non-premium, a 403 FORBIDDEN response code will be returned 882 | console.log('Something went wrong!', err); 883 | }); 884 | 885 | // Start/Resume a User's Playback 886 | spotifyApi.play() 887 | .then(function() { 888 | console.log('Playback started'); 889 | }, function(err) { 890 | //if the user making the request is non-premium, a 403 FORBIDDEN response code will be returned 891 | console.log('Something went wrong!', err); 892 | }); 893 | 894 | // Toggle Shuffle For User’s Playback 895 | spotifyApi.setShuffle(true) 896 | .then(function() { 897 | console.log('Shuffle is on.'); 898 | }, function (err) { 899 | //if the user making the request is non-premium, a 403 FORBIDDEN response code will be returned 900 | console.log('Something went wrong!', err); 901 | }); 902 | 903 | // Transfer a User's Playback 904 | spotifyApi.transferMyPlayback(deviceIds) 905 | .then(function() { 906 | console.log('Transfering playback to ' + deviceIds); 907 | }, function(err) { 908 | //if the user making the request is non-premium, a 403 FORBIDDEN response code will be returned 909 | console.log('Something went wrong!', err); 910 | }); 911 | 912 | 913 | /** 914 | * Personalization Endpoints 915 | */ 916 | 917 | /* Get a User’s Top Artists*/ 918 | spotifyApi.getMyTopArtists() 919 | .then(function(data) { 920 | let topArtists = data.body.items; 921 | console.log(topArtists); 922 | }, function(err) { 923 | console.log('Something went wrong!', err); 924 | }); 925 | 926 | /* Get a User’s Top Tracks*/ 927 | spotifyApi.getMyTopTracks() 928 | .then(function(data) { 929 | let topTracks = data.body.items; 930 | console.log(topTracks); 931 | }, function(err) { 932 | console.log('Something went wrong!', err); 933 | }); 934 | 935 | ``` 936 | 937 | ### Chaining calls 938 | 939 | ```javascript 940 | // track detail information for album tracks 941 | spotifyApi 942 | .getAlbum('5U4W9E5WsYb2jUQWePT8Xm') 943 | .then(function(data) { 944 | return data.body.tracks.map(function(t) { 945 | return t.id; 946 | }); 947 | }) 948 | .then(function(trackIds) { 949 | return spotifyApi.getTracks(trackIds); 950 | }) 951 | .then(function(data) { 952 | console.log(data.body); 953 | }) 954 | .catch(function(error) { 955 | console.error(error); 956 | }); 957 | 958 | // album detail for the first 10 Elvis' albums 959 | spotifyApi 960 | .getArtistAlbums('43ZHCT0cAZBISjO8DG9PnE', { limit: 10 }) 961 | .then(function(data) { 962 | return data.body.albums.map(function(a) { 963 | return a.id; 964 | }); 965 | }) 966 | .then(function(albums) { 967 | return spotifyApi.getAlbums(albums); 968 | }) 969 | .then(function(data) { 970 | console.log(data.body); 971 | }); 972 | ``` 973 | 974 | ### Authorization 975 | Supplying an access token is required for all requests to the Spotify API. This wrapper supports three authorization flows - The Authorization Code flow (signed by a user), the Client Credentials flow (application authentication - the user isn't involved), and the Implicit Grant Flow (For completely clientside applications). See Spotify's [Authorization guide](https://developer.spotify.com/spotify-web-api/authorization-guide/) for detailed information on these flows. 976 | 977 | **Important: If you are writing a universal/isomorphic web app using this library, you will not be able to use methods that send a client secret to the Spotify authorization service. Client secrets should be kept server-side and not exposed to client browsers. Never include your client secret in the public JS served to the browser.** 978 | 979 | The first thing you need to do is to [create an application](https://developer.spotify.com/my-applications/). A step-by-step tutorial is offered by Spotify in this [tutorial](https://developer.spotify.com/spotify-web-api/tutorial/). 980 | 981 | #### Authorization code flow 982 | 983 | With the application created and its redirect URI set, the only thing necessary for the application to retrieve an **authorization code** is the user's permission. Which permissions you're able to ask for is documented in Spotify's [Using Scopes section](https://developer.spotify.com/spotify-web-api/using-scopes/). 984 | 985 | In order to get permissions, you need to direct the user to [Spotify's Accounts service](https://accounts.spotify.com). Generate the URL by using the wrapper's authorization URL method. 986 | 987 | ```javascript 988 | var scopes = ['user-read-private', 'user-read-email'], 989 | redirectUri = 'https://example.com/callback', 990 | clientId = '5fe01282e44241328a84e7c5cc169165', 991 | state = 'some-state-of-my-choice'; 992 | 993 | // Setting credentials can be done in the wrapper's constructor, or using the API object's setters. 994 | var spotifyApi = new SpotifyWebApi({ 995 | redirectUri: redirectUri, 996 | clientId: clientId 997 | }); 998 | 999 | // Create the authorization URL 1000 | var authorizeURL = spotifyApi.createAuthorizeURL(scopes, state); 1001 | 1002 | // https://accounts.spotify.com:443/authorize?client_id=5fe01282e44241328a84e7c5cc169165&response_type=code&redirect_uri=https://example.com/callback&scope=user-read-private%20user-read-email&state=some-state-of-my-choice 1003 | console.log(authorizeURL); 1004 | ``` 1005 | 1006 | The example below uses a hardcoded authorization code, retrieved from the Accounts service as described above. 1007 | 1008 | ```javascript 1009 | var credentials = { 1010 | clientId: 'someClientId', 1011 | clientSecret: 'someClientSecret', 1012 | redirectUri: 'http://www.michaelthelin.se/test-callback' 1013 | }; 1014 | 1015 | var spotifyApi = new SpotifyWebApi(credentials); 1016 | 1017 | // The code that's returned as a query parameter to the redirect URI 1018 | var code = 'MQCbtKe23z7YzzS44KzZzZgjQa621hgSzHN'; 1019 | 1020 | // Retrieve an access token and a refresh token 1021 | spotifyApi.authorizationCodeGrant(code).then( 1022 | function(data) { 1023 | console.log('The token expires in ' + data.body['expires_in']); 1024 | console.log('The access token is ' + data.body['access_token']); 1025 | console.log('The refresh token is ' + data.body['refresh_token']); 1026 | 1027 | // Set the access token on the API object to use it in later calls 1028 | spotifyApi.setAccessToken(data.body['access_token']); 1029 | spotifyApi.setRefreshToken(data.body['refresh_token']); 1030 | }, 1031 | function(err) { 1032 | console.log('Something went wrong!', err); 1033 | } 1034 | ); 1035 | ``` 1036 | 1037 | Since the access token was set on the API object in the previous success callback, **it's going to be used in future calls**. As it was retrieved using the Authorization Code flow, it can also be refreshed. 1038 | 1039 | ```javascript 1040 | // clientId, clientSecret and refreshToken has been set on the api object previous to this call. 1041 | spotifyApi.refreshAccessToken().then( 1042 | function(data) { 1043 | console.log('The access token has been refreshed!'); 1044 | 1045 | // Save the access token so that it's used in future calls 1046 | spotifyApi.setAccessToken(data.body['access_token']); 1047 | }, 1048 | function(err) { 1049 | console.log('Could not refresh access token', err); 1050 | } 1051 | ); 1052 | ``` 1053 | 1054 | #### Client Credential flow 1055 | 1056 | The Client Credential flow doesn't require the user to give permissions, so it's suitable for requests where the application just needs to authenticate itself. This is the case with for example retrieving a playlist. However, note that the access token cannot be refreshed, and that it isn't connected to a specific user. 1057 | 1058 | ```javascript 1059 | var clientId = 'someClientId', 1060 | clientSecret = 'someClientSecret'; 1061 | 1062 | // Create the api object with the credentials 1063 | var spotifyApi = new SpotifyWebApi({ 1064 | clientId: clientId, 1065 | clientSecret: clientSecret 1066 | }); 1067 | 1068 | // Retrieve an access token. 1069 | spotifyApi.clientCredentialsGrant().then( 1070 | function(data) { 1071 | console.log('The access token expires in ' + data.body['expires_in']); 1072 | console.log('The access token is ' + data.body['access_token']); 1073 | 1074 | // Save the access token so that it's used in future calls 1075 | spotifyApi.setAccessToken(data.body['access_token']); 1076 | }, 1077 | function(err) { 1078 | console.log('Something went wrong when retrieving an access token', err); 1079 | } 1080 | ); 1081 | ``` 1082 | 1083 | #### Implicit Grant flow 1084 | 1085 | The Implicit Grant can be used to allow users to login to your completely client-side application. This method still requires a registered application, but won't expose your client secret. 1086 | This method of authentication won't return any refresh tokens, so you will need to fully reauthenticate the user everytime a token expires. 1087 | 1088 | ```javascript 1089 | var scopes = ['user-read-private', 'user-read-email'], 1090 | redirectUri = 'https://example.com/callback', 1091 | clientId = '5fe01282e44241328a84e7c5cc169165', 1092 | state = 'some-state-of-my-choice', 1093 | showDialog = true, 1094 | responseType = 'token'; 1095 | 1096 | // Setting credentials can be done in the wrapper's constructor, or using the API object's setters. 1097 | var spotifyApi = new SpotifyWebApi({ 1098 | redirectUri: redirectUri, 1099 | clientId: clientId 1100 | }); 1101 | 1102 | // Create the authorization URL 1103 | var authorizeURL = spotifyApi.createAuthorizeURL( 1104 | scopes, 1105 | state, 1106 | showDialog, 1107 | responseType 1108 | ); 1109 | 1110 | // https://accounts.spotify.com/authorize?client_id=5fe01282e44241328a84e7c5cc169165&response_type=token&redirect_uri=https://example.com/callback&scope=user-read-private%20user-read-email&state=some-state-of-my-choice&show_dialog=true 1111 | console.log(authorizeURL); 1112 | ``` 1113 | 1114 | When the client returns, it will have a token we can directly pass to the library: 1115 | 1116 | ```javascript 1117 | // The code that's returned as a hash fragment query string parameter to the redirect URI 1118 | var code = 'MQCbtKe23z7YzzS44KzZzZgjQa621hgSzHN'; 1119 | var credentials = { 1120 | clientId: 'someClientId', 1121 | clientSecret: 'someClientSecret', 1122 | //Either here 1123 | accessToken: code 1124 | }; 1125 | 1126 | var spotifyApi = new SpotifyWebApi(credentials); 1127 | 1128 | //Or with a method 1129 | spotifyApi.setAccessToken(code); 1130 | ``` 1131 | 1132 | #### Setting credentials 1133 | 1134 | Credentials are either set when constructing the API object or set after the object has been created using setters. They can be set all at once or one at a time. 1135 | 1136 | Using setters, getters and resetters. 1137 | 1138 | ```javascript 1139 | // Use setters to set all credentials one by one 1140 | var spotifyApi = new SpotifyWebApi(); 1141 | spotifyApi.setAccessToken('myAccessToken'); 1142 | spotifyApi.setRefreshToken('myRefreshToken'); 1143 | spotifyApi.setRedirectURI('http://www.example.com/test-callback'); 1144 | spotifyApi.setClientId('myOwnClientId'); 1145 | spotifyApi.setClientSecret('someSuperSecretString'); 1146 | 1147 | // Set all credentials at the same time 1148 | spotifyApi.setCredentials({ 1149 | accessToken: 'myAccessToken', 1150 | refreshToken: 'myRefreshToken', 1151 | redirectUri: 'http://www.example.com/test-callback', 1152 | 'clientId ': 'myClientId', 1153 | clientSecret: 'myClientSecret' 1154 | }); 1155 | 1156 | // Get the credentials one by one 1157 | console.log('The access token is ' + spotifyApi.getAccessToken()); 1158 | console.log('The refresh token is ' + spotifyApi.getRefreshToken()); 1159 | console.log('The redirectURI is ' + spotifyApi.getRedirectURI()); 1160 | console.log('The client ID is ' + spotifyApi.getClientId()); 1161 | console.log('The client secret is ' + spotifyApi.getClientSecret()); 1162 | 1163 | // Get all credentials 1164 | console.log('The credentials are ' + spotifyApi.getCredentials()); 1165 | 1166 | // Reset the credentials 1167 | spotifyApi.resetAccessToken(); 1168 | spotifyApi.resetRefreshToken(); 1169 | spotifyApi.resetRedirectURI(); 1170 | spotifyApi.resetClientId(); 1171 | spotifyApi.resetClientSecret(); 1172 | spotifyApi.resetCode(); 1173 | 1174 | // Reset all credentials at the same time 1175 | spotifyApi.resetCredentials(); 1176 | ``` 1177 | 1178 | Using the constructor. 1179 | 1180 | ```javascript 1181 | // Set necessary parts of the credentials on the constructor 1182 | var spotifyApi = new SpotifyWebApi({ 1183 | clientId: 'myClientId', 1184 | clientSecret: 'myClientSecret' 1185 | }); 1186 | 1187 | // Get an access token and 'save' it using a setter 1188 | spotifyApi.clientCredentialsGrant().then( 1189 | function(data) { 1190 | console.log('The access token is ' + data.body['access_token']); 1191 | spotifyApi.setAccessToken(data.body['access_token']); 1192 | }, 1193 | function(err) { 1194 | console.log('Something went wrong!', err); 1195 | } 1196 | ); 1197 | ``` 1198 | 1199 | ```javascript 1200 | // Set the credentials when making the request 1201 | var spotifyApi = new SpotifyWebApi({ 1202 | accessToken: 'njd9wng4d0ycwnn3g4d1jm30yig4d27iom5lg4d3' 1203 | }); 1204 | 1205 | // Do search using the access token 1206 | spotifyApi.searchTracks('artist:Love').then( 1207 | function(data) { 1208 | console.log(data.body); 1209 | }, 1210 | function(err) { 1211 | console.log('Something went wrong!', err); 1212 | } 1213 | ); 1214 | ``` 1215 | 1216 | ```javascript 1217 | // Set the credentials when making the request 1218 | var spotifyApi = new SpotifyWebApi({ 1219 | accessToken: 'njd9wng4d0ycwnn3g4d1jm30yig4d27iom5lg4d3' 1220 | }); 1221 | 1222 | // Get tracks in a playlist 1223 | api 1224 | .getPlaylistTracks('3ktAYNcRHpazJ9qecm3ptn', { 1225 | offset: 1, 1226 | limit: 5, 1227 | fields: 'items' 1228 | }) 1229 | .then( 1230 | function(data) { 1231 | console.log('The playlist contains these tracks', data.body); 1232 | }, 1233 | function(err) { 1234 | console.log('Something went wrong!', err); 1235 | } 1236 | ); 1237 | ``` 1238 | 1239 | ## Development 1240 | 1241 | See something you think can be improved? [Open an issue](https://github.com/thelinmichael/spotify-web-api-node/issues/new) or clone the project and send a pull request with your changes. 1242 | 1243 | ### Running tests 1244 | 1245 | You can run the unit tests executing `npm test` and get a test coverage report running `npm test -- --coverage`. 1246 | -------------------------------------------------------------------------------- /__mocks__/superagent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //mock for superagent - __mocks__/superagent.js 4 | 5 | var mockDelay; 6 | var mockError; 7 | var mockResponse = { 8 | status() { 9 | return 200; 10 | }, 11 | ok() { 12 | return true; 13 | }, 14 | get: jest.fn(), 15 | toError: jest.fn() 16 | }; 17 | 18 | var Request = { 19 | put() { 20 | return this; 21 | }, 22 | del() { 23 | return this; 24 | }, 25 | post() { 26 | return this; 27 | }, 28 | get() { 29 | return this; 30 | }, 31 | send() { 32 | return this; 33 | }, 34 | query() { 35 | return this; 36 | }, 37 | field() { 38 | return this; 39 | }, 40 | set() { 41 | return this; 42 | }, 43 | accept() { 44 | return this; 45 | }, 46 | timeout() { 47 | return this; 48 | }, 49 | end: jest.fn().mockImplementation(function(callback) { 50 | if (mockDelay) { 51 | this.delayTimer = setTimeout(callback, 0, mockError, mockResponse); 52 | return; 53 | } 54 | 55 | callback(mockError, mockResponse); 56 | }), 57 | //expose helper methods for tests to set 58 | __setMockDelay(boolValue) { 59 | mockDelay = boolValue; 60 | }, 61 | __setMockResponse(mockRes) { 62 | mockResponse = mockRes; 63 | }, 64 | __setMockError(mockErr) { 65 | mockError = mockErr; 66 | }, 67 | __reset() { 68 | this.__setMockResponse({ 69 | status() { 70 | return 200; 71 | }, 72 | ok() { 73 | return true; 74 | } 75 | }); 76 | this.__setMockError(null); 77 | this.__setMockDelay(false); 78 | } 79 | }; 80 | 81 | module.exports = Request; 82 | -------------------------------------------------------------------------------- /__tests__/authentication-request.js: -------------------------------------------------------------------------------- 1 | var AuthenticationRequest = require('../src/authentication-request'); 2 | 3 | describe('Create Authentication Requests', () => { 4 | test('Should use default settings if none are supplied', () => { 5 | var request = AuthenticationRequest.builder().build(); 6 | 7 | expect(request.getHost()).toBe('accounts.spotify.com'); 8 | expect(request.getPort()).toBe(443); 9 | expect(request.getScheme()).toBe('https'); 10 | expect(request.getHeaders()).toBeFalsy(); 11 | expect(request.getPath()).toBeFalsy(); 12 | expect(request.getQueryParameters()).toBeFalsy(); 13 | expect(request.getBodyParameters()).toBeFalsy(); 14 | }); 15 | 16 | test('Can overwrite one of the default parameters', () => { 17 | var request = AuthenticationRequest.builder() 18 | .withHost('such.host.wow') 19 | .build(); 20 | 21 | expect(request.getHost()).toBe('such.host.wow'); 22 | expect(request.getPort()).toBe(443); 23 | expect(request.getScheme()).toBe('https'); 24 | expect(request.getHeaders()).toBeFalsy(); 25 | expect(request.getPath()).toBeFalsy(); 26 | expect(request.getQueryParameters()).toBeFalsy(); 27 | expect(request.getBodyParameters()).toBeFalsy(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /__tests__/base-request.js: -------------------------------------------------------------------------------- 1 | var Request = require('../src/base-request'); 2 | 3 | describe('Create Requests', () => { 4 | test('Should create host, port, and scheme', () => { 5 | var request = Request.builder() 6 | .withHost('such.api.wow') 7 | .withPort(1337) 8 | .withScheme('http') 9 | .build(); 10 | 11 | expect(request.getHost()).toBe('such.api.wow'); 12 | expect(request.getPort()).toBe(1337); 13 | expect(request.getScheme()).toBe('http'); 14 | }); 15 | 16 | test('Should add query parameters', () => { 17 | var request = Request.builder() 18 | .withHost('such.api.wow') 19 | .withPort(1337) 20 | .withScheme('http') 21 | .withQueryParameters({ 22 | oneParameter: 1, 23 | anotherParameter: true, 24 | thirdParameter: 'hello' 25 | }) 26 | .build(); 27 | 28 | expect(request.getQueryParameters().oneParameter).toBe(1); 29 | expect(request.getQueryParameters().anotherParameter).toBe(true); 30 | expect(request.getQueryParameters().thirdParameter).toBe('hello'); 31 | }); 32 | 33 | test('Should add query parameters (multiple calls)', () => { 34 | var request = Request.builder() 35 | .withHost('such.api.wow') 36 | .withPort(1337) 37 | .withScheme('http') 38 | .withQueryParameters({ 39 | oneParameter: 1, 40 | anotherParameter: true 41 | }) 42 | .withQueryParameters({ 43 | thirdParameter: 'hello' 44 | }) 45 | .build(); 46 | 47 | expect(request.getQueryParameters().oneParameter).toBe(1); 48 | expect(request.getQueryParameters().anotherParameter).toBe(true); 49 | expect(request.getQueryParameters().thirdParameter).toBe('hello'); 50 | }); 51 | 52 | test('Should add query parameters (combine calls)', () => { 53 | var request = Request.builder() 54 | .withHost('such.api.wow') 55 | .withPort(1337) 56 | .withScheme('http') 57 | .withQueryParameters( 58 | { 59 | oneParameter: 1, 60 | anotherParameter: true 61 | }, 62 | { 63 | thirdParameter: 'hello' 64 | } 65 | ) 66 | .build(); 67 | 68 | expect(request.getQueryParameters().oneParameter).toBe(1); 69 | expect(request.getQueryParameters().anotherParameter).toBe(true); 70 | expect(request.getQueryParameters().thirdParameter).toBe('hello'); 71 | }); 72 | 73 | test('Should add body parameters', () => { 74 | var request = Request.builder() 75 | .withHost('such.api.wow') 76 | .withPort(1337) 77 | .withScheme('http') 78 | .withBodyParameters({ 79 | one: 1, 80 | two: true, 81 | three: 'world' 82 | }) 83 | .build(); 84 | 85 | expect(request.getBodyParameters().one).toBe(1); 86 | expect(request.getBodyParameters().two).toBe(true); 87 | expect(request.getBodyParameters().three).toBe('world'); 88 | }); 89 | 90 | test('Should add array to body parameters', () => { 91 | var request = Request.builder() 92 | .withHost('such.api.wow') 93 | .withPort(1337) 94 | .withScheme('http') 95 | .withBodyParameters(['3VNWq8rTnQG6fM1eldSpZ0']) 96 | .build(); 97 | 98 | expect(request.getBodyParameters()).toEqual(['3VNWq8rTnQG6fM1eldSpZ0']); 99 | }); 100 | 101 | test('Should add header parameters', () => { 102 | var request = Request.builder() 103 | .withHost('such.api.wow') 104 | .withPort(1337) 105 | .withScheme('http') 106 | .withHeaders({ 107 | Authorization: 'Basic WOOP', 108 | 'Content-Type': 'application/lol' 109 | }) 110 | .build(); 111 | 112 | expect(request.getHeaders().Authorization).toBe('Basic WOOP'); 113 | expect(request.getHeaders()['Content-Type']).toBe('application/lol'); 114 | }); 115 | 116 | test('Should add path', () => { 117 | var request = Request.builder() 118 | .withHost('such.api.wow') 119 | .withPort(1337) 120 | .withPath('/v1/users/meriosweg') 121 | .build(); 122 | 123 | expect(request.getPath()).toBe('/v1/users/meriosweg'); 124 | }); 125 | 126 | test('Should build URI', () => { 127 | var request = Request.builder() 128 | .withHost('such.api.wow') 129 | .withScheme('https') 130 | .withPort(1337) 131 | .withPath('/v1/users/meriosweg') 132 | .build(); 133 | 134 | expect(request.getURI()).toBe( 135 | 'https://such.api.wow:1337/v1/users/meriosweg' 136 | ); 137 | }); 138 | 139 | test('Should construct empty query paramaters string', () => { 140 | var request = Request.builder() 141 | .withQueryParameters({}) 142 | .build(); 143 | 144 | expect(request.getQueryParameterString()).toBeFalsy(); 145 | }); 146 | 147 | test('Should construct query paramaters string for one parameter', () => { 148 | var request = Request.builder() 149 | .withQueryParameters({ 150 | one: 1 151 | }) 152 | .build(); 153 | 154 | expect(request.getQueryParameterString()).toBe('?one=1'); 155 | }); 156 | 157 | test('Should construct query paramaters string for multiple parameters', () => { 158 | var request = Request.builder() 159 | .withQueryParameters({ 160 | one: 1, 161 | two: true, 162 | three: 'world' 163 | }) 164 | .build(); 165 | 166 | expect(request.getQueryParameterString()).toBe( 167 | '?one=1&two=true&three=world' 168 | ); 169 | }); 170 | 171 | test('Should construct query paramaters string and exclude undefined values', () => { 172 | var request = Request.builder() 173 | .withQueryParameters({ 174 | one: 1, 175 | two: undefined, 176 | three: 'world' 177 | }) 178 | .build(); 179 | 180 | expect(request.getQueryParameterString()).toBe('?one=1&three=world'); 181 | }); 182 | }); 183 | -------------------------------------------------------------------------------- /__tests__/http-manager.js: -------------------------------------------------------------------------------- 1 | var Request = require('../src/base-request'), 2 | superagent = require('superagent'), 3 | { 4 | TimeoutError, 5 | WebapiError, 6 | WebapiRegularError, 7 | WebapiAuthenticationError, 8 | WebapiPlayerError 9 | } = require('../src/response-error'); 10 | 11 | var HttpManager = require('../src/http-manager'); 12 | var request = Request.builder() 13 | .withHost('such.api.wow') 14 | .withPort(1337) 15 | .withScheme('http') 16 | .build(); 17 | 18 | describe('Make requests', () => { 19 | 20 | afterEach(() => { 21 | superagent.__reset(); 22 | jest.restoreAllMocks(); 23 | }); 24 | 25 | test('Should make a successful GET request', done => { 26 | superagent.__setMockResponse({ 27 | statusCode: 200, 28 | headers: { 'Content-Type' : 'application/json' }, 29 | body: 'some data' 30 | }); 31 | 32 | HttpManager.get(request, function(error, result) { 33 | expect(result.body).toBe('some data'); 34 | expect(result.statusCode).toBe(200); 35 | expect(result.headers['Content-Type']).toBe('application/json'); 36 | 37 | done(error); 38 | }); 39 | }); 40 | 41 | test('Should process an error of unknown type', done => { 42 | superagent.__setMockError({ 43 | response : { 44 | body: 'GET request error', 45 | headers : {}, 46 | statusCode: 400 47 | } 48 | }); 49 | 50 | HttpManager.get(request, function(error, result) { 51 | expect(error).toBeInstanceOf(WebapiError); 52 | expect(error.message).toBe('GET request error'); 53 | done(); 54 | }); 55 | }); 56 | 57 | test('Should process an error of regular type', done => { 58 | superagent.__setMockError({ 59 | response : { 60 | body : { 61 | error: { 62 | status : 400, 63 | message : 'There is a problem in your request' 64 | }, 65 | }, 66 | headers : {}, 67 | statusCode : 400 68 | } 69 | }); 70 | 71 | HttpManager.get(request, function(error) { 72 | expect(error).toBeInstanceOf(WebapiRegularError); 73 | expect(error.message).toBe('An error occurred while communicating with Spotify\'s Web API.\nDetails: There is a problem in your request.'); 74 | done(); 75 | }); 76 | }); 77 | 78 | test('Should process an error of player type', done => { 79 | superagent.__setMockError({ 80 | response: { 81 | body: { 82 | error : { 83 | message: 'Detailed Web API Error message', 84 | status: 400, 85 | reason: 'You messed up!' 86 | } 87 | }, 88 | statusCode : 400, 89 | headers : [] 90 | } 91 | }); 92 | 93 | HttpManager.get(request, function(error) { 94 | expect(error).toBeInstanceOf(WebapiPlayerError); 95 | expect(error.message).toBe('An error occurred while communicating with Spotify\'s Web API.\nDetails: Detailed Web API Error message You messed up!.'); 96 | expect(error.body.error.reason).toBe('You messed up!'); 97 | expect(error.body.error.message).toBe('Detailed Web API Error message'); 98 | done(); 99 | }); 100 | }); 101 | 102 | test('should process error of authentication type', done => { 103 | superagent.__setMockError({ 104 | response : { 105 | body : { 106 | error: 'invalid_client', 107 | error_description : 'Invalid client' 108 | }, 109 | headers: { 'Content-Type' : 'application/json'}, 110 | statusCode : 400 111 | } 112 | }); 113 | 114 | HttpManager.get(request, function(error) { 115 | expect(error).toBeInstanceOf(WebapiAuthenticationError); 116 | expect(error.statusCode).toBe(400); 117 | expect(error.headers['Content-Type']).toBe('application/json'); 118 | expect(error.message).toBe('An authentication error occurred while communicating with Spotify\'s Web API.\nDetails: invalid_client Invalid client.'); 119 | 120 | done(); 121 | }); 122 | 123 | }); 124 | 125 | test('should process error of authentication type with missing description', done => { 126 | superagent.__setMockError({ 127 | response : { 128 | body : { 129 | error: 'invalid_client' 130 | }, 131 | headers: { 'Content-Type' : 'application/json'}, 132 | statusCode : 400 133 | } 134 | }); 135 | 136 | HttpManager.get(request, function(error) { 137 | expect(error).toBeInstanceOf(WebapiAuthenticationError); 138 | expect(error.message).toBe('An authentication error occurred while communicating with Spotify\'s Web API.\nDetails: invalid_client.'); 139 | 140 | done(); 141 | }); 142 | 143 | }); 144 | 145 | test('Should get Retry Headers', done => { 146 | superagent.__setMockError({ 147 | response: { 148 | body: { 149 | error : { 150 | message: 'Rate limit exceeded', 151 | status : 429 152 | } 153 | }, 154 | statusCode : 429, 155 | headers : { 'Retry-After' : '5' } 156 | } 157 | }); 158 | 159 | HttpManager.get(request, function(error) { 160 | expect(error).toBeInstanceOf(WebapiRegularError); 161 | expect(error.body.error.message).toBe('Rate limit exceeded'); 162 | expect(error.headers['Retry-After']).toBe('5'); 163 | expect(error.message).toBe('An error occurred while communicating with Spotify\'s Web API.\nDetails: Rate limit exceeded.') 164 | done(); 165 | }); 166 | }); 167 | 168 | test('Should make a successful POST request', done => { 169 | superagent.__setMockResponse({ 170 | status: 200, 171 | data: 'some data' 172 | }); 173 | 174 | HttpManager.post(request, function(error) { 175 | done(error); 176 | }); 177 | }); 178 | 179 | test('Should make a successful PUT request', done => { 180 | superagent.__setMockResponse({ 181 | status: 200, 182 | data: 'some data' 183 | }); 184 | 185 | HttpManager.put(request, function(error) { 186 | done(error); 187 | }); 188 | }); 189 | 190 | test('Should make a successful DELETE request', done => { 191 | superagent.__setMockResponse({ 192 | status: 200, 193 | data: 'some data' 194 | }); 195 | 196 | HttpManager.del(request, function(error) { 197 | done(error); 198 | }); 199 | }); 200 | 201 | test('Should handle timeouts', done => { 202 | superagent.__setMockError({ 203 | timeout: true 204 | }); 205 | 206 | HttpManager.get(request, function(error) { 207 | expect(error).toBeInstanceOf(TimeoutError); 208 | done(); 209 | }); 210 | }); 211 | 212 | test('Should handle arbitrary exceptions', done => { 213 | superagent.__setMockError(new Error('ops')); 214 | 215 | HttpManager.get(request, function(error) { 216 | expect(error).toBeInstanceOf(Error); 217 | done(); 218 | }); 219 | }); 220 | }); 221 | 222 | -------------------------------------------------------------------------------- /__tests__/response-error.js: -------------------------------------------------------------------------------- 1 | const { TimeoutError, WebapiError, WebapiRegularError, WebapiAuthenticationError, WebapiPlayerError } = require("../src/response-error"); 2 | 3 | describe('Test error classes', () => { 4 | 5 | test('Timeout', done => { 6 | let error = new TimeoutError(); 7 | 8 | expect(error.name).toBe('TimeoutError'); 9 | expect(error.message).toBe('A timeout occurred while communicating with Spotify\'s Web API.'); 10 | done(); 11 | }); 12 | 13 | test('WebapiError', done => { 14 | const body = { 15 | success : false 16 | }, 17 | headers = { 18 | 'Content-Type' : 'application/json', 19 | 'X-Experimental' : false 20 | }, 21 | statusCode = 400, 22 | message = 'An unfortunate error occurred.'; 23 | 24 | let error = new WebapiError(body, headers, statusCode, message); 25 | 26 | expect(error.name).toBe('WebapiError'); 27 | expect(error.body).toBe(body); 28 | expect(error.headers).toBe(headers); 29 | expect(error.statusCode).toBe(statusCode); 30 | expect(error.message).toBe(message); 31 | 32 | done(); 33 | }); 34 | 35 | test('WebapiRegularError', done => { 36 | const body = { 37 | error : { 38 | message : 'Not found', 39 | status : 404 40 | } 41 | }, 42 | headers = { 43 | 'Content-Type' : 'application/json', 44 | 'X-Experimental' : true 45 | }, 46 | statusCode = 404, 47 | message = 'An error occurred while communicating with Spotify\'s Web API.\nDetails: Not found.'; 48 | 49 | let error = new WebapiRegularError(body, headers, statusCode, message); 50 | 51 | expect(error.name).toBe('WebapiRegularError'); 52 | expect(error.body).toBe(body); 53 | expect(error.headers).toBe(headers); 54 | expect(error.statusCode).toBe(statusCode); 55 | expect(error.message).toBe(message); 56 | 57 | done(); 58 | }); 59 | 60 | test('WebapiAuthenticationError', done => { 61 | const body = { 62 | error : 'invalid client id', 63 | error_description : 'Invalid Client ID' 64 | }, 65 | headers = { 66 | 'Content-Type' : 'application/json' 67 | }, 68 | statusCode = 400, 69 | message = 'An authentication error occurred while communicating with Spotify\'s Web API.\nDetails: invalid client id Invalid Client ID.'; 70 | 71 | let error = new WebapiAuthenticationError(body, headers, statusCode, message); 72 | 73 | expect(error.name).toBe('WebapiAuthenticationError'); 74 | expect(error.body).toBe(body); 75 | expect(error.headers).toBe(headers); 76 | expect(error.statusCode).toBe(statusCode); 77 | expect(error.message).toBe(message); 78 | 79 | done(); 80 | }); 81 | 82 | test('WebapiPlayerError', done => { 83 | const body = { 84 | error : { 85 | message : 'Not allowed to shuffle', 86 | status : 403, 87 | reason : 'Not premium' 88 | } 89 | }, 90 | headers = { 91 | 'Content-Type' : 'application/json' 92 | }, 93 | statusCode = 403, 94 | message = 'An error occurred while communicating with Spotify\'s Web API.\nDetails: Not allowed to shuffle Not premium.'; 95 | 96 | let error = new WebapiPlayerError(body, headers, statusCode, message); 97 | 98 | expect(error.name).toBe('WebapiPlayerError'); 99 | expect(error.body).toBe(body); 100 | expect(error.headers).toBe(headers); 101 | expect(error.statusCode).toBe(statusCode); 102 | expect(error.message).toBe(message); 103 | 104 | done(); 105 | }); 106 | 107 | }); -------------------------------------------------------------------------------- /__tests__/webapi-request.js: -------------------------------------------------------------------------------- 1 | var WebApiRequest = require('../src/webapi-request'); 2 | 3 | describe('Create Web Api Requests', () => { 4 | test('Should use default settings if none are supplied', () => { 5 | var request = WebApiRequest.builder('token').build(); 6 | 7 | expect(request.getHost()).toBe('api.spotify.com'); 8 | expect(request.getPort()).toBe(443); 9 | expect(request.getScheme()).toBe('https'); 10 | expect(request.getHeaders().Authorization).toBeTruthy(); 11 | expect(request.getPath()).toBeFalsy(); 12 | expect(request.getQueryParameters()).toBeFalsy(); 13 | expect(request.getBodyParameters()).toBeFalsy(); 14 | }); 15 | 16 | test('Can overwrite one of the default parameters', () => { 17 | var request = WebApiRequest.builder('token') 18 | .withHost('such.host.wow') 19 | .build(); 20 | 21 | expect(request.getHost()).toBe('such.host.wow'); 22 | expect(request.getPort()).toBe(443); 23 | expect(request.getScheme()).toBe('https'); 24 | expect(request.getHeaders().Authorization).toBeTruthy(); 25 | expect(request.getPath()).toBeFalsy(); 26 | expect(request.getQueryParameters()).toBeFalsy(); 27 | expect(request.getBodyParameters()).toBeFalsy(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /examples/access-token-refresh.js: -------------------------------------------------------------------------------- 1 | const SpotifyWebApi = require('../'); 2 | 3 | /** 4 | * This example refreshes an access token. Refreshing access tokens is only possible access tokens received using the 5 | * Authorization Code flow, documented here: https://developer.spotify.com/spotify-web-api/authorization-guide/#authorization_code_flow 6 | */ 7 | 8 | /* Retrieve an authorization code as documented here: 9 | * https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow 10 | * or in the Authorization section of the README. 11 | * 12 | * Codes are given for a set of scopes. For this example, the scopes are user-read-private and user-read-email. 13 | * Scopes are documented here: 14 | * https://developer.spotify.com/documentation/general/guides/scopes/ 15 | */ 16 | const authorizationCode = 17 | ''; 18 | 19 | /** 20 | * Get the credentials from Spotify's Dashboard page. 21 | * https://developer.spotify.com/dashboard/applications 22 | */ 23 | const spotifyApi = new SpotifyWebApi({ 24 | clientId: '', 25 | clientSecret: '', 26 | redirectUri: '' 27 | }); 28 | 29 | // When our access token will expire 30 | let tokenExpirationEpoch; 31 | 32 | // First retrieve an access token 33 | spotifyApi.authorizationCodeGrant(authorizationCode).then( 34 | function(data) { 35 | // Set the access token and refresh token 36 | spotifyApi.setAccessToken(data.body['access_token']); 37 | spotifyApi.setRefreshToken(data.body['refresh_token']); 38 | 39 | // Save the amount of seconds until the access token expired 40 | tokenExpirationEpoch = 41 | new Date().getTime() / 1000 + data.body['expires_in']; 42 | console.log( 43 | 'Retrieved token. It expires in ' + 44 | Math.floor(tokenExpirationEpoch - new Date().getTime() / 1000) + 45 | ' seconds!' 46 | ); 47 | }, 48 | function(err) { 49 | console.log( 50 | 'Something went wrong when retrieving the access token!', 51 | err.message 52 | ); 53 | } 54 | ); 55 | 56 | // Continually print out the time left until the token expires.. 57 | let numberOfTimesUpdated = 0; 58 | 59 | setInterval(function() { 60 | console.log( 61 | 'Time left: ' + 62 | Math.floor(tokenExpirationEpoch - new Date().getTime() / 1000) + 63 | ' seconds left!' 64 | ); 65 | 66 | // OK, we need to refresh the token. Stop printing and refresh. 67 | if (++numberOfTimesUpdated > 5) { 68 | clearInterval(this); 69 | 70 | // Refresh token and print the new time to expiration. 71 | spotifyApi.refreshAccessToken().then( 72 | function(data) { 73 | tokenExpirationEpoch = 74 | new Date().getTime() / 1000 + data.body['expires_in']; 75 | console.log( 76 | 'Refreshed token. It now expires in ' + 77 | Math.floor(tokenExpirationEpoch - new Date().getTime() / 1000) + 78 | ' seconds!' 79 | ); 80 | }, 81 | function(err) { 82 | console.log('Could not refresh the token!', err.message); 83 | } 84 | ); 85 | } 86 | }, 1000); 87 | -------------------------------------------------------------------------------- /examples/access-token-using-client-credentials.js: -------------------------------------------------------------------------------- 1 | const SpotifyWebApi = require('../'); 2 | 3 | /** 4 | * This example retrieves an access token using the Client Credentials Flow, documented at: 5 | * https://developer.spotify.com/documentation/general/guides/authorization-guide/#client-credentials-flow 6 | */ 7 | 8 | /** 9 | * Get the credentials from Spotify's Dashboard page. 10 | * https://developer.spotify.com/dashboard/applications 11 | */ 12 | const spotifyApi = new SpotifyWebApi({ 13 | clientId: '', 14 | clientSecret: '' 15 | }); 16 | 17 | // Retrieve an access token 18 | spotifyApi.clientCredentialsGrant().then( 19 | function(data) { 20 | console.log('The access token expires in ' + data.body['expires_in']); 21 | console.log('The access token is ' + data.body['access_token']); 22 | 23 | // Save the access token so that it's used in future calls 24 | spotifyApi.setAccessToken(data.body['access_token']); 25 | }, 26 | function(err) { 27 | console.log( 28 | 'Something went wrong when retrieving an access token', 29 | err.message 30 | ); 31 | } 32 | ); 33 | -------------------------------------------------------------------------------- /examples/add-remove-replace-tracks-in-a-playlist.js: -------------------------------------------------------------------------------- 1 | const SpotifyWebApi = require('../'); 2 | 3 | /** 4 | * This example demonstrates adding tracks, removing tracks, and replacing tracks in a playlist. At the time of writing this 5 | * documentation, this is the available playlist track modification feature in the Spotify Web API. 6 | * 7 | * Since authorization is required, this example retrieves an access token using the Authorization Code Grant flow, 8 | * documented here: https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow 9 | * 10 | * Codes are given for a set of scopes. For this example, the scopes are playlist-modify-public. 11 | * Scopes are documented here: 12 | * https://developer.spotify.com/documentation/general/guides/scopes/ 13 | */ 14 | 15 | /* Obtain the `authorizationCode` below as described in the Authorization section of the README. 16 | */ 17 | const authorizationCode = 18 | ''; 19 | 20 | /** 21 | * Get the credentials from Spotify's Dashboard page. 22 | * https://developer.spotify.com/dashboard/applications 23 | */ 24 | const spotifyApi = new SpotifyWebApi({ 25 | clientId: '', 26 | clientSecret: '', 27 | redirectUri: '' 28 | }); 29 | 30 | let playlistId; 31 | 32 | // First retrieve an access token 33 | spotifyApi 34 | .authorizationCodeGrant(authorizationCode) 35 | .then(function(data) { 36 | // Save the access token so that it's used in future requests 37 | spotifyApi.setAccessToken(data['access_token']); 38 | 39 | // Create a playlist 40 | return spotifyApi.createPlaylist( 41 | 'thelinmichael', 42 | 'My New Awesome Playlist' 43 | ); 44 | }) 45 | .then(function(data) { 46 | console.log('Ok. Playlist created!'); 47 | playlistId = data.body['id']; 48 | 49 | // Add tracks to the playlist 50 | return spotifyApi.addTracksToPlaylist(playlistId, [ 51 | 'spotify:track:4iV5W9uYEdYUVa79Axb7Rh', 52 | 'spotify:track:6tcfwoGcDjxnSc6etAkDRR', 53 | 'spotify:track:4iV5W9uYEdYUVa79Axb7Rh' 54 | ]); 55 | }) 56 | .then(function(data) { 57 | console.log('Ok. Tracks added!'); 58 | 59 | // Woops! Made a duplicate. Remove one of the duplicates from the playlist 60 | return spotifyApi.removeTracksFromPlaylist('thelinmichael', playlistId, [ 61 | { 62 | uri: 'spotify:track:4iV5W9uYEdYUVa79Axb7Rh', 63 | positions: [0] 64 | } 65 | ]); 66 | }) 67 | .then(function(data) { 68 | console.log('Ok. Tracks removed!'); 69 | 70 | // Actually, lets just replace all tracks in the playlist with something completely different 71 | return spotifyApi.replaceTracksInPlaylist('thelinmichael', playlistId, [ 72 | 'spotify:track:5Wd2bfQ7wc6GgSa32OmQU3', 73 | 'spotify:track:4r8lRYnoOGdEi6YyI5OC1o', 74 | 'spotify:track:4TZZvblv2yzLIBk2JwJ6Un', 75 | 'spotify:track:2IA4WEsWAYpV9eKkwR2UYv', 76 | 'spotify:track:6hDH3YWFdcUNQjubYztIsG' 77 | ]); 78 | }) 79 | .then(function(data) { 80 | console.log('Ok. Tracks replaced!'); 81 | }) 82 | .catch(function(err) { 83 | console.log('Something went wrong:', err.message); 84 | }); 85 | -------------------------------------------------------------------------------- /examples/add-tracks-to-a-playlist.js: -------------------------------------------------------------------------------- 1 | const SpotifyWebApi = require('../'); 2 | 3 | /** 4 | * This example demonstrates adding tracks to a specified position in a playlist. 5 | * 6 | * Since authorization is required, this example retrieves an access token using the Authorization Code Grant flow, 7 | * documented here: https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow 8 | * 9 | * Codes are given for a set of scopes. For this example, the scopes are playlist-modify-public. 10 | * Scopes are documented here: 11 | * https://developer.spotify.com/documentation/general/guides/scopes/ 12 | */ 13 | 14 | /* Obtain the `authorizationCode` below as described in the Authorization section of the README. 15 | */ 16 | const authorizationCode = ''; 17 | 18 | /** 19 | * Get the credentials from Spotify's Dashboard page. 20 | * https://developer.spotify.com/dashboard/applications 21 | */ 22 | const spotifyApi = new SpotifyWebApi({ 23 | clientId: '', 24 | clientSecret: '', 25 | redirectUri: '' 26 | }); 27 | 28 | // First retrieve an access token 29 | spotifyApi 30 | .authorizationCodeGrant(authorizationCode) 31 | .then(function(data) { 32 | spotifyApi.setAccessToken(data.body['access_token']); 33 | return spotifyApi.addTracksToPlaylist( 34 | '5ieJqeLJjjI8iJWaxeBLuK', 35 | [ 36 | 'spotify:track:4iV5W9uYEdYUVa79Axb7Rh', 37 | 'spotify:track:1301WleyT98MSxVHPZCA6M' 38 | ], 39 | { 40 | position: 10 41 | } 42 | ); 43 | }) 44 | .then(function(data) { 45 | console.log('Added tracks to the playlist!'); 46 | }) 47 | .catch(function(err) { 48 | console.log('Something went wrong:', err.message); 49 | }); 50 | -------------------------------------------------------------------------------- /examples/client-credentials.js: -------------------------------------------------------------------------------- 1 | const { util } = require('prettier'); 2 | var SpotifyWebApi = require('../'); 3 | 4 | /** 5 | * This example uses the Client Credentials authorization flow. 6 | */ 7 | 8 | /** 9 | * Get the credentials from Spotify's Dashboard page. 10 | * https://developer.spotify.com/dashboard/applications 11 | */ 12 | const spotifyApi = new SpotifyWebApi({ 13 | clientId: '', 14 | clientSecret: '' 15 | }); 16 | 17 | // Retrieve an access token using your credentials 18 | spotifyApi.clientCredentialsGrant(). 19 | then(function(result) { 20 | console.log('It worked! Your access token is: ' + result.body.access_token); 21 | }).catch(function(err) { 22 | console.log('If this is printed, it probably means that you used invalid ' + 23 | 'clientId and clientSecret values. Please check!'); 24 | console.log('Hint: '); 25 | console.log(err); 26 | }); 27 | -------------------------------------------------------------------------------- /examples/get-info-about-current-user.js: -------------------------------------------------------------------------------- 1 | const SpotifyWebApi = require('../'); 2 | 3 | /** 4 | * This example retrieves information about the 'current' user. The current user is the user that has 5 | * authorized the application to access its data. 6 | */ 7 | 8 | /* Retrieve a code as documented here: 9 | * https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow 10 | * 11 | * Codes are given for a set of scopes. For this example, the scopes are user-read-private and user-read-email. 12 | * Scopes are documented here: 13 | * https://developer.spotify.com/documentation/general/guides/scopes/ 14 | */ 15 | const authorizationCode = 16 | ''; 17 | 18 | /* Get the credentials from Spotify's Dashboard page. 19 | * https://developer.spotify.com/dashboard/applications 20 | */ 21 | const spotifyApi = new SpotifyWebApi({ 22 | clientId: '', 23 | clientSecret: '', 24 | redirectUri: '' 25 | }); 26 | 27 | // First retrieve an access token 28 | spotifyApi 29 | .authorizationCodeGrant(authorizationCode) 30 | .then(function(data) { 31 | console.log('Retrieved access token', data.body['access_token']); 32 | 33 | // Set the access token 34 | spotifyApi.setAccessToken(data.body['access_token']); 35 | 36 | // Use the access token to retrieve information about the user connected to it 37 | return spotifyApi.getMe(); 38 | }) 39 | .then(function(data) { 40 | // "Retrieved data for Faruk Sahin" 41 | console.log('Retrieved data for ' + data.body['display_name']); 42 | 43 | // "Email is farukemresahin@gmail.com" 44 | console.log('Email is ' + data.body.email); 45 | 46 | // "Image URL is http://media.giphy.com/media/Aab07O5PYOmQ/giphy.gif" 47 | console.log('Image URL is ' + data.body.images[0].url); 48 | 49 | // "This user has a premium account" 50 | console.log('This user has a ' + data.body.product + ' account'); 51 | }) 52 | .catch(function(err) { 53 | console.log('Something went wrong:', err.message); 54 | }); 55 | -------------------------------------------------------------------------------- /examples/get-related-artists.js: -------------------------------------------------------------------------------- 1 | var SpotifyWebApi = require('../'); 2 | 3 | /* 4 | * This example shows how to get artists related to another artists. The endpoint is documented here: 5 | * https://developer.spotify.com/web-api/get-related-artists/ 6 | 7 | * Please note that authorization is now required and so this example retrieves an access token using the Authorization Code Flow, 8 | * documented here: https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow 9 | */ 10 | 11 | var authorizationCode = 12 | 'AQAgjS78s64u1axMCBCRA0cViW_ZDDU0pbgENJ_-WpZr3cEO7V5O-JELcEPU6pGLPp08SfO3dnHmu6XJikKqrU8LX9W6J11NyoaetrXtZFW-Y58UGeV69tuyybcNUS2u6eyup1EgzbTEx4LqrP_eCHsc9xHJ0JUzEhi7xcqzQG70roE4WKM_YrlDZO-e7GDRMqunS9RMoSwF_ov-gOMpvy9OMb7O58nZoc3LSEdEwoZPCLU4N4TTJ-IF6YsQRhQkEOJK'; 13 | 14 | /* Set the credentials given on Spotify's My Applications page. 15 | * https://developer.spotify.com/my-applications 16 | */ 17 | var spotifyApi = new SpotifyWebApi({ 18 | clientId: '', 19 | clientSecret: '', 20 | redirectUri: '' 21 | }); 22 | 23 | var artistId = '0qeei9KQnptjwb8MgkqEoy'; 24 | 25 | spotifyApi 26 | .authorizationCodeGrant(authorizationCode) 27 | .then(function(data) { 28 | console.log('Retrieved access token', data.body['access_token']); 29 | 30 | // Set the access token 31 | spotifyApi.setAccessToken(data.body['access_token']); 32 | 33 | // Use the access token to retrieve information about the user connected to it 34 | return spotifyApi.getArtistRelatedArtists(artistId); 35 | }) 36 | .then(function(data) { 37 | if (data.body.artists.length) { 38 | // Print the number of similar artists 39 | console.log('I got ' + data.body.artists.length + ' similar artists!'); 40 | 41 | console.log('The most similar one is ' + data.body.artists[0].name); 42 | } else { 43 | console.log("I didn't find any similar artists.. Sorry."); 44 | } 45 | }, 46 | function(err) { 47 | console.log('Something went wrong:', err.message); 48 | } 49 | ); 50 | -------------------------------------------------------------------------------- /examples/get-top-tracks-for-artist.js: -------------------------------------------------------------------------------- 1 | const SpotifyWebApi = require('../'); 2 | 3 | /** 4 | * This example retrieves the top tracks for an artist. 5 | * https://developer.spotify.com/documentation/web-api/reference/artists/get-artists-top-tracks/ 6 | */ 7 | 8 | /** 9 | * This endpoint doesn't require an access token, but it's beneficial to use one as it 10 | * gives the application a higher rate limit. 11 | * 12 | * Since it's not necessary to get an access token connected to a specific user, this example 13 | * uses the Client Credentials flow. This flow uses only the client ID and the client secret. 14 | * https://developer.spotify.com/documentation/general/guides/authorization-guide/#client-credentials-flow 15 | */ 16 | const spotifyApi = new SpotifyWebApi({ 17 | clientId: '', 18 | clientSecret: '' 19 | }); 20 | 21 | // Retrieve an access token 22 | spotifyApi 23 | .clientCredentialsGrant() 24 | .then(function(data) { 25 | // Set the access token on the API object so that it's used in all future requests 26 | spotifyApi.setAccessToken(data.body['access_token']); 27 | 28 | // Get the most popular tracks by David Bowie in Great Britain 29 | return spotifyApi.getArtistTopTracks('0oSGxfWSnnOXhD2fKuz2Gy', 'GB'); 30 | }) 31 | .then(function(data) { 32 | console.log('The most popular tracks for David Bowie is..'); 33 | console.log('Drum roll..'); 34 | console.log('...'); 35 | 36 | /* 37 | * 1. Space Oddity - 2009 Digital Remaster (popularity is 51) 38 | * 2. Heroes - 1999 Digital Remaster (popularity is 33) 39 | * 3. Let's Dance - 1999 Digital Remaster (popularity is 20) 40 | * 4. ... 41 | */ 42 | data.body.tracks.forEach(function(track, index) { 43 | console.log( 44 | index + 45 | 1 + 46 | '. ' + 47 | track.name + 48 | ' (popularity is ' + 49 | track.popularity + 50 | ')' 51 | ); 52 | }); 53 | }) 54 | .catch(function(err) { 55 | console.log('Unfortunately, something has gone wrong.', err.message); 56 | }); 57 | -------------------------------------------------------------------------------- /examples/search-for-tracks.js: -------------------------------------------------------------------------------- 1 | const SpotifyWebApi = require('../'); 2 | 3 | /* 4 | * This example shows how to search for a track. The endpoint is documented here: 5 | * https://developer.spotify.com/documentation/web-api/reference/search/ 6 | 7 | * Since authorization is now required, this example retrieves an access token using the Authorization Code Grant flow, 8 | * documented here: https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow 9 | * 10 | * Obtain the `authorizationCode` below as described in the Authorization section of the README. 11 | */ 12 | 13 | const authorizationCode = ''; 14 | 15 | /** 16 | * Get the credentials from Spotify's Dashboard page. 17 | * https://developer.spotify.com/dashboard/applications 18 | */ 19 | const spotifyApi = new SpotifyWebApi({ 20 | clientId: '', 21 | clientSecret: '', 22 | redirectUri: '' 23 | }); 24 | 25 | spotifyApi 26 | .authorizationCodeGrant(authorizationCode) 27 | .then(function(data) { 28 | console.log('Retrieved access token', data.body['access_token']); 29 | 30 | // Set the access token 31 | spotifyApi.setAccessToken(data.body['access_token']); 32 | 33 | // Use the access token to retrieve information about the user connected to it 34 | return spotifyApi.searchTracks('Love'); 35 | }) 36 | .then(function(data) { 37 | // Print some information about the results 38 | console.log('I got ' + data.body.tracks.total + ' results!'); 39 | 40 | // Go through the first page of results 41 | var firstPage = data.body.tracks.items; 42 | console.log('The tracks in the first page are (popularity in parentheses):'); 43 | 44 | /* 45 | * 0: All of Me (97) 46 | * 1: My Love (91) 47 | * 2: I Love This Life (78) 48 | * ... 49 | */ 50 | firstPage.forEach(function(track, index) { 51 | console.log(index + ': ' + track.name + ' (' + track.popularity + ')'); 52 | }); 53 | }).catch(function(err) { 54 | console.log('Something went wrong:', err.message); 55 | }); 56 | -------------------------------------------------------------------------------- /examples/tutorial/00-get-access-token.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This example is using the Authorization Code flow. 3 | * 4 | * In root directory run 5 | * 6 | * npm install express 7 | * 8 | * then run with the followinng command. If you don't have a client_id and client_secret yet, 9 | * create an application on Create an application here: https://developer.spotify.com/my-applications to get them. 10 | * Make sure you whitelist the correct redirectUri in line 26. 11 | * 12 | * node access-token-server.js "" "" 13 | * 14 | * and visit in your Browser. 15 | */ 16 | const SpotifyWebApi = require('../../'); 17 | const express = require('../../node_modules/express'); 18 | 19 | const scopes = [ 20 | 'ugc-image-upload', 21 | 'user-read-playback-state', 22 | 'user-modify-playback-state', 23 | 'user-read-currently-playing', 24 | 'streaming', 25 | 'app-remote-control', 26 | 'user-read-email', 27 | 'user-read-private', 28 | 'playlist-read-collaborative', 29 | 'playlist-modify-public', 30 | 'playlist-read-private', 31 | 'playlist-modify-private', 32 | 'user-library-modify', 33 | 'user-library-read', 34 | 'user-top-read', 35 | 'user-read-playback-position', 36 | 'user-read-recently-played', 37 | 'user-follow-read', 38 | 'user-follow-modify' 39 | ]; 40 | 41 | const spotifyApi = new SpotifyWebApi({ 42 | redirectUri: 'http://localhost:8888/callback', 43 | clientId: process.argv.slice(2)[0], 44 | clientSecret: process.argv.slice(2)[1] 45 | }); 46 | 47 | const app = express(); 48 | 49 | app.get('/login', (req, res) => { 50 | res.redirect(spotifyApi.createAuthorizeURL(scopes)); 51 | }); 52 | 53 | app.get('/callback', (req, res) => { 54 | const error = req.query.error; 55 | const code = req.query.code; 56 | const state = req.query.state; 57 | 58 | if (error) { 59 | console.error('Callback Error:', error); 60 | res.send(`Callback Error: ${error}`); 61 | return; 62 | } 63 | 64 | spotifyApi 65 | .authorizationCodeGrant(code) 66 | .then(data => { 67 | const access_token = data.body['access_token']; 68 | const refresh_token = data.body['refresh_token']; 69 | const expires_in = data.body['expires_in']; 70 | 71 | spotifyApi.setAccessToken(access_token); 72 | spotifyApi.setRefreshToken(refresh_token); 73 | 74 | console.log('access_token:', access_token); 75 | console.log('refresh_token:', refresh_token); 76 | 77 | console.log( 78 | `Sucessfully retreived access token. Expires in ${expires_in} s.` 79 | ); 80 | res.send('Success! You can now close the window.'); 81 | 82 | setInterval(async () => { 83 | const data = await spotifyApi.refreshAccessToken(); 84 | const access_token = data.body['access_token']; 85 | 86 | console.log('The access token has been refreshed!'); 87 | console.log('access_token:', access_token); 88 | spotifyApi.setAccessToken(access_token); 89 | }, expires_in / 2 * 1000); 90 | }) 91 | .catch(error => { 92 | console.error('Error getting Tokens:', error); 93 | res.send(`Error getting Tokens: ${error}`); 94 | }); 95 | }); 96 | 97 | app.listen(8888, () => 98 | console.log( 99 | 'HTTP Server up. Now go to http://localhost:8888/login in your browser.' 100 | ) 101 | ); 102 | -------------------------------------------------------------------------------- /examples/tutorial/01-basics/01-get-info-about-current-user.js: -------------------------------------------------------------------------------- 1 | const SpotifyWebApi = require('../../../'); 2 | 3 | const spotifyApi = new SpotifyWebApi(); 4 | spotifyApi.setAccessToken(process.env.SPOTIFY_ACCESS_TOKEN); 5 | 6 | (async () => { 7 | const me = await spotifyApi.getMe(); 8 | console.log(me); 9 | })().catch(e => { 10 | console.error(e); 11 | }); 12 | -------------------------------------------------------------------------------- /examples/tutorial/README.md: -------------------------------------------------------------------------------- 1 | Execute all commands from the root folder of this repository. 2 | 3 | Start with 4 | 5 | git clone 6 | cd spotify-web-api-node 7 | npm install 8 | npm install express 9 | node examples/tutorial/00-get-access-token.js "" "" 10 | 11 | and visit in your browser to get an `access_token`. 12 | If you don't have a `client_id` and `client_secret` yet, create an application here: to get them. Make sure you whitelist the correct redirectUri when creating your application, which is `http://localhost:8888/callback`. 13 | 14 | After you got the `access_token`, call all other examples with this token in ENV variable `SPOTIFY_ACCESS_TOKEN`. The easiest way is to call: 15 | 16 | export SPOTIFY_ACCESS_TOKEN="" 17 | node examples/tutorial/01-basics/01-get-info-about-current-user.js 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spotify-web-api-node", 3 | "version": "5.0.2", 4 | "homepage": "https://github.com/thelinmichael/spotify-web-api-node", 5 | "description": "A Node.js wrapper for Spotify's Web API", 6 | "main": "./src/server.js", 7 | "author": "Michael Thelin", 8 | "contributors": [ 9 | { 10 | "name": "José M. Perez", 11 | "url": "https://github.com/JMPerez" 12 | } 13 | ], 14 | "license": "MIT", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/thelinmichael/spotify-web-api-node.git" 18 | }, 19 | "scripts": { 20 | "test": "jest", 21 | "travis": "npm test -- --coverage && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", 22 | "precommit": "lint-staged" 23 | }, 24 | "jest": { 25 | "verbose": true, 26 | "testURL": "http://localhost/" 27 | }, 28 | "lint-staged": { 29 | "*.{js,json,css,md}": [ 30 | "prettier --single-quote --write", 31 | "git add" 32 | ] 33 | }, 34 | "dependencies": { 35 | "superagent": "^6.1.0" 36 | }, 37 | "devDependencies": { 38 | "coveralls": "^3.1.0", 39 | "husky": "^4.3.0", 40 | "jest": "^26.6.3", 41 | "lint-staged": "^10.4.0", 42 | "prettier": "^2.1.2", 43 | "sinon": "^9.0.3", 44 | "canvas": "^2.6.1", 45 | "bufferutil": "^4.0.1", 46 | "utf-8-validate": "^5.0.2", 47 | "jest-resolve": "^26.6.2", 48 | "minimist": "^1.2.5", 49 | "set-value": ">=2.0.1", 50 | "mixin-deep": ">=1.3.2", 51 | "ini": ">=1.3.6" 52 | }, 53 | "keywords": [ 54 | "spotify", 55 | "echonest", 56 | "music", 57 | "api", 58 | "wrapper", 59 | "client", 60 | "web api" 61 | ], 62 | "browser": { 63 | "./src/server.js": "./src/client.js" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/authentication-request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Request = require('./base-request'); 4 | 5 | var DEFAULT_HOST = 'accounts.spotify.com', 6 | DEFAULT_PORT = 443, 7 | DEFAULT_SCHEME = 'https'; 8 | 9 | module.exports.builder = function() { 10 | return Request.builder() 11 | .withHost(DEFAULT_HOST) 12 | .withPort(DEFAULT_PORT) 13 | .withScheme(DEFAULT_SCHEME); 14 | }; 15 | -------------------------------------------------------------------------------- /src/base-request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Request = function(builder) { 4 | if (!builder) { 5 | throw new Error('No builder supplied to constructor'); 6 | } 7 | 8 | this.host = builder.host; 9 | this.port = builder.port; 10 | this.scheme = builder.scheme; 11 | this.queryParameters = builder.queryParameters; 12 | this.bodyParameters = builder.bodyParameters; 13 | this.headers = builder.headers; 14 | this.path = builder.path; 15 | }; 16 | 17 | Request.prototype._getter = function(key) { 18 | return function() { 19 | return this[key]; 20 | }; 21 | }; 22 | 23 | Request.prototype.getHost = Request.prototype._getter('host'); 24 | 25 | Request.prototype.getPort = Request.prototype._getter('port'); 26 | 27 | Request.prototype.getScheme = Request.prototype._getter('scheme'); 28 | 29 | Request.prototype.getPath = Request.prototype._getter('path'); 30 | 31 | Request.prototype.getQueryParameters = Request.prototype._getter( 32 | 'queryParameters' 33 | ); 34 | 35 | Request.prototype.getBodyParameters = Request.prototype._getter( 36 | 'bodyParameters' 37 | ); 38 | 39 | Request.prototype.getHeaders = Request.prototype._getter('headers'); 40 | 41 | Request.prototype.getURI = function() { 42 | if (!this.scheme || !this.host || !this.port) { 43 | throw new Error('Missing components necessary to construct URI'); 44 | } 45 | var uri = this.scheme + '://' + this.host; 46 | if ( 47 | (this.scheme === 'http' && this.port !== 80) || 48 | (this.scheme === 'https' && this.port !== 443) 49 | ) { 50 | uri += ':' + this.port; 51 | } 52 | if (this.path) { 53 | uri += this.path; 54 | } 55 | return uri; 56 | }; 57 | 58 | Request.prototype.getURL = function() { 59 | var uri = this.getURI(); 60 | if (this.getQueryParameters()) { 61 | return uri + this.getQueryParameterString(this.getQueryParameters()); 62 | } else { 63 | return uri; 64 | } 65 | }; 66 | 67 | Request.prototype.getQueryParameterString = function() { 68 | var queryParameters = this.getQueryParameters(); 69 | if (queryParameters) { 70 | return ( 71 | '?' + 72 | Object.keys(queryParameters) 73 | .filter(function(key) { 74 | return queryParameters[key] !== undefined; 75 | }) 76 | .map(function(key) { 77 | return key + '=' + queryParameters[key]; 78 | }) 79 | .join('&') 80 | ); 81 | } 82 | }; 83 | 84 | Request.prototype.execute = function(method, callback) { 85 | if (callback) { 86 | method(this, callback); 87 | return; 88 | } 89 | var _self = this; 90 | 91 | return new Promise(function(resolve, reject) { 92 | method(_self, function(error, result) { 93 | if (error) { 94 | reject(error); 95 | } else { 96 | resolve(result); 97 | } 98 | }); 99 | }); 100 | }; 101 | 102 | var Builder = function() {}; 103 | 104 | Builder.prototype._setter = function(key) { 105 | return function(value) { 106 | this[key] = value; 107 | return this; 108 | }; 109 | }; 110 | 111 | Builder.prototype.withHost = Builder.prototype._setter('host'); 112 | 113 | Builder.prototype.withPort = Builder.prototype._setter('port'); 114 | 115 | Builder.prototype.withScheme = Builder.prototype._setter('scheme'); 116 | 117 | Builder.prototype.withPath = Builder.prototype._setter('path'); 118 | 119 | Builder.prototype._assigner = function(key) { 120 | return function() { 121 | for (var i = 0; i < arguments.length; i++) { 122 | this[key] = this._assign(this[key], arguments[i]); 123 | } 124 | 125 | return this; 126 | }; 127 | }; 128 | 129 | Builder.prototype.withQueryParameters = Builder.prototype._assigner( 130 | 'queryParameters' 131 | ); 132 | 133 | Builder.prototype.withBodyParameters = Builder.prototype._assigner( 134 | 'bodyParameters' 135 | ); 136 | 137 | Builder.prototype.withHeaders = Builder.prototype._assigner('headers'); 138 | 139 | Builder.prototype.withAuth = function(accessToken) { 140 | if (accessToken) { 141 | this.withHeaders({ Authorization: 'Bearer ' + accessToken }); 142 | } 143 | return this; 144 | }; 145 | 146 | Builder.prototype._assign = function(src, obj) { 147 | if (obj && Array.isArray(obj)) { 148 | return obj; 149 | } 150 | if (obj && typeof obj === 'string') { 151 | return obj; 152 | } 153 | if (obj && Object.keys(obj).length > 0) { 154 | return Object.assign(src || {}, obj); 155 | } 156 | return src; 157 | }; 158 | 159 | Builder.prototype.build = function() { 160 | return new Request(this); 161 | }; 162 | 163 | module.exports.builder = function() { 164 | return new Builder(); 165 | }; 166 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./spotify-web-api'); 2 | -------------------------------------------------------------------------------- /src/http-manager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var superagent = require('superagent'), 4 | { TimeoutError, 5 | WebapiError, 6 | WebapiRegularError, 7 | WebapiAuthenticationError, 8 | WebapiPlayerError 9 | } = require('./response-error'); 10 | 11 | var HttpManager = {}; 12 | 13 | /* Create superagent options from the base request */ 14 | var _getParametersFromRequest = function(request) { 15 | var options = {}; 16 | 17 | if (request.getQueryParameters()) { 18 | options.query = request.getQueryParameters(); 19 | } 20 | 21 | if (request.getHeaders() && request.getHeaders()['Content-Type'] === 'application/json') { 22 | options.data = JSON.stringify(request.getBodyParameters()); 23 | } else if (request.getBodyParameters()) { 24 | options.data = request.getBodyParameters(); 25 | } 26 | 27 | if (request.getHeaders()) { 28 | options.headers = request.getHeaders(); 29 | } 30 | return options; 31 | }; 32 | 33 | var _toError = function(response) { 34 | if (typeof response.body === 'object' && response.body.error && typeof response.body.error === 'object' && response.body.error.reason) { 35 | return new WebapiPlayerError(response.body, response.headers, response.statusCode); 36 | } 37 | 38 | if (typeof response.body === 'object' && response.body.error && typeof response.body.error === 'object') { 39 | return new WebapiRegularError(response.body, response.headers, response.statusCode); 40 | } 41 | 42 | if (typeof response.body === 'object' && response.body.error && typeof response.body.error === 'string') { 43 | return new WebapiAuthenticationError(response.body, response.headers, response.statusCode); 44 | } 45 | 46 | /* Other type of error, or unhandled Web API error format */ 47 | return new WebapiError(response.body, response.headers, response.statusCode, response.body); 48 | }; 49 | 50 | /* Make the request to the Web API */ 51 | HttpManager._makeRequest = function(method, options, uri, callback) { 52 | var req = method.bind(superagent)(uri); 53 | 54 | if (options.query) { 55 | req.query(options.query); 56 | } 57 | 58 | if (options.headers) { 59 | req.set(options.headers); 60 | } 61 | 62 | if (options.data) { 63 | req.send(options.data); 64 | } 65 | 66 | req.end(function(err, response) { 67 | if (err) { 68 | if (err.timeout) { 69 | return callback(new TimeoutError()); 70 | } else if (err.response) { 71 | return callback(_toError(err.response)); 72 | } else { 73 | return callback(err); 74 | } 75 | } 76 | 77 | return callback(null, { 78 | body: response.body, 79 | headers: response.headers, 80 | statusCode: response.statusCode 81 | }); 82 | }); 83 | }; 84 | 85 | /** 86 | * Make a HTTP GET request. 87 | * @param {BaseRequest} The request. 88 | * @param {Function} The callback function. 89 | */ 90 | HttpManager.get = function(request, callback) { 91 | var options = _getParametersFromRequest(request); 92 | var method = superagent.get; 93 | 94 | HttpManager._makeRequest(method, options, request.getURI(), callback); 95 | }; 96 | 97 | /** 98 | * Make a HTTP POST request. 99 | * @param {BaseRequest} The request. 100 | * @param {Function} The callback function. 101 | */ 102 | HttpManager.post = function(request, callback) { 103 | var options = _getParametersFromRequest(request); 104 | var method = superagent.post; 105 | 106 | HttpManager._makeRequest(method, options, request.getURI(), callback); 107 | }; 108 | 109 | /** 110 | * Make a HTTP DELETE request. 111 | * @param {BaseRequest} The request. 112 | * @param {Function} The callback function. 113 | */ 114 | HttpManager.del = function(request, callback) { 115 | var options = _getParametersFromRequest(request); 116 | var method = superagent.del; 117 | 118 | HttpManager._makeRequest(method, options, request.getURI(), callback); 119 | }; 120 | 121 | /** 122 | * Make a HTTP PUT request. 123 | * @param {BaseRequest} The request. 124 | * @param {Function} The callback function. 125 | */ 126 | HttpManager.put = function(request, callback) { 127 | var options = _getParametersFromRequest(request); 128 | var method = superagent.put; 129 | 130 | HttpManager._makeRequest(method, options, request.getURI(), callback); 131 | }; 132 | 133 | module.exports = HttpManager; -------------------------------------------------------------------------------- /src/response-error.js: -------------------------------------------------------------------------------- 1 | /* Timeout */ 2 | class NamedError extends Error { 3 | get name() { 4 | return this.constructor.name; 5 | } 6 | } 7 | 8 | class TimeoutError extends NamedError { 9 | constructor() { 10 | const message = 'A timeout occurred while communicating with Spotify\'s Web API.'; 11 | super(message); 12 | } 13 | 14 | } 15 | 16 | /* Web API Parent and fallback error */ 17 | class WebapiError extends NamedError { 18 | constructor(body, headers, statusCode, message) { 19 | super(message); 20 | this.body = body; 21 | this.headers = headers; 22 | this.statusCode = statusCode; 23 | } 24 | 25 | } 26 | 27 | /** 28 | * Regular Error 29 | * { status : , message : } 30 | */ 31 | class WebapiRegularError extends WebapiError { 32 | constructor(body, headers, statusCode) { 33 | const message = 'An error occurred while communicating with Spotify\'s Web API.\n' + 34 | 'Details: ' + body.error.message + '.'; 35 | 36 | super(body, headers, statusCode, message); 37 | } 38 | } 39 | 40 | /** 41 | * Authentication Error 42 | * { error : , error_description : } 43 | */ 44 | class WebapiAuthenticationError extends WebapiError { 45 | constructor(body, headers, statusCode) { 46 | const message = 'An authentication error occurred while communicating with Spotify\'s Web API.\n' + 47 | 'Details: ' + body.error + (body.error_description ? ' ' + body.error_description + '.' : '.'); 48 | 49 | super(body, headers, statusCode, message); 50 | } 51 | } 52 | 53 | /** 54 | * Player Error 55 | * { status : , message : , reason : } 56 | */ 57 | class WebapiPlayerError extends WebapiError { 58 | constructor(body, headers, statusCode) { 59 | const message = 'An error occurred while communicating with Spotify\'s Web API.\n' + 60 | 'Details: ' + body.error.message + (body.error.reason ? ' ' + body.error.reason + '.' : '.'); 61 | 62 | super(body, headers, statusCode, message); 63 | } 64 | } 65 | 66 | module.exports = { WebapiError, TimeoutError, WebapiRegularError, WebapiAuthenticationError, WebapiPlayerError }; -------------------------------------------------------------------------------- /src/server-methods.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var AuthenticationRequest = require('./authentication-request'); 4 | var HttpManager = require('./http-manager'); 5 | 6 | module.exports = { 7 | 8 | /** 9 | * Retrieve a URL where the user can give the application permissions. 10 | * @param {string[]} scopes The scopes corresponding to the permissions the application needs. 11 | * @param {string} state A parameter that you can use to maintain a value between the request and the callback to redirect_uri.It is useful to prevent CSRF exploits. 12 | * @param {boolean} showDialog A parameter that you can use to force the user to approve the app on each login rather than being automatically redirected. 13 | * @param {string} responseType An optional parameter that you can use to specify the code response based on the authentication type - can be set to 'code' or 'token'. Default 'code' to ensure backwards compatability. 14 | * @returns {string} The URL where the user can give application permissions. 15 | */ 16 | createAuthorizeURL: function(scopes, state, showDialog, responseType = 'code') { 17 | return AuthenticationRequest.builder() 18 | .withPath('/authorize') 19 | .withQueryParameters({ 20 | client_id: this.getClientId(), 21 | response_type: responseType, 22 | redirect_uri: this.getRedirectURI(), 23 | scope: scopes.join('%20'), 24 | state: state, 25 | show_dialog: showDialog && !!showDialog 26 | }) 27 | .build() 28 | .getURL(); 29 | }, 30 | 31 | /** 32 | * Request an access token using the Client Credentials flow. 33 | * Requires that client ID and client secret has been set previous to the call. 34 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 35 | * @returns {Promise|undefined} A promise that if successful, resolves into an object containing the access token, 36 | * token type and time to expiration. If rejected, it contains an error object. Not returned if a callback is given. 37 | */ 38 | clientCredentialsGrant: function(callback) { 39 | return AuthenticationRequest.builder() 40 | .withPath('/api/token') 41 | .withBodyParameters({ 42 | grant_type: 'client_credentials' 43 | }) 44 | .withHeaders({ 45 | Authorization: 46 | 'Basic ' + 47 | new Buffer( 48 | this.getClientId() + ':' + this.getClientSecret() 49 | ).toString('base64'), 50 | 'Content-Type' : 'application/x-www-form-urlencoded' 51 | }) 52 | .build() 53 | .execute(HttpManager.post, callback); 54 | }, 55 | 56 | /** 57 | * Request an access token using the Authorization Code flow. 58 | * Requires that client ID, client secret, and redirect URI has been set previous to the call. 59 | * @param {string} code The authorization code returned in the callback in the Authorization Code flow. 60 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 61 | * @returns {Promise|undefined} A promise that if successful, resolves into an object containing the access token, 62 | * refresh token, token type and time to expiration. If rejected, it contains an error object. 63 | * Not returned if a callback is given. 64 | */ 65 | authorizationCodeGrant: function(code, callback) { 66 | return AuthenticationRequest.builder() 67 | .withPath('/api/token') 68 | .withBodyParameters({ 69 | grant_type: 'authorization_code', 70 | redirect_uri: this.getRedirectURI(), 71 | code: code, 72 | client_id: this.getClientId(), 73 | client_secret: this.getClientSecret() 74 | }) 75 | .withHeaders({ 'Content-Type' : 'application/x-www-form-urlencoded' }) 76 | .build() 77 | .execute(HttpManager.post, callback); 78 | }, 79 | 80 | /** 81 | * Refresh the access token given that it hasn't expired. 82 | * Requires that client ID, client secret and refresh token has been set previous to the call. 83 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 84 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing the 85 | * access token, time to expiration and token type. If rejected, it contains an error object. 86 | * Not returned if a callback is given. 87 | */ 88 | refreshAccessToken: function(callback) { 89 | return AuthenticationRequest.builder() 90 | .withPath('/api/token') 91 | .withBodyParameters({ 92 | grant_type: 'refresh_token', 93 | refresh_token: this.getRefreshToken() 94 | }) 95 | .withHeaders({ 96 | Authorization: 97 | 'Basic ' + 98 | new Buffer( 99 | this.getClientId() + ':' + this.getClientSecret() 100 | ).toString('base64'), 101 | 'Content-Type' : 'application/x-www-form-urlencoded' 102 | }) 103 | .build() 104 | .execute(HttpManager.post, callback); 105 | } 106 | }; 107 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | var SpotifyWebApi = require('./spotify-web-api'); 2 | var ServerMethods = require('./server-methods'); 3 | SpotifyWebApi._addMethods(ServerMethods); 4 | module.exports = SpotifyWebApi; 5 | -------------------------------------------------------------------------------- /src/spotify-web-api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var AuthenticationRequest = require('./authentication-request'), 4 | WebApiRequest = require('./webapi-request'), 5 | HttpManager = require('./http-manager'); 6 | 7 | function SpotifyWebApi(credentials) { 8 | this._credentials = credentials || {}; 9 | } 10 | 11 | SpotifyWebApi.prototype = { 12 | setCredentials: function(credentials) { 13 | for (var key in credentials) { 14 | if (credentials.hasOwnProperty(key)) { 15 | this._credentials[key] = credentials[key]; 16 | } 17 | } 18 | }, 19 | 20 | getCredentials: function() { 21 | return this._credentials; 22 | }, 23 | 24 | resetCredentials: function() { 25 | this._credentials = null; 26 | }, 27 | 28 | setClientId: function(clientId) { 29 | this._setCredential('clientId', clientId); 30 | }, 31 | 32 | setClientSecret: function(clientSecret) { 33 | this._setCredential('clientSecret', clientSecret); 34 | }, 35 | 36 | setAccessToken: function(accessToken) { 37 | this._setCredential('accessToken', accessToken); 38 | }, 39 | 40 | setRefreshToken: function(refreshToken) { 41 | this._setCredential('refreshToken', refreshToken); 42 | }, 43 | 44 | setRedirectURI: function(redirectUri) { 45 | this._setCredential('redirectUri', redirectUri); 46 | }, 47 | 48 | getRedirectURI: function() { 49 | return this._getCredential('redirectUri'); 50 | }, 51 | 52 | getClientId: function() { 53 | return this._getCredential('clientId'); 54 | }, 55 | 56 | getClientSecret: function() { 57 | return this._getCredential('clientSecret'); 58 | }, 59 | 60 | getAccessToken: function() { 61 | return this._getCredential('accessToken'); 62 | }, 63 | 64 | getRefreshToken: function() { 65 | return this._getCredential('refreshToken'); 66 | }, 67 | 68 | resetClientId: function() { 69 | this._resetCredential('clientId'); 70 | }, 71 | 72 | resetClientSecret: function() { 73 | this._resetCredential('clientSecret'); 74 | }, 75 | 76 | resetAccessToken: function() { 77 | this._resetCredential('accessToken'); 78 | }, 79 | 80 | resetRefreshToken: function() { 81 | this._resetCredential('refreshToken'); 82 | }, 83 | 84 | resetRedirectURI: function() { 85 | this._resetCredential('redirectUri'); 86 | }, 87 | 88 | _setCredential: function(credentialKey, value) { 89 | this._credentials = this._credentials || {}; 90 | this._credentials[credentialKey] = value; 91 | }, 92 | 93 | _getCredential: function(credentialKey) { 94 | if (!this._credentials) { 95 | return; 96 | } else { 97 | return this._credentials[credentialKey]; 98 | } 99 | }, 100 | 101 | _resetCredential: function(credentialKey) { 102 | if (!this._credentials) { 103 | return; 104 | } else { 105 | this._credentials[credentialKey] = null; 106 | } 107 | }, 108 | 109 | /** 110 | * Look up a track. 111 | * @param {string} trackId The track's ID. 112 | * @param {Object} [options] The possible options, currently only market. 113 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 114 | * @example getTrack('3Qm86XLflmIXVm1wcwkgDK').then(...) 115 | * @returns {Promise|undefined} A promise that if successful, returns an object containing information 116 | * about the track. Not returned if a callback is given. 117 | */ 118 | getTrack: function(trackId, options, callback) { 119 | return WebApiRequest.builder(this.getAccessToken()) 120 | .withPath('/v1/tracks/' + trackId) 121 | .withQueryParameters(options) 122 | .build() 123 | .execute(HttpManager.get, callback); 124 | }, 125 | 126 | /** 127 | * Look up several tracks. 128 | * @param {string[]} trackIds The IDs of the artists. 129 | * @param {Object} [options] The possible options, currently only market. 130 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 131 | * @example getArtists(['0oSGxfWSnnOXhD2fKuz2Gy', '3dBVyJ7JuOMt4GE9607Qin']).then(...) 132 | * @returns {Promise|undefined} A promise that if successful, returns an object containing information 133 | * about the artists. Not returned if a callback is given. 134 | */ 135 | getTracks: function(trackIds, options, callback) { 136 | return WebApiRequest.builder(this.getAccessToken()) 137 | .withPath('/v1/tracks') 138 | .withQueryParameters( 139 | { 140 | ids: trackIds.join(',') 141 | }, 142 | options 143 | ) 144 | .build() 145 | .execute(HttpManager.get, callback); 146 | }, 147 | 148 | /** 149 | * Look up an album. 150 | * @param {string} albumId The album's ID. 151 | * @param {Object} [options] The possible options, currently only market. 152 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 153 | * @example getAlbum('0sNOF9WDwhWunNAHPD3Baj').then(...) 154 | * @returns {Promise|undefined} A promise that if successful, returns an object containing information 155 | * about the album. Not returned if a callback is given. 156 | */ 157 | getAlbum: function(albumId, options, callback) { 158 | return WebApiRequest.builder(this.getAccessToken()) 159 | .withPath('/v1/albums/' + albumId) 160 | .withQueryParameters(options) 161 | .build() 162 | .execute(HttpManager.get, callback); 163 | }, 164 | 165 | /** 166 | * Look up several albums. 167 | * @param {string[]} albumIds The IDs of the albums. 168 | * @param {Object} [options] The possible options, currently only market. 169 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 170 | * @example getAlbums(['0oSGxfWSnnOXhD2fKuz2Gy', '3dBVyJ7JuOMt4GE9607Qin']).then(...) 171 | * @returns {Promise|undefined} A promise that if successful, returns an object containing information 172 | * about the albums. Not returned if a callback is given. 173 | */ 174 | getAlbums: function(albumIds, options, callback) { 175 | return WebApiRequest.builder(this.getAccessToken()) 176 | .withPath('/v1/albums') 177 | .withQueryParameters( 178 | { 179 | ids: albumIds.join(',') 180 | }, 181 | options 182 | ) 183 | .build() 184 | .execute(HttpManager.get, callback); 185 | }, 186 | 187 | /** 188 | * Look up an artist. 189 | * @param {string} artistId The artist's ID. 190 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 191 | * @example api.getArtist('1u7kkVrr14iBvrpYnZILJR').then(...) 192 | * @returns {Promise|undefined} A promise that if successful, returns an object containing information 193 | * about the artist. Not returned if a callback is given. 194 | */ 195 | getArtist: function(artistId, callback) { 196 | return WebApiRequest.builder(this.getAccessToken()) 197 | .withPath('/v1/artists/' + artistId) 198 | .build() 199 | .execute(HttpManager.get, callback); 200 | }, 201 | 202 | /** 203 | * Look up several artists. 204 | * @param {string[]} artistIds The IDs of the artists. 205 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 206 | * @example getArtists(['0oSGxfWSnnOXhD2fKuz2Gy', '3dBVyJ7JuOMt4GE9607Qin']).then(...) 207 | * @returns {Promise|undefined} A promise that if successful, returns an object containing information 208 | * about the artists. Not returned if a callback is given. 209 | */ 210 | getArtists: function(artistIds, callback) { 211 | return WebApiRequest.builder(this.getAccessToken()) 212 | .withPath('/v1/artists') 213 | .withQueryParameters({ 214 | ids: artistIds.join(',') 215 | }) 216 | .build() 217 | .execute(HttpManager.get, callback); 218 | }, 219 | 220 | /** 221 | * Search for music entities of certain types. 222 | * @param {string} query The search query. 223 | * @param {string[]} types An array of item types to search across. 224 | * Valid types are: 'album', 'artist', 'playlist', 'track', 'show', and 'episode'. 225 | * @param {Object} [options] The possible options, e.g. limit, offset. 226 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 227 | * @example search('Abba', ['track', 'playlist'], { limit : 5, offset : 1 }).then(...) 228 | * @returns {Promise|undefined} A promise that if successful, returns an object containing the 229 | * search results. The result is paginated. If the promise is rejected, 230 | * it contains an error object. Not returned if a callback is given. 231 | */ 232 | search: function(query, types, options, callback) { 233 | return WebApiRequest.builder(this.getAccessToken()) 234 | .withPath('/v1/search/') 235 | .withQueryParameters( 236 | { 237 | type: types.join(','), 238 | q: query 239 | }, 240 | options 241 | ) 242 | .build() 243 | .execute(HttpManager.get, callback); 244 | }, 245 | 246 | /** 247 | * Search for an album. 248 | * @param {string} query The search query. 249 | * @param {Object} [options] The possible options, e.g. limit, offset. 250 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 251 | * @example searchAlbums('Space Oddity', { limit : 5, offset : 1 }).then(...) 252 | * @returns {Promise|undefined} A promise that if successful, returns an object containing the 253 | * search results. The result is paginated. If the promise is rejected, 254 | * it contains an error object. Not returned if a callback is given. 255 | */ 256 | searchAlbums: function(query, options, callback) { 257 | return this.search(query, ['album'], options, callback); 258 | }, 259 | 260 | /** 261 | * Search for an artist. 262 | * @param {string} query The search query. 263 | * @param {Object} [options] The possible options, e.g. limit, offset. 264 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 265 | * @example searchArtists('David Bowie', { limit : 5, offset : 1 }).then(...) 266 | * @returns {Promise|undefined} A promise that if successful, returns an object containing the 267 | * search results. The result is paginated. If the promise is rejected, 268 | * it contains an error object. Not returned if a callback is given. 269 | */ 270 | searchArtists: function(query, options, callback) { 271 | return this.search(query, ['artist'], options, callback); 272 | }, 273 | 274 | /** 275 | * Search for a track. 276 | * @param {string} query The search query. 277 | * @param {Object} [options] The possible options, e.g. limit, offset. 278 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 279 | * @example searchTracks('Mr. Brightside', { limit : 3, offset : 2 }).then(...) 280 | * @returns {Promise|undefined} A promise that if successful, returns an object containing the 281 | * search results. The result is paginated. If the promise is rejected, 282 | * it contains an error object. Not returned if a callback is given. 283 | */ 284 | searchTracks: function(query, options, callback) { 285 | return this.search(query, ['track'], options, callback); 286 | }, 287 | 288 | /** 289 | * Search for playlists. 290 | * @param {string} query The search query. 291 | * @param {Object} options The possible options. 292 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 293 | * @example searchPlaylists('workout', { limit : 1, offset : 0 }).then(...) 294 | * @returns {Promise|undefined} A promise that if successful, returns an object containing the 295 | * search results. The result is paginated. If the promise is rejected, 296 | * it contains an error object. Not returned if a callback is given. 297 | */ 298 | searchPlaylists: function(query, options, callback) { 299 | return this.search(query, ['playlist'], options, callback); 300 | }, 301 | 302 | /** 303 | * Get an artist's albums. 304 | * @param {string} artistId The artist's ID. 305 | * @options {Object} [options] The possible options, e.g. limit, offset. 306 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 307 | * @example getArtistAlbums('0oSGxfWSnnOXhD2fKuz2Gy', { album_type : 'album', country : 'GB', limit : 2, offset : 5 }).then(...) 308 | * @returns {Promise|undefined} A promise that if successful, returns an object containing the albums 309 | * for the given artist. The result is paginated. If the promise is rejected, 310 | * it contains an error object. Not returned if a callback is given. 311 | */ 312 | getArtistAlbums: function(artistId, options, callback) { 313 | return WebApiRequest.builder(this.getAccessToken()) 314 | .withPath('/v1/artists/' + artistId + '/albums') 315 | .withQueryParameters(options) 316 | .build() 317 | .execute(HttpManager.get, callback); 318 | }, 319 | 320 | /** 321 | * Get the tracks of an album. 322 | * @param albumId the album's ID. 323 | * @options {Object} [options] The possible options, e.g. limit. 324 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 325 | * @example getAlbumTracks('41MnTivkwTO3UUJ8DrqEJJ', { limit : 5, offset : 1 }).then(...) 326 | * @returns {Promise|undefined} A promise that if successful, returns an object containing the 327 | * tracks in the album. The result is paginated. If the promise is rejected. 328 | * it contains an error object. Not returned if a callback is given. 329 | */ 330 | getAlbumTracks: function(albumId, options, callback) { 331 | return WebApiRequest.builder(this.getAccessToken()) 332 | .withPath('/v1/albums/' + albumId + '/tracks') 333 | .withQueryParameters(options) 334 | .build() 335 | .execute(HttpManager.get, callback); 336 | }, 337 | 338 | /** 339 | * Get an artist's top tracks. 340 | * @param {string} artistId The artist's ID. 341 | * @param {string} country The country/territory where the tracks are most popular. (format: ISO 3166-1 alpha-2) 342 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 343 | * @example getArtistTopTracks('0oSGxfWSnnOXhD2fKuz2Gy', 'GB').then(...) 344 | * @returns {Promise|undefined} A promise that if successful, returns an object containing the 345 | * artist's top tracks in the given country. If the promise is rejected, 346 | * it contains an error object. Not returned if a callback is given. 347 | */ 348 | getArtistTopTracks: function(artistId, country, callback) { 349 | return WebApiRequest.builder(this.getAccessToken()) 350 | .withPath('/v1/artists/' + artistId + '/top-tracks') 351 | .withQueryParameters({ 352 | country: country 353 | }) 354 | .build() 355 | .execute(HttpManager.get, callback); 356 | }, 357 | 358 | /** 359 | * Get related artists. 360 | * @param {string} artistId The artist's ID. 361 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 362 | * @example getArtistRelatedArtists('0oSGxfWSnnOXhD2fKuz2Gy').then(...) 363 | * @returns {Promise|undefined} A promise that if successful, returns an object containing the 364 | * related artists. If the promise is rejected, it contains an error object. Not returned if a callback is given. 365 | */ 366 | getArtistRelatedArtists: function(artistId, callback) { 367 | return WebApiRequest.builder(this.getAccessToken()) 368 | .withPath('/v1/artists/' + artistId + '/related-artists') 369 | .build() 370 | .execute(HttpManager.get, callback); 371 | }, 372 | 373 | /** 374 | * Get information about a user. 375 | * @param userId The user ID. 376 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 377 | * @example getUser('thelinmichael').then(...) 378 | * @returns {Promise|undefined} A promise that if successful, resolves to an object 379 | * containing information about the user. If the promise is 380 | * rejected, it contains an error object. Not returned if a callback is given. 381 | */ 382 | getUser: function(userId, callback) { 383 | return WebApiRequest.builder(this.getAccessToken()) 384 | .withPath('/v1/users/' + encodeURIComponent(userId)) 385 | .build() 386 | .execute(HttpManager.get, callback); 387 | }, 388 | 389 | /** 390 | * Get information about the user that has signed in (the current user). 391 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 392 | * @example getMe().then(...) 393 | * @returns {Promise|undefined} A promise that if successful, resolves to an object 394 | * containing information about the user. The amount of information 395 | * depends on the permissions given by the user. If the promise is 396 | * rejected, it contains an error object. Not returned if a callback is given. 397 | */ 398 | getMe: function(callback) { 399 | return WebApiRequest.builder(this.getAccessToken()) 400 | .withPath('/v1/me') 401 | .build() 402 | .execute(HttpManager.get, callback); 403 | }, 404 | 405 | /** 406 | * Get a user's playlists. 407 | * @param {string} userId An optional id of the user. If you know the Spotify URI it is easy 408 | * to find the id (e.g. spotify:user:). If not provided, the id of the user that granted 409 | * the permissions will be used. 410 | * @param {Object} [options] The options supplied to this request. 411 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 412 | * @example getUserPlaylists('thelinmichael').then(...) 413 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing 414 | * a list of playlists. If rejected, it contains an error object. Not returned if a callback is given. 415 | */ 416 | getUserPlaylists: function(userId, options, callback) { 417 | var path; 418 | if (typeof userId === 'string') { 419 | path = '/v1/users/' + encodeURIComponent(userId) + '/playlists'; 420 | } else if (typeof userId === 'object') { 421 | callback = options; 422 | options = userId; 423 | path = '/v1/me/playlists'; 424 | } /* undefined */ else { 425 | path = '/v1/me/playlists'; 426 | } 427 | 428 | return WebApiRequest.builder(this.getAccessToken()) 429 | .withPath(path) 430 | .withQueryParameters(options) 431 | .build() 432 | .execute(HttpManager.get, callback); 433 | }, 434 | 435 | /** 436 | * Get a playlist. 437 | * @param {string} playlistId The playlist's ID. 438 | * @param {Object} [options] The options supplied to this request. 439 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 440 | * @example getPlaylist('3EsfV6XzCHU8SPNdbnFogK').then(...) 441 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing 442 | * the playlist. If rejected, it contains an error object. Not returned if a callback is given. 443 | */ 444 | getPlaylist: function(playlistId, options, callback) { 445 | return WebApiRequest.builder(this.getAccessToken()) 446 | .withPath('/v1/playlists/' + playlistId) 447 | .withQueryParameters(options) 448 | .build() 449 | .execute(HttpManager.get, callback); 450 | }, 451 | 452 | /** 453 | * Get tracks in a playlist. 454 | * @param {string} playlistId The playlist's ID. 455 | * @param {Object} [options] Optional options, such as fields. 456 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 457 | * @example getPlaylistTracks('3ktAYNcRHpazJ9qecm3ptn').then(...) 458 | * @returns {Promise|undefined} A promise that if successful, resolves to an object that containing 459 | * the tracks in the playlist. If rejected, it contains an error object. Not returned if a callback is given. 460 | */ 461 | getPlaylistTracks: function(playlistId, options, callback) { 462 | return WebApiRequest.builder(this.getAccessToken()) 463 | .withPath('/v1/playlists/' + playlistId + '/tracks') 464 | .withQueryParameters(options) 465 | .build() 466 | .execute(HttpManager.get, callback); 467 | }, 468 | 469 | /** 470 | * Create a playlist. 471 | * @param {string} [name] The name of the playlist. 472 | * @param {Object} [options] The possible options, being description, collaborative and public. 473 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 474 | * @example createPlaylist('My playlist', {''description': 'My description', 'collaborative' : false, 'public': true}).then(...) 475 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing information about the 476 | * created playlist. If rejected, it contains an error object. Not returned if a callback is given. 477 | */ 478 | createPlaylist: function(name, options, callback) { 479 | return WebApiRequest.builder(this.getAccessToken()) 480 | .withPath('/v1/me/playlists') 481 | .withHeaders({ 'Content-Type': 'application/json' }) 482 | .withBodyParameters({ 483 | name : name, 484 | }, options) 485 | .build() 486 | .execute(HttpManager.post, callback); 487 | }, 488 | 489 | /** 490 | * Follow a playlist. 491 | * @param {string} playlistId The playlist's ID 492 | * @param {Object} [options] The possible options, currently only public. 493 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 494 | * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected, 495 | * it contains an error object. Not returned if a callback is given. 496 | */ 497 | followPlaylist: function(playlistId, options, callback) { 498 | return WebApiRequest.builder(this.getAccessToken()) 499 | .withPath('/v1/playlists/' + playlistId + '/followers') 500 | .withHeaders({ 'Content-Type': 'application/json' }) 501 | .withBodyParameters(options) 502 | .build() 503 | .execute(HttpManager.put, callback); 504 | }, 505 | 506 | /** 507 | * Unfollow a playlist. 508 | * @param {string} playlistId The playlist's ID 509 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 510 | * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected, 511 | * it contains an error object. Not returned if a callback is given. 512 | */ 513 | unfollowPlaylist: function(playlistId, callback) { 514 | return WebApiRequest.builder(this.getAccessToken()) 515 | .withPath('/v1/playlists/' + playlistId + '/followers') 516 | .build() 517 | .execute(HttpManager.del, callback); 518 | }, 519 | 520 | /** 521 | * Change playlist details. 522 | * @param {string} playlistId The playlist's ID 523 | * @param {Object} [options] The possible options, e.g. name, public. 524 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 525 | * @example changePlaylistDetails('3EsfV6XzCHU8SPNdbnFogK', {name: 'New name', public: true}).then(...) 526 | * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected, 527 | * it contains an error object. Not returned if a callback is given. 528 | */ 529 | changePlaylistDetails: function(playlistId, options, callback) { 530 | return WebApiRequest.builder(this.getAccessToken()) 531 | .withPath('/v1/playlists/' + playlistId) 532 | .withHeaders({ 'Content-Type': 'application/json' }) 533 | .withBodyParameters(options) 534 | .build() 535 | .execute(HttpManager.put, callback); 536 | }, 537 | 538 | /** 539 | * Replace the image used to represent a specific playlist. 540 | * @param {string} playlistId The playlist's ID 541 | * @param {string} base64URI Base64 encoded JPEG image data, maximum payload size is 256 KB 542 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 543 | * @example uploadCustomPlaylistCoverImage('3EsfV6XzCHU8SPNdbnFogK', 'longbase64uri').then(...) 544 | * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected, 545 | * it contains an error object. Not returned if a callback is given. 546 | */ 547 | uploadCustomPlaylistCoverImage: function(playlistId, base64URI, callback) { 548 | return WebApiRequest.builder(this.getAccessToken()) 549 | .withPath('/v1/playlists/' + playlistId + '/images') 550 | .withHeaders({ 'Content-Type': 'image/jpeg' }) 551 | .withBodyParameters(base64URI) 552 | .build() 553 | .execute(HttpManager.put, callback); 554 | }, 555 | 556 | /** 557 | * Add tracks to a playlist. 558 | * @param {string} playlistId The playlist's ID 559 | * @param {string[]} tracks URIs of the tracks to add to the playlist. 560 | * @param {Object} [options] Options, position being the only one. 561 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 562 | * @example addTracksToPlaylist('3EsfV6XzCHU8SPNdbnFogK', 563 | '["spotify:track:4iV5W9uYEdYUVa79Axb7Rh", "spotify:track:1301WleyT98MSxVHPZCA6M"]').then(...) 564 | * @returns {Promise|undefined} A promise that if successful returns an object containing a snapshot_id. If rejected, 565 | * it contains an error object. Not returned if a callback is given. 566 | */ 567 | addTracksToPlaylist: function(playlistId, tracks, options, callback) { 568 | return WebApiRequest.builder(this.getAccessToken()) 569 | .withPath('/v1/playlists/' + playlistId + '/tracks') 570 | .withHeaders({ 'Content-Type': 'application/json' }) 571 | .withQueryParameters(options) 572 | .withBodyParameters({ 573 | uris: tracks 574 | }) 575 | .build() 576 | .execute(HttpManager.post, callback); 577 | }, 578 | 579 | /** 580 | * Remove tracks from a playlist. 581 | * @param {string} playlistId The playlist's ID 582 | * @param {Object[]} tracks An array of objects containing a property called uri with the track URI (String), and 583 | * an optional property called positions (int[]), e.g. { uri : "spotify:track:491rM2JN8KvmV6p0oDDuJT", positions : [0, 15] } 584 | * @param {Object} options Options, snapshot_id being the only one. 585 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 586 | * @returns {Promise|undefined} A promise that if successful returns an object containing a snapshot_id. If rejected, 587 | * it contains an error object. Not returned if a callback is given. 588 | */ 589 | removeTracksFromPlaylist: function(playlistId, tracks, options, callback) { 590 | return WebApiRequest.builder(this.getAccessToken()) 591 | .withPath('/v1/playlists/' + playlistId + '/tracks') 592 | .withHeaders({ 'Content-Type': 'application/json' }) 593 | .withBodyParameters( 594 | { 595 | tracks: tracks 596 | }, 597 | options 598 | ) 599 | .build() 600 | .execute(HttpManager.del, callback); 601 | }, 602 | 603 | /** 604 | * Remove tracks from a playlist by position instead of specifying the tracks' URIs. 605 | * @param {string} playlistId The playlist's ID 606 | * @param {int[]} positions The positions of the tracks in the playlist that should be removed 607 | * @param {string} snapshot_id The snapshot ID, or version, of the playlist. Required 608 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 609 | * @returns {Promise|undefined} A promise that if successful returns an object containing a snapshot_id. If rejected, 610 | * it contains an error object. Not returned if a callback is given. 611 | */ 612 | removeTracksFromPlaylistByPosition: function( 613 | playlistId, 614 | positions, 615 | snapshotId, 616 | callback 617 | ) { 618 | return WebApiRequest.builder(this.getAccessToken()) 619 | .withPath('/v1/playlists/' + playlistId + '/tracks') 620 | .withHeaders({ 'Content-Type': 'application/json' }) 621 | .withBodyParameters({ 622 | positions: positions, 623 | snapshot_id: snapshotId 624 | }) 625 | .build() 626 | .execute(HttpManager.del, callback); 627 | }, 628 | 629 | /** 630 | * Replace tracks in a playlist. 631 | * @param {string} playlistId The playlist's ID 632 | * @param {Object[]} uris An array of track URIs (strings) 633 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 634 | * @returns {Promise|undefined} A promise that if successful returns an empty object. If rejected, 635 | * it contains an error object. Not returned if a callback is given. 636 | */ 637 | replaceTracksInPlaylist: function(playlistId, uris, callback) { 638 | return WebApiRequest.builder(this.getAccessToken()) 639 | .withPath('/v1/playlists/' + playlistId + '/tracks') 640 | .withHeaders({ 'Content-Type': 'application/json' }) 641 | .withBodyParameters({ 642 | uris: uris 643 | }) 644 | .build() 645 | .execute(HttpManager.put, callback); 646 | }, 647 | 648 | /** 649 | * Reorder tracks in a playlist. 650 | * @param {string} playlistId The playlist's ID 651 | * @param {int} rangeStart The position of the first track to be reordered. 652 | * @param {int} insertBefore The position where the tracks should be inserted. 653 | * @param {Object} options Optional parameters, i.e. range_length and snapshot_id. 654 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 655 | * @returns {Promise|undefined} A promise that if successful returns an object containing a snapshot_id. If rejected, 656 | * it contains an error object. Not returned if a callback is given. 657 | */ 658 | reorderTracksInPlaylist: function( 659 | playlistId, 660 | rangeStart, 661 | insertBefore, 662 | options, 663 | callback 664 | ) { 665 | return WebApiRequest.builder(this.getAccessToken()) 666 | .withPath('/v1/playlists/' + playlistId + '/tracks') 667 | .withHeaders({ 'Content-Type': 'application/json' }) 668 | .withBodyParameters( 669 | { 670 | range_start: rangeStart, 671 | insert_before: insertBefore 672 | }, 673 | options 674 | ) 675 | .build() 676 | .execute(HttpManager.put, callback); 677 | }, 678 | 679 | /** 680 | * Get audio features for a single track identified by its unique Spotify ID. 681 | * @param {string} trackId The track ID 682 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 683 | * @example getAudioFeaturesForTrack('38P3Q4QcdjQALGF2Z92BmR').then(...) 684 | * @returns {Promise|undefined} A promise that if successful, resolves to an object 685 | * containing information about the audio features. If the promise is 686 | * rejected, it contains an error object. Not returned if a callback is given. 687 | */ 688 | getAudioFeaturesForTrack: function(trackId, callback) { 689 | return WebApiRequest.builder(this.getAccessToken()) 690 | .withPath('/v1/audio-features/' + trackId) 691 | .build() 692 | .execute(HttpManager.get, callback); 693 | }, 694 | 695 | /** 696 | * Get audio analysis for a single track identified by its unique Spotify ID. 697 | * @param {string} trackId The track ID 698 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 699 | * @example getAudioAnalysisForTrack('38P3Q4QcdjQALGF2Z92BmR').then(...) 700 | * @returns {Promise|undefined} A promise that if successful, resolves to an object 701 | * containing information about the audio analysis. If the promise is 702 | * rejected, it contains an error object. Not returned if a callback is given. 703 | */ 704 | getAudioAnalysisForTrack: function(trackId, callback) { 705 | return WebApiRequest.builder(this.getAccessToken()) 706 | .withPath('/v1/audio-analysis/' + trackId) 707 | .build() 708 | .execute(HttpManager.get, callback); 709 | }, 710 | 711 | /** 712 | * Get audio features for multiple tracks identified by their unique Spotify ID. 713 | * @param {string[]} trackIds The track IDs 714 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 715 | * @example getAudioFeaturesForTracks(['38P3Q4QcdjQALGF2Z92BmR', '2HO2bnoMrpnZUbUqiilLHi']).then(...) 716 | * @returns {Promise|undefined} A promise that if successful, resolves to an object 717 | * containing information about the audio features for the tracks. If the promise is 718 | * rejected, it contains an error object. Not returned if a callback is given. 719 | */ 720 | getAudioFeaturesForTracks: function(trackIds, callback) { 721 | return WebApiRequest.builder(this.getAccessToken()) 722 | .withPath('/v1/audio-features') 723 | .withQueryParameters({ 724 | ids: trackIds.join(',') 725 | }) 726 | .build() 727 | .execute(HttpManager.get, callback); 728 | }, 729 | 730 | /** 731 | * Create a playlist-style listening experience based on seed artists, tracks and genres. 732 | * @param {Object} [options] The options supplied to this request. 733 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 734 | * @example getRecommendations({ min_energy: 0.4, seed_artists: ['6mfK6Q2tzLMEchAr0e9Uzu', '4DYFVNKZ1uixa6SQTvzQwJ'], min_popularity: 50 }).then(...) 735 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing 736 | * a list of tracks and a list of seeds. If rejected, it contains an error object. Not returned if a callback is given. 737 | */ 738 | getRecommendations: function(options, callback) { 739 | var _opts = {}; 740 | var optionsOfTypeArray = ['seed_artists', 'seed_genres', 'seed_tracks']; 741 | for (var option in options) { 742 | if (options.hasOwnProperty(option)) { 743 | if ( 744 | optionsOfTypeArray.indexOf(option) !== -1 && 745 | Object.prototype.toString.call(options[option]) === '[object Array]' 746 | ) { 747 | _opts[option] = options[option].join(','); 748 | } else { 749 | _opts[option] = options[option]; 750 | } 751 | } 752 | } 753 | 754 | return WebApiRequest.builder(this.getAccessToken()) 755 | .withPath('/v1/recommendations') 756 | .withQueryParameters(_opts) 757 | .build() 758 | .execute(HttpManager.get, callback); 759 | }, 760 | 761 | /** 762 | * Retrieve a list of available genres seed parameter values for recommendations. 763 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 764 | * @example getAvailableGenreSeeds().then(...) 765 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing 766 | * a list of available genres to be used as seeds for recommendations. 767 | * If rejected, it contains an error object. Not returned if a callback is given. 768 | */ 769 | getAvailableGenreSeeds: function(callback) { 770 | return WebApiRequest.builder(this.getAccessToken()) 771 | .withPath('/v1/recommendations/available-genre-seeds') 772 | .build() 773 | .execute(HttpManager.get, callback); 774 | }, 775 | 776 | /** 777 | * Retrieve the tracks that are saved to the authenticated users Your Music library. 778 | * @param {Object} [options] Options, being market, limit, and/or offset. 779 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 780 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object which in turn contains 781 | * playlist track objects. Not returned if a callback is given. 782 | */ 783 | getMySavedTracks: function(options, callback) { 784 | return WebApiRequest.builder(this.getAccessToken()) 785 | .withPath('/v1/me/tracks') 786 | .withQueryParameters(options) 787 | .build() 788 | .execute(HttpManager.get, callback); 789 | }, 790 | 791 | /** 792 | * Check if one or more tracks is already saved in the current Spotify user’s “Your Music” library. 793 | * @param {string[]} trackIds The track IDs 794 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 795 | * @returns {Promise|undefined} A promise that if successful, resolves into an array of booleans. The order 796 | * of the returned array's elements correspond to the track ID in the request. 797 | * The boolean value of true indicates that the track is part of the user's library, otherwise false. 798 | * Not returned if a callback is given. 799 | */ 800 | containsMySavedTracks: function(trackIds, callback) { 801 | return WebApiRequest.builder(this.getAccessToken()) 802 | .withPath('/v1/me/tracks/contains') 803 | .withQueryParameters({ 804 | ids: trackIds.join(',') 805 | }) 806 | .build() 807 | .execute(HttpManager.get, callback); 808 | }, 809 | 810 | /** 811 | * Remove a track from the authenticated user's Your Music library. 812 | * @param {string[]} trackIds The track IDs 813 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 814 | * @returns {Promise|undefined} A promise that if successful returns null, otherwise an error. 815 | * Not returned if a callback is given. 816 | */ 817 | removeFromMySavedTracks: function(trackIds, callback) { 818 | return WebApiRequest.builder(this.getAccessToken()) 819 | .withPath('/v1/me/tracks') 820 | .withHeaders({ 'Content-Type': 'application/json' }) 821 | .withBodyParameters({ ids: trackIds }) 822 | .build() 823 | .execute(HttpManager.del, callback); 824 | }, 825 | 826 | /** 827 | * Add a track from the authenticated user's Your Music library. 828 | * @param {string[]} trackIds The track IDs 829 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 830 | * @returns {Promise|undefined} A promise that if successful returns null, otherwise an error. Not returned if a callback is given. 831 | */ 832 | addToMySavedTracks: function(trackIds, callback) { 833 | return WebApiRequest.builder(this.getAccessToken()) 834 | .withPath('/v1/me/tracks') 835 | .withHeaders({ 'Content-Type': 'application/json' }) 836 | .withBodyParameters({ ids: trackIds }) 837 | .build() 838 | .execute(HttpManager.put, callback); 839 | }, 840 | 841 | /** 842 | * Remove an album from the authenticated user's Your Music library. 843 | * @param {string[]} albumIds The album IDs 844 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 845 | * @returns {Promise|undefined} A promise that if successful returns null, otherwise an error. 846 | * Not returned if a callback is given. 847 | */ 848 | removeFromMySavedAlbums: function(albumIds, callback) { 849 | return WebApiRequest.builder(this.getAccessToken()) 850 | .withPath('/v1/me/albums') 851 | .withHeaders({ 'Content-Type': 'application/json' }) 852 | .withBodyParameters(albumIds) 853 | .build() 854 | .execute(HttpManager.del, callback); 855 | }, 856 | 857 | /** 858 | * Add an album from the authenticated user's Your Music library. 859 | * @param {string[]} albumIds The track IDs 860 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 861 | * @returns {Promise|undefined} A promise that if successful returns null, otherwise an error. Not returned if a callback is given. 862 | */ 863 | addToMySavedAlbums: function(albumIds, callback) { 864 | return WebApiRequest.builder(this.getAccessToken()) 865 | .withPath('/v1/me/albums') 866 | .withHeaders({ 'Content-Type': 'application/json' }) 867 | .withBodyParameters(albumIds) 868 | .build() 869 | .execute(HttpManager.put, callback); 870 | }, 871 | 872 | /** 873 | * Retrieve the albums that are saved to the authenticated users Your Music library. 874 | * @param {Object} [options] Options, being market, limit, and/or offset. 875 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 876 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object which in turn contains 877 | * playlist album objects. Not returned if a callback is given. 878 | */ 879 | getMySavedAlbums: function(options, callback) { 880 | return WebApiRequest.builder(this.getAccessToken()) 881 | .withPath('/v1/me/albums') 882 | .withQueryParameters(options) 883 | .build() 884 | .execute(HttpManager.get, callback); 885 | }, 886 | 887 | /** 888 | * Check if one or more albums is already saved in the current Spotify user’s “Your Music” library. 889 | * @param {string[]} albumIds The album IDs 890 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 891 | * @returns {Promise|undefined} A promise that if successful, resolves into an array of booleans. The order 892 | * of the returned array's elements correspond to the album ID in the request. 893 | * The boolean value of true indicates that the album is part of the user's library, otherwise false. 894 | * Not returned if a callback is given. 895 | */ 896 | containsMySavedAlbums: function(albumIds, callback) { 897 | return WebApiRequest.builder(this.getAccessToken()) 898 | .withPath('/v1/me/albums/contains') 899 | .withQueryParameters({ 900 | ids: albumIds.join(',') 901 | }) 902 | .build() 903 | .execute(HttpManager.get, callback); 904 | }, 905 | 906 | /** 907 | * Get the current user's top artists based on calculated affinity. 908 | * @param {Object} [options] Options, being time_range, limit, offset. 909 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 910 | * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of artists, 911 | * otherwise an error. Not returned if a callback is given. 912 | */ 913 | getMyTopArtists: function(options, callback) { 914 | return WebApiRequest.builder(this.getAccessToken()) 915 | .withPath('/v1/me/top/artists') 916 | .withQueryParameters(options) 917 | .build() 918 | .execute(HttpManager.get, callback); 919 | }, 920 | 921 | /** 922 | * Get the current user's top tracks based on calculated affinity. 923 | * @param {Object} [options] Options, being time_range, limit, offset. 924 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 925 | * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks, 926 | * otherwise an error. Not returned if a callback is given. 927 | */ 928 | getMyTopTracks: function(options, callback) { 929 | return WebApiRequest.builder(this.getAccessToken()) 930 | .withPath('/v1/me/top/tracks') 931 | .withQueryParameters(options) 932 | .build() 933 | .execute(HttpManager.get, callback); 934 | }, 935 | 936 | /** 937 | * Get the Current User's Recently Played Tracks 938 | * @param {Object} [options] Options, being type, after, limit, before. 939 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 940 | * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of play history objects, 941 | * otherwise an error. Not returned if a callback is given. Note that the response will be empty 942 | * in case the user has enabled private session. 943 | */ 944 | getMyRecentlyPlayedTracks: function(options, callback) { 945 | return WebApiRequest.builder(this.getAccessToken()) 946 | .withPath('/v1/me/player/recently-played') 947 | .withQueryParameters(options) 948 | .build() 949 | .execute(HttpManager.get, callback); 950 | }, 951 | 952 | /** 953 | * Add track or episode to device queue 954 | * @param {string} [uri] uri of the track or episode to add 955 | * @param {Object} [options] Options, being device_id. 956 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 957 | * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks, 958 | * otherwise an error. Not returned if a callback is given. 959 | */ 960 | addToQueue: function(uri, options, callback) { 961 | return WebApiRequest.builder(this.getAccessToken()) 962 | .withPath('/v1/me/player/queue') 963 | .withQueryParameters( 964 | { 965 | uri: uri 966 | }, 967 | options 968 | ) 969 | .build() 970 | .execute(HttpManager.post, callback); 971 | }, 972 | 973 | 974 | /** 975 | * Get the Current User's Available Devices 976 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 977 | * @returns {Promise|undefined} A promise that if successful, resolves into an array of device objects, 978 | * otherwise an error. Not returned if a callback is given. 979 | */ 980 | getMyDevices: function(callback) { 981 | return WebApiRequest.builder(this.getAccessToken()) 982 | .withPath('/v1/me/player/devices') 983 | .build() 984 | .execute(HttpManager.get, callback); 985 | }, 986 | 987 | /** 988 | * Get the Current User's Currently Playing Track. 989 | * @param {Object} [options] Options, being market. 990 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 991 | * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks, 992 | * otherwise an error. Not returned if a callback is given. 993 | */ 994 | getMyCurrentPlayingTrack: function(options, callback) { 995 | return WebApiRequest.builder(this.getAccessToken()) 996 | .withPath('/v1/me/player/currently-playing') 997 | .withQueryParameters(options) 998 | .build() 999 | .execute(HttpManager.get, callback); 1000 | }, 1001 | 1002 | /** 1003 | * Get Information About The User's Current Playback State 1004 | * @param {Object} [options] Options, being market and additional_types. 1005 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1006 | * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks, 1007 | * otherwise an error. Not returned if a callback is given. 1008 | */ 1009 | getMyCurrentPlaybackState: function(options, callback) { 1010 | return WebApiRequest.builder(this.getAccessToken()) 1011 | .withPath('/v1/me/player') 1012 | .withQueryParameters(options) 1013 | .build() 1014 | .execute(HttpManager.get, callback); 1015 | }, 1016 | 1017 | /** 1018 | * Transfer a User's Playback 1019 | * @param {string[]} [deviceIds] An _array_ containing a device ID on which playback should be started/transferred. 1020 | * (NOTE: The API is currently only supporting a single device ID.) 1021 | * @param {Object} [options] Options, the only one being 'play'. 1022 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1023 | * @returns {Promise|undefined} A promise that if successful, resolves into an empty response, 1024 | * otherwise an error. Not returned if a callback is given. 1025 | */ 1026 | transferMyPlayback: function(deviceIds, options, callback) { 1027 | return WebApiRequest.builder(this.getAccessToken()) 1028 | .withPath('/v1/me/player') 1029 | .withHeaders({ 'Content-Type': 'application/json' }) 1030 | .withBodyParameters( 1031 | { 1032 | device_ids: deviceIds, 1033 | }, 1034 | options 1035 | ) 1036 | .build() 1037 | .execute(HttpManager.put, callback); 1038 | }, 1039 | 1040 | /** 1041 | * Starts o Resumes the Current User's Playback 1042 | * @param {Object} [options] Options, being device_id, context_uri, offset, uris, position_ms. 1043 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1044 | * @example play({context_uri: 'spotify:album:5ht7ItJgpBH7W6vJ5BqpPr'}).then(...) 1045 | * @returns {Promise|undefined} A promise that if successful, resolves into an empty response, 1046 | * otherwise an error. Not returned if a callback is given. 1047 | */ 1048 | play: function(options, callback) { 1049 | /*jshint camelcase: false */ 1050 | var _options = options || {}; 1051 | var queryParams = _options.device_id 1052 | ? { device_id: _options.device_id } 1053 | : null; 1054 | var postData = {}; 1055 | ['context_uri', 'uris', 'offset', 'position_ms'].forEach(function(field) { 1056 | if (field in _options) { 1057 | postData[field] = _options[field]; 1058 | } 1059 | }); 1060 | return WebApiRequest.builder(this.getAccessToken()) 1061 | .withPath('/v1/me/player/play') 1062 | .withQueryParameters(queryParams) 1063 | .withHeaders({ 'Content-Type': 'application/json' }) 1064 | .withBodyParameters(postData) 1065 | .build() 1066 | .execute(HttpManager.put, callback); 1067 | }, 1068 | 1069 | /** 1070 | * Pauses the Current User's Playback 1071 | * @param {Object} [options] Options, being device_id. If left empty will target the user's currently active device. 1072 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1073 | * @example pause().then(...) 1074 | * @returns {Promise|undefined} A promise that if successful, resolves into an empty response, 1075 | * otherwise an error. Not returned if a callback is given. 1076 | */ 1077 | pause: function(options, callback) { 1078 | return ( 1079 | WebApiRequest.builder(this.getAccessToken()) 1080 | .withPath('/v1/me/player/pause') 1081 | /*jshint camelcase: false */ 1082 | .withQueryParameters( 1083 | options && options.device_id ? { device_id: options.device_id } : null 1084 | ) 1085 | .withHeaders({ 'Content-Type': 'application/json' }) 1086 | .build() 1087 | .execute(HttpManager.put, callback) 1088 | ); 1089 | }, 1090 | 1091 | /** 1092 | * Skip the Current User's Playback To Previous Track 1093 | * @param {Object} [options] Options, being device_id. If left empty will target the user's currently active device. 1094 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1095 | * @example skipToPrevious().then(...) 1096 | * @returns {Promise|undefined} A promise that if successful, resolves into an empty response, 1097 | * otherwise an error. Not returned if a callback is given. 1098 | */ 1099 | skipToPrevious: function(options, callback) { 1100 | return WebApiRequest.builder(this.getAccessToken()) 1101 | .withPath('/v1/me/player/previous') 1102 | .withQueryParameters( 1103 | options && options.device_id ? { device_id: options.device_id } : null 1104 | ) 1105 | .build() 1106 | .execute(HttpManager.post, callback); 1107 | }, 1108 | 1109 | /** 1110 | * Skip the Current User's Playback To Next Track 1111 | * @param {Object} [options] Options, being device_id. If left empty will target the user's currently active device. 1112 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1113 | * @example skipToNext().then(...) 1114 | * @returns {Promise|undefined} A promise that if successful, resolves into an empty response, 1115 | * otherwise an error. Not returned if a callback is given. 1116 | */ 1117 | skipToNext: function(options, callback) { 1118 | return WebApiRequest.builder(this.getAccessToken()) 1119 | .withPath('/v1/me/player/next') 1120 | .withQueryParameters( 1121 | options && options.device_id ? { device_id: options.device_id } : null 1122 | ) 1123 | .build() 1124 | .execute(HttpManager.post, callback); 1125 | }, 1126 | 1127 | /** 1128 | * Seeks to the given position in the user’s currently playing track. 1129 | * 1130 | * @param {number} positionMs The position in milliseconds to seek to. Must be a positive number. 1131 | * @param {Object} options Options, being device_id. If left empty will target the user's currently active device. 1132 | * @param {function(Object,Object)} callback An optional callback that receives 2 parameters. The first 1133 | * one is the error object (null if no error), and the second is the value if the request succeeded. 1134 | * @return {Object} Null if a callback is provided, a `Promise` object otherwise 1135 | */ 1136 | seek: function(positionMs, options, callback) { 1137 | var params = { 1138 | /* jshint camelcase: false */ 1139 | position_ms: positionMs 1140 | }; 1141 | if (options && 'device_id' in options) { 1142 | /* jshint camelcase: false */ 1143 | params.device_id = options.device_id; 1144 | } 1145 | return WebApiRequest.builder(this.getAccessToken()) 1146 | .withPath('/v1/me/player/seek') 1147 | .withQueryParameters(params) 1148 | .build() 1149 | .execute(HttpManager.put, callback); 1150 | }, 1151 | 1152 | /** 1153 | * Set Repeat Mode On The Current User's Playback 1154 | * @param {string} [state] State (track, context, or off) 1155 | * @param {Object} [options] Options, being device_id. If left empty will target the user's currently active device. 1156 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1157 | * @example setRepeat('context', {}).then(...) 1158 | * @returns {Promise|undefined} A promise that if successful, resolves into an empty response, 1159 | * otherwise an error. Not returned if a callback is given. 1160 | */ 1161 | setRepeat: function(state, options, callback) { 1162 | var params = { 1163 | state: state 1164 | }; 1165 | if (options && 'device_id' in options) { 1166 | /* jshint camelcase: false */ 1167 | params.device_id = options.device_id; 1168 | } 1169 | return WebApiRequest.builder(this.getAccessToken()) 1170 | .withPath('/v1/me/player/repeat') 1171 | .withQueryParameters(params) 1172 | .build() 1173 | .execute(HttpManager.put, callback); 1174 | }, 1175 | 1176 | /** 1177 | * Set Shuffle Mode On The Current User's Playback 1178 | * @param {boolean} [state] State 1179 | * @param {Object} [options] Options, being device_id. If left empty will target the user's currently active device. 1180 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1181 | * @example setShuffle({state: 'false'}).then(...) 1182 | * @returns {Promise|undefined} A promise that if successful, resolves into an empty response, 1183 | * otherwise an error. Not returned if a callback is given. 1184 | */ 1185 | setShuffle: function(state, options, callback) { 1186 | var params = { 1187 | state: state 1188 | }; 1189 | if (options && 'device_id' in options) { 1190 | /* jshint camelcase: false */ 1191 | params.device_id = options.device_id; 1192 | } 1193 | return WebApiRequest.builder(this.getAccessToken()) 1194 | .withPath('/v1/me/player/shuffle') 1195 | .withQueryParameters(params) 1196 | .build() 1197 | .execute(HttpManager.put, callback); 1198 | }, 1199 | 1200 | /** 1201 | * Set the volume for the user’s current playback device. 1202 | * @param {number} volumePercent The volume to set. Must be a value from 0 to 100. 1203 | * @param {Object} options Options, being device_id. If left empty will target the user's currently active device. 1204 | * @param {function(Object,Object)} callback An optional callback that receives 2 parameters. The first 1205 | * one is the error object (null if no error), and the second is the value if the request succeeded. 1206 | * @return {Object} Null if a callback is provided, a `Promise` object otherwise 1207 | */ 1208 | setVolume: function(volumePercent, options, callback) { 1209 | var params = { 1210 | /* jshint camelcase: false */ 1211 | volume_percent: volumePercent 1212 | }; 1213 | if (options && 'device_id' in options) { 1214 | /* jshint camelcase: false */ 1215 | params.device_id = options.device_id; 1216 | } 1217 | return WebApiRequest.builder(this.getAccessToken()) 1218 | .withPath('/v1/me/player/volume') 1219 | .withQueryParameters(params) 1220 | .build() 1221 | .execute(HttpManager.put, callback); 1222 | }, 1223 | 1224 | /** 1225 | * Add the current user as a follower of one or more other Spotify users. 1226 | * @param {string[]} userIds The IDs of the users to be followed. 1227 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1228 | * @example followUsers(['thelinmichael', 'wizzler']).then(...) 1229 | * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected, 1230 | * it contains an error object. Not returned if a callback is given. 1231 | */ 1232 | followUsers: function(userIds, callback) { 1233 | return WebApiRequest.builder(this.getAccessToken()) 1234 | .withPath('/v1/me/following') 1235 | .withQueryParameters({ 1236 | ids: userIds.join(','), 1237 | type: 'user' 1238 | }) 1239 | .build() 1240 | .execute(HttpManager.put, callback); 1241 | }, 1242 | 1243 | /** 1244 | * Add the current user as a follower of one or more artists. 1245 | * @param {string[]} artistIds The IDs of the artists to be followed. 1246 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1247 | * @example followArtists(['0LcJLqbBmaGUft1e9Mm8HV', '3gqv1kgivAc92KnUm4elKv']).then(...) 1248 | * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected, 1249 | * it contains an error object. Not returned if a callback is given. 1250 | */ 1251 | followArtists: function(artistIds, callback) { 1252 | return WebApiRequest.builder(this.getAccessToken()) 1253 | .withPath('/v1/me/following') 1254 | .withQueryParameters({ 1255 | ids: artistIds.join(','), 1256 | type: 'artist' 1257 | }) 1258 | .build() 1259 | .execute(HttpManager.put, callback); 1260 | }, 1261 | 1262 | /** 1263 | * Remove the current user as a follower of one or more other Spotify users. 1264 | * @param {string[]} userIds The IDs of the users to be unfollowed. 1265 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1266 | * @example unfollowUsers(['thelinmichael', 'wizzler']).then(...) 1267 | * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected, 1268 | * it contains an error object. Not returned if a callback is given. 1269 | */ 1270 | unfollowUsers: function(userIds, callback) { 1271 | return WebApiRequest.builder(this.getAccessToken()) 1272 | .withPath('/v1/me/following') 1273 | .withQueryParameters({ 1274 | ids: userIds.join(','), 1275 | type: 'user' 1276 | }) 1277 | .build() 1278 | .execute(HttpManager.del, callback); 1279 | }, 1280 | 1281 | /** 1282 | * Remove the current user as a follower of one or more artists. 1283 | * @param {string[]} artistIds The IDs of the artists to be unfollowed. 1284 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1285 | * @example unfollowArtists(['0LcJLqbBmaGUft1e9Mm8HV', '3gqv1kgivAc92KnUm4elKv']).then(...) 1286 | * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected, 1287 | * it contains an error object. Not returned if a callback is given. 1288 | */ 1289 | unfollowArtists: function(artistIds, callback) { 1290 | return WebApiRequest.builder(this.getAccessToken()) 1291 | .withPath('/v1/me/following') 1292 | .withQueryParameters({ 1293 | ids: artistIds.join(','), 1294 | type: 'artist' 1295 | }) 1296 | .build() 1297 | .execute(HttpManager.del, callback); 1298 | }, 1299 | 1300 | /** 1301 | * Check to see if the current user is following one or more other Spotify users. 1302 | * @param {string[]} userIds The IDs of the users to check if are followed by the current user. 1303 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1304 | * @example isFollowingUsers(['thelinmichael', 'wizzler']).then(...) 1305 | * @returns {Promise|undefined} A promise that if successful, resolves into an array of booleans. The order 1306 | * of the returned array's elements correspond to the users IDs in the request. 1307 | * The boolean value of true indicates that the user is following that user, otherwise is not. 1308 | * Not returned if a callback is given. 1309 | */ 1310 | isFollowingUsers: function(userIds, callback) { 1311 | return WebApiRequest.builder(this.getAccessToken()) 1312 | .withPath('/v1/me/following/contains') 1313 | .withQueryParameters({ 1314 | ids: userIds.join(','), 1315 | type: 'user' 1316 | }) 1317 | .build() 1318 | .execute(HttpManager.get, callback); 1319 | }, 1320 | 1321 | /** 1322 | * Get the current user's followed artists. 1323 | * @param {Object} [options] Options, being after and limit. 1324 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1325 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object which contains 1326 | * album objects. Not returned if a callback is given. 1327 | */ 1328 | getFollowedArtists: function(options, callback) { 1329 | return WebApiRequest.builder(this.getAccessToken()) 1330 | .withPath('/v1/me/following') 1331 | .withQueryParameters( 1332 | { 1333 | type: 'artist' 1334 | }, 1335 | options 1336 | ) 1337 | .build() 1338 | .execute(HttpManager.get, callback); 1339 | }, 1340 | 1341 | /** 1342 | * Check if users are following a playlist. 1343 | * @param {string} userId The playlist's owner's user ID 1344 | * @param {string} playlistId The playlist's ID 1345 | * @param {String[]} User IDs of the following users 1346 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1347 | * @returns {Promise|undefined} A promise that if successful returns an array of booleans. If rejected, 1348 | * it contains an error object. Not returned if a callback is given. 1349 | */ 1350 | areFollowingPlaylist: function(userId, playlistId, followerIds, callback) { 1351 | return WebApiRequest.builder(this.getAccessToken()) 1352 | .withPath( 1353 | '/v1/users/' + 1354 | encodeURIComponent(userId) + 1355 | '/playlists/' + 1356 | playlistId + 1357 | '/followers/contains' 1358 | ) 1359 | .withQueryParameters({ 1360 | ids: followerIds.join(',') 1361 | }) 1362 | .build() 1363 | .execute(HttpManager.get, callback); 1364 | }, 1365 | 1366 | /** 1367 | * Check to see if the current user is following one or more artists. 1368 | * @param {string[]} artistIds The IDs of the artists to check if are followed by the current user. 1369 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1370 | * @example isFollowingArtists(['0LcJLqbBmaGUft1e9Mm8HV', '3gqv1kgivAc92KnUm4elKv']).then(...) 1371 | * @returns {Promise|undefined} A promise that if successful, resolves into an array of booleans. The order 1372 | * of the returned array's elements correspond to the artists IDs in the request. 1373 | * The boolean value of true indicates that the user is following that artist, otherwise is not. 1374 | * Not returned if a callback is given. 1375 | */ 1376 | isFollowingArtists: function(artistIds, callback) { 1377 | return WebApiRequest.builder(this.getAccessToken()) 1378 | .withPath('/v1/me/following/contains') 1379 | .withQueryParameters({ 1380 | ids: artistIds.join(','), 1381 | type: 'artist' 1382 | }) 1383 | .build() 1384 | .execute(HttpManager.get, callback); 1385 | }, 1386 | 1387 | /** 1388 | * Retrieve new releases 1389 | * @param {Object} [options] Options, being country, limit and/or offset. 1390 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1391 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object which contains 1392 | * album objects. Not returned if a callback is given. 1393 | */ 1394 | getNewReleases: function(options, callback) { 1395 | return WebApiRequest.builder(this.getAccessToken()) 1396 | .withPath('/v1/browse/new-releases') 1397 | .withQueryParameters(options) 1398 | .build() 1399 | .execute(HttpManager.get, callback); 1400 | }, 1401 | 1402 | /** 1403 | * Retrieve featured playlists 1404 | * @param {Object} [options] Options, being country, locale, timestamp, limit, offset. 1405 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1406 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object which contains 1407 | * featured playlists. Not returned if a callback is given. 1408 | */ 1409 | getFeaturedPlaylists: function(options, callback) { 1410 | return WebApiRequest.builder(this.getAccessToken()) 1411 | .withPath('/v1/browse/featured-playlists') 1412 | .withQueryParameters(options) 1413 | .build() 1414 | .execute(HttpManager.get, callback); 1415 | }, 1416 | 1417 | /** 1418 | * Retrieve a list of categories used to tag items in Spotify (e.g. in the 'Browse' tab) 1419 | * @param {Object} [options] Options, being country, locale, limit, offset. 1420 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1421 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object of categories. 1422 | * Not returned if a callback is given. 1423 | */ 1424 | getCategories: function(options, callback) { 1425 | return WebApiRequest.builder(this.getAccessToken()) 1426 | .withPath('/v1/browse/categories') 1427 | .withQueryParameters(options) 1428 | .build() 1429 | .execute(HttpManager.get, callback); 1430 | }, 1431 | 1432 | /** 1433 | * Retrieve a category. 1434 | * @param {string} categoryId The id of the category to retrieve. 1435 | * @param {Object} [options] Options, being country, locale. 1436 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1437 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a category object. 1438 | * Not returned if a callback is given. 1439 | */ 1440 | getCategory: function(categoryId, options, callback) { 1441 | return WebApiRequest.builder(this.getAccessToken()) 1442 | .withPath('/v1/browse/categories/' + categoryId) 1443 | .withQueryParameters(options) 1444 | .build() 1445 | .execute(HttpManager.get, callback); 1446 | }, 1447 | 1448 | /** 1449 | * Retrieve playlists for a category. 1450 | * @param {string} categoryId The id of the category to retrieve playlists for. 1451 | * @param {Object} [options] Options, being country, limit, offset. 1452 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1453 | * @returns {Promise|undefined} A promise that if successful, resolves to a paging object containing simple playlists. 1454 | * Not returned if a callback is given. 1455 | */ 1456 | getPlaylistsForCategory: function(categoryId, options, callback) { 1457 | return WebApiRequest.builder(this.getAccessToken()) 1458 | .withPath('/v1/browse/categories/' + categoryId + '/playlists') 1459 | .withQueryParameters(options) 1460 | .build() 1461 | .execute(HttpManager.get, callback); 1462 | }, 1463 | 1464 | /** 1465 | * Get a show. 1466 | * @param {string} showId The show's ID. 1467 | * @param {Object} [options] The possible options, currently only market. 1468 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1469 | * @example getShow('3Qm86XLflmIXVm1wcwkgDK').then(...) 1470 | * @returns {Promise|undefined} A promise that if successful, returns an object containing information 1471 | * about the show. Not returned if a callback is given. 1472 | */ 1473 | getShow: function(showId, options, callback) { 1474 | return WebApiRequest.builder(this.getAccessToken()) 1475 | .withPath('/v1/shows/' + showId) 1476 | .withQueryParameters(options) 1477 | .build() 1478 | .execute(HttpManager.get, callback); 1479 | }, 1480 | 1481 | /** 1482 | * Look up several shows. 1483 | * @param {string[]} showIds The IDs of the shows. 1484 | * @param {Object} [options] The possible options, currently only market. 1485 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1486 | * @example getShows(['0oSGxfWSnnOXhD2fKuz2Gy', '3dBVyJ7JuOMt4GE9607Qin']).then(...) 1487 | * @returns {Promise|undefined} A promise that if successful, returns an object containing information 1488 | * about the shows. Not returned if a callback is given. 1489 | */ 1490 | getShows: function(showIds, options, callback) { 1491 | return WebApiRequest.builder(this.getAccessToken()) 1492 | .withPath('/v1/shows') 1493 | .withQueryParameters( 1494 | { 1495 | ids: showIds.join(',') 1496 | }, 1497 | options 1498 | ) 1499 | .build() 1500 | .execute(HttpManager.get, callback); 1501 | }, 1502 | 1503 | /** 1504 | * Check if one or more shows is already saved in the current Spotify user’s “Your Music” library. 1505 | * @param {string[]} showIds The show IDs 1506 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1507 | * @returns {Promise|undefined} A promise that if successful, resolves into an array of booleans. The order 1508 | * of the returned array's elements correspond to the show ID in the request. 1509 | * The boolean value of true indicates that the show is part of the user's library, otherwise false. 1510 | * Not returned if a callback is given. 1511 | */ 1512 | containsMySavedShows: function(showIds, callback) { 1513 | return WebApiRequest.builder(this.getAccessToken()) 1514 | .withPath('/v1/me/shows/contains') 1515 | .withQueryParameters({ 1516 | ids: showIds.join(',') 1517 | }) 1518 | .build() 1519 | .execute(HttpManager.get, callback); 1520 | }, 1521 | 1522 | /** 1523 | * Remove an show from the authenticated user's Your Music library. 1524 | * @param {string[]} showIds The show IDs 1525 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1526 | * @returns {Promise|undefined} A promise that if successful returns null, otherwise an error. 1527 | * Not returned if a callback is given. 1528 | */ 1529 | removeFromMySavedShows: function(showIds, callback) { 1530 | return WebApiRequest.builder(this.getAccessToken()) 1531 | .withPath('/v1/me/shows') 1532 | .withHeaders({ 'Content-Type': 'application/json' }) 1533 | .withBodyParameters(showIds) 1534 | .build() 1535 | .execute(HttpManager.del, callback); 1536 | }, 1537 | 1538 | /** 1539 | * Add a show from the authenticated user's Your Music library. 1540 | * @param {string[]} showIds The show IDs 1541 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1542 | * @returns {Promise|undefined} A promise that if successful returns null, otherwise an error. Not returned if a callback is given. 1543 | */ 1544 | addToMySavedShows: function(showIds, callback) { 1545 | return WebApiRequest.builder(this.getAccessToken()) 1546 | .withPath('/v1/me/shows') 1547 | .withHeaders({ 'Content-Type': 'application/json' }) 1548 | .withBodyParameters(showIds) 1549 | .build() 1550 | .execute(HttpManager.put, callback); 1551 | }, 1552 | 1553 | /** 1554 | * Retrieve the shows that are saved to the authenticated users Your Music library. 1555 | * @param {Object} [options] Options, being market, limit, and/or offset. 1556 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1557 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object which in turn contains 1558 | * playlist show objects. Not returned if a callback is given. 1559 | */ 1560 | getMySavedShows: function(options, callback) { 1561 | return WebApiRequest.builder(this.getAccessToken()) 1562 | .withPath('/v1/me/shows') 1563 | .withQueryParameters(options) 1564 | .build() 1565 | .execute(HttpManager.get, callback); 1566 | }, 1567 | 1568 | /** 1569 | * Get the episodes of an show. 1570 | * @param showId the show's ID. 1571 | * @options {Object} [options] The possible options, being limit, offset, and market. 1572 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1573 | * @example getShowEpisodes('41MnTivkwTO3UUJ8DrqEJJ', { limit : 5, offset : 1 }).then(...) 1574 | * @returns {Promise|undefined} A promise that if successful, returns an object containing the 1575 | * episodes in the album. The result is paginated. If the promise is rejected. 1576 | * it contains an error object. Not returned if a callback is given. 1577 | */ 1578 | getShowEpisodes: function(showId, options, callback) { 1579 | return WebApiRequest.builder(this.getAccessToken()) 1580 | .withPath('/v1/shows/' + showId + '/episodes') 1581 | .withQueryParameters(options) 1582 | .build() 1583 | .execute(HttpManager.get, callback); 1584 | }, 1585 | 1586 | /** 1587 | * Search for a show. 1588 | * @param {string} query The search query. 1589 | * @param {Object} [options] The possible options, e.g. limit, offset. 1590 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1591 | * @example searchShows('Space Oddity', { limit : 5, offset : 1 }).then(...) 1592 | * @returns {Promise|undefined} A promise that if successful, returns an object containing the 1593 | * search results. The result is paginated. If the promise is rejected, 1594 | * it contains an error object. Not returned if a callback is given. 1595 | */ 1596 | searchShows: function(query, options, callback) { 1597 | return this.search(query, ['show'], options, callback); 1598 | }, 1599 | 1600 | /** 1601 | * Search for an episode. 1602 | * @param {string} query The search query. 1603 | * @param {Object} [options] The possible options, e.g. limit, offset. 1604 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1605 | * @example searchEpisodes('Space Oddity', { limit : 5, offset : 1 }).then(...) 1606 | * @returns {Promise|undefined} A promise that if successful, returns an object containing the 1607 | * search results. The result is paginated. If the promise is rejected, 1608 | * it contains an error object. Not returned if a callback is given. 1609 | */ 1610 | searchEpisodes: function(query, options, callback) { 1611 | return this.search(query, ['episode'], options, callback); 1612 | }, 1613 | 1614 | /** 1615 | * Look up an episode. 1616 | * @param {string} episodeId The episode's ID. 1617 | * @param {Object} [options] The possible options, currently only market. 1618 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1619 | * @example getEpisode('3Qm86XLflmIXVm1wcwkgDK').then(...) 1620 | * @returns {Promise|undefined} A promise that if successful, returns an object containing information 1621 | * about the episode. Not returned if a callback is given. 1622 | */ 1623 | getEpisode: function(episodeId, options, callback) { 1624 | return WebApiRequest.builder(this.getAccessToken()) 1625 | .withPath('/v1/episodes/' + episodeId) 1626 | .withQueryParameters(options) 1627 | .build() 1628 | .execute(HttpManager.get, callback); 1629 | }, 1630 | 1631 | /** 1632 | * Look up several episodes. 1633 | * @param {string[]} episodeIds The IDs of the episodes. 1634 | * @param {Object} [options] The possible options, currently only market. 1635 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1636 | * @example getEpisodes(['0oSGxfWSnnOXhD2fKuz2Gy', '3dBVyJ7JuOMt4GE9607Qin']).then(...) 1637 | * @returns {Promise|undefined} A promise that if successful, returns an object containing information 1638 | * about the episodes. Not returned if a callback is given. 1639 | */ 1640 | getEpisodes: function(episodeIds, options, callback) { 1641 | return WebApiRequest.builder(this.getAccessToken()) 1642 | .withPath('/v1/episodes') 1643 | .withQueryParameters( 1644 | { 1645 | ids: episodeIds.join(',') 1646 | }, 1647 | options 1648 | ) 1649 | .build() 1650 | .execute(HttpManager.get, callback); 1651 | }, 1652 | }; 1653 | 1654 | SpotifyWebApi._addMethods = function(methods) { 1655 | for (var i in methods) { 1656 | if (methods.hasOwnProperty(i)) { 1657 | this.prototype[i] = methods[i]; 1658 | } 1659 | } 1660 | }; 1661 | 1662 | module.exports = SpotifyWebApi; 1663 | -------------------------------------------------------------------------------- /src/webapi-request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Request = require('./base-request'); 4 | 5 | var DEFAULT_HOST = 'api.spotify.com', 6 | DEFAULT_PORT = 443, 7 | DEFAULT_SCHEME = 'https'; 8 | 9 | module.exports.builder = function(accessToken) { 10 | return Request.builder() 11 | .withHost(DEFAULT_HOST) 12 | .withPort(DEFAULT_PORT) 13 | .withScheme(DEFAULT_SCHEME) 14 | .withAuth(accessToken); 15 | }; 16 | --------------------------------------------------------------------------------