├── .coveralls.yml ├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── 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 ├── get-info-about-current-user.js ├── get-related-artists.js ├── get-top-tracks-for-artist.js └── search-for-tracks.js ├── package.json ├── src ├── authentication-request.js ├── base-request.js ├── client.js ├── http-manager.js ├── server-methods.js ├── server.js ├── spotify-web-api.js ├── webapi-error.js └── webapi-request.js └── test ├── authentication-request.js ├── base-request.js ├── http-manager.js ├── spotify-web-api.js ├── webapi-error.js └── webapi-request.js /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | repo_token: vuiBI2GvUrtb2lU2yChUBCOPggEAxIdRs -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | coverage.html -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": false, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true 21 | } 22 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | *.log 3 | /test 4 | Gruntfile.js 5 | .jshintrc 6 | .npmignore -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'node' 4 | - '7' 5 | - '6' 6 | - '5.11' 7 | after_success: 8 | - npm run coveralls 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Change log 2 | 3 | 4 | #### 2.5.0 (4 Sep 2017) 5 | - 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). 6 | - 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). 7 | - 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). 8 | - Add support for currently playing. Thanks [@dustinblackman](https://github.com/dustinblackman) for [the PR](https://github.com/thelinmichael/spotify-web-api-node/pull/145). 9 | - 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). 10 | 11 | #### 2.4.0 (2 May 2017) 12 | - 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) 13 | - 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). 14 | 15 | #### 2.3.6 (15 October 2016) 16 | - Add language bindings for the **[Get Audio Analysis for a Track](https://developer.spotify.com/web-api/get-audio-analysis/)** endpoint. 17 | 18 | #### 2.3.5 (20 July 2016) 19 | - 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. 20 | 21 | #### 2.3.4 (18 July 2016) 22 | - Fixed a bug in `clientCredentialsGrant()`. 23 | 24 | #### 2.3.3 (18 July 2016) 25 | - 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. 26 | 27 | #### 2.3.2 (10 July 2016) 28 | - 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). 29 | 30 | #### 2.3.1 (3 July 2016) 31 | - 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. 32 | 33 | #### 2.3.0 (2 April 2016) 34 | - 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/). 35 | - 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. 36 | 37 | #### 2.2.0 (23 November 2015) 38 | - 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. 39 | 40 | #### 2.1.1 (23 November 2015) 41 | - Username encoding bugfix. 42 | 43 | #### 2.1.0 (16 July 2015) 44 | - Add language binding for **[Get Followed Artists](https://developer.spotify.com/web-api/get-followed-artists/)** 45 | 46 | #### 2.0.2 (11 May 2015) 47 | - Bugfix for retrieving an access token through the Client Credentials flow. (Thanks [Nate Wilkins](https://github.com/Nate-Wilkins)!) 48 | - Add test coverage and Travis CI. 49 | 50 | #### 2.0.1 (2 Mar 2015) 51 | - Return WebApiError objects if error occurs during authentication. 52 | 53 | #### 2.0.0 (27 Feb 2015) 54 | - **Breaking change**: Response object changed. Add headers and status code to all responses to enable users to implement caching. 55 | 56 | #### 1.3.13 (26 Feb 2015) 57 | - Add language binding for **[Reorder tracks in a Playlist](https://developer.spotify.com/web-api/reorder-playlists-tracks/)** 58 | 59 | #### 1.3.12 (22 Feb 2015) 60 | - Add language binding for **[Remove tracks in a Playlist by Position](https://developer.spotify.com/web-api/remove-tracks-playlist/)** 61 | 62 | #### 1.3.11 63 | - Add **[Search for Playlists](https://developer.spotify.com/web-api/search-item/)** endpoint. 64 | 65 | #### 1.3.10 66 | - Add market parameter to endpoints supporting **[Track Relinking](https://developer.spotify.com/web-api/track-relinking-guide/)**. 67 | - Improve SEO by adding keywords to the package.json file. ;-) 68 | 69 | #### 1.3.8 70 | - 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. 71 | 72 | #### 1.3.7 73 | - Add **[Check if Users are Following Playlist](https://developer.spotify.com/web-api/check-user-following-playlist/)** endpoint. 74 | 75 | #### 1.3.5 76 | - Add missing options parameter in createPlaylist (issue #19). Thanks for raising this [allinallin](https://github.com/allinallin). 77 | 78 | #### 1.3.4 79 | - Add **[Follow Playlist](https://developer.spotify.com/web-api/follow-playlist/)** and **[Unfollow Playlist](https://developer.spotify.com/web-api/unfollow-playlist/)** endpoints. 80 | 81 | #### 1.3.3 82 | - [Fix](https://github.com/thelinmichael/spotify-web-api-node/pull/18) error format. Thanks [extrakt](https://github.com/extrakt). 83 | 84 | #### 1.3.2 85 | - Add ability to use callback methods instead of promise. 86 | 87 | #### 1.2.2 88 | - Bugfix. api.addTracksToPlaylist tracks parameter can be a string or an array. Thanks [ofagbemi](https://github.com/ofagbemi)! 89 | 90 | #### 1.2.1 91 | - Add **[Follow endpoints](https://developer.spotify.com/web-api/web-api-follow-endpoints/)**. Great work [JMPerez](https://github.com/JMPerez). 92 | 93 | #### 1.1.0 94 | - Add **[Browse endpoints](https://developer.spotify.com/web-api/browse-endpoints/)**. Thanks [fsahin](https://github.com/fsahin). 95 | 96 | #### 1.0.2 97 | - Specify module's git repository. Thanks [vincentorback](https://github.com/vincentorback). 98 | 99 | #### 1.0.1 100 | - Allow options to be set when retrieving a user's playlists. Thanks [EaterOfCode](https://github.com/EaterOfCode). 101 | 102 | #### 1.0.0 103 | 104 | - Add **[Replace tracks in a Playlist](https://developer.spotify.com/web-api/replace-playlists-tracks/)** endpoint 105 | - Add **[Remove tracks in a Playlist](https://developer.spotify.com/web-api/remove-tracks-playlist/)** endpoint 106 | - Return errors as Error objects instead of unparsed JSON. Thanks [niftylettuce](https://github.com/niftylettuce). 107 | 108 | #### 0.0.11 109 | 110 | - 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). 111 | 112 | #### 0.0.10 113 | 114 | - 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/)**). 115 | - 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). 116 | 117 | #### 0.0.9 118 | 119 | - Add **[Related artists](https://developer.spotify.com/web-api/get-related-artists/)** endpoint 120 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | grunt.initConfig({ 5 | jshint: { 6 | all: [ 7 | 'src/*.js' 8 | ], 9 | options: { 10 | jshintrc: '.jshintrc', 11 | reporterOutput: '' 12 | } 13 | }, 14 | watch: { 15 | all: { 16 | files: [ 17 | 'src/*.js', 18 | 'test/*.js' 19 | ], 20 | tasks: ['default'] 21 | } 22 | }, 23 | simplemocha: { 24 | options: { 25 | globals: ['should'], 26 | ignoreLeaks: false, 27 | ui: 'bdd', 28 | reporter: 'spec' 29 | }, 30 | all: { 31 | src: ['test/*.js'] 32 | } 33 | }, 34 | }); 35 | grunt.loadNpmTasks('grunt-simple-mocha'); 36 | grunt.loadNpmTasks('grunt-contrib-jshint'); 37 | grunt.loadNpmTasks('grunt-contrib-watch'); 38 | grunt.registerTask('dev', ['jshint','simplemocha','watch']); 39 | grunt.registerTask('default', ['jshint','simplemocha']); 40 | }; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 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 | 4 | [![Greenkeeper badge](https://badges.greenkeeper.io/JMPerez/spotify-web-api-node.svg)](https://greenkeeper.io/) 5 | 6 | [![Tests](https://travis-ci.org/thelinmichael/spotify-web-api-node.svg?branch=master)](https://travis-ci.org/thelinmichael/spotify-web-api-node) 7 | [![Coverage Status](https://coveralls.io/repos/thelinmichael/spotify-web-api-node/badge.svg)](https://coveralls.io/r/thelinmichael/spotify-web-api-node) 8 | 9 | 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/web-api/code-examples/). 10 | 11 | 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). 12 | 13 | It includes helper functions to do the following: 14 | 15 | #### Music metadata 16 | - Albums, artists, and tracks 17 | - Audio features and analysis for tracks 18 | - Albums for a specific artist 19 | - Top tracks for a specific artist 20 | - Artists similar to a specific artist 21 | 22 | #### Profiles 23 | - User's emails, product type, display name, birthdate, image 24 | 25 | #### Search 26 | - Albums, artists, tracks, and playlists 27 | 28 | #### Playlist manipulation 29 | - Get a user's playlists 30 | - Create playlists 31 | - Change playlist details 32 | - Add tracks to a playlist 33 | - Remove tracks from a playlist 34 | - Replace tracks in a playlist 35 | - Reorder tracks in a playlist 36 | 37 | #### Your Music library 38 | - Add, remove, and get tracks and albums that are in the signed in user's Your Music library 39 | - Check if a track or album is in the signed in user's Your Music library 40 | 41 | #### Personalization 42 | - Get a user’s top artists and tracks based on calculated affinity 43 | - Get current user’s recently played tracks 44 | 45 | #### Browse 46 | - Get New Releases 47 | - Get Featured Playlists 48 | - Get a List of Categories 49 | - Get a Category 50 | - Get a Category's Playlists 51 | - Get recommendations based on seeds 52 | - Get available genre seeds 53 | 54 | #### Follow 55 | - Follow and unfollow users 56 | - Follow and unfollow artists 57 | - Check if the logged in user follows a user or artist 58 | - Follow a playlist 59 | - Unfollow a playlist 60 | - Get followed artists 61 | - Check if users are following a Playlist 62 | 63 | #### Player 64 | - Get a user's available devices 65 | - Get information about the user's current playback 66 | - Transfer a user's playback 67 | - Resume a user's playback 68 | - Skip a user's playback to next track 69 | - Skip a user's playback to previous track 70 | - Set a user's shuffle mode 71 | - Set a user's repeat mode 72 | 73 | All methods require authentication, which can be done using these flows: 74 | 75 | - [Client credentials flow](http://tools.ietf.org/html/rfc6749#section-4.4) (Application-only authentication) 76 | - [Authorization code grant](http://tools.ietf.org/html/rfc6749#section-4.1) (Signed by user) 77 | 78 | ##### Dependencies 79 | 80 | This project depends on [superagent](https://github.com/visionmedia/superagent) to make HTTP requests, and [promise](https://github.com/then/promise) as its [Promises/A+](http://promises-aplus.github.io/promises-spec/) implementation. 81 | 82 | ## Installation 83 | 84 | $ npm install spotify-web-api-node --save 85 | 86 | ## Usage 87 | 88 | First, instantiate the wrapper. 89 | ```javascript 90 | var SpotifyWebApi = require('spotify-web-api-node'); 91 | 92 | // credentials are optional 93 | var spotifyApi = new SpotifyWebApi({ 94 | clientId : 'fcecfc72172e4cd267473117a17cbd4d', 95 | clientSecret : 'a6338157c9bb5ac9c71924cb2940e1a7', 96 | redirectUri : 'http://www.example.com/callback' 97 | }); 98 | ``` 99 | 100 | 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. 101 | ```javascript 102 | spotifyApi.setAccessToken(''); 103 | ``` 104 | 105 | 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. 106 | ```javascript 107 | // Get Elvis' albums 108 | spotifyApi.getArtistAlbums('43ZHCT0cAZBISjO8DG9PnE') 109 | .then(function(data) { 110 | console.log('Artist albums', data.body); 111 | }, function(err) { 112 | console.error(err); 113 | }); 114 | ``` 115 | 116 | If you dont wan't to use promises, you can provide a callback method instead. 117 | ```javascript 118 | // Get Elvis' albums 119 | spotifyApi.getArtistAlbums('43ZHCT0cAZBISjO8DG9PnE', { limit: 10, offset: 20 }, function(err, data) { 120 | if (err) { 121 | console.error('Something went wrong!'); 122 | } else { 123 | console.log(data.body); 124 | } 125 | }); 126 | ``` 127 | 128 | 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. 129 | 130 | Note that the **options** parameter is currently **required if you're using callback methods**. 131 | 132 | ```javascript 133 | // Passing a callback - get Elvis' albums in range [20...29] 134 | spotifyApi.getArtistAlbums('43ZHCT0cAZBISjO8DG9PnE', {limit: 10, offset: 20}) 135 | .then(function(data) { 136 | console.log('Album information', data.body); 137 | }, function(err) { 138 | console.error(err); 139 | }); 140 | ``` 141 | 142 | ### Response body, statuscode, and headers 143 | To enable caching, this wrapper now exposes the response headers and not just the response body. Since version 2.0.0, the response object has the format 144 | 145 | ```json 146 | { 147 | "body" : { 148 | 149 | }, 150 | "headers" : { 151 | 152 | }, 153 | "statusCode" : 154 | } 155 | ``` 156 | 157 | In previous versions, the response object was the same as the response body. 158 | 159 | #### Example of a response 160 | 161 | Retrieving a track's metadata in `spotify-web-api-node` version 1.4.0 and later 162 | 163 | ```json 164 | { 165 | "body": 166 | { 167 | "name": "Golpe Maestro", 168 | "popularity": 42, 169 | "preview_url": "https://p.scdn.co/mp3-preview/4ac44a56e3a4b7b354c1273d7550bbad38c51f5d", 170 | "track_number": 1, 171 | "type": "track", 172 | "uri": "spotify:track:3Qm86XLflmIXVm1wcwkgDK" 173 | }, 174 | "headers": { 175 | "date": "Fri, 27 Feb 2015 09:25:48 GMT", 176 | "content-type": "application/json; charset=utf-8", 177 | "cache-control": "public, max-age=7200", 178 | }, 179 | "statusCode": 200 180 | } 181 | ``` 182 | 183 | The response object for the same request in earlier versions than to 2.0.0. 184 | ```json 185 | { 186 | "name": "Golpe Maestro", 187 | "popularity": 42, 188 | "preview_url": "https://p.scdn.co/mp3-preview/4ac44a56e3a4b7b354c1273d7550bbad38c51f5d", 189 | "track_number": 1, 190 | "type": "track", 191 | "uri": "spotify:track:3Qm86XLflmIXVm1wcwkgDK" 192 | } 193 | ``` 194 | 195 | 196 | ### More examples 197 | 198 | Below are examples for all helper functions. Longer examples of some requests can be found in the [examples folder](examples/). 199 | 200 | Please note that since version 1.3.2 all methods accept an optional callback method as their last parameter. These examples however only use promises. 201 | 202 | ```javascript 203 | var SpotifyWebApi = require('spotify-web-api-node'); 204 | 205 | var spotifyApi = new SpotifyWebApi(); 206 | 207 | // Get multiple albums 208 | spotifyApi.getAlbums(['5U4W9E5WsYb2jUQWePT8Xm', '3KyVcddATClQKIdtaap4bV']) 209 | .then(function(data) { 210 | console.log('Albums information', data.body); 211 | }, function(err) { 212 | console.error(err); 213 | }); 214 | 215 | // Get an artist 216 | spotifyApi.getArtist('2hazSY4Ef3aB9ATXW7F5w3') 217 | .then(function(data) { 218 | console.log('Artist information', data.body); 219 | }, function(err) { 220 | console.error(err); 221 | }); 222 | 223 | // Get multiple artists 224 | spotifyApi.getArtists(['2hazSY4Ef3aB9ATXW7F5w3', '6J6yx1t3nwIDyPXk5xa7O8']) 225 | .then(function(data) { 226 | console.log('Artists information', data.body); 227 | }, function(err) { 228 | console.error(err); 229 | }); 230 | 231 | // Get albums by a certain artist 232 | spotifyApi.getArtistAlbums('43ZHCT0cAZBISjO8DG9PnE') 233 | .then(function(data) { 234 | console.log('Artist albums', data.body); 235 | }, function(err) { 236 | console.error(err); 237 | }); 238 | 239 | // Search tracks whose name, album or artist contains 'Love' 240 | spotifyApi.searchTracks('Love') 241 | .then(function(data) { 242 | console.log('Search by "Love"', data.body); 243 | }, function(err) { 244 | console.error(err); 245 | }); 246 | 247 | // Search artists whose name contains 'Love' 248 | spotifyApi.searchArtists('Love') 249 | .then(function(data) { 250 | console.log('Search artists by "Love"', data.body); 251 | }, function(err) { 252 | console.error(err); 253 | }); 254 | 255 | // Search tracks whose artist's name contains 'Love' 256 | spotifyApi.searchTracks('artist:Love') 257 | .then(function(data) { 258 | console.log('Search tracks by "Love" in the artist name', data.body); 259 | }, function(err) { 260 | console.log('Something went wrong!', err); 261 | }); 262 | 263 | // Search tracks whose artist's name contains 'Kendrick Lamar', and track name contains 'Alright' 264 | spotifyApi.searchTracks('track:Alright artist:Kendrick Lamar') 265 | .then(function(data) { 266 | console.log('Search tracks by "Alright" in the track name and "Kendrick Lamar" in the artist name', data.body); 267 | }, function(err) { 268 | console.log('Something went wrong!', err); 269 | }); 270 | 271 | 272 | // Search playlists whose name or description contains 'workout' 273 | spotifyApi.searchPlaylists('workout') 274 | .then(function(data) { 275 | console.log('Found playlists are', data.body); 276 | }, function(err) { 277 | console.log('Something went wrong!', err); 278 | }); 279 | 280 | // Get tracks in an album 281 | spotifyApi.getAlbumTracks('41MnTivkwTO3UUJ8DrqEJJ', { limit : 5, offset : 1 }) 282 | .then(function(data) { 283 | console.log(data.body); 284 | }, function(err) { 285 | console.log('Something went wrong!', err); 286 | }); 287 | 288 | // Get an artist's top tracks 289 | spotifyApi.getArtistTopTracks('0oSGxfWSnnOXhD2fKuz2Gy', 'GB') 290 | .then(function(data) { 291 | console.log(data.body); 292 | }, function(err) { 293 | console.log('Something went wrong!', err); 294 | }); 295 | 296 | // Get artists related to an artist 297 | spotifyApi.getArtistRelatedArtists('0qeei9KQnptjwb8MgkqEoy') 298 | .then(function(data) { 299 | console.log(data.body); 300 | }, function(err) { 301 | done(err); 302 | }); 303 | 304 | /* Get Audio Features for a Track */ 305 | spotifyApi.getAudioFeaturesForTrack('3Qm86XLflmIXVm1wcwkgDK') 306 | .then(function(data) { 307 | console.log(data.body); 308 | }, function(err) { 309 | done(err); 310 | }); 311 | 312 | /* Get Audio Analysis for a Track */ 313 | spotifyApi.getAudioAnalysisForTrack('3Qm86XLflmIXVm1wcwkgDK') 314 | .then(function(data) { 315 | console.log(data.body); 316 | }, function(err) { 317 | done(err); 318 | }); 319 | 320 | /* Get Audio Features for several tracks */ 321 | spotifyApi.getAudioFeaturesForTracks(['4iV5W9uYEdYUVa79Axb7Rh', '3Qm86XLflmIXVm1wcwkgDK']) 322 | .then(function(data) { 323 | console.log(data.body); 324 | }, function(err) { 325 | done(err); 326 | }); 327 | 328 | 329 | /* 330 | * User methods 331 | */ 332 | 333 | // Get a user 334 | spotifyApi.getUser('petteralexis') 335 | .then(function(data) { 336 | console.log('Some information about this user', data.body); 337 | }, function(err) { 338 | console.log('Something went wrong!', err); 339 | }); 340 | 341 | // Get the authenticated user 342 | spotifyApi.getMe() 343 | .then(function(data) { 344 | console.log('Some information about the authenticated user', data.body); 345 | }, function(err) { 346 | console.log('Something went wrong!', err); 347 | }); 348 | 349 | /* 350 | * Playlist methods 351 | */ 352 | 353 | // Get a playlist 354 | spotifyApi.getPlaylist('thelinmichael', '5ieJqeLJjjI8iJWaxeBLuK') 355 | .then(function(data) { 356 | console.log('Some information about this playlist', data.body); 357 | }, function(err) { 358 | console.log('Something went wrong!', err); 359 | }); 360 | 361 | // Get a user's playlists 362 | spotifyApi.getUserPlaylists('thelinmichael') 363 | .then(function(data) { 364 | console.log('Retrieved playlists', data.body); 365 | },function(err) { 366 | console.log('Something went wrong!', err); 367 | }); 368 | 369 | // Create a private playlist 370 | spotifyApi.createPlaylist('thelinmichael', 'My Cool Playlist', { 'public' : false }) 371 | .then(function(data) { 372 | console.log('Created playlist!'); 373 | }, function(err) { 374 | console.log('Something went wrong!', err); 375 | }); 376 | 377 | // Add tracks to a playlist 378 | spotifyApi.addTracksToPlaylist('thelinmichael', '5ieJqeLJjjI8iJWaxeBLuK', ["spotify:track:4iV5W9uYEdYUVa79Axb7Rh", "spotify:track:1301WleyT98MSxVHPZCA6M"]) 379 | .then(function(data) { 380 | console.log('Added tracks to playlist!'); 381 | }, function(err) { 382 | console.log('Something went wrong!', err); 383 | }); 384 | 385 | // Add tracks to a specific position in a playlist 386 | spotifyApi.addTracksToPlaylist('thelinmichael', '5ieJqeLJjjI8iJWaxeBLuK', ["spotify:track:4iV5W9uYEdYUVa79Axb7Rh", "spotify:track:1301WleyT98MSxVHPZCA6M"], 387 | { 388 | position : 5 389 | }) 390 | .then(function(data) { 391 | console.log('Added tracks to playlist!'); 392 | }, function(err) { 393 | console.log('Something went wrong!', err); 394 | }); 395 | 396 | // Remove tracks from a playlist at a specific position 397 | spotifyApi.removeTracksFromPlaylistByPosition('thelinmichael', '5ieJqeLJjjI8iJWaxeBLuK', [0, 2, 130], "0wD+DKCUxiSR/WY8lF3fiCTb7Z8X4ifTUtqn8rO82O4Mvi5wsX8BsLj7IbIpLVM9") 398 | .then(function(data) { 399 | console.log('Tracks removed from playlist!'); 400 | }, function(err) { 401 | console.log('Something went wrong!', err); 402 | }); 403 | 404 | // Remove all occurrence of a track 405 | var tracks = [{ uri : "spotify:track:4iV5W9uYEdYUVa79Axb7Rh" }]; 406 | var options = { snapshot_id : "0wD+DKCUxiSR/WY8lF3fiCTb7Z8X4ifTUtqn8rO82O4Mvi5wsX8BsLj7IbIpLVM9" }; 407 | spotifyApi.removeTracksFromPlaylist('thelinmichael', '5ieJqeLJjjI8iJWaxeBLuK', tracks, options) 408 | .then(function(data) { 409 | console.log('Tracks removed from playlist!'); 410 | }, function(err) { 411 | console.log('Something went wrong!', err); 412 | }); 413 | 414 | // Reorder the first two tracks in a playlist to the place before the track at the 10th position 415 | var options = { "range_length" : 2 }; 416 | spotifyApi.reorderTracksInPlaylist('thelinmichael', '5ieJqeLJjjI8iJWaxeBLuK', 0, 10, options) 417 | .then(function(data) { 418 | console.log('Tracks reordered in playlist!'); 419 | }, function(err) { 420 | console.log('Something went wrong!', err); 421 | }); 422 | 423 | // Change playlist details 424 | spotifyApi.changePlaylistDetails('thelinmichael', '5ieJqeLJjjI8iJWaxeBLuK', 425 | { 426 | name: 'This is a new name for my Cool Playlist, and will become private', 427 | 'public' : false 428 | }).then(function(data) { 429 | console.log('Playlist is now private!'); 430 | }, function(err) { 431 | console.log('Something went wrong!', err); 432 | }); 433 | 434 | // Upload a custom playlist cover image 435 | spotifyApi.uploadCustomPlaylistCoverImage('thelinmichael', '5ieJqeLJjjI8iJWaxeBLuK','longbase64uri') 436 | .then(function(data) { 437 | console.log('Playlsit cover image uploaded!'); 438 | }, function(err) { 439 | console.log('Something went wrong!', err); 440 | }); 441 | 442 | // Follow a playlist (privately) 443 | spotifyApi.followPlaylist('thelinmichael', '5ieJqeLJjjI8iJWaxeBLuK', 444 | { 445 | 'public' : false 446 | }).then(function(data) { 447 | console.log('Playlist successfully followed privately!'); 448 | }, function(err) { 449 | console.log('Something went wrong!', err); 450 | }); 451 | 452 | // Unfollow a playlist 453 | spotifyApi.unfollowPlaylist('thelinmichael', '5ieJqeLJjjI8iJWaxeBLuK') 454 | .then(function(data) { 455 | console.log('Playlist successfully unfollowed!'); 456 | }, function(err) { 457 | console.log('Something went wrong!', err); 458 | }); 459 | 460 | // Check if Users are following a Playlist 461 | spotifyApi.areFollowingPlaylist('thelinmichael', '5ieJqeLJjjI8iJWaxeBLuK', ['thelinmichael', 'ella']) 462 | .then(function(data) { 463 | data.body.forEach(function(isFollowing) { 464 | console.log("User is following: " + isFollowing); 465 | }); 466 | }, function(err) { 467 | console.log('Something went wrong!', err); 468 | }); 469 | 470 | /* 471 | * Following Users and Artists methods 472 | */ 473 | 474 | /* Get followed artists */ 475 | spotifyApi.getFollowedArtists({ limit : 1 }) 476 | .then(function(data) { 477 | // 'This user is following 1051 artists!' 478 | console.log('This user is following ', data.body.artists.total, ' artists!'); 479 | }, function(err) { 480 | console.log('Something went wrong!', err); 481 | }); 482 | 483 | /* Follow a user */ 484 | // TBD. 485 | 486 | /* Follow an artist */ 487 | // TBD. 488 | 489 | /* Unfollow a user */ 490 | // TBD 491 | 492 | /* Unfollow an artist */ 493 | // TBD 494 | 495 | /* Check if a user is following a user */ 496 | // TBD 497 | 498 | /* Check if a user is following an artist */ 499 | // TBD 500 | 501 | /* 502 | * Your Music library methods 503 | */ 504 | 505 | /* Tracks */ 506 | 507 | // Get tracks in the signed in user's Your Music library 508 | spotifyApi.getMySavedTracks({ 509 | limit : 2, 510 | offset: 1 511 | }) 512 | .then(function(data) { 513 | console.log('Done!'); 514 | }, function(err) { 515 | console.log('Something went wrong!', err); 516 | }); 517 | 518 | 519 | // Check if tracks are in the signed in user's Your Music library 520 | spotifyApi.containsMySavedTracks(["5ybJm6GczjQOgTqmJ0BomP"]) 521 | .then(function(data) { 522 | 523 | // An array is returned, where the first element corresponds to the first track ID in the query 524 | var trackIsInYourMusic = data.body[0]; 525 | 526 | if (trackIsInYourMusic) { 527 | console.log('Track was found in the user\'s Your Music library'); 528 | } else { 529 | console.log('Track was not found.'); 530 | } 531 | }, function(err) { 532 | console.log('Something went wrong!', err); 533 | }); 534 | 535 | // Remove tracks from the signed in user's Your Music library 536 | spotifyApi.removeFromMySavedTracks(["3VNWq8rTnQG6fM1eldSpZ0"]) 537 | .then(function(data) { 538 | console.log('Removed!'); 539 | }, function(err) { 540 | console.log('Something went wrong!', err); 541 | }); 542 | }); 543 | 544 | // Add tracks to the signed in user's Your Music library 545 | spotifyApi.addToMySavedTracks(["3VNWq8rTnQG6fM1eldSpZ0"]) 546 | .then(function(data) { 547 | console.log('Added track!'); 548 | }, function(err) { 549 | console.log('Something went wrong!', err); 550 | }); 551 | }); 552 | 553 | /* Albums */ 554 | 555 | // Get albums in the signed in user's Your Music library 556 | spotifyApi.getMySavedAlbums({ 557 | limit : 1, 558 | offset: 0 559 | }) 560 | .then(function(data) { 561 | // Output items 562 | console.log(data.body.items); 563 | }, function(err) { 564 | console.log('Something went wrong!', err); 565 | }); 566 | 567 | 568 | // Check if albums are in the signed in user's Your Music library 569 | spotifyApi.containsMySavedAlbums(["1H8AHEB8VSE8irHViGOIrF"]) 570 | .then(function(data) { 571 | 572 | // An array is returned, where the first element corresponds to the first album ID in the query 573 | var albumIsInYourMusic = data.body[0]; 574 | 575 | if (albumIsInYourMusic) { 576 | console.log('Album was found in the user\'s Your Music library'); 577 | } else { 578 | console.log('Album was not found.'); 579 | } 580 | }, function(err) { 581 | console.log('Something went wrong!', err); 582 | }); 583 | 584 | // Remove albums from the signed in user's Your Music library 585 | spotifyApi.removeFromMySavedAlbums(["1H8AHEB8VSE8irHViGOIrF"]) 586 | .then(function(data) { 587 | console.log('Removed!'); 588 | }, function(err) { 589 | console.log('Something went wrong!', err); 590 | }); 591 | }); 592 | 593 | // Add albums to the signed in user's Your Music library 594 | spotifyApi.addToMySavedAlbums(["1H8AHEB8VSE8irHViGOIrF"]) 595 | .then(function(data) { 596 | console.log('Added album!'); 597 | }, function(err) { 598 | console.log('Something went wrong!', err); 599 | }); 600 | }); 601 | 602 | 603 | /* 604 | * Browse methods 605 | */ 606 | 607 | // Retrieve new releases 608 | spotifyApi.getNewReleases({ limit : 5, offset: 0, country: 'SE' }) 609 | .then(function(data) { 610 | console.log(data.body); 611 | done(); 612 | }, function(err) { 613 | console.log("Something went wrong!", err); 614 | }); 615 | }); 616 | 617 | // Retrieve featured playlists 618 | spotifyApi.getFeaturedPlaylists({ limit : 3, offset: 1, country: 'SE', locale: 'sv_SE', timestamp:'2014-10-23T09:00:00' }) 619 | .then(function(data) { 620 | console.log(data.body); 621 | }, function(err) { 622 | console.log("Something went wrong!", err); 623 | }); 624 | 625 | // Get a List of Categories 626 | spotifyApi.getCategories({ 627 | limit : 5, 628 | offset: 0, 629 | country: 'SE', 630 | locale: 'sv_SE' 631 | }) 632 | .then(function(data) { 633 | console.log(data.body); 634 | }, function(err) { 635 | console.log("Something went wrong!", err); 636 | }); 637 | 638 | // Get a Category (in Sweden) 639 | spotifyApi.getCategory('party', { 640 | country: 'SE', 641 | locale: 'sv_SE' 642 | }) 643 | .then(function(data) { 644 | console.log(data.body); 645 | }, function(err) { 646 | console.log("Something went wrong!", err); 647 | }); 648 | 649 | // Get Playlists for a Category (Party in Brazil) 650 | spotifyApi.getPlaylistsForCategory('party', { 651 | country: 'BR', 652 | limit : 2, 653 | offset : 0 654 | }) 655 | .then(function(data) { 656 | console.log(data.body); 657 | }, function(err) { 658 | console.log("Something went wrong!", err); 659 | }); 660 | 661 | 662 | 663 | /* Player */ 664 | 665 | // Get information about current playing song for signed in user 666 | spotifyApi.getMyCurrentPlaybackState({ 667 | }) 668 | .then(function(data) { 669 | // Output items 670 | console.log("Now Playing: ",data.body); 671 | }, function(err) { 672 | console.log('Something went wrong!', err); 673 | }); 674 | /* Get Recommendations Based on Seeds */ 675 | // TBD 676 | 677 | 678 | /** 679 | * Personalization Endpoints 680 | */ 681 | 682 | /* Get a User’s Top Artists and Tracks */ 683 | // TBD 684 | 685 | Get a User’s Top Artists and Tracks 686 | ``` 687 | 688 | ### Nesting calls 689 | ```javascript 690 | // track detail information for album tracks 691 | spotifyApi.getAlbum('5U4W9E5WsYb2jUQWePT8Xm') 692 | .then(function(data) { 693 | return data.body.tracks.map(function(t) { return t.id; }); 694 | }) 695 | .then(function(trackIds) { 696 | return spotifyApi.getTracks(trackIds); 697 | }) 698 | .then(function(data) { 699 | console.log(data.body); 700 | }) 701 | .catch(function(error) { 702 | console.error(error); 703 | }); 704 | 705 | // album detail for the first 10 Elvis' albums 706 | spotifyApi.getArtistAlbums('43ZHCT0cAZBISjO8DG9PnE', {limit: 10}) 707 | .then(function(data) { 708 | return data.body.albums.map(function(a) { return a.id; }); 709 | }) 710 | .then(function(albums) { 711 | return spotifyApi.getAlbums(albums); 712 | }).then(function(data) { 713 | console.log(data.body); 714 | }); 715 | ``` 716 | 717 | ### Authorization 718 | 719 | Supplying an access token is required for all requests to the Spotify API. This wrapper supports two authorization flows - The Authorization Code flow (signed by a user), and the Client Credentials flow (application authentication - the user isn't involved). See Spotify's [Authorization guide](https://developer.spotify.com/spotify-web-api/authorization-guide/) for detailed information on these flows. 720 | 721 | **Important: If you are writing a universal/isomorphic web app using this library, you will not be able to use those methods that send a client secret to the Spotify authorization service. Client secrets should be kept server-side and not exposed. Never include your client secret in the public JS served to the browser.** 722 | 723 | 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/). 724 | 725 | #### Authorization code flow 726 | 727 | 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/). 728 | 729 | In order to get permissions, you need to direct the user to our Accounts service. Generate the URL by using the wrapper's authorization URL method. 730 | 731 | ```javascript 732 | var scopes = ['user-read-private', 'user-read-email'], 733 | redirectUri = 'https://example.com/callback', 734 | clientId = '5fe01282e44241328a84e7c5cc169165', 735 | state = 'some-state-of-my-choice'; 736 | 737 | // Setting credentials can be done in the wrapper's constructor, or using the API object's setters. 738 | var spotifyApi = new SpotifyWebApi({ 739 | redirectUri : redirectUri, 740 | clientId : clientId 741 | }); 742 | 743 | // Create the authorization URL 744 | var authorizeURL = spotifyApi.createAuthorizeURL(scopes, state); 745 | 746 | // 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 747 | console.log(authorizeURL); 748 | ``` 749 | 750 | The example below uses a hardcoded authorization code, retrieved from the Accounts service as described above. 751 | 752 | ```javascript 753 | var credentials = { 754 | clientId : 'someClientId', 755 | clientSecret : 'someClientSecret', 756 | redirectUri : 'http://www.michaelthelin.se/test-callback' 757 | }; 758 | 759 | var spotifyApi = new SpotifyWebApi(credentials); 760 | 761 | // The code that's returned as a query parameter to the redirect URI 762 | var code = 'MQCbtKe23z7YzzS44KzZzZgjQa621hgSzHN'; 763 | 764 | // Retrieve an access token and a refresh token 765 | spotifyApi.authorizationCodeGrant(code) 766 | .then(function(data) { 767 | console.log('The token expires in ' + data.body['expires_in']); 768 | console.log('The access token is ' + data.body['access_token']); 769 | console.log('The refresh token is ' + data.body['refresh_token']); 770 | 771 | // Set the access token on the API object to use it in later calls 772 | spotifyApi.setAccessToken(data.body['access_token']); 773 | spotifyApi.setRefreshToken(data.body['refresh_token']); 774 | }, function(err) { 775 | console.log('Something went wrong!', err); 776 | }); 777 | ``` 778 | 779 | 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 unless it has expired. 780 | 781 | ```javascript 782 | // clientId, clientSecret and refreshToken has been set on the api object previous to this call. 783 | spotifyApi.refreshAccessToken() 784 | .then(function(data) { 785 | console.log('The access token has been refreshed!'); 786 | 787 | // Save the access token so that it's used in future calls 788 | spotifyApi.setAccessToken(data.body['access_token']); 789 | }, function(err) { 790 | console.log('Could not refresh access token', err); 791 | }); 792 | ``` 793 | 794 | #### Client Credential flow 795 | 796 | 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. 797 | 798 | ```javascript 799 | var clientId = 'someClientId', 800 | clientSecret = 'someClientSecret'; 801 | 802 | // Create the api object with the credentials 803 | var spotifyApi = new SpotifyWebApi({ 804 | clientId : clientId, 805 | clientSecret : clientSecret 806 | }); 807 | 808 | // Retrieve an access token. 809 | spotifyApi.clientCredentialsGrant() 810 | .then(function(data) { 811 | console.log('The access token expires in ' + data.body['expires_in']); 812 | console.log('The access token is ' + data.body['access_token']); 813 | 814 | // Save the access token so that it's used in future calls 815 | spotifyApi.setAccessToken(data.body['access_token']); 816 | }, function(err) { 817 | console.log('Something went wrong when retrieving an access token', err); 818 | }); 819 | ``` 820 | 821 | #### Setting credentials 822 | 823 | 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. 824 | 825 | Using setters, getters and resetters. 826 | ```javascript 827 | // Use setters to set all credentials one by one 828 | var spotifyApi = new SpotifyWebApi(); 829 | spotifyApi.setAccessToken('myAccessToken'); 830 | spotifyApi.setRefreshToken('myRefreshToken'); 831 | spotifyApi.setRedirectURI('http://www.example.com/test-callback'); 832 | spotifyApi.setClientId('myOwnClientId'); 833 | spotifyApi.setClientSecret('someSuperSecretString'); 834 | 835 | // Set all credentials at the same time 836 | spotifyApi.setCredentials({ 837 | 'accessToken' : 'myAccessToken', 838 | 'refreshToken' : 'myRefreshToken', 839 | 'redirectUri' : 'http://www.example.com/test-callback', 840 | 'clientId ' : 'myClientId', 841 | 'clientSecret' : 'myClientSecret' 842 | }); 843 | 844 | // Get the credentials one by one 845 | console.log('The access token is ' + spotifyApi.getAccessToken()); 846 | console.log('The refresh token is ' + spotifyApi.getRefreshToken()); 847 | console.log('The redirectURI is ' + spotifyApi.getRedirectURI()); 848 | console.log('The client ID is ' + spotifyApi.getClientId()); 849 | console.log('The client secret is ' + spotifyApi.getClientSecret()); 850 | 851 | // Get all credentials 852 | console.log('The credentials are ' + spotifyApi.getCredentials()); 853 | 854 | // Reset the credentials 855 | spotifyApi.resetAccessToken(); 856 | spotifyApi.resetRefreshToken(); 857 | spotifyApi.resetRedirectURI(); 858 | spotifyApi.resetClientId(); 859 | spotifyApi.resetClientSecret(); 860 | spotifyApi.resetCode(); 861 | 862 | // Reset all credentials at the same time 863 | spotifyApi.resetCredentials(); 864 | ``` 865 | 866 | Using the constructor. 867 | ```javascript 868 | // Set necessary parts of the credentials on the constructor 869 | var spotifyApi = new SpotifyWebApi({ 870 | clientId : 'myClientId', 871 | clientSecret : 'myClientSecret' 872 | }); 873 | 874 | // Get an access token and 'save' it using a setter 875 | spotifyApi.clientCredentialsGrant() 876 | .then(function(data) { 877 | console.log('The access token is ' + data.body['access_token']); 878 | spotifyApi.setAccessToken(data.body['access_token']); 879 | }, function(err) { 880 | console.log('Something went wrong!', err); 881 | }); 882 | ``` 883 | 884 | ```javascript 885 | // Set the credentials when making the request 886 | var spotifyApi = new SpotifyWebApi({ 887 | accessToken : 'njd9wng4d0ycwnn3g4d1jm30yig4d27iom5lg4d3' 888 | }); 889 | 890 | // Do search using the access token 891 | spotifyApi.searchTracks('artist:Love') 892 | .then(function(data) { 893 | console.log(data.body); 894 | }, function(err) { 895 | console.log('Something went wrong!', err); 896 | }); 897 | ``` 898 | 899 | ```javascript 900 | // Set the credentials when making the request 901 | var spotifyApi = new SpotifyWebApi({ 902 | accessToken : 'njd9wng4d0ycwnn3g4d1jm30yig4d27iom5lg4d3' 903 | }); 904 | 905 | // Get tracks in a playlist 906 | api.getPlaylistTracks('thelinmichael', '3ktAYNcRHpazJ9qecm3ptn', { 'offset' : 1, 'limit' : 5, 'fields' : 'items' }) 907 | .then(function(data) { 908 | console.log('The playlist contains these tracks', data.body); 909 | }, function(err) { 910 | console.log('Something went wrong!', err); 911 | }); 912 | ``` 913 | 914 | 915 | ## Development 916 | 917 | 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. 918 | 919 | ### Running tests 920 | 921 | You can run the unit tests executing `mocha` and get a test coverage report running `mocha -r blanket -R html-cov > coverage.html`. 922 | -------------------------------------------------------------------------------- /examples/access-token-refresh.js: -------------------------------------------------------------------------------- 1 | var 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 a code as documented here: 9 | * https://developer.spotify.com/spotify-web-api/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/spotify-web-api/using-scopes/ 14 | */ 15 | var authorizationCode = 'AQAgjS78s64u1axMCBCRA0cViW_ZDDU0pbgENJ_-WpZr3cEO7V5O-JELcEPU6pGLPp08SfO3dnHmu6XJikKqrU8LX9W6J11NyoaetrXtZFW-Y58UGeV69tuyybcNUS2u6eyup1EgzbTEx4LqrP_eCHsc9xHJ0JUzEhi7xcqzQG70roE4WKM_YrlDZO-e7GDRMqunS9RMoSwF_ov-gOMpvy9OMb7O58nZoc3LSEdEwoZPCLU4N4TTJ-IF6YsQRhQkEOJK'; 16 | 17 | /** 18 | * Set the credentials given on Spotify's My Applications page. 19 | * https://developer.spotify.com/my-applications 20 | */ 21 | var spotifyApi = new SpotifyWebApi({ 22 | clientId : '', 23 | clientSecret : '', 24 | redirectUri : '' 25 | }); 26 | 27 | // When our access token will expire 28 | var tokenExpirationEpoch; 29 | 30 | // First retrieve an access token 31 | spotifyApi.authorizationCodeGrant(authorizationCode) 32 | .then(function(data) { 33 | 34 | // Set the access token and refresh token 35 | spotifyApi.setAccessToken(data.body['access_token']); 36 | spotifyApi.setRefreshToken(data.body['refresh_token']); 37 | 38 | // Save the amount of seconds until the access token expired 39 | tokenExpirationEpoch = (new Date().getTime() / 1000) + data.body['expires_in']; 40 | console.log('Retrieved token. It expires in ' + Math.floor(tokenExpirationEpoch - new Date().getTime() / 1000) + ' seconds!'); 41 | }, function(err) { 42 | console.log('Something went wrong when retrieving the access token!', err.message); 43 | }); 44 | 45 | // Continually print out the time left until the token expires.. 46 | var numberOfTimesUpdated = 0; 47 | 48 | setInterval(function() { 49 | console.log('Time left: ' + Math.floor((tokenExpirationEpoch - new Date().getTime() / 1000)) + ' seconds left!'); 50 | 51 | // OK, we need to refresh the token. Stop printing and refresh. 52 | if (++numberOfTimesUpdated > 5) { 53 | clearInterval(this); 54 | 55 | // Refresh token and print the new time to expiration. 56 | spotifyApi.refreshAccessToken() 57 | .then(function(data) { 58 | tokenExpirationEpoch = (new Date().getTime() / 1000) + data.body['expires_in']; 59 | console.log('Refreshed token. It now expires in ' + Math.floor(tokenExpirationEpoch - new Date().getTime() / 1000) + ' seconds!'); 60 | }, function(err) { 61 | console.log('Could not refresh the token!', err.message); 62 | }); 63 | } 64 | }, 1000); 65 | -------------------------------------------------------------------------------- /examples/access-token-using-client-credentials.js: -------------------------------------------------------------------------------- 1 | var SpotifyWebApi = require("../"); 2 | 3 | /** 4 | * This example retrives an access token using the Client Credentials Flow. It's well documented here: 5 | * https://developer.spotify.com/web-api/authorization-guide/#client_credentials_flow 6 | */ 7 | 8 | /* 9 | * https://developer.spotify.com/spotify-web-api/using-scopes/ 10 | */ 11 | 12 | /** 13 | * Set the credentials given on Spotify's My Applications page. 14 | * https://developer.spotify.com/my-applications 15 | */ 16 | var spotifyApi = new SpotifyWebApi({ 17 | clientId : '', 18 | clientSecret : '', 19 | }); 20 | 21 | // Retrieve an access token 22 | spotifyApi.clientCredentialsGrant() 23 | .then(function(data) { 24 | console.log('The access token expires in ' + data.body['expires_in']); 25 | console.log('The access token is ' + data.body['access_token']); 26 | 27 | // Save the access token so that it's used in future calls 28 | spotifyApi.setAccessToken(data.body['access_token']); 29 | }, function(err) { 30 | console.log('Something went wrong when retrieving an access token', err.message); 31 | }); 32 | -------------------------------------------------------------------------------- /examples/add-remove-replace-tracks-in-a-playlist.js: -------------------------------------------------------------------------------- 1 | var SpotifyWebApi = require("../"); 2 | 3 | /** 4 | * This example demonstrates adding tracks, removing tracks, and replacing tracks in a playlist. At this time of writing this 5 | * documentation, this is the available playlist track modification features 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/spotify-web-api/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/spotify-web-api/using-scopes/ 13 | */ 14 | 15 | /* This code is hardcoded. For a working implementation, the code needs to be retrieved from the user. See documentation about 16 | * the Authorization Code flow for more information. 17 | */ 18 | var authorizationCode = ''; 19 | 20 | /** 21 | * Set the credentials given on Spotify's My Applications page. 22 | * https://developer.spotify.com/my-applications 23 | */ 24 | var spotifyApi = new SpotifyWebApi({ 25 | clientId : '', 26 | clientSecret : '', 27 | redirectUri : '' 28 | }); 29 | 30 | var playlistId; 31 | 32 | // First retrieve an access token 33 | spotifyApi.authorizationCodeGrant(authorizationCode) 34 | .then(function(data) { 35 | 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('thelinmichael', 'My New Awesome Playlist'); 41 | }).then(function(data) { 42 | console.log('Ok. Playlist created!'); 43 | playlistId = data.body['id']; 44 | 45 | // Add tracks to the playlist 46 | return spotifyApi.addTracksToPlaylist('thelinmichael', playlistId, ["spotify:track:4iV5W9uYEdYUVa79Axb7Rh", 47 | "spotify:track:6tcfwoGcDjxnSc6etAkDRR", 48 | "spotify:track:4iV5W9uYEdYUVa79Axb7Rh"]); 49 | 50 | }).then(function(data) { 51 | console.log('Ok. Tracks added!'); 52 | 53 | 54 | // Woops! Made a duplicate. Remove one of the duplicates from the playlist 55 | return spotifyApi.removeTracksFromPlaylist('thelinmichael', playlistId, 56 | [{ 57 | 'uri' : 'spotify:track:4iV5W9uYEdYUVa79Axb7Rh', 58 | 'positions' : [0] 59 | }]) 60 | 61 | }).then(function(data) { 62 | console.log('Ok. Tracks removed!'); 63 | 64 | // Actually, lets just replace all tracks in the playlist with something completely different 65 | return spotifyApi.replaceTracksInPlaylist('thelinmichael', playlistId, ['spotify:track:5Wd2bfQ7wc6GgSa32OmQU3', 66 | 'spotify:track:4r8lRYnoOGdEi6YyI5OC1o', 'spotify:track:4TZZvblv2yzLIBk2JwJ6Un', 'spotify:track:2IA4WEsWAYpV9eKkwR2UYv', 67 | 'spotify:track:6hDH3YWFdcUNQjubYztIsG']); 68 | 69 | }).then(function(data) { 70 | console.log('Ok. Tracks replaced!'); 71 | }).catch(function(err) { 72 | console.log(err.message); 73 | console.log('Something went wrong!'); 74 | }); 75 | -------------------------------------------------------------------------------- /examples/add-tracks-to-a-playlist.js: -------------------------------------------------------------------------------- 1 | var 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/spotify-web-api/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/spotify-web-api/using-scopes/ 12 | */ 13 | 14 | /* This code is hardcoded. For a working implementation, the code needs to be retrieved from the user. See documentation about 15 | * the Authorization Code flow for more information. 16 | */ 17 | var authorizationCode = ''; 18 | 19 | /** 20 | * Set the credentials given on Spotify's My Applications page. 21 | * https://developer.spotify.com/my-applications 22 | */ 23 | var spotifyApi = new SpotifyWebApi({ 24 | clientId : '', 25 | clientSecret : '', 26 | redirectUri : '' 27 | }); 28 | 29 | // First retrieve an access token 30 | spotifyApi.authorizationCodeGrant(authorizationCode) 31 | .then(function(data) { 32 | spotifyApi.setAccessToken(data.body['access_token']); 33 | return spotifyApi.addTracksToPlaylist('thelinmichael', '5ieJqeLJjjI8iJWaxeBLuK', ["spotify:track:4iV5W9uYEdYUVa79Axb7Rh", "spotify:track:1301WleyT98MSxVHPZCA6M"], 34 | { 35 | position : 10 36 | }) 37 | }).then(function(data) { 38 | console.log('Added tracks to the playlist!'); 39 | }).catch(function(err) { 40 | console.log('Something went wrong!', err.message); 41 | }); 42 | -------------------------------------------------------------------------------- /examples/get-info-about-current-user.js: -------------------------------------------------------------------------------- 1 | var 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/spotify-web-api/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/spotify-web-api/using-scopes/ 14 | */ 15 | var authorizationCode = 'AQAgjS78s64u1axMCBCRA0cViW_ZDDU0pbgENJ_-WpZr3cEO7V5O-JELcEPU6pGLPp08SfO3dnHmu6XJikKqrU8LX9W6J11NyoaetrXtZFW-Y58UGeV69tuyybcNUS2u6eyup1EgzbTEx4LqrP_eCHsc9xHJ0JUzEhi7xcqzQG70roE4WKM_YrlDZO-e7GDRMqunS9RMoSwF_ov-gOMpvy9OMb7O58nZoc3LSEdEwoZPCLU4N4TTJ-IF6YsQRhQkEOJK'; 16 | 17 | /* Set the credentials given on Spotify's My Applications page. 18 | * https://developer.spotify.com/my-applications 19 | */ 20 | var spotifyApi = new SpotifyWebApi({ 21 | clientId : '', 22 | clientSecret : '', 23 | redirectUri : '' 24 | }); 25 | 26 | // First retrieve an access token 27 | spotifyApi.authorizationCodeGrant(authorizationCode) 28 | .then(function(data) { 29 | console.log('Retrieved access token', data.body['access_token']); 30 | 31 | // Set the access token 32 | spotifyApi.setAccessToken(data.body['access_token']); 33 | 34 | // Use the access token to retrieve information about the user connected to it 35 | return spotifyApi.getMe(); 36 | }) 37 | .then(function(data) { 38 | // "Retrieved data for Faruk Sahin" 39 | console.log('Retrieved data for ' + data.body['display_name']); 40 | 41 | // "Email is farukemresahin@gmail.com" 42 | console.log('Email is ' + data.body.email); 43 | 44 | // "Image URL is http://media.giphy.com/media/Aab07O5PYOmQ/giphy.gif" 45 | console.log('Image URL is ' + data.body.images[0].url); 46 | 47 | // "This user has a premium account" 48 | console.log('This user has a ' + data.body.product + ' account'); 49 | }) 50 | .catch(function(err) { 51 | console.log('Something went wrong', err.message); 52 | }); 53 | -------------------------------------------------------------------------------- /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 this endpoint does not require authentication. However, using an access token 8 | * when making requests will give your application a higher rate limit. 9 | */ 10 | 11 | var spotifyApi = new SpotifyWebApi(); 12 | 13 | 14 | var artistId = '0qeei9KQnptjwb8MgkqEoy'; 15 | 16 | spotifyApi.getArtistRelatedArtists(artistId) 17 | .then(function(data) { 18 | 19 | if (data.body.artists.length) { 20 | // Print the number of similar artists 21 | console.log('I got ' + data.body.artists.length + ' similar artists!'); 22 | 23 | console.log('The most similar one is ' + data.body.artists[0].name); 24 | } else { 25 | console.log('I didn\'t find any similar artists.. Sorry.'); 26 | } 27 | 28 | }, function(err) { 29 | console.log('Something went wrong..', err.message); 30 | }); -------------------------------------------------------------------------------- /examples/get-top-tracks-for-artist.js: -------------------------------------------------------------------------------- 1 | var SpotifyWebApi = require("../"); 2 | 3 | /** 4 | * This example retrieves the top tracks for an artist. 5 | * https://developer.spotify.com/spotify-web-api/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/spotify-web-api/authorization-guide/#client_credentials_flow 15 | */ 16 | var spotifyApi = new SpotifyWebApi({ 17 | clientId : '', 18 | clientSecret : '' 19 | }); 20 | 21 | // Retrieve an access token 22 | spotifyApi.clientCredentialsGrant() 23 | .then(function(data) { 24 | // Set the access token on the API object so that it's used in all future requests 25 | spotifyApi.setAccessToken(data.body['access_token']); 26 | 27 | // Get the most popular tracks by David Bowie in Great Britain 28 | return spotifyApi.getArtistTopTracks('0oSGxfWSnnOXhD2fKuz2Gy', 'GB') 29 | }).then(function(data) { 30 | console.log('The most popular tracks for David Bowie is..'); 31 | console.log('Drum roll..') 32 | console.log('...') 33 | 34 | /* 35 | * 1. Space Oddity - 2009 Digital Remaster (popularity is 51) 36 | * 2. Heroes - 1999 Digital Remaster (popularity is 33) 37 | * 3. Let's Dance - 1999 Digital Remaster (popularity is 20) 38 | * 4. ... 39 | */ 40 | data.body.tracks.forEach(function(track, index) { 41 | console.log((index+1) + '. ' + track.name + ' (popularity is ' + track.popularity + ')'); 42 | }); 43 | 44 | }).catch(function(err) { 45 | console.log('Unfortunately, something has gone wrong.', err.message); 46 | }); 47 | -------------------------------------------------------------------------------- /examples/search-for-tracks.js: -------------------------------------------------------------------------------- 1 | var SpotifyWebApi = require("../"); 2 | 3 | /* 4 | * This example shows how to search for a track. The endpoint is documented here: 5 | * https://developer.spotify.com/web-api/search-item/ 6 | 7 | * Please note that this endpoint does not require authentication. However, using an access token 8 | * when making requests will give your application a higher rate limit. 9 | */ 10 | 11 | var spotifyApi = new SpotifyWebApi(); 12 | 13 | spotifyApi.searchTracks('Love', function(err, data) { 14 | if (err) { 15 | console.error('Something went wrong', err.message); 16 | return; 17 | } 18 | 19 | // Print some information about the results 20 | console.log('I got ' + data.body.tracks.total + ' results!'); 21 | 22 | // Go through the first page of results 23 | var firstPage = data.body.tracks.items; 24 | console.log('The tracks in the first page are.. (popularity in parentheses)'); 25 | 26 | /* 27 | * 0: All of Me (97) 28 | * 1: My Love (91) 29 | * 2: I Love This Life (78) 30 | * ... 31 | */ 32 | firstPage.forEach(function(track, index) { 33 | console.log(index + ': ' + track.name + ' (' + track.popularity + ')'); 34 | }); 35 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spotify-web-api-node", 3 | "version": "3.0.0", 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 | "license": "MIT", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/thelinmichael/spotify-web-api-node.git" 12 | }, 13 | "dependencies": { 14 | "superagent": "^3.8.2" 15 | }, 16 | "config": { 17 | "blanket": { 18 | "pattern": "src", 19 | "data-cover-never": "node_modules" 20 | } 21 | }, 22 | "scripts": { 23 | "lint": "grunt jshint", 24 | "test": "npm run lint && ./node_modules/.bin/mocha --bail", 25 | "coveralls": "./node_modules/.bin/mocha -r blanket -R mocha-lcov-reporter | ./node_modules/coveralls/bin/coveralls.js" 26 | }, 27 | "devDependencies": { 28 | "blanket": "^1.1.7", 29 | "coveralls": "^3.0.0", 30 | "grunt": "^1.0.1", 31 | "grunt-contrib-jshint": "~1.1.0", 32 | "grunt-contrib-watch": "~1.1.0", 33 | "grunt-simple-mocha": "0.4.x", 34 | "mocha": "~5.2.0", 35 | "mocha-lcov-reporter": "1.3.0", 36 | "mockery": "^2.1.0", 37 | "should": "~13.2.1", 38 | "sinon": "~7.3.0", 39 | "xunit-file": "1.0.0" 40 | }, 41 | "keywords": [ 42 | "spotify", 43 | "echonest", 44 | "music", 45 | "api", 46 | "wrapper", 47 | "client", 48 | "web api" 49 | ], 50 | "browser": { 51 | "./src/server.js": "./src/client.js" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /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 | }; -------------------------------------------------------------------------------- /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 () { return this[key]; }; 19 | }; 20 | 21 | Request.prototype.getHost = Request.prototype._getter('host'); 22 | 23 | Request.prototype.getPort = Request.prototype._getter('port'); 24 | 25 | Request.prototype.getScheme = Request.prototype._getter('scheme'); 26 | 27 | Request.prototype.getPath = Request.prototype._getter('path'); 28 | 29 | Request.prototype.getQueryParameters = Request.prototype._getter('queryParameters'); 30 | 31 | Request.prototype.getBodyParameters = Request.prototype._getter('bodyParameters'); 32 | 33 | Request.prototype.getHeaders = Request.prototype._getter('headers'); 34 | 35 | Request.prototype.getURI = function() { 36 | if (!this.scheme || !this.host || !this.port) { 37 | throw new Error('Missing components necessary to construct URI'); 38 | } 39 | var uri = this.scheme + '://' + this.host; 40 | if (this.scheme === 'http' && this.port !== 80 || 41 | this.scheme === 'https' && this.port !== 443) { 42 | uri += ':' + this.port; 43 | } 44 | if (this.path) { 45 | uri += this.path; 46 | } 47 | return uri; 48 | }; 49 | 50 | Request.prototype.getURL = function() { 51 | var uri = this.getURI(); 52 | if (this.getQueryParameters()) { 53 | return uri + this.getQueryParameterString(this.getQueryParameters()); 54 | } else { 55 | return uri; 56 | } 57 | }; 58 | 59 | Request.prototype.getQueryParameterString = function() { 60 | var queryParameters = this.getQueryParameters(); 61 | if (queryParameters) { 62 | return '?' + Object.keys(queryParameters).filter(function (key) { 63 | return queryParameters[key] !== undefined; 64 | }).map(function (key) { 65 | return key + '=' + queryParameters[key]; 66 | }).join('&'); 67 | } 68 | }; 69 | 70 | Request.prototype.execute = function (method, callback) { 71 | if (callback) { 72 | method(this, callback); 73 | return; 74 | } 75 | var _self = this; 76 | 77 | return new Promise(function(resolve, reject) { 78 | method(_self, function(error, result) { 79 | if (error) { 80 | reject(error); 81 | } else { 82 | resolve(result); 83 | } 84 | }); 85 | }); 86 | }; 87 | 88 | var Builder = function() { 89 | }; 90 | 91 | Builder.prototype._setter = function (key) { 92 | return function (value) { 93 | this[key] = value; 94 | return this; 95 | }; 96 | }; 97 | 98 | Builder.prototype.withHost = Builder.prototype._setter('host'); 99 | 100 | Builder.prototype.withPort = Builder.prototype._setter('port'); 101 | 102 | Builder.prototype.withScheme = Builder.prototype._setter('scheme'); 103 | 104 | Builder.prototype.withPath = Builder.prototype._setter('path'); 105 | 106 | Builder.prototype._assigner = function (key) { 107 | return function () { 108 | for (var i = 0; i < arguments.length; i++) { 109 | this[key] = this._assign(this[key], arguments[i]); 110 | } 111 | return this; 112 | }; 113 | }; 114 | 115 | Builder.prototype.withQueryParameters = Builder.prototype._assigner('queryParameters'); 116 | 117 | Builder.prototype.withBodyParameters = Builder.prototype._assigner('bodyParameters'); 118 | 119 | Builder.prototype.withHeaders = Builder.prototype._assigner('headers'); 120 | 121 | Builder.prototype.withAuth = function(accessToken) { 122 | if (accessToken) { 123 | this.withHeaders( 124 | {'Authorization' : 'Bearer ' + accessToken} 125 | ); 126 | } 127 | return this; 128 | }; 129 | 130 | Builder.prototype._assign = function(src, obj) { 131 | if (obj && Array.isArray(obj)) { 132 | return Object.assign(src || [], obj); 133 | } 134 | if (obj && Object.keys(obj).length > 0) { 135 | return Object.assign(src || {}, obj); 136 | } 137 | return src; 138 | }; 139 | 140 | Builder.prototype.build = function() { 141 | return new Request(this); 142 | }; 143 | 144 | module.exports.builder = function() { 145 | return new Builder(); 146 | }; 147 | -------------------------------------------------------------------------------- /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 | WebApiError = require('./webapi-error'); 5 | 6 | var HttpManager = {}; 7 | 8 | /* Create superagent options from the base request */ 9 | var _getParametersFromRequest = function(request) { 10 | 11 | var options = {}; 12 | 13 | if (request.getQueryParameters()) { 14 | options.query = request.getQueryParameters(); 15 | } 16 | 17 | if (request.getHeaders() && 18 | request.getHeaders()['Content-Type'] === 'application/json') { 19 | options.data = JSON.stringify(request.getBodyParameters()); 20 | } else if (request.getBodyParameters()) { 21 | options.data = request.getBodyParameters(); 22 | } 23 | 24 | if (request.getHeaders()) { 25 | options.headers = request.getHeaders(); 26 | } 27 | return options; 28 | }; 29 | 30 | /* Create an error object from an error returned from the Web API */ 31 | var _getErrorObject = function(defaultMessage, err) { 32 | var errorObject; 33 | if (typeof err.error === 'object' && typeof err.error.message === 'string') { 34 | // Web API Error format 35 | errorObject = new WebApiError(err.error.message, err.error.status); 36 | } else if (typeof err.error === 'string') { 37 | // Authorization Error format 38 | /* jshint ignore:start */ 39 | errorObject = new WebApiError(err.error + ': ' + err['error_description']); 40 | /* jshint ignore:end */ 41 | } else if (typeof err === 'string') { 42 | // Serialized JSON error 43 | try { 44 | var parsedError = JSON.parse(err); 45 | errorObject = new WebApiError(parsedError.error.message, parsedError.error.status); 46 | } catch (err) { 47 | // Error not JSON formatted 48 | } 49 | } 50 | 51 | if(!errorObject) { 52 | // Unexpected format 53 | errorObject = new WebApiError(defaultMessage + ': ' + JSON.stringify(err)); 54 | } 55 | 56 | return errorObject; 57 | }; 58 | 59 | /* Make the request to the Web API */ 60 | HttpManager._makeRequest = function(method, options, uri, callback) { 61 | var req = method(uri); 62 | 63 | if (options.query) { 64 | req.query(options.query); 65 | } 66 | 67 | if (options.data && (!options.headers || options.headers['Content-Type'] !== 'application/json')) { 68 | req.type('form'); 69 | req.send(options.data); 70 | } else if (options.data) { 71 | req.send(options.data); 72 | } 73 | 74 | if (options.headers) { 75 | req.set(options.headers); 76 | } 77 | 78 | req.end(function (err, response) { 79 | if (err) { 80 | var errorObject = _getErrorObject('Request error', { 81 | error: err 82 | }); 83 | return callback(errorObject); 84 | } 85 | 86 | return callback(null, { 87 | body: response.body, 88 | headers: response.headers, 89 | statusCode: response.statusCode 90 | }); 91 | }); 92 | }; 93 | 94 | /** 95 | * Make a HTTP GET request. 96 | * @param {BaseRequest} The request. 97 | * @param {Function} The callback function. 98 | */ 99 | HttpManager.get = function(request, callback) { 100 | var options = _getParametersFromRequest(request); 101 | var method = superagent.get; 102 | 103 | HttpManager._makeRequest(method, options, request.getURI(), callback); 104 | }; 105 | 106 | /** 107 | * Make a HTTP POST request. 108 | * @param {BaseRequest} The request. 109 | * @param {Function} The callback function. 110 | */ 111 | HttpManager.post = function(request, callback) { 112 | 113 | var options = _getParametersFromRequest(request); 114 | var method = superagent.post; 115 | 116 | HttpManager._makeRequest(method, options, request.getURI(), callback); 117 | }; 118 | 119 | /** 120 | * Make a HTTP DELETE request. 121 | * @param {BaseRequest} The request. 122 | * @param {Function} The callback function. 123 | */ 124 | HttpManager.del = function(request, callback) { 125 | 126 | var options = _getParametersFromRequest(request); 127 | var method = superagent.del; 128 | 129 | HttpManager._makeRequest(method, options, request.getURI(), callback); 130 | }; 131 | 132 | /** 133 | * Make a HTTP PUT request. 134 | * @param {BaseRequest} The request. 135 | * @param {Function} The callback function. 136 | */ 137 | HttpManager.put = function(request, callback) { 138 | 139 | var options = _getParametersFromRequest(request); 140 | var method = superagent.put; 141 | 142 | HttpManager._makeRequest(method, options, request.getURI(), callback); 143 | }; 144 | 145 | module.exports = HttpManager; 146 | -------------------------------------------------------------------------------- /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 | * Request an access token using the Client Credentials flow. 9 | * Requires that client ID and client secret has been set previous to the call. 10 | * @param {Object} options Options. 11 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 12 | * @returns {Promise|undefined} A promise that if successful, resolves into an object containing the access token, 13 | * token type and time to expiration. If rejected, it contains an error object. Not returned if a callback is given. 14 | */ 15 | clientCredentialsGrant: function(options, callback) { 16 | return AuthenticationRequest.builder() 17 | .withPath('/api/token') 18 | .withBodyParameters({ 19 | 'grant_type' : 'client_credentials' 20 | }) 21 | .withBodyParameters(options) 22 | .withHeaders({ 23 | Authorization : ('Basic ' + new Buffer(this.getClientId() + ':' + this.getClientSecret()).toString('base64')) 24 | }) 25 | .build() 26 | .execute(HttpManager.post, callback); 27 | }, 28 | 29 | /** 30 | * Request an access token using the Authorization Code flow. 31 | * Requires that client ID, client secret, and redirect URI has been set previous to the call. 32 | * @param {string} code The authorization code returned in the callback in the Authorization Code flow. 33 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 34 | * @returns {Promise|undefined} A promise that if successful, resolves into an object containing the access token, 35 | * refresh token, token type and time to expiration. If rejected, it contains an error object. 36 | * Not returned if a callback is given. 37 | */ 38 | authorizationCodeGrant: function(code, callback) { 39 | return AuthenticationRequest.builder() 40 | .withPath('/api/token') 41 | .withBodyParameters({ 42 | 'grant_type' : 'authorization_code', 43 | 'redirect_uri' : this.getRedirectURI(), 44 | 'code' : code, 45 | 'client_id' : this.getClientId(), 46 | 'client_secret' : this.getClientSecret() 47 | }) 48 | .build() 49 | .execute(HttpManager.post, callback); 50 | }, 51 | 52 | /** 53 | * Refresh the access token given that it hasn't expired. 54 | * Requires that client ID, client secret and refresh token has been set previous to the call. 55 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 56 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing the 57 | * access token, time to expiration and token type. If rejected, it contains an error object. 58 | * Not returned if a callback is given. 59 | */ 60 | refreshAccessToken: function(callback) { 61 | return AuthenticationRequest.builder() 62 | .withPath('/api/token') 63 | .withBodyParameters({ 64 | 'grant_type' : 'refresh_token', 65 | 'refresh_token' : this.getRefreshToken() 66 | }) 67 | .withHeaders({ 68 | Authorization : ('Basic ' + new Buffer(this.getClientId() + ':' + this.getClientSecret()).toString('base64')) 69 | }) 70 | .build() 71 | .execute(HttpManager.post, callback); 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /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 | 13 | setCredentials: function(credentials) { 14 | for (var key in credentials) { 15 | if (credentials.hasOwnProperty(key)) { 16 | this._credentials[key] = credentials[key]; 17 | } 18 | } 19 | }, 20 | 21 | getCredentials: function() { 22 | return this._credentials; 23 | }, 24 | 25 | resetCredentials: function() { 26 | this._credentials = null; 27 | }, 28 | 29 | setClientId: function(clientId) { 30 | this._setCredential('clientId', clientId); 31 | }, 32 | 33 | setClientSecret: function(clientSecret) { 34 | this._setCredential('clientSecret', clientSecret); 35 | }, 36 | 37 | setAccessToken: function(accessToken) { 38 | this._setCredential('accessToken', accessToken); 39 | }, 40 | 41 | setRefreshToken: function(refreshToken) { 42 | this._setCredential('refreshToken', refreshToken); 43 | }, 44 | 45 | setRedirectURI: function(redirectUri) { 46 | this._setCredential('redirectUri', redirectUri); 47 | }, 48 | 49 | getRedirectURI: function() { 50 | return this._getCredential('redirectUri'); 51 | }, 52 | 53 | getClientId: function() { 54 | return this._getCredential('clientId'); 55 | }, 56 | 57 | getClientSecret: function() { 58 | return this._getCredential('clientSecret'); 59 | }, 60 | 61 | getAccessToken: function() { 62 | return this._getCredential('accessToken'); 63 | }, 64 | 65 | getRefreshToken: function() { 66 | return this._getCredential('refreshToken'); 67 | }, 68 | 69 | resetClientId: function() { 70 | this._resetCredential('clientId'); 71 | }, 72 | 73 | resetClientSecret: function() { 74 | this._resetCredential('clientSecret'); 75 | }, 76 | 77 | resetAccessToken: function() { 78 | this._resetCredential('accessToken'); 79 | }, 80 | 81 | resetRefreshToken: function() { 82 | this._resetCredential('refreshToken'); 83 | }, 84 | 85 | resetRedirectURI: function() { 86 | this._resetCredential('redirectUri'); 87 | }, 88 | 89 | _setCredential: function(credentialKey, value) { 90 | this._credentials = this._credentials || {}; 91 | this._credentials[credentialKey] = value; 92 | }, 93 | 94 | _getCredential: function(credentialKey) { 95 | if (!this._credentials) { 96 | return; 97 | } else { 98 | return this._credentials[credentialKey]; 99 | } 100 | }, 101 | 102 | _resetCredential: function(credentialKey) { 103 | if (!this._credentials) { 104 | return; 105 | } else { 106 | this._credentials[credentialKey] = null; 107 | } 108 | }, 109 | 110 | /** 111 | * Look up a track. 112 | * @param {string} trackId The track's ID. 113 | * @param {Object} [options] The possible options, currently only market. 114 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 115 | * @example getTrack('3Qm86XLflmIXVm1wcwkgDK').then(...) 116 | * @returns {Promise|undefined} A promise that if successful, returns an object containing information 117 | * about the track. Not returned if a callback is given. 118 | */ 119 | getTrack: function(trackId, options, callback) { 120 | // In case someone is using a version where options parameter did not exist. 121 | var actualCallback, actualOptions; 122 | if (typeof options === 'function' && !callback) { 123 | actualCallback = options; 124 | actualOptions = {}; 125 | } else { 126 | actualCallback = callback; 127 | actualOptions = options; 128 | } 129 | 130 | return WebApiRequest.builder(this.getAccessToken()) 131 | .withPath('/v1/tracks/' + trackId) 132 | .withQueryParameters(actualOptions) 133 | .build() 134 | .execute(HttpManager.get, actualCallback); 135 | }, 136 | 137 | /** 138 | * Look up several tracks. 139 | * @param {string[]} trackIds The IDs of the artists. 140 | * @param {Object} [options] The possible options, currently only market. 141 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 142 | * @example getArtists(['0oSGxfWSnnOXhD2fKuz2Gy', '3dBVyJ7JuOMt4GE9607Qin']).then(...) 143 | * @returns {Promise|undefined} A promise that if successful, returns an object containing information 144 | * about the artists. Not returned if a callback is given. 145 | */ 146 | getTracks: function(trackIds, options, callback) { 147 | // In case someone is using a version where options parameter did not exist. 148 | var actualCallback, actualOptions; 149 | if (typeof options === 'function' && !callback) { 150 | actualCallback = options; 151 | actualOptions = {}; 152 | } else { 153 | actualCallback = callback; 154 | actualOptions = options; 155 | } 156 | 157 | return WebApiRequest.builder(this.getAccessToken()) 158 | .withPath('/v1/tracks') 159 | .withQueryParameters({ 160 | 'ids' : trackIds.join(',') 161 | }, actualOptions) 162 | .build() 163 | .execute(HttpManager.get, actualCallback); 164 | }, 165 | 166 | /** 167 | * Look up an album. 168 | * @param {string} albumId The album's ID. 169 | * @param {Object} [options] The possible options, currently only market. 170 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 171 | * @example getAlbum('0sNOF9WDwhWunNAHPD3Baj').then(...) 172 | * @returns {Promise|undefined} A promise that if successful, returns an object containing information 173 | * about the album. Not returned if a callback is given. 174 | */ 175 | getAlbum: function(albumId, options, callback) { 176 | // In case someone is using a version where options parameter did not exist. 177 | var actualCallback, actualOptions; 178 | if (typeof options === 'function' && !callback) { 179 | actualCallback = options; 180 | actualOptions = {}; 181 | } else { 182 | actualCallback = callback; 183 | actualOptions = options; 184 | } 185 | 186 | return WebApiRequest.builder(this.getAccessToken()) 187 | .withPath('/v1/albums/' + albumId) 188 | .withQueryParameters(actualOptions) 189 | .build() 190 | .execute(HttpManager.get, actualCallback); 191 | }, 192 | 193 | /** 194 | * Look up several albums. 195 | * @param {string[]} albumIds The IDs of the albums. 196 | * @param {Object} [options] The possible options, currently only market. 197 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 198 | * @example getAlbums(['0oSGxfWSnnOXhD2fKuz2Gy', '3dBVyJ7JuOMt4GE9607Qin']).then(...) 199 | * @returns {Promise|undefined} A promise that if successful, returns an object containing information 200 | * about the albums. Not returned if a callback is given. 201 | */ 202 | getAlbums: function(albumIds, options, callback) { 203 | // In case someone is using a version where options parameter did not exist. 204 | var actualCallback, actualOptions; 205 | if (typeof options === 'function' && !callback) { 206 | actualCallback = options; 207 | actualOptions = {}; 208 | } else { 209 | actualCallback = callback; 210 | actualOptions = options; 211 | } 212 | 213 | return WebApiRequest.builder(this.getAccessToken()) 214 | .withPath('/v1/albums') 215 | .withQueryParameters({ 216 | 'ids' : albumIds.join(',') 217 | }, actualOptions) 218 | .build() 219 | .execute(HttpManager.get, actualCallback); 220 | }, 221 | 222 | /** 223 | * Look up an artist. 224 | * @param {string} artistId The artist's ID. 225 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 226 | * @example api.getArtist('1u7kkVrr14iBvrpYnZILJR').then(...) 227 | * @returns {Promise|undefined} A promise that if successful, returns an object containing information 228 | * about the artist. Not returned if a callback is given. 229 | */ 230 | getArtist: function(artistId, callback) { 231 | return WebApiRequest.builder(this.getAccessToken()) 232 | .withPath('/v1/artists/' + artistId) 233 | .build() 234 | .execute(HttpManager.get, callback); 235 | }, 236 | 237 | /** 238 | * Look up several artists. 239 | * @param {string[]} artistIds The IDs of the artists. 240 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 241 | * @example getArtists(['0oSGxfWSnnOXhD2fKuz2Gy', '3dBVyJ7JuOMt4GE9607Qin']).then(...) 242 | * @returns {Promise|undefined} A promise that if successful, returns an object containing information 243 | * about the artists. Not returned if a callback is given. 244 | */ 245 | getArtists: function(artistIds, callback) { 246 | return WebApiRequest.builder(this.getAccessToken()) 247 | .withPath('/v1/artists') 248 | .withQueryParameters({ 249 | 'ids' : artistIds.join(',') 250 | }) 251 | .build() 252 | .execute(HttpManager.get, callback); 253 | }, 254 | 255 | /** 256 | * Search for music entities of certain types. 257 | * @param {string} query The search query. 258 | * @param {string[]} types An array of item types to search across. 259 | * Valid types are: 'album', 'artist', 'playlist', and 'track'. 260 | * @param {Object} [options] The possible options, e.g. limit, offset. 261 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 262 | * @example search('Abba', ['track', 'playlist'], { limit : 5, offset : 1 }).then(...) 263 | * @returns {Promise|undefined} A promise that if successful, returns an object containing the 264 | * search results. The result is paginated. If the promise is rejected, 265 | * it contains an error object. Not returned if a callback is given. 266 | */ 267 | search: function(query, types, options, callback) { 268 | return WebApiRequest.builder(this.getAccessToken()) 269 | .withPath('/v1/search/') 270 | .withQueryParameters({ 271 | type : types.join(','), 272 | q : query 273 | }, options) 274 | .build() 275 | .execute(HttpManager.get, callback); 276 | }, 277 | 278 | /** 279 | * Search for an album. 280 | * @param {string} query The search query. 281 | * @param {Object} [options] The possible options, e.g. limit, offset. 282 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 283 | * @example searchAlbums('Space Oddity', { limit : 5, offset : 1 }).then(...) 284 | * @returns {Promise|undefined} A promise that if successful, returns an object containing the 285 | * search results. The result is paginated. If the promise is rejected, 286 | * it contains an error object. Not returned if a callback is given. 287 | */ 288 | searchAlbums: function(query, options, callback) { 289 | return this.search(query, ['album'], options, callback); 290 | }, 291 | 292 | /** 293 | * Search for an artist. 294 | * @param {string} query The search query. 295 | * @param {Object} [options] The possible options, e.g. limit, offset. 296 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 297 | * @example searchArtists('David Bowie', { limit : 5, offset : 1 }).then(...) 298 | * @returns {Promise|undefined} A promise that if successful, returns an object containing the 299 | * search results. The result is paginated. If the promise is rejected, 300 | * it contains an error object. Not returned if a callback is given. 301 | */ 302 | searchArtists: function(query, options, callback) { 303 | return this.search(query, ['artist'], options, callback); 304 | }, 305 | 306 | /** 307 | * Search for a track. 308 | * @param {string} query The search query. 309 | * @param {Object} [options] The possible options, e.g. limit, offset. 310 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 311 | * @example searchTracks('Mr. Brightside', { limit : 3, offset : 2 }).then(...) 312 | * @returns {Promise|undefined} A promise that if successful, returns an object containing the 313 | * search results. The result is paginated. If the promise is rejected, 314 | * it contains an error object. Not returned if a callback is given. 315 | */ 316 | searchTracks: function(query, options, callback) { 317 | return this.search(query, ['track'], options, callback); 318 | }, 319 | 320 | /** 321 | * Search for playlists. 322 | * @param {string} query The search query. 323 | * @param {Object} options The possible options. 324 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 325 | * @example searchPlaylists('workout', { limit : 1, offset : 0 }).then(...) 326 | * @returns {Promise|undefined} A promise that if successful, returns an object containing the 327 | * search results. The result is paginated. If the promise is rejected, 328 | * it contains an error object. Not returned if a callback is given. 329 | */ 330 | searchPlaylists: function(query, options, callback) { 331 | return this.search(query, ['playlist'], options, callback); 332 | }, 333 | 334 | /** 335 | * Get an artist's albums. 336 | * @param {string} artistId The artist's ID. 337 | * @options {Object} [options] The possible options, e.g. limit, offset. 338 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 339 | * @example getArtistAlbums('0oSGxfWSnnOXhD2fKuz2Gy', { album_type : 'album', country : 'GB', limit : 2, offset : 5 }).then(...) 340 | * @returns {Promise|undefined} A promise that if successful, returns an object containing the albums 341 | * for the given artist. The result is paginated. If the promise is rejected, 342 | * it contains an error object. Not returned if a callback is given. 343 | */ 344 | getArtistAlbums: function(artistId, options, callback) { 345 | return WebApiRequest.builder(this.getAccessToken()) 346 | .withPath('/v1/artists/' + artistId + '/albums') 347 | .withQueryParameters(options) 348 | .build() 349 | .execute(HttpManager.get, callback); 350 | }, 351 | 352 | /** 353 | * Get the tracks of an album. 354 | * @param albumId the album's ID. 355 | * @options {Object} [options] The possible options, e.g. limit. 356 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 357 | * @example getAlbumTracks('41MnTivkwTO3UUJ8DrqEJJ', { limit : 5, offset : 1 }).then(...) 358 | * @returns {Promise|undefined} A promise that if successful, returns an object containing the 359 | * tracks in the album. The result is paginated. If the promise is rejected. 360 | * it contains an error object. Not returned if a callback is given. 361 | */ 362 | getAlbumTracks: function(albumId, options, callback) { 363 | return WebApiRequest.builder(this.getAccessToken()) 364 | .withPath('/v1/albums/' + albumId + '/tracks') 365 | .withQueryParameters(options) 366 | .build() 367 | .execute(HttpManager.get, callback); 368 | }, 369 | 370 | /** 371 | * Get an artist's top tracks. 372 | * @param {string} artistId The artist's ID. 373 | * @param {string} country The country/territory where the tracks are most popular. (format: ISO 3166-1 alpha-2) 374 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 375 | * @example getArtistTopTracks('0oSGxfWSnnOXhD2fKuz2Gy', 'GB').then(...) 376 | * @returns {Promise|undefined} A promise that if successful, returns an object containing the 377 | * artist's top tracks in the given country. If the promise is rejected, 378 | * it contains an error object. Not returned if a callback is given. 379 | */ 380 | getArtistTopTracks: function(artistId, country, callback) { 381 | return WebApiRequest.builder(this.getAccessToken()) 382 | .withPath('/v1/artists/' + artistId + '/top-tracks') 383 | .withQueryParameters({ 384 | 'country' : country 385 | }) 386 | .build() 387 | .execute(HttpManager.get, callback); 388 | }, 389 | 390 | /** 391 | * Get related artists. 392 | * @param {string} artistId The artist's ID. 393 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 394 | * @example getArtistRelatedArtists('0oSGxfWSnnOXhD2fKuz2Gy').then(...) 395 | * @returns {Promise|undefined} A promise that if successful, returns an object containing the 396 | * related artists. If the promise is rejected, it contains an error object. Not returned if a callback is given. 397 | */ 398 | getArtistRelatedArtists: function(artistId, callback) { 399 | return WebApiRequest.builder(this.getAccessToken()) 400 | .withPath('/v1/artists/' + artistId + '/related-artists') 401 | .build() 402 | .execute(HttpManager.get, callback); 403 | }, 404 | 405 | /** 406 | * Get information about a user. 407 | * @param userId The user ID. 408 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 409 | * @example getUser('thelinmichael').then(...) 410 | * @returns {Promise|undefined} A promise that if successful, resolves to an object 411 | * containing information about the user. If the promise is 412 | * rejected, it contains an error object. Not returned if a callback is given. 413 | */ 414 | getUser: function(userId, callback) { 415 | return WebApiRequest.builder(this.getAccessToken()) 416 | .withPath('/v1/users/' + encodeURIComponent(userId)) 417 | .build() 418 | .execute(HttpManager.get, callback); 419 | }, 420 | 421 | /** 422 | * Get information about the user that has signed in (the current user). 423 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 424 | * @example getMe().then(...) 425 | * @returns {Promise|undefined} A promise that if successful, resolves to an object 426 | * containing information about the user. The amount of information 427 | * depends on the permissions given by the user. If the promise is 428 | * rejected, it contains an error object. Not returned if a callback is given. 429 | */ 430 | getMe: function(callback) { 431 | return WebApiRequest.builder(this.getAccessToken()) 432 | .withPath('/v1/me') 433 | .build() 434 | .execute(HttpManager.get, callback); 435 | }, 436 | 437 | /** 438 | * Get a user's playlists. 439 | * @param {string} userId An optional id of the user. If you know the Spotify URI it is easy 440 | * to find the id (e.g. spotify:user:). If not provided, the id of the user that granted 441 | * the permissions will be used. 442 | * @param {Object} [options] The options supplied to this request. 443 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 444 | * @example getUserPlaylists('thelinmichael').then(...) 445 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing 446 | * a list of playlists. If rejected, it contains an error object. Not returned if a callback is given. 447 | */ 448 | getUserPlaylists: function(userId, options, callback) { 449 | var path; 450 | if (typeof userId === 'string') { 451 | path = '/v1/users/' + encodeURIComponent(userId) + '/playlists'; 452 | } else { 453 | path = '/v1/me/playlists'; 454 | } 455 | 456 | return WebApiRequest.builder(this.getAccessToken()) 457 | .withPath(path) 458 | .withQueryParameters(options) 459 | .build() 460 | .execute(HttpManager.get, callback); 461 | }, 462 | 463 | /** 464 | * Get a playlist. 465 | * @param {string} userId The playlist's owner's user ID. 466 | * @param {string} playlistId The playlist's ID. 467 | * @param {Object} [options] The options supplied to this request. 468 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 469 | * @example getPlaylist('thelinmichael', '3EsfV6XzCHU8SPNdbnFogK').then(...) 470 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing 471 | * the playlist. If rejected, it contains an error object. Not returned if a callback is given. 472 | */ 473 | getPlaylist: function(userId, playlistId, options, callback) { 474 | return WebApiRequest.builder(this.getAccessToken()) 475 | .withPath('/v1/users/' + encodeURIComponent(userId) + '/playlists/' + playlistId) 476 | .withQueryParameters(options) 477 | .build() 478 | .execute(HttpManager.get, callback); 479 | }, 480 | 481 | /** 482 | * Get tracks in a playlist. 483 | * @param {string} userId THe playlist's owner's user ID. 484 | * @param {string} playlistId The playlist's ID. 485 | * @param {Object} [options] Optional options, such as fields. 486 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 487 | * @example getPlaylistTracks('thelinmichael', '3ktAYNcRHpazJ9qecm3ptn').then(...) 488 | * @returns {Promise|undefined} A promise that if successful, resolves to an object that containing 489 | * the tracks in the playlist. If rejected, it contains an error object. Not returned if a callback is given. 490 | */ 491 | getPlaylistTracks: function(userId, playlistId, options, callback) { 492 | return WebApiRequest.builder(this.getAccessToken()) 493 | .withPath('/v1/users/' + encodeURIComponent(userId) + '/playlists/' + playlistId + '/tracks') 494 | .withQueryParameters(options) 495 | .build() 496 | .execute(HttpManager.get, callback); 497 | }, 498 | 499 | /** 500 | * Create a playlist. 501 | * @param {string} userId The playlist's owner's user ID. 502 | * @param {string} playlistName The name of the playlist. 503 | * @param {Object} [options] The possible options, currently only public. 504 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 505 | * @example createPlaylist('thelinmichael', 'My cool playlist!', { public : false }).then(...) 506 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing information about the 507 | * created playlist. If rejected, it contains an error object. Not returned if a callback is given. 508 | */ 509 | createPlaylist: function(userId, playlistName, options, callback) { 510 | // In case someone is using a version where options parameter did not exist. 511 | var actualCallback; 512 | if (typeof options === 'function' && !callback) { 513 | actualCallback = options; 514 | } else { 515 | actualCallback = callback; 516 | } 517 | 518 | var actualOptions = { 'name' : playlistName }; 519 | if (typeof options === 'object') { 520 | Object.keys(options).forEach(function(key) { 521 | actualOptions[key] = options[key]; 522 | }); 523 | } 524 | 525 | return WebApiRequest.builder(this.getAccessToken()) 526 | .withPath('/v1/users/' + encodeURIComponent(userId) + '/playlists') 527 | .withHeaders({ 'Content-Type' : 'application/json' }) 528 | .withBodyParameters(actualOptions) 529 | .build() 530 | .execute(HttpManager.post, actualCallback); 531 | }, 532 | 533 | /** 534 | * Follow a playlist. 535 | * @param {string} userId The playlist's owner's user ID 536 | * @param {string} playlistId The playlist's ID 537 | * @param {Object} [options] The possible options, currently only public. 538 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 539 | * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected, 540 | * it contains an error object. Not returned if a callback is given. 541 | */ 542 | followPlaylist: function(userId, playlistId, options, callback) { 543 | return WebApiRequest.builder(this.getAccessToken()) 544 | .withPath('/v1/users/' + encodeURIComponent(userId) + '/playlists/' + playlistId + '/followers') 545 | .withHeaders({ 'Content-Type' : 'application/json' }) 546 | .withBodyParameters(options) 547 | .build() 548 | .execute(HttpManager.put, callback); 549 | }, 550 | 551 | /** 552 | * Unfollow a playlist. 553 | * @param {string} userId The playlist's owner's user ID 554 | * @param {string} playlistId The playlist's ID 555 | * @param {Object} [options] The possible options, currently only public. 556 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 557 | * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected, 558 | * it contains an error object. Not returned if a callback is given. 559 | */ 560 | unfollowPlaylist: function(userId, playlistId, callback) { 561 | return WebApiRequest.builder(this.getAccessToken()) 562 | .withPath('/v1/users/' + encodeURIComponent(userId) + '/playlists/' + playlistId + '/followers') 563 | .withHeaders({ 'Content-Type' : 'application/json' }) 564 | .build() 565 | .execute(HttpManager.del, callback); 566 | 567 | }, 568 | 569 | /** 570 | * Change playlist details. 571 | * @param {string} userId The playlist's owner's user ID 572 | * @param {string} playlistId The playlist's ID 573 | * @param {Object} [options] The possible options, e.g. name, public. 574 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 575 | * @example changePlaylistDetails('thelinmichael', '3EsfV6XzCHU8SPNdbnFogK', {name: 'New name', public: true}).then(...) 576 | * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected, 577 | * it contains an error object. Not returned if a callback is given. 578 | */ 579 | changePlaylistDetails: function(userId, playlistId, options, callback) { 580 | return WebApiRequest.builder(this.getAccessToken()) 581 | .withPath('/v1/users/' + encodeURIComponent(userId) + '/playlists/' + playlistId) 582 | .withHeaders({ 'Content-Type' : 'application/json' }) 583 | .withBodyParameters(options) 584 | .build() 585 | .execute(HttpManager.put, callback); 586 | }, 587 | 588 | /** 589 | * Replace the image used to represent a specific playlist. 590 | * @param {string} userId The playlist's owner's user ID 591 | * @param {string} playlistId The playlist's ID 592 | * @param {string} base64URI Base64 encoded JPEG image data, maximum payload size is 256 KB 593 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 594 | * @example uploadCustomPlaylistCoverImage('thelinmichael', '3EsfV6XzCHU8SPNdbnFogK', 'longbase64uri').then(...) 595 | * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected, 596 | * it contains an error object. Not returned if a callback is given. 597 | */ 598 | uploadCustomPlaylistCoverImage: function(userId, playlistId, base64URI, callback) { 599 | return WebApiRequest.builder(this.getAccessToken()) 600 | .withPath('/v1/users/' + encodeURIComponent(userId) + '/playlists/' + playlistId + '/images') 601 | .withHeaders({ 'Content-Type' : 'image/jpeg' }) 602 | .withBodyParameters(base64URI) 603 | .build() 604 | .execute(HttpManager.put, callback); 605 | }, 606 | 607 | /** 608 | * Add tracks to a playlist. 609 | * @param {string} userId The playlist's owner's user ID 610 | * @param {string} playlistId The playlist's ID 611 | * @param {string[]} tracks URIs of the tracks to add to the playlist. 612 | * @param {Object} [options] Options, position being the only one. 613 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 614 | * @example addTracksToPlaylist('thelinmichael', '3EsfV6XzCHU8SPNdbnFogK', 615 | '["spotify:track:4iV5W9uYEdYUVa79Axb7Rh", "spotify:track:1301WleyT98MSxVHPZCA6M"]').then(...) 616 | * @returns {Promise|undefined} A promise that if successful returns an object containing a snapshot_id. If rejected, 617 | * it contains an error object. Not returned if a callback is given. 618 | */ 619 | addTracksToPlaylist: function(userId, playlistId, tracks, options, callback) { 620 | return WebApiRequest.builder(this.getAccessToken()) 621 | .withPath('/v1/users/' + encodeURIComponent(userId) + '/playlists/' + playlistId + '/tracks') 622 | .withHeaders({ 'Content-Type' : 'application/json' }) 623 | .withQueryParameters(options) 624 | .withBodyParameters({ 625 | uris: tracks 626 | }) 627 | .build() 628 | .execute(HttpManager.post, callback); 629 | }, 630 | 631 | /** 632 | * Remove tracks from a playlist. 633 | * @param {string} userId The playlist's owner's user ID 634 | * @param {string} playlistId The playlist's ID 635 | * @param {Object[]} tracks An array of objects containing a property called uri with the track URI (String), and 636 | * a an optional property called positions (int[]), e.g. { uri : "spotify:track:491rM2JN8KvmV6p0oDDuJT", positions : [0, 15] } 637 | * @param {Object} options Options, snapshot_id being the only one. 638 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 639 | * @returns {Promise|undefined} A promise that if successful returns an object containing a snapshot_id. If rejected, 640 | * it contains an error object. Not returned if a callback is given. 641 | */ 642 | removeTracksFromPlaylist: function(userId, playlistId, tracks, options, callback) { 643 | return WebApiRequest.builder(this.getAccessToken()) 644 | .withPath('/v1/users/' + encodeURIComponent(userId) + '/playlists/' + playlistId + '/tracks') 645 | .withHeaders({ 'Content-Type' : 'application/json' }) 646 | .withBodyParameters({ 647 | 'tracks': tracks 648 | }, options) 649 | .build() 650 | .execute(HttpManager.del, callback); 651 | 652 | }, 653 | 654 | /** 655 | * Remove tracks from a playlist by position instead of specifying the tracks' URIs. 656 | * @param {string} userId The playlist's owner's user ID 657 | * @param {string} playlistId The playlist's ID 658 | * @param {int[]} positions The positions of the tracks in the playlist that should be removed 659 | * @param {string} snapshot_id The snapshot ID, or version, of the playlist. Required 660 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 661 | * @returns {Promise|undefined} A promise that if successful returns an object containing a snapshot_id. If rejected, 662 | * it contains an error object. Not returned if a callback is given. 663 | */ 664 | removeTracksFromPlaylistByPosition: function(userId, playlistId, positions, snapshotId, callback) { 665 | return WebApiRequest.builder(this.getAccessToken()) 666 | .withPath('/v1/users/' + encodeURIComponent(userId) + '/playlists/' + playlistId + '/tracks') 667 | .withHeaders({ 'Content-Type' : 'application/json' }) 668 | .withBodyParameters({ 669 | 'positions': positions, 670 | 'snapshot_id' : snapshotId 671 | }) 672 | .build() 673 | .execute(HttpManager.del, callback); 674 | 675 | }, 676 | 677 | /** 678 | * Replace tracks in a playlist. 679 | * @param {string} userId The playlist's owner's user ID 680 | * @param {string} playlistId The playlist's ID 681 | * @param {Object[]} uris An array of track URIs (strings) 682 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 683 | * @returns {Promise|undefined} A promise that if successful returns an empty object. If rejected, 684 | * it contains an error object. Not returned if a callback is given. 685 | */ 686 | replaceTracksInPlaylist: function(userId, playlistId, uris, callback) { 687 | return WebApiRequest.builder(this.getAccessToken()) 688 | .withPath('/v1/users/' + encodeURIComponent(userId) + '/playlists/' + playlistId + '/tracks') 689 | .withHeaders({ 'Content-Type' : 'application/json' }) 690 | .withBodyParameters({ 691 | 'uris': uris 692 | }) 693 | .build() 694 | .execute(HttpManager.put, callback); 695 | }, 696 | 697 | /** 698 | * Reorder tracks in a playlist. 699 | * @param {string} userId The playlist's owner's user ID 700 | * @param {string} playlistId The playlist's ID 701 | * @param {int} rangeStart The position of the first track to be reordered. 702 | * @param {int} insertBefore The position where the tracks should be inserted. 703 | * @param {Object} options Optional parameters, i.e. range_length and snapshot_id. 704 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 705 | * @returns {Promise|undefined} A promise that if successful returns an object containing a snapshot_id. If rejected, 706 | * it contains an error object. Not returned if a callback is given. 707 | */ 708 | reorderTracksInPlaylist: function(userId, playlistId, rangeStart, insertBefore, options, callback) { 709 | return WebApiRequest.builder(this.getAccessToken()) 710 | .withPath('/v1/users/' + encodeURIComponent(userId) + '/playlists/' + playlistId + '/tracks') 711 | .withHeaders({ 'Content-Type' : 'application/json' }) 712 | .withBodyParameters({ 713 | 'range_start': rangeStart, 714 | 'insert_before' : insertBefore 715 | }, options) 716 | .build() 717 | .execute(HttpManager.put, callback); 718 | }, 719 | 720 | /** 721 | * Get audio features for a single track identified by its unique Spotify ID. 722 | * @param {string} trackId The track ID 723 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 724 | * @example getAudioFeaturesForTrack('38P3Q4QcdjQALGF2Z92BmR').then(...) 725 | * @returns {Promise|undefined} A promise that if successful, resolves to an object 726 | * containing information about the audio features. If the promise is 727 | * rejected, it contains an error object. Not returned if a callback is given. 728 | */ 729 | getAudioFeaturesForTrack: function(trackId, callback) { 730 | return WebApiRequest.builder(this.getAccessToken()) 731 | .withPath('/v1/audio-features/' + trackId) 732 | .build() 733 | .execute(HttpManager.get, callback); 734 | }, 735 | 736 | /** 737 | * Get audio analysis for a single track identified by its unique Spotify ID. 738 | * @param {string} trackId The track ID 739 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 740 | * @example getAudioAnalysisForTrack('38P3Q4QcdjQALGF2Z92BmR').then(...) 741 | * @returns {Promise|undefined} A promise that if successful, resolves to an object 742 | * containing information about the audio analysis. If the promise is 743 | * rejected, it contains an error object. Not returned if a callback is given. 744 | */ 745 | getAudioAnalysisForTrack: function(trackId, callback) { 746 | return WebApiRequest.builder(this.getAccessToken()) 747 | .withPath('/v1/audio-analysis/' + trackId) 748 | .build() 749 | .execute(HttpManager.get, callback); 750 | }, 751 | 752 | /** 753 | * Get audio features for multiple tracks identified by their unique Spotify ID. 754 | * @param {string[]} trackIds The track IDs 755 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 756 | * @example getAudioFeaturesForTracks(['38P3Q4QcdjQALGF2Z92BmR', '2HO2bnoMrpnZUbUqiilLHi']).then(...) 757 | * @returns {Promise|undefined} A promise that if successful, resolves to an object 758 | * containing information about the audio features for the tracks. If the promise is 759 | * rejected, it contains an error object. Not returned if a callback is given. 760 | */ 761 | getAudioFeaturesForTracks: function(trackIds, callback) { 762 | return WebApiRequest.builder(this.getAccessToken()) 763 | .withPath('/v1/audio-features') 764 | .withQueryParameters({ 765 | 'ids' : trackIds.join(',') 766 | }) 767 | .build() 768 | .execute(HttpManager.get, callback); 769 | }, 770 | 771 | /** 772 | * Create a playlist-style listening experience based on seed artists, tracks and genres. 773 | * @param {Object} [options] The options supplied to this request. 774 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 775 | * @example getRecommendations({ min_energy: 0.4, seed_artists: ['6mfK6Q2tzLMEchAr0e9Uzu', '4DYFVNKZ1uixa6SQTvzQwJ'], min_popularity: 50 }).then(...) 776 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing 777 | * a list of tracks and a list of seeds. If rejected, it contains an error object. Not returned if a callback is given. 778 | */ 779 | getRecommendations: function(options, callback) { 780 | var _opts = {}; 781 | var optionsOfTypeArray = ['seed_artists', 'seed_genres', 'seed_tracks']; 782 | for (var option in options) { 783 | if (options.hasOwnProperty(option)) { 784 | if (optionsOfTypeArray.indexOf(option) !== -1 && 785 | Object.prototype.toString.call(options[option]) === '[object Array]') { 786 | _opts[option] = options[option].join(','); 787 | } else { 788 | _opts[option] = options[option]; 789 | } 790 | } 791 | } 792 | 793 | return WebApiRequest.builder(this.getAccessToken()) 794 | .withPath('/v1/recommendations') 795 | .withQueryParameters(_opts) 796 | .build() 797 | .execute(HttpManager.get, callback); 798 | }, 799 | 800 | /** 801 | * Retrieve a list of available genres seed parameter values for recommendations. 802 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 803 | * @example getAvailableGenreSeeds().then(...) 804 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing 805 | * a list of available genres to be used as seeds for recommendations. 806 | * If rejected, it contains an error object. Not returned if a callback is given. 807 | */ 808 | getAvailableGenreSeeds: function(callback) { 809 | return WebApiRequest.builder(this.getAccessToken()) 810 | .withPath('/v1/recommendations/available-genre-seeds') 811 | .build() 812 | .execute(HttpManager.get, callback); 813 | }, 814 | 815 | /** 816 | * Retrieve a URL where the user can give the application permissions. 817 | * @param {string[]} scopes The scopes corresponding to the permissions the application needs. 818 | * @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. 819 | * @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. 820 | * @returns {string} The URL where the user can give application permissions. 821 | */ 822 | createAuthorizeURL: function(scopes, state, showDialog) { 823 | return AuthenticationRequest.builder() 824 | .withPath('/authorize') 825 | .withQueryParameters({ 826 | 'client_id' : this.getClientId(), 827 | 'response_type' : 'code', 828 | 'redirect_uri' : this.getRedirectURI(), 829 | 'scope' : scopes.join('%20'), 830 | 'state' : state, 831 | 'show_dialog': showDialog && !!showDialog 832 | }) 833 | .build() 834 | .getURL(); 835 | }, 836 | 837 | /** 838 | * Retrieve the tracks that are saved to the authenticated users Your Music library. 839 | * @param {Object} [options] Options, being market, limit, and/or offset. 840 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 841 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object which in turn contains 842 | * playlist track objects. Not returned if a callback is given. 843 | */ 844 | getMySavedTracks: function(options, callback) { 845 | return WebApiRequest.builder(this.getAccessToken()) 846 | .withPath('/v1/me/tracks') 847 | .withQueryParameters(options) 848 | .build() 849 | .execute(HttpManager.get, callback); 850 | }, 851 | 852 | /** 853 | * Check if one or more tracks is already saved in the current Spotify user’s “Your Music” library. 854 | * @param {string[]} trackIds The track IDs 855 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 856 | * @returns {Promise|undefined} A promise that if successful, resolves into an array of booleans. The order 857 | * of the returned array's elements correspond to the track ID in the request. 858 | * The boolean value of true indicates that the track is part of the user's library, otherwise false. 859 | * Not returned if a callback is given. 860 | */ 861 | containsMySavedTracks: function(trackIds, callback) { 862 | return WebApiRequest.builder(this.getAccessToken()) 863 | .withPath('/v1/me/tracks/contains') 864 | .withQueryParameters({ 865 | 'ids' : trackIds.join(',') 866 | }) 867 | .build() 868 | .execute(HttpManager.get, callback); 869 | }, 870 | 871 | /** 872 | * Remove a track from the authenticated user's Your Music library. 873 | * @param {string[]} trackIds The track IDs 874 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 875 | * @returns {Promise|undefined} A promise that if successful returns null, otherwise an error. 876 | * Not returned if a callback is given. 877 | */ 878 | removeFromMySavedTracks: function(trackIds, callback) { 879 | return WebApiRequest.builder(this.getAccessToken()) 880 | .withPath('/v1/me/tracks') 881 | .withHeaders({ 'Content-Type' : 'application/json' }) 882 | .withBodyParameters(trackIds) 883 | .build() 884 | .execute(HttpManager.del, callback); 885 | 886 | }, 887 | 888 | /** 889 | * Add a track from the authenticated user's Your Music library. 890 | * @param {string[]} trackIds The track IDs 891 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 892 | * @returns {Promise|undefined} A promise that if successful returns null, otherwise an error. Not returned if a callback is given. 893 | */ 894 | addToMySavedTracks: function(trackIds, callback) { 895 | return WebApiRequest.builder(this.getAccessToken()) 896 | .withPath('/v1/me/tracks') 897 | .withHeaders({ 'Content-Type' : 'application/json' }) 898 | .withBodyParameters(trackIds) 899 | .build() 900 | .execute(HttpManager.put, callback); 901 | }, 902 | 903 | /** 904 | * Remove an album from the authenticated user's Your Music library. 905 | * @param {string[]} albumIds The album IDs 906 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 907 | * @returns {Promise|undefined} A promise that if successful returns null, otherwise an error. 908 | * Not returned if a callback is given. 909 | */ 910 | removeFromMySavedAlbums: function(albumIds, callback) { 911 | return WebApiRequest.builder(this.getAccessToken()) 912 | .withPath('/v1/me/albums') 913 | .withHeaders({ 'Content-Type' : 'application/json' }) 914 | .withBodyParameters(albumIds) 915 | .build() 916 | .execute(HttpManager.del, callback); 917 | 918 | }, 919 | 920 | /** 921 | * Add an album from the authenticated user's Your Music library. 922 | * @param {string[]} albumIds The track IDs 923 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 924 | * @returns {Promise|undefined} A promise that if successful returns null, otherwise an error. Not returned if a callback is given. 925 | */ 926 | addToMySavedAlbums: function(albumIds, callback) { 927 | return WebApiRequest.builder(this.getAccessToken()) 928 | .withPath('/v1/me/albums') 929 | .withHeaders({ 'Content-Type' : 'application/json' }) 930 | .withBodyParameters(albumIds) 931 | .build() 932 | .execute(HttpManager.put, callback); 933 | }, 934 | 935 | /** 936 | * Retrieve the albums that are saved to the authenticated users Your Music library. 937 | * @param {Object} [options] Options, being market, limit, and/or offset. 938 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 939 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object which in turn contains 940 | * playlist album objects. Not returned if a callback is given. 941 | */ 942 | getMySavedAlbums: function(options, callback) { 943 | return WebApiRequest.builder(this.getAccessToken()) 944 | .withPath('/v1/me/albums') 945 | .withQueryParameters(options) 946 | .build() 947 | .execute(HttpManager.get, callback); 948 | }, 949 | 950 | /** 951 | * Check if one or more albums is already saved in the current Spotify user’s “Your Music” library. 952 | * @param {string[]} albumIds The album IDs 953 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 954 | * @returns {Promise|undefined} A promise that if successful, resolves into an array of booleans. The order 955 | * of the returned array's elements correspond to the album ID in the request. 956 | * The boolean value of true indicates that the album is part of the user's library, otherwise false. 957 | * Not returned if a callback is given. 958 | */ 959 | containsMySavedAlbums: function(albumIds, callback) { 960 | return WebApiRequest.builder(this.getAccessToken()) 961 | .withPath('/v1/me/albums/contains') 962 | .withQueryParameters({ 963 | 'ids' : albumIds.join(',') 964 | }) 965 | .build() 966 | .execute(HttpManager.get, callback); 967 | }, 968 | 969 | /** 970 | * Get the current user's top artists based on calculated affinity. 971 | * @param {Object} [options] Options, being time_range, limit, offset. 972 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 973 | * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of artists, 974 | * otherwise an error. Not returned if a callback is given. 975 | */ 976 | getMyTopArtists: function(options, callback) { 977 | return WebApiRequest.builder(this.getAccessToken()) 978 | .withPath('/v1/me/top/artists') 979 | .withQueryParameters(options) 980 | .build() 981 | .execute(HttpManager.get, callback); 982 | }, 983 | 984 | /** 985 | * Get the current user's top tracks based on calculated affinity. 986 | * @param {Object} [options] Options, being time_range, limit, offset. 987 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 988 | * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks, 989 | * otherwise an error. Not returned if a callback is given. 990 | */ 991 | getMyTopTracks: function(options, callback) { 992 | return WebApiRequest.builder(this.getAccessToken()) 993 | .withPath('/v1/me/top/tracks') 994 | .withQueryParameters(options) 995 | .build() 996 | .execute(HttpManager.get, callback); 997 | }, 998 | 999 | /** 1000 | * Get the Current User's Recently Played Tracks 1001 | * @param {Object} [options] Options, being type, after, limit, before. 1002 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1003 | * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks, 1004 | * otherwise an error. Not returned if a callback is given. 1005 | */ 1006 | getMyRecentlyPlayedTracks: function(options, callback) { 1007 | return WebApiRequest.builder(this.getAccessToken()) 1008 | .withPath('/v1/me/player/recently-played') 1009 | .withQueryParameters(options) 1010 | .build() 1011 | .execute(HttpManager.get, callback); 1012 | }, 1013 | 1014 | /** 1015 | * Get the Current User's Connect Devices 1016 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1017 | * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks, 1018 | * otherwise an error. Not returned if a callback is given. 1019 | */ 1020 | getMyDevices: function(callback) { 1021 | return WebApiRequest.builder(this.getAccessToken()) 1022 | .withPath('/v1/me/player/devices') 1023 | .build() 1024 | .execute(HttpManager.get, callback); 1025 | }, 1026 | 1027 | 1028 | /** 1029 | * Get the Current User's Currently Playing Track. 1030 | * @param {Object} [options] Options, being market. 1031 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1032 | * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks, 1033 | * otherwise an error. Not returned if a callback is given. 1034 | */ 1035 | getMyCurrentPlayingTrack: function(options, callback) { 1036 | return WebApiRequest.builder(this.getAccessToken()) 1037 | .withPath('/v1/me/player/currently-playing') 1038 | .withQueryParameters(options) 1039 | .build() 1040 | .execute(HttpManager.get, callback); 1041 | }, 1042 | 1043 | /** 1044 | * Get the Current User's Current Playback State 1045 | * @param {Object} [options] Options, being market. 1046 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1047 | * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks, 1048 | * otherwise an error. Not returned if a callback is given. 1049 | */ 1050 | getMyCurrentPlaybackState: function(options, callback) { 1051 | return WebApiRequest.builder(this.getAccessToken()) 1052 | .withPath('/v1/me/player') 1053 | .withQueryParameters(options) 1054 | .build() 1055 | .execute(HttpManager.get, callback); 1056 | }, 1057 | 1058 | /** 1059 | * Transfer a User's Playback 1060 | * @param {Object} [options] Options, being market. 1061 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1062 | * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks, 1063 | * otherwise an error. Not returned if a callback is given. 1064 | */ 1065 | transferMyPlayback: function(options, callback) { 1066 | return WebApiRequest.builder(this.getAccessToken()) 1067 | .withPath('/v1/me/player') 1068 | .withHeaders({ 'Content-Type' : 'application/json' }) 1069 | .withBodyParameters({ 1070 | 'device_ids': options.deviceIds, 1071 | 'play': !!options.play 1072 | }) 1073 | .build() 1074 | .execute(HttpManager.put, callback); 1075 | }, 1076 | 1077 | /** 1078 | * Starts o Resumes the Current User's Playback 1079 | * @param {Object} [options] Options, being device_id, context_uri, offset, uris. 1080 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1081 | * @example playbackResume({context_uri: 'spotify:album:5ht7ItJgpBH7W6vJ5BqpPr'}).then(...) 1082 | * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks, 1083 | * otherwise an error. Not returned if a callback is given. 1084 | */ 1085 | play: function(options, callback) { 1086 | /*jshint camelcase: false */ 1087 | var _options = options || {}; 1088 | var queryParams = _options.device_id ? {device_id: _options.device_id} : null; 1089 | var postData = {}; 1090 | ['context_uri', 'uris', 'offset'].forEach(function(field) { 1091 | if (field in _options) { 1092 | postData[field] = _options[field]; 1093 | } 1094 | }); 1095 | return WebApiRequest.builder(this.getAccessToken()) 1096 | .withPath('/v1/me/player/play') 1097 | .withQueryParameters(queryParams) 1098 | .withHeaders({ 'Content-Type' : 'application/json' }) 1099 | .withBodyParameters(postData) 1100 | .build() 1101 | .execute(HttpManager.put, callback); 1102 | }, 1103 | 1104 | /** 1105 | * Pauses the Current User's Playback 1106 | * @param {Object} [options] Options, for now device_id, 1107 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1108 | * @example playbackPause().then(...) 1109 | * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks, 1110 | * otherwise an error. Not returned if a callback is given. 1111 | */ 1112 | pause: function(options, callback) { 1113 | return WebApiRequest.builder(this.getAccessToken()) 1114 | .withPath('/v1/me/player/pause') 1115 | /*jshint camelcase: false */ 1116 | .withQueryParameters(options && options.device_id ? {device_id: options.device_id} : null) 1117 | .withHeaders({ 'Content-Type' : 'application/json' }) 1118 | .build() 1119 | .execute(HttpManager.put, callback); 1120 | }, 1121 | 1122 | /** 1123 | * Skip the Current User's Playback To Previous Track 1124 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1125 | * @example playbackPrevious().then(...) 1126 | * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks, 1127 | * otherwise an error. Not returned if a callback is given. 1128 | */ 1129 | skipToPrevious: function(callback) { 1130 | return WebApiRequest.builder(this.getAccessToken()) 1131 | .withPath('/v1/me/player/previous') 1132 | .withHeaders({ 'Content-Type' : 'application/json' }) 1133 | .build() 1134 | .execute(HttpManager.post, callback); 1135 | }, 1136 | 1137 | /** 1138 | * Skip the Current User's Playback To Next Track 1139 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1140 | * @example playbackNext().then(...) 1141 | * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks, 1142 | * otherwise an error. Not returned if a callback is given. 1143 | */ 1144 | skipToNext: function(callback) { 1145 | return WebApiRequest.builder(this.getAccessToken()) 1146 | .withPath('/v1/me/player/next') 1147 | .withHeaders({ 'Content-Type' : 'application/json' }) 1148 | .build() 1149 | .execute(HttpManager.post, callback); 1150 | }, 1151 | 1152 | /** 1153 | * Set Repeat Mode On The Current User's Playback 1154 | * @param {Object} [options] Options, being state (track, context, off). 1155 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1156 | * @example playbackRepeat({state: 'context'}).then(...) 1157 | * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks, 1158 | * otherwise an error. Not returned if a callback is given. 1159 | */ 1160 | setRepeat: function(options, callback) { 1161 | return WebApiRequest.builder(this.getAccessToken()) 1162 | .withPath('/v1/me/player/repeat') 1163 | .withQueryParameters({ 1164 | 'state': options.state || 'off' 1165 | }) 1166 | .build() 1167 | .execute(HttpManager.put, callback); 1168 | }, 1169 | 1170 | /** 1171 | * Set Shuffle Mode On The Current User's Playback 1172 | * @param {Object} [options] Options, being state (true, false). 1173 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1174 | * @example playbackShuffle({state: 'false'}).then(...) 1175 | * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks, 1176 | * otherwise an error. Not returned if a callback is given. 1177 | */ 1178 | setShuffle: function(options, callback) { 1179 | return WebApiRequest.builder(this.getAccessToken()) 1180 | .withPath('/v1/me/player/shuffle') 1181 | .withQueryParameters({ 1182 | 'state': options.state || 'false' 1183 | }) 1184 | .build() 1185 | .execute(HttpManager.put, callback); 1186 | }, 1187 | 1188 | /** 1189 | * Add the current user as a follower of one or more other Spotify users. 1190 | * @param {string[]} userIds The IDs of the users to be followed. 1191 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1192 | * @example followUsers(['thelinmichael', 'wizzler']).then(...) 1193 | * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected, 1194 | * it contains an error object. Not returned if a callback is given. 1195 | */ 1196 | followUsers: function(userIds, callback) { 1197 | return WebApiRequest.builder(this.getAccessToken()) 1198 | .withPath('/v1/me/following') 1199 | .withQueryParameters({ 1200 | ids: userIds.join(','), 1201 | type: 'user' 1202 | }) 1203 | .build() 1204 | .execute(HttpManager.put, callback); 1205 | }, 1206 | 1207 | /** 1208 | * Add the current user as a follower of one or more artists. 1209 | * @param {string[]} artistIds The IDs of the artists to be followed. 1210 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1211 | * @example followArtists(['0LcJLqbBmaGUft1e9Mm8HV', '3gqv1kgivAc92KnUm4elKv']).then(...) 1212 | * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected, 1213 | * it contains an error object. Not returned if a callback is given. 1214 | */ 1215 | followArtists: function(artistIds, callback) { 1216 | return WebApiRequest.builder(this.getAccessToken()) 1217 | .withPath('/v1/me/following') 1218 | .withQueryParameters({ 1219 | ids: artistIds.join(','), 1220 | type: 'artist' 1221 | }) 1222 | .build() 1223 | .execute(HttpManager.put, callback); 1224 | }, 1225 | 1226 | /** 1227 | * Remove the current user as a follower of one or more other Spotify users. 1228 | * @param {string[]} userIds The IDs of the users to be unfollowed. 1229 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1230 | * @example unfollowUsers(['thelinmichael', 'wizzler']).then(...) 1231 | * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected, 1232 | * it contains an error object. Not returned if a callback is given. 1233 | */ 1234 | unfollowUsers: function(userIds, callback) { 1235 | return WebApiRequest.builder(this.getAccessToken()) 1236 | .withPath('/v1/me/following') 1237 | .withQueryParameters({ 1238 | ids: userIds.join(','), 1239 | type: 'user' 1240 | }) 1241 | .build() 1242 | .execute(HttpManager.del, callback); 1243 | 1244 | }, 1245 | 1246 | /** 1247 | * Remove the current user as a follower of one or more artists. 1248 | * @param {string[]} artistIds The IDs of the artists to be unfollowed. 1249 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1250 | * @example unfollowArtists(['0LcJLqbBmaGUft1e9Mm8HV', '3gqv1kgivAc92KnUm4elKv']).then(...) 1251 | * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected, 1252 | * it contains an error object. Not returned if a callback is given. 1253 | */ 1254 | unfollowArtists: function(artistIds, callback) { 1255 | return WebApiRequest.builder(this.getAccessToken()) 1256 | .withPath('/v1/me/following') 1257 | .withQueryParameters({ 1258 | ids: artistIds.join(','), 1259 | type: 'artist' 1260 | }) 1261 | .build() 1262 | .execute(HttpManager.del, callback); 1263 | 1264 | }, 1265 | 1266 | /** 1267 | * Check to see if the current user is following one or more other Spotify users. 1268 | * @param {string[]} userIds The IDs of the users to check if are followed by the current user. 1269 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1270 | * @example isFollowingUsers(['thelinmichael', 'wizzler']).then(...) 1271 | * @returns {Promise|undefined} A promise that if successful, resolves into an array of booleans. The order 1272 | * of the returned array's elements correspond to the users IDs in the request. 1273 | * The boolean value of true indicates that the user is following that user, otherwise is not. 1274 | * Not returned if a callback is given. 1275 | */ 1276 | isFollowingUsers: function(userIds, callback) { 1277 | return WebApiRequest.builder(this.getAccessToken()) 1278 | .withPath('/v1/me/following/contains') 1279 | .withQueryParameters({ 1280 | ids: userIds.join(','), 1281 | type: 'user' 1282 | }) 1283 | .build() 1284 | .execute(HttpManager.get, callback); 1285 | }, 1286 | 1287 | /** 1288 | * Get the current user's followed artists. 1289 | * @param {Object} [options] Options, being after and limit. 1290 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1291 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object which contains 1292 | * album objects. Not returned if a callback is given. 1293 | */ 1294 | getFollowedArtists: function(options, callback) { 1295 | return WebApiRequest.builder(this.getAccessToken()) 1296 | .withPath('/v1/me/following') 1297 | .withHeaders({ 'Content-Type' : 'application/json' }) 1298 | .withQueryParameters({ 1299 | type : 'artist' 1300 | }, options) 1301 | .build() 1302 | .execute(HttpManager.get, callback); 1303 | }, 1304 | 1305 | /** 1306 | * Check if users are following a playlist. 1307 | * @param {string} userId The playlist's owner's user ID 1308 | * @param {string} playlistId The playlist's ID 1309 | * @param {String[]} User IDs of the following users 1310 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1311 | * @returns {Promise|undefined} A promise that if successful returns an array of booleans. If rejected, 1312 | * it contains an error object. Not returned if a callback is given. 1313 | */ 1314 | areFollowingPlaylist: function(userId, playlistId, followerIds, callback) { 1315 | return WebApiRequest.builder(this.getAccessToken()) 1316 | .withPath('/v1/users/' + encodeURIComponent(userId) + '/playlists/' + playlistId + '/followers/contains') 1317 | .withQueryParameters({ 1318 | ids : followerIds.join(',') 1319 | }) 1320 | .build() 1321 | .execute(HttpManager.get, callback); 1322 | }, 1323 | 1324 | /** 1325 | * Check to see if the current user is following one or more artists. 1326 | * @param {string[]} artistIds The IDs of the artists to check if are followed by the current user. 1327 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1328 | * @example isFollowingArtists(['0LcJLqbBmaGUft1e9Mm8HV', '3gqv1kgivAc92KnUm4elKv']).then(...) 1329 | * @returns {Promise|undefined} A promise that if successful, resolves into an array of booleans. The order 1330 | * of the returned array's elements correspond to the artists IDs in the request. 1331 | * The boolean value of true indicates that the user is following that artist, otherwise is not. 1332 | * Not returned if a callback is given. 1333 | */ 1334 | isFollowingArtists: function(artistIds, callback) { 1335 | return WebApiRequest.builder(this.getAccessToken()) 1336 | .withPath('/v1/me/following/contains') 1337 | .withQueryParameters({ 1338 | ids: artistIds.join(','), 1339 | type: 'artist' 1340 | }) 1341 | .build() 1342 | .execute(HttpManager.get, callback); 1343 | }, 1344 | 1345 | /** 1346 | * Retrieve new releases 1347 | * @param {Object} [options] Options, being country, limit and/or offset. 1348 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1349 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object which contains 1350 | * album objects. Not returned if a callback is given. 1351 | */ 1352 | getNewReleases: function(options, callback) { 1353 | return WebApiRequest.builder(this.getAccessToken()) 1354 | .withPath('/v1/browse/new-releases') 1355 | .withHeaders({ 'Content-Type' : 'application/json' }) 1356 | .withQueryParameters(options) 1357 | .build() 1358 | .execute(HttpManager.get, callback); 1359 | }, 1360 | 1361 | /** 1362 | * Retrieve featured playlists 1363 | * @param {Object} [options] Options, being country, locale, timestamp, limit, offset. 1364 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1365 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object which contains 1366 | * featured playlists. Not returned if a callback is given. 1367 | */ 1368 | getFeaturedPlaylists: function(options, callback) { 1369 | return WebApiRequest.builder(this.getAccessToken()) 1370 | .withPath('/v1/browse/featured-playlists') 1371 | .withHeaders({ 'Content-Type' : 'application/json' }) 1372 | .withQueryParameters(options) 1373 | .build() 1374 | .execute(HttpManager.get, callback); 1375 | }, 1376 | 1377 | /** 1378 | * Retrieve a list of categories used to tag items in Spotify (e.g. in the 'Browse' tab) 1379 | * @param {Object} [options] Options, being country, locale, limit, offset. 1380 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1381 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object of categories. 1382 | * Not returned if a callback is given. 1383 | */ 1384 | getCategories: function(options, callback) { 1385 | return WebApiRequest.builder(this.getAccessToken()) 1386 | .withPath('/v1/browse/categories') 1387 | .withQueryParameters(options) 1388 | .build() 1389 | .execute(HttpManager.get, callback); 1390 | }, 1391 | 1392 | /** 1393 | * Retrieve a category. 1394 | * @param {string} categoryId The id of the category to retrieve. 1395 | * @param {Object} [options] Options, being country, locale. 1396 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1397 | * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a category object. 1398 | * Not returned if a callback is given. 1399 | */ 1400 | getCategory: function(categoryId, options, callback) { 1401 | return WebApiRequest.builder(this.getAccessToken()) 1402 | .withPath('/v1/browse/categories/' + categoryId) 1403 | .withQueryParameters(options) 1404 | .build() 1405 | .execute(HttpManager.get, callback); 1406 | }, 1407 | 1408 | /** 1409 | * Retrieve playlists for a category. 1410 | * @param {string} categoryId The id of the category to retrieve playlists for. 1411 | * @param {Object} [options] Options, being country, limit, offset. 1412 | * @param {requestCallback} [callback] Optional callback method to be called instead of the promise. 1413 | * @returns {Promise|undefined} A promise that if successful, resolves to a paging object containing simple playlists. 1414 | * Not returned if a callback is given. 1415 | */ 1416 | getPlaylistsForCategory: function(categoryId, options, callback) { 1417 | return WebApiRequest.builder(this.getAccessToken()) 1418 | .withPath('/v1/browse/categories/' + categoryId + '/playlists') 1419 | .withQueryParameters(options) 1420 | .build() 1421 | .execute(HttpManager.get, callback); 1422 | } 1423 | }; 1424 | 1425 | SpotifyWebApi._addMethods = function(methods) { 1426 | for (var i in methods) { 1427 | if (methods.hasOwnProperty(i)) { 1428 | this.prototype[i] = methods[i]; 1429 | } 1430 | } 1431 | }; 1432 | 1433 | module.exports = SpotifyWebApi; 1434 | -------------------------------------------------------------------------------- /src/webapi-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function WebapiError(message, statusCode) { 4 | this.name = 'WebapiError'; 5 | this.message = (message || ''); 6 | this.statusCode = statusCode; 7 | } 8 | 9 | WebapiError.prototype = Error.prototype; 10 | 11 | module.exports = WebapiError; -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/authentication-request.js: -------------------------------------------------------------------------------- 1 | var AuthenticationRequest = require("../src/authentication-request"), 2 | should = require("should"); 3 | 4 | describe("Create Authentication Requests", function() { 5 | 6 | it("Should use default settings if none are supplied", function() { 7 | var request = AuthenticationRequest.builder().build(); 8 | 9 | request.getHost().should.equal("accounts.spotify.com"); 10 | request.getPort().should.equal(443); 11 | request.getScheme().should.equal("https"); 12 | should.not.exist(request.getHeaders()); 13 | should.not.exist(request.getPath()); 14 | should.not.exist(request.getQueryParameters()); 15 | should.not.exist(request.getBodyParameters()); 16 | }); 17 | 18 | it("Can overwrite one of the default parameters", function() { 19 | var request = AuthenticationRequest.builder().withHost("such.host.wow").build(); 20 | 21 | request.getHost().should.equal("such.host.wow"); 22 | request.getPort().should.equal(443); 23 | request.getScheme().should.equal("https"); 24 | should.not.exist(request.getHeaders()); 25 | should.not.exist(request.getPath()); 26 | should.not.exist(request.getQueryParameters()); 27 | should.not.exist(request.getBodyParameters()); 28 | }); 29 | 30 | }); -------------------------------------------------------------------------------- /test/base-request.js: -------------------------------------------------------------------------------- 1 | var Request = require("../src/base-request"), 2 | should = require("should"); 3 | 4 | describe("Create Requests", function() { 5 | 6 | it("Should create host, port, and scheme", function() { 7 | var request = Request.builder() 8 | .withHost("such.api.wow") 9 | .withPort(1337) 10 | .withScheme("http") 11 | .build(); 12 | 13 | request.getHost().should.equal("such.api.wow"); 14 | request.getPort().should.equal(1337); 15 | request.getScheme().should.equal("http"); 16 | }); 17 | 18 | it("Should add query parameters", function() { 19 | var request = Request.builder() 20 | .withHost("such.api.wow") 21 | .withPort(1337) 22 | .withScheme("http") 23 | .withQueryParameters({ 24 | "oneParameter" : 1, 25 | "anotherParameter" : true, 26 | "thirdParameter" : "hello" 27 | }) 28 | .build(); 29 | 30 | request.getQueryParameters().oneParameter.should.equal(1); 31 | request.getQueryParameters().anotherParameter.should.equal(true); 32 | request.getQueryParameters().thirdParameter.should.equal("hello"); 33 | }); 34 | 35 | it("Should add query parameters (multiple calls)", function() { 36 | var request = Request.builder() 37 | .withHost("such.api.wow") 38 | .withPort(1337) 39 | .withScheme("http") 40 | .withQueryParameters({ 41 | "oneParameter" : 1, 42 | "anotherParameter" : true 43 | }) 44 | .withQueryParameters({ 45 | "thirdParameter" : "hello" 46 | }) 47 | .build(); 48 | 49 | request.getQueryParameters().oneParameter.should.equal(1); 50 | request.getQueryParameters().anotherParameter.should.equal(true); 51 | request.getQueryParameters().thirdParameter.should.equal("hello"); 52 | }); 53 | 54 | it("Should add query parameters (combine calls)", function() { 55 | var request = Request.builder() 56 | .withHost("such.api.wow") 57 | .withPort(1337) 58 | .withScheme("http") 59 | .withQueryParameters({ 60 | "oneParameter" : 1, 61 | "anotherParameter" : true 62 | }, { 63 | "thirdParameter" : "hello" 64 | }) 65 | .build(); 66 | 67 | request.getQueryParameters().oneParameter.should.equal(1); 68 | request.getQueryParameters().anotherParameter.should.equal(true); 69 | request.getQueryParameters().thirdParameter.should.equal("hello"); 70 | }); 71 | 72 | it("Should add body parameters", function() { 73 | var request = Request.builder() 74 | .withHost("such.api.wow") 75 | .withPort(1337) 76 | .withScheme("http") 77 | .withBodyParameters({ 78 | "one" : 1, 79 | "two" : true, 80 | "three" : "world" 81 | }) 82 | .build(); 83 | 84 | request.getBodyParameters().one.should.equal(1); 85 | request.getBodyParameters().two.should.equal(true); 86 | request.getBodyParameters().three.should.equal("world"); 87 | }); 88 | 89 | it("Should add header parameters", function() { 90 | var request = Request.builder() 91 | .withHost("such.api.wow") 92 | .withPort(1337) 93 | .withScheme("http") 94 | .withHeaders({ 95 | "Authorization" : "Basic WOOP", 96 | "Content-Type" : "application/lol" 97 | }) 98 | .build(); 99 | 100 | request.getHeaders().Authorization.should.equal("Basic WOOP"); 101 | request.getHeaders()["Content-Type"].should.equal("application/lol"); 102 | }); 103 | 104 | it("Should add path", function() { 105 | var request = Request.builder() 106 | .withHost("such.api.wow") 107 | .withPort(1337) 108 | .withPath("/v1/users/meriosweg") 109 | .build(); 110 | 111 | request.getPath().should.equal("/v1/users/meriosweg"); 112 | }); 113 | 114 | it("Should build URI", function() { 115 | var request = Request.builder() 116 | .withHost("such.api.wow") 117 | .withScheme("https") 118 | .withPort(1337) 119 | .withPath("/v1/users/meriosweg") 120 | .build(); 121 | 122 | request.getURI().should.equal("https://such.api.wow:1337/v1/users/meriosweg"); 123 | }); 124 | 125 | it("Should construct empty query paramaters string", function() { 126 | var request = Request.builder() 127 | .withQueryParameters({}) 128 | .build(); 129 | 130 | should.not.exist(request.getQueryParameterString()); 131 | }); 132 | 133 | it("Should construct query paramaters string for one parameter", function() { 134 | var request = Request.builder() 135 | .withQueryParameters({ 136 | "one" : 1 137 | }) 138 | .build(); 139 | 140 | request.getQueryParameterString().should.equal("?one=1"); 141 | }); 142 | 143 | it("Should construct query paramaters string for multiple parameters", function() { 144 | var request = Request.builder() 145 | .withQueryParameters({ 146 | "one" : 1, 147 | "two" : true, 148 | "three" : "world" 149 | }) 150 | .build(); 151 | 152 | request.getQueryParameterString().should.equal("?one=1&two=true&three=world"); 153 | }); 154 | 155 | it("Should construct query paramaters string and exclude undefined values", function() { 156 | var request = Request.builder() 157 | .withQueryParameters({ 158 | "one" : 1, 159 | "two" : undefined, 160 | "three" : "world" 161 | }) 162 | .build(); 163 | 164 | request.getQueryParameterString().should.equal("?one=1&three=world"); 165 | }); 166 | }); -------------------------------------------------------------------------------- /test/http-manager.js: -------------------------------------------------------------------------------- 1 | var mockery = require('mockery'); 2 | var Request = require("../src/base-request"); 3 | 4 | function useSuperagentMock(method, error, response) { 5 | // method 6 | // response 7 | // data 8 | // headers 9 | // statusCode 10 | 11 | var onEnd = function (callback) { 12 | if (error) { 13 | return callback(error) 14 | } 15 | callback(null, { 16 | body: response.data, 17 | headers: response.headers || {}, 18 | statusCode: response.statusCode || 200 19 | }); 20 | }; 21 | 22 | var noop = function () {} 23 | 24 | var superagentMock = { }; 25 | superagentMock[method] = function (url) { 26 | return { 27 | query: noop, 28 | send: noop, 29 | set: noop, 30 | end: onEnd 31 | } 32 | }; 33 | 34 | mockery.registerMock('superagent', superagentMock); 35 | } 36 | 37 | describe("Make requests", function() { 38 | 39 | beforeEach(function(){ 40 | 41 | mockery.enable({ 42 | warnOnReplace: false, 43 | warnOnUnregistered: false, 44 | useCleanCache: true 45 | }); 46 | }); 47 | 48 | afterEach(function(){ 49 | mockery.deregisterAll(); 50 | mockery.disable(); 51 | }); 52 | 53 | describe("GET requests", function() { 54 | 55 | it("Should make a successful GET request", function(done) { 56 | 57 | useSuperagentMock('get', null, { 58 | data: 'some data', 59 | headers: {}, 60 | statusCode: 200 61 | }); 62 | 63 | var HttpManager = require('../src/http-manager'); 64 | var request = Request.builder() 65 | .withHost("such.api.wow") 66 | .withPort(1337) 67 | .withScheme("http") 68 | .build(); 69 | 70 | HttpManager.get(request, function(errorObject) { 71 | done(errorObject); 72 | }); 73 | }); 74 | 75 | it("Should process an error GET request", function(done) { 76 | 77 | useSuperagentMock('get', new Error('GET request error')); 78 | 79 | var HttpManager = require('../src/http-manager'); 80 | var request = Request.builder() 81 | .withHost("such.api.wow") 82 | .withPort(1337) 83 | .withScheme("http") 84 | .build(); 85 | 86 | HttpManager.get(request, function(errorObject) { 87 | errorObject.should.be.an.instanceOf(Error); 88 | errorObject.message.should.equal('GET request error'); 89 | done(); 90 | }); 91 | }); 92 | 93 | it("Should process an error GET request with an error message", function(done) { 94 | 95 | useSuperagentMock('get', new Error('There is a problem in your request')); 96 | 97 | var HttpManager = require('../src/http-manager'); 98 | var request = Request.builder() 99 | .withHost("such.api.wow") 100 | .withPort(1337) 101 | .withScheme("http") 102 | .build(); 103 | 104 | HttpManager.get(request, function(errorObject) { 105 | errorObject.should.be.an.instanceOf(Error); 106 | errorObject.message.should.equal('There is a problem in your request'); 107 | done(); 108 | }); 109 | }); 110 | }); 111 | 112 | it("Should make a successful POST request", function(done) { 113 | 114 | useSuperagentMock('post', null, { 115 | data: 'some data', 116 | headers: {}, 117 | statusCode: 200 118 | }); 119 | 120 | var HttpManager = require('../src/http-manager'); 121 | var request = Request.builder() 122 | .withHost("such.api.wow") 123 | .withPort(1337) 124 | .withScheme("http") 125 | .build(); 126 | 127 | HttpManager.post(request, function(errorObject) { 128 | done(errorObject); 129 | }); 130 | }); 131 | 132 | it("Should make a successful PUT request", function(done) { 133 | 134 | useSuperagentMock('put', null, { 135 | data: 'some data', 136 | headers: {}, 137 | statusCode: 200 138 | }); 139 | 140 | var HttpManager = require('../src/http-manager'); 141 | var request = Request.builder() 142 | .withHost("such.api.wow") 143 | .withPort(1337) 144 | .withScheme("http") 145 | .build(); 146 | 147 | HttpManager.put(request, function(errorObject) { 148 | done(errorObject); 149 | }); 150 | }); 151 | 152 | it("Should make a successful DELETE request", function(done) { 153 | 154 | useSuperagentMock('del', null, { 155 | data: 'some data', 156 | headers: {}, 157 | statusCode: 200 158 | }); 159 | 160 | var HttpManager = require('../src/http-manager'); 161 | var request = Request.builder() 162 | .withHost("such.api.wow") 163 | .withPort(1337) 164 | .withScheme("http") 165 | .build(); 166 | 167 | HttpManager.del(request, function(errorObject) { 168 | done(errorObject); 169 | }); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /test/webapi-error.js: -------------------------------------------------------------------------------- 1 | var WebApiError = require("../src/webapi-error"), 2 | should = require("should"); 3 | 4 | describe("Create Web Api Error", function() { 5 | 6 | it("should create error with message and status code", function() { 7 | var error = new WebApiError("Something has gone terribly wrong!", 500); 8 | ("Something has gone terribly wrong!").should.equal(error.message); 9 | (500).should.equal(error.statusCode); 10 | }); 11 | 12 | }); -------------------------------------------------------------------------------- /test/webapi-request.js: -------------------------------------------------------------------------------- 1 | var WebApiRequest = require("../src/webapi-request"), 2 | should = require("should"); 3 | 4 | describe("Create Web Api Requests", function() { 5 | 6 | it("Should use default settings if none are supplied", function() { 7 | var request = WebApiRequest.builder("token").build(); 8 | 9 | request.getHost().should.equal("api.spotify.com"); 10 | request.getPort().should.equal(443); 11 | request.getScheme().should.equal("https"); 12 | should.exist(request.getHeaders().Authorization); 13 | should.not.exist(request.getPath()); 14 | should.not.exist(request.getQueryParameters()); 15 | should.not.exist(request.getBodyParameters()); 16 | }); 17 | 18 | it("Can overwrite one of the default parameters", function() { 19 | var request = WebApiRequest.builder("token").withHost("such.host.wow").build(); 20 | 21 | request.getHost().should.equal("such.host.wow"); 22 | request.getPort().should.equal(443); 23 | request.getScheme().should.equal("https"); 24 | should.exist(request.getHeaders().Authorization); 25 | should.not.exist(request.getPath()); 26 | should.not.exist(request.getQueryParameters()); 27 | should.not.exist(request.getBodyParameters()); 28 | }); 29 | 30 | }); --------------------------------------------------------------------------------