├── .bowerrc ├── .editorconfig ├── .github └── dependabot.yml ├── .gitignore ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── LICENSE.md ├── README.md ├── bower.json ├── dist └── angular-spotify.min.js ├── examples ├── callback.html ├── index.html └── main.controller.js ├── package.json ├── src └── angular-spotify.js └── test ├── .jshintrc ├── karma.conf.js ├── mock ├── album.error.json ├── album.json ├── albums.invalid-id.json ├── albums.json ├── albums.tracks.json ├── featured-playlists.json ├── new-releases.json ├── search.artist.json └── search.missing-type.json ├── spec └── angular-spotify.spec.js └── vendor ├── jasmine-jquery.js └── jquery-1.11.1.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "19:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: karma 11 | versions: 12 | - 6.0.3 13 | - 6.0.4 14 | - 6.1.0 15 | - 6.1.1 16 | - 6.1.2 17 | - 6.2.0 18 | - 6.3.0 19 | - 6.3.1 20 | - dependency-name: jasmine-core 21 | versions: 22 | - 3.6.0 23 | - 3.7.0 24 | - dependency-name: grunt-contrib-uglify 25 | versions: 26 | - 5.0.0 27 | - dependency-name: grunt-karma 28 | versions: 29 | - 3.0.0 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dev/ 2 | .tmp/ 3 | .DS_Store 4 | .idea 5 | *.sublime-project 6 | *.sublime-workspace 7 | bower_components/ 8 | node_modules/ 9 | /pages/ 10 | /docs/ 11 | /test/coverage/ 12 | !.gitignore 13 | !dist/ 14 | npm-debug.log 15 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": false, 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": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "angular": false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | - 0.12 5 | - 4 6 | - 5 7 | sudo: false 8 | before_install: npm install -g grunt-cli 9 | before_script: 10 | - npm i -g bower 11 | - bower install 12 | - grunt 13 | after_script: 14 | - npm run codecov 15 | - npm run coveralls 16 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (grunt) { 4 | 5 | // Load grunt tasks automatically 6 | require('load-grunt-tasks')(grunt); 7 | 8 | // Time how long tasks take. Can help when optimizing build times 9 | require('time-grunt')(grunt); 10 | 11 | grunt.initConfig({ 12 | pkg: grunt.file.readJSON('bower.json'), 13 | 14 | //Settings 15 | app: { 16 | src: 'src', 17 | dist: 'dist' 18 | }, 19 | 20 | uglify: { 21 | options: { 22 | banner: '/*! <%= pkg.name %> v<%= pkg.version %> <%= grunt.template.today("yyyy-mm-dd") %> */\n' 23 | }, 24 | build: { 25 | src: '<%= app.src %>/<%= pkg.name %>.js', 26 | dest: '<%= app.dist %>/<%= pkg.name %>.min.js' 27 | } 28 | }, 29 | 30 | // Make sure code styles are up to par and there are no obvious mistakes 31 | jshint: { 32 | options: { 33 | jshintrc: '.jshintrc', 34 | reporter: require('jshint-stylish') 35 | }, 36 | all: { 37 | src: [ 38 | 'Gruntfile.js', 39 | '<%= app.src %>/<%= pkg.name %>.js' 40 | ] 41 | }, 42 | test: { 43 | options: { 44 | jshintrc: 'test/.jshintrc' 45 | }, 46 | src: ['test/spec/{,*/}*.js'] 47 | } 48 | }, 49 | 50 | // Empties folders to start fresh 51 | clean: { 52 | dist: { 53 | files: [{ 54 | dot: true, 55 | src: [ 56 | '.tmp', 57 | '<%= app.dist %>/{,*/}*', 58 | '!<%= app.dist %>/.git*' 59 | ] 60 | }] 61 | } 62 | }, 63 | 64 | // Test settings 65 | karma: { 66 | unit: { 67 | configFile: 'test/karma.conf.js', 68 | singleRun: true 69 | } 70 | } 71 | }); 72 | 73 | grunt.registerTask('test', [ 74 | 'karma' 75 | ]); 76 | 77 | // Default task(s). 78 | grunt.registerTask('default', [ 79 | 'clean:dist', 80 | 'jshint', 81 | 'uglify' 82 | ]); 83 | 84 | }; 85 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Chinmay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-spotify 2 | 3 | [![Build Status](https://travis-ci.org/eddiemoore/angular-spotify.svg?branch=master)](https://travis-ci.org/eddiemoore/angular-spotify) [![codecov.io](http://codecov.io/github/eddiemoore/angular-spotify/coverage.svg?branch=master)](http://codecov.io/github/eddiemoore/angular-spotify?branch=master) [![Coverage Status](https://img.shields.io/coveralls/eddiemoore/angular-spotify.svg)](https://coveralls.io/r/eddiemoore/angular-spotify) [![devDependency Status](https://david-dm.org/eddiemoore/angular-spotify/dev-status.svg)](https://david-dm.org/eddiemoore/angular-spotify#info=devDependencies) [![Code Climate](https://codeclimate.com/github/eddiemoore/angular-spotify/badges/gpa.svg)](https://codeclimate.com/github/eddiemoore/angular-spotify) 4 | [![Greenkeeper badge](https://badges.greenkeeper.io/eddiemoore/angular-spotify.svg)](https://greenkeeper.io/) 5 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/eddiemoore/angular-spotify?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 6 | 7 | angular service to connect to the [Spotify Web API](https://developer.spotify.com/web-api/) 8 | 9 | angular-spotify makes heavy use of promises throughout the service 10 | 11 | ## Usage 12 | 13 | Install angular-spotify via bower. Use the --save property to save into your bower.json file. 14 | ```shell 15 | bower install angular-spotify --save 16 | ``` 17 | Also available on npm 18 | ```shell 19 | npm install angular-spotify --save 20 | ``` 21 | 22 | Include spotify into your angular module 23 | ```js 24 | var app = angular.module('example', ['spotify']); 25 | ``` 26 | 27 | Most of the functions in Spotify do not require you to authenticate your application. However if you do need to gain access to playlists or a user's data then configure it like this: 28 | ```js 29 | app.config(function (SpotifyProvider) { 30 | SpotifyProvider.setClientId(''); 31 | SpotifyProvider.setRedirectUri(''); 32 | SpotifyProvider.setScope(''); 33 | // If you already have an auth token 34 | SpotifyProvider.setAuthToken(''); 35 | }); 36 | ``` 37 | For example: 38 | ```js 39 | app.config(function (SpotifyProvider) { 40 | SpotifyProvider.setClientId('ABC123DEF456GHI789JKL'); 41 | SpotifyProvider.setRedirectUri('http://www.example.com/callback.html'); 42 | SpotifyProvider.setScope('user-read-private playlist-read-private playlist-modify-private playlist-modify-public'); 43 | // If you already have an auth token 44 | SpotifyProvider.setAuthToken('zoasliu1248sdfuiknuha7882iu4rnuwehifskmkiuwhjg23'); 45 | }); 46 | ``` 47 | 48 | 49 | Inject Spotify into a controller to gain access to all the functions available 50 | ```js 51 | app.controller('MainCtrl', function (Spotify) { 52 | 53 | }); 54 | ``` 55 | 56 | 57 | ### Albums 58 | 59 | #### Get an Album 60 | Get Spotify catalog information for a single album. 61 | ```js 62 | Spotify.getAlbum('AlbumID or Spotify Album URI'); 63 | ``` 64 | Example: 65 | ```js 66 | Spotify.getAlbum('0sNOF9WDwhWunNAHPD3Baj').then(function (data) { 67 | console.log(data); 68 | }); 69 | ``` 70 | 71 | 72 | #### Get Several Albums 73 | Get Spotify catalog information for multiple albums identified by their Spotify IDs. 74 | ```js 75 | Spotify.getAlbums('Array or comma separated list of Album IDs'); 76 | ``` 77 | Example: 78 | ```js 79 | Spotify 80 | .getAlbums('41MnTivkwTO3UUJ8DrqEJJ,6JWc4iAiJ9FjyK0B59ABb4,6UXCm6bOO4gFlDQZV5yL37') 81 | .then(function (data) { 82 | console.log(data); 83 | }); 84 | ``` 85 | 86 | 87 | #### Get an Album’s Tracks 88 | Get Spotify catalog information about an album’s tracks. Optional parameters can be used to limit the number of tracks returned. 89 | ```js 90 | Spotify.getAlbumTracks('AlbumID or Spotify Album URI', options); 91 | ``` 92 | ##### Options Object (Optional) 93 | - limit - Optional. The maximum number of tracks to return. Default: 20. Minimum: 1. Maximum: 50. 94 | - offset - Optional. The index of the first track to return. Default: 0 (the first object). Use with limit to get the next set of tracks. 95 | 96 | Example: 97 | ```js 98 | Spotify.getAlbumTracks('6akEvsycLGftJxYudPjmqK').then(function (data) { 99 | console.log(data); 100 | }); 101 | ``` 102 | 103 | 104 | ### Artists 105 | #### Get an Artist 106 | Get Spotify catalog information for a single artist identified by their unique Spotify ID or Spotify URI. 107 | 108 | ```js 109 | Spotify.getArtist('Artist Id or Spotify Artist URI'); 110 | ``` 111 | Example 112 | ```js 113 | Spotify.getArtist('0LcJLqbBmaGUft1e9Mm8HV').then(function (data) { 114 | console.log(data); 115 | }); 116 | ``` 117 | 118 | #### Get Several Artists 119 | Get Spotify catalog information for several artists based on their Spotify IDs. 120 | ```js 121 | Spotify.getArtists('Comma separated string or array of Artist Ids'); 122 | ``` 123 | Example: 124 | ```js 125 | Spotify 126 | .getArtists('0oSGxfWSnnOXhD2fKuz2Gy,3dBVyJ7JuOMt4GE9607Qin') 127 | .then(function (data) { 128 | console.log(data); 129 | }); 130 | ``` 131 | 132 | #### Get an Artist’s Albums 133 | Get Spotify catalog information about an artist’s albums. Optional parameters can be passed in to filter and sort the response. 134 | ```js 135 | Spotify.getArtistAlbums('Artist Id or Spotify Artist URI', options); 136 | ``` 137 | 138 | ##### Options Object (Optional) 139 | - album_type - Optional A comma-separated list of keywords that will be used to filter the response. If not supplied, all album types will be returned. Valid values are: 140 | - album 141 | - single 142 | - appears_on 143 | - compilation 144 | 145 | Example: { album_type: 'album,single' } 146 | - country - Optional. An ISO 3166-1 alpha-2 country code. Supply this parameter to limit the response to one particular country. Note if you do not provide this field, you are likely to get duplicate results per album, one for each country in which the album is available! 147 | - limit - The number of album objects to return. Default: 20. Minimum: 1. Maximum: 50. For example: { limit: 2 } 148 | - offset - Optional. The index of the first album to return. Default: 0 (i.e., the first album). Use with limit to get the next set of albums. 149 | 150 | 151 | Example: 152 | ```js 153 | Spotify.getArtistAlbums('1vCWHaC5f2uS3yhpwWbIA6').then(function (data) { 154 | console.log(data); 155 | }); 156 | ``` 157 | 158 | 159 | #### Get an Artist’s Top Tracks 160 | Get Spotify catalog information about an artist’s top tracks by country. 161 | ```js 162 | Spotify.getArtistTopTracks('Artist Id or Spotify Artist URI', 'Country Code'); 163 | ``` 164 | - The country: an ISO 3166-1 alpha-2 country code. 165 | 166 | 167 | Example: 168 | ```js 169 | Spotify 170 | .getArtistTopTracks('1vCWHaC5f2uS3yhpwWbIA6', 'AU') 171 | .then(function (data) { 172 | console.log(data); 173 | }); 174 | ``` 175 | 176 | 177 | #### Get an Artist’s Related Artists 178 | Get Spotify catalog information about artists similar to a given artist. Similarity is based on analysis of the Spotify community’s listening history. 179 | ```js 180 | Spotify.getRelatedArtists('Artist Id or Spotify Artist URI'); 181 | ``` 182 | Example: 183 | ```js 184 | Spotify.getRelatedArtists('1vCWHaC5f2uS3yhpwWbIA6').then(function (data) { 185 | console.log(data); 186 | }); 187 | ``` 188 | 189 | 190 | ### Browse 191 | Discover new releases and featured playlists. User needs to be logged in to gain access to these features. 192 | 193 | #### Get the featured playlists 194 | Get a list of Spotify featured playlists 195 | ```js 196 | Spotify.getFeaturedPlaylists(options); 197 | ``` 198 | ##### Options Object (Optional) 199 | - locale - string - Optional. The desired language, consisting of a lowercase ISO 639 language code and an uppercase ISO 3166-1 alpha-2 country code, joined by an underscore. For example: es_MX, meaning "Spanish (Mexico)". Provide this parameter if you want the results returned in a particular language (where available). 200 | - country - string - Optional. A country: an ISO 3166-1 alpha-2 country code. Provide this parameter if you want the list of returned items to be relevant to a particular country. If omitted, the returned items will be relevant to all countries. 201 | - timestamp - string - Optional. A timestamp in ISO 8601 format: yyyy-MM-ddTHH:mm:ss. Use this parameter to specify the user's local time to get results tailored for that specific date and time in the day. If not provided, the response defaults to the current UTC time. Example: "2014-10-23T09:00:00" for a user whose local time is 9AM. 202 | 203 | Example: 204 | ```js 205 | Spotify 206 | .getFeaturedPlaylists({ locale: "nl_NL", country: "NL" }) 207 | .then(function (data) { 208 | console.log(data); 209 | }); 210 | ``` 211 | 212 | #### Get new releases 213 | Get a list of new album releases featured in Spotify 214 | ```js 215 | Spotify.getNewReleases(options); 216 | ``` 217 | ##### Options Object (Optional) 218 | - country - string - Optional. A country: an ISO 3166-1 alpha-2 country code. Provide this parameter if you want the list of returned items to be relevant to a particular country. If omitted, the returned items will be relevant to all countries. 219 | 220 | Example: 221 | ```js 222 | Spotify.getNewReleases({ country: "NL" }).then(function (data) { 223 | console.log(data); 224 | }); 225 | ``` 226 | 227 | #### Get categories 228 | Get a list of categories used to tag items in Spotify (on, for example, the Spotify player’s “Browse” tab). 229 | ```js 230 | Spotify.getCategories(options); 231 | ``` 232 | 233 | ##### Options Object (Optional) 234 | - country - string - Optional. A country: an ISO 3166-1 alpha-2 country code. Provide this parameter if you want the list of returned items to be relevant to a particular country. If omitted, the returned items will be relevant to all countries. 235 | - locale - string - Optional. The desired language, consisting of an ISO 639 language code and an ISO 3166-1 alpha-2 country code, joined by an underscore. For example: es_MX, meaning "Spanish (Mexico)". Provide this parameter if you want the category metadata returned in a particular language. 236 | - limit - number - Optional. The maximum number of categories to return. Default: 20. Minimum: 1. Maximum: 50. 237 | - offset - number - Optional. The index of the first item to return. Default: 0 (the first object). Use with ```limit``` to get the next set of categories. 238 | 239 | Example: 240 | ```js 241 | Spotify.getCategories({ country: 'SG' }).then(function (data) { 242 | console.log(data); 243 | }); 244 | ``` 245 | 246 | #### Get category 247 | Get a single category used to tag items in Spotify (on, for example, the Spotify player’s “Browse” tab). 248 | ```js 249 | Spotify.getCategory(category_id, options); 250 | ``` 251 | 252 | ##### Required 253 | - category_id - The Spotify category ID for the category. 254 | 255 | ##### Options Object (Optional) 256 | - country - string - Optional. A country: an ISO 3166-1 alpha-2 country code. Provide this parameter if you want the list of returned items to be relevant to a particular country. If omitted, the returned items will be relevant to all countries. 257 | - locale - string - Optional. The desired language, consisting of an ISO 639 language code and an ISO 3166-1 alpha-2 country code, joined by an underscore. For example: es_MX, meaning "Spanish (Mexico)". Provide this parameter if you want the category metadata returned in a particular language. 258 | 259 | Example: 260 | ```js 261 | Spotify.getCategory('party').then(function (data) { 262 | console.log(data); 263 | }) 264 | ``` 265 | 266 | #### Get category playlists 267 | Get a list of Spotify playlists tagged with a particular category. 268 | ```js 269 | Spotify.getCategoryPlaylists(category_id, options); 270 | ``` 271 | 272 | ##### Required 273 | - category_id - The Spotify category ID for the category. 274 | 275 | ##### Options Object (Optional) 276 | - country - string - Optional. A country: an ISO 3166-1 alpha-2 country code. Provide this parameter if you want the list of returned items to be relevant to a particular country. If omitted, the returned items will be relevant to all countries. 277 | - limit - number - Optional. The maximum number of items to return. Default: 20. Minimum: 1. Maximum: 50. 278 | - offset - number - Optional. The index of the first item to return. Default: 0 (the first object). Use with ```limit``` to get the next set of items. 279 | 280 | Example: 281 | ```js 282 | Spotify.getCategoryPlaylists('party').then(function (data) { 283 | console.log(data); 284 | }) 285 | ``` 286 | 287 | #### Get Recommendations 288 | Create a playlist-style listening experience based on seed artists, tracks and genres. 289 | ```js 290 | Spotify.getRecommendations(options); 291 | ``` 292 | 293 | ##### Options Object 294 | - limit - number - Optional. The target size of the list of recommended tracks. Default: 20. Minimum: 1. Maximum: 100. 295 | - market - string - Optional. An ISO 3166-1 alpha-2 country code. 296 | - max_* - number - Optional. Multiple values. For each tunable track attribute, a hard ceiling on the selected track attribute’s value can be provided. See tunable track attributes below for the list of available options. For example, max_instrumentalness=0.35 would filter out most tracks that are likely to be instrumental. 297 | - min_* - number Optional. Multiple values. For each tunable track attribute, a hard floor on the selected track attribute’s value can be provided. See tunable track attributes below for the list of available options. For example, min_tempo=140 would restrict results to only those tracks with a tempo of greater than 140 beats per minute. 298 | - seed_artists - A comma separated list of Spotify IDs for seed artists. 299 | Up to 5 seed values may be provided in any combination of seed_artists, seed_tracks and seed_genres. 300 | - seed_genres - A comma separated list of any genres in the set of available genre seeds. 301 | Up to 5 seed values may be provided in any combination of seed_artists, seed_tracks and seed_genres. 302 | - seed_tracks - A comma separated list of Spotify IDs for a seed track. 303 | Up to 5 seed values may be provided in any combination of seed_artists, seed_tracks and seed_genres. 304 | - target_* - Optional. Multiple values. For each of the tunable track attributes (below) a target value may be provided. Tracks with the attribute values nearest to the target values will be preferred. For example, you might request target_energy=0.6 and target_danceability=0.8. All target values will be weighed equally in ranking results. 305 | 306 | Example: 307 | ```js 308 | Spotify.getRecommendations({ seed_artists: '4NHQUGzhtTLFvgF5SZesLK' }).then(function (data) { 309 | console.log(data); 310 | }); 311 | ``` 312 | 313 | #### Get Available Genre Seeds 314 | Retrieve a list of available genres seed parameter values for recommendations. 315 | ```js 316 | Spotify.getAvailableGenreSeeds(); 317 | ``` 318 | 319 | Example: 320 | ```js 321 | Spotify.getAvailableGenreSeeds().then(function (data) { 322 | console.log(data); 323 | }); 324 | ``` 325 | 326 | 327 | ### Follow 328 | These endpoints allow you manage the list of artists and users that a logged in user follows. Following and unfollowing requires the ```user-follow-modify``` scope. Check if Current User Follows requires the ```user-follow-read``` scope. 329 | 330 | #### Get User’s Followed Artists 331 | Get the current user’s followed artists. 332 | 333 | ```js 334 | Spotify.following('type', options) 335 | ``` 336 | - type: Required. currently only ```artist``` is supported. 337 | 338 | 339 | ```js 340 | Spotify.following('artists', { limit: 10 }).then(function (artists) { 341 | console.log(artists); 342 | }) 343 | ``` 344 | 345 | #### Follow Artists or Users 346 | Add the current user as a follower of one or more artists or other Spotify users. 347 | ```js 348 | Spotify.follow('type', 'ids'); 349 | ``` 350 | - type: Required. either ```artist``` or ```user``` 351 | 352 | Example: 353 | ```js 354 | Spotify.follow('user', 'exampleuser01').then(function () { 355 | // no response from Spotify 356 | }); 357 | ``` 358 | 359 | #### Unfollow Artists or Users 360 | Remove the current user as a follower of one or more artists or other Spotify users. 361 | ```js 362 | Spotify.unfollow('type', 'ids'); 363 | ``` 364 | - type: Required. either ```artist``` or ```user``` 365 | 366 | Example: 367 | ```js 368 | Spotify.unfollow('user', 'exampleuser01').then(function () { 369 | // no response from Spotify 370 | }); 371 | ``` 372 | 373 | #### Check if Current User Follows 374 | Check to see if the current user is following one or more artists or other Spotify users. 375 | ```js 376 | Spotify.userFollowingContains('type', 'ids'); 377 | ``` 378 | - type: Required. either ```artist``` or ```user``` 379 | - ids: Required. comma-separated list. 380 | 381 | Example: 382 | ```js 383 | Spotify.userFollowingContains('user', 'exampleuser01').then(function (data) { 384 | console.log(data); 385 | }); 386 | ``` 387 | 388 | #### Follow a Playlist 389 | Add the current user as a follower of a playlist. Requires ```playlist-modify-public``` or ```playlist-modify-private``` scope to work. 390 | ```js 391 | Spotify.followPlaylist('owner_id', 'playlist_id', isPublic); 392 | ``` 393 | - owner_id: The Spotify user ID of the person who owns the playlist. 394 | - playlist_id: The Spotify ID of the playlist. Any playlist can be followed, regardless of its public/private status, as long as you know its playlist ID. 395 | - isPublic: Boolean (Optional), default true. If true the playlist will be included in user's public playlists, if false it will remain private. 396 | 397 | Example: 398 | ```js 399 | Spotify 400 | .followPlaylist('jmperezperez', '2v3iNvBX8Ay1Gt2uXtUKUT', false) 401 | .then(function (data) { 402 | console.log(data); 403 | }); 404 | ``` 405 | 406 | #### Unfollow a Playlist 407 | Remove the current user as a follower of a playlist. Requires ```playlist-modify-public``` or ```playlist-modify-private``` scope to work. 408 | ```js 409 | Spotify.unfollowPlaylist('owner_id', 'playlist_id', isPublic); 410 | ``` 411 | - owner_id: The Spotify user ID of the person who owns the playlist. 412 | - playlist_id: The Spotify ID of the playlist that is to be no longer followed. 413 | 414 | Example: 415 | ```js 416 | Spotify 417 | .unfollowPlaylist('jmperezperez', '2v3iNvBX8Ay1Gt2uXtUKUT') 418 | .then(function (data) { 419 | console.log(data); 420 | }); 421 | ``` 422 | 423 | #### Check if Users Follow a Playlist 424 | Check to see if one or more Spotify users are following a specified playlist.Following a playlist can be done publicly or privately. Checking if a user publicly follows a playlist doesn't require any scopes; if the user is publicly following the playlist, this endpoint returns true. 425 | 426 | Checking if the user is privately following a playlist is only possible for the current user when that user has granted access to the ```playlist-read-private``` scope. 427 | ```js 428 | Spotify 429 | .playlistFollowingContains('owner_id', 'playlist_id', 'comma separated string or array of user ids'); 430 | ``` 431 | Example: 432 | ```js 433 | Spotify.playlistFollowingContains('jmperezperez', '2v3iNvBX8Ay1Gt2uXtUKUT', 'possan,elogain').then(function (data) { 434 | console.log(data); 435 | }); 436 | ``` 437 | 438 | 439 | ### Library *(may have name changes in next version)* 440 | #### Get Current User’s Saved Tracks 441 | Get a list of the songs saved in the current Spotify user’s “Your Music” library. Requires the ```user-library-read``` scope. 442 | ```js 443 | Spotify.getSavedUserTracks(options); 444 | ``` 445 | ##### Options Object (Optional) 446 | 447 | - limit - Optional. The maximum number of objects to return. Default: 20. Minimum: 1. Maximum: 50. 448 | - offset - Optional. The index of the first object to return. Default: 0 (i.e., the first object). Use with limit to get the next set of objects. 449 | 450 | ```js 451 | Spotify.getSavedUserTracks().then(function (data) { 452 | console.log(data); 453 | }); 454 | ``` 455 | 456 | 457 | #### Check Current User’s Saved Tracks 458 | Check if one or more tracks is already saved in the current Spotify user’s “Your Music” library. Requires the ```user-library-read``` scope. 459 | 460 | ```js 461 | Spotify.userTracksContains('comma separated string or array of spotify track ids'); 462 | ``` 463 | Example: 464 | ```js 465 | Spotify 466 | .userTracksContains('0udZHhCi7p1YzMlvI4fXoK,3SF5puV5eb6bgRSxBeMOk9') 467 | .then(function (data) { 468 | console.log(data); 469 | }); 470 | ``` 471 | 472 | 473 | #### Save Tracks for Current User 474 | Save one or more tracks to the current user’s “Your Music” library. Requires the ```user-library-modify``` scope. 475 | ```js 476 | Spotify.saveUserTracks('comma separated string or array of spotify track ids'); 477 | ``` 478 | Example: 479 | ```js 480 | Spotify 481 | .saveUserTracks('0udZHhCi7p1YzMlvI4fXoK,3SF5puV5eb6bgRSxBeMOk9') 482 | .then(function (data) { 483 | console.log(data); 484 | }); 485 | ``` 486 | 487 | 488 | #### Remove Tracks for Current User 489 | Remove one or more tracks from the current user’s “Your Music” library. Requires the ```user-library-modify``` scope. 490 | ```js 491 | Spotify.removeUserTracks('comma separated string or array of spotify track ids'); 492 | ``` 493 | Example: 494 | ```js 495 | Spotify 496 | .removeUserTracks('0udZHhCi7p1YzMlvI4fXoK,3SF5puV5eb6bgRSxBeMOk9') 497 | .then(function (data) { 498 | console.log(data); 499 | }); 500 | ``` 501 | 502 | 503 | #### Save Albums for Current User 504 | Save one or more albums to the current user’s “Your Music” library. Requires the ```user-library-modify``` scope. 505 | ```js 506 | Spotify.saveUserAlbums('comma separated string or array of spotify album ids'); 507 | ``` 508 | Example: 509 | ```js 510 | Spotify 511 | .saveUserAlbums('4iV5W9uYEdYUVa79Axb7Rh,1301WleyT98MSxVHPZCA6M') 512 | .then(function (data) { 513 | console.log(data); 514 | }); 515 | ``` 516 | 517 | #### Get Current User’s Saved Albums 518 | Get a list of the albums saved in the current Spotify user’s “Your Music” library. Requires the ```user-library-read``` scope. 519 | ```js 520 | Spotify.getSavedUserAlbums(options); 521 | ``` 522 | ##### Options Object (Optional) 523 | 524 | - limit - Optional. The maximum number of objects to return. Default: 20. Minimum: 1. Maximum: 50. 525 | - offset - Optional. The index of the first object to return. Default: 0 (i.e., the first object). Use with limit to get the next set of objects. 526 | - market - Optional. An ISO 3166-1 alpha-2 country code. Provide this parameter if you want to apply Track Relinking. 527 | 528 | ```js 529 | Spotify.getSavedUserAlbums().then(function (data) { 530 | console.log(data); 531 | }); 532 | ``` 533 | 534 | #### Remove Albums for Current User 535 | Remove one or more albums from the current user’s “Your Music” library. Requires the ```user-library-modify``` scope. 536 | ```js 537 | Spotify.removeUserAlbums('comma separated string or array of spotify album ids'); 538 | ``` 539 | Example: 540 | ```js 541 | Spotify 542 | .removeUserAlbums('4iV5W9uYEdYUVa79Axb7Rh,1301WleyT98MSxVHPZCA6M') 543 | .then(function (data) { 544 | console.log(data); 545 | }); 546 | ``` 547 | 548 | 549 | #### Check User’s Saved Albums 550 | Check if one or more albums is already saved in the current Spotify user’s “Your Music” library. Requires the ```user-library-read``` scope. 551 | 552 | ```js 553 | Spotify.userAlbumsContains('comma separated string or array of spotify album ids'); 554 | ``` 555 | Example: 556 | ```js 557 | Spotify 558 | .userAlbumsContains('4iV5W9uYEdYUVa79Axb7Rh,1301WleyT98MSxVHPZCA6M') 559 | .then(function (data) { 560 | console.log(data); 561 | }); 562 | ``` 563 | 564 | 565 | ### Personalization 566 | Endpoints for retrieving information about the user’s listening habits. 567 | 568 | #### Get a User’s Top Artists 569 | Get the current user’s top artists based on calculated affinity. 570 | ```js 571 | Spotify.getUserTopArtists(options); 572 | ``` 573 | 574 | ##### Options Object (Optional) 575 | - limit - number - Optional. The number of entities to return. Default: 20. Minimum: 1. Maximum: 50. 576 | - offset - number - Optional. The index of the first entity to return. Default: 0 (i.e., the first track). Use with limit to get the next set of entities. 577 | - time_range - Optional. Over what time frame the affinities are computed. Valid values: long_term (calculated from several years of data and including all new data as it becomes available), medium_term (approximately last 6 months), short_term (approximately last 4 weeks). Default: medium_term. 578 | 579 | Example: 580 | ```js 581 | Spotify.getUserTopArtists({ limit: 50 }).then(function (data) { 582 | console.log(data); 583 | }); 584 | ``` 585 | 586 | #### Get a User’s Top Tracks 587 | Get the current user’s top tracks based on calculated affinity. 588 | ```js 589 | Spotify.getUserTopTracks(options); 590 | ``` 591 | 592 | ##### Options Object (Optional) 593 | - limit - number - Optional. The number of entities to return. Default: 20. Minimum: 1. Maximum: 50. 594 | - offset - number - Optional. The index of the first entity to return. Default: 0 (i.e., the first track). Use with limit to get the next set of entities. 595 | - time_range - Optional. Over what time frame the affinities are computed. Valid values: long_term (calculated from several years of data and including all new data as it becomes available), medium_term (approximately last 6 months), short_term (approximately last 4 weeks). Default: medium_term. 596 | 597 | Example: 598 | ```js 599 | Spotify.getUserTopTracks({ limit: 50 }).then(function (data) { 600 | console.log(data); 601 | }); 602 | ``` 603 | 604 | 605 | ### Playlists 606 | User needs to be logged in to gain access to playlists 607 | 608 | #### Get a List of a User’s Playlists 609 | Get a list of the playlists owned by a Spotify user. Requires the ```playlist-read-private``` scope 610 | ```js 611 | Spotify.getUserPlaylists('user_id', options); 612 | ``` 613 | ##### Options Object (Optional) 614 | - limit - Optional. The maximum number of playlists to return. Default: 20. Minimum: 1. Maximum: 50. 615 | - offset - Optional. The index of the first playlist to return. Default: 0 (the first object). Use with limit to get the next set of playlists. 616 | 617 | Example: 618 | ```js 619 | Spotify.getUserPlaylists('wizzler').then(function (data) { 620 | console.log(data); 621 | }); 622 | ``` 623 | 624 | 625 | #### Get a Playlist 626 | Get a playlist owned by a Spotify user. 627 | ```js 628 | Spotify.getPlaylist('user_id', 'playlist_id', options); 629 | ``` 630 | ##### Options Object (Optional) 631 | - fields - Optional. Filters for the query: a comma-separated list of the fields to return. If omitted, all fields are returned. Sub-fields can be excluded by prefixing them with an exclamation mark. [More Info](https://developer.spotify.com/web-api/get-playlist/) 632 | 633 | ```js 634 | Spotify 635 | .getPlaylist('1176458919', '6Df19VKaShrdWrAnHinwVO') 636 | .then(function (data) { 637 | console.log(data); 638 | }); 639 | ``` 640 | 641 | 642 | #### Get a Playlist’s Tracks 643 | Get full details of the tracks of a playlist owned by a Spotify user. Requires the ```playlist-read-private``` scope. 644 | ```js 645 | Spotify.getPlaylistTracks('user_id', 'playlist_id', options); 646 | ``` 647 | Example: 648 | ```js 649 | Spotify 650 | .getPlaylistTracks('1176458919', '6Df19VKaShrdWrAnHinwVO') 651 | .then(function (data) { 652 | console.log(data); 653 | }); 654 | ``` 655 | 656 | #### Create a Playlist 657 | Create a playlist for a Spotify user. (The playlist will be empty until you add tracks.) Creating a public playlist requires the ```playlist-modify-public``` scope. Creating a private playlist requires the ```playlist-modify-private``` scope. 658 | ```js 659 | Spotify.createPlaylist('user_id', options); 660 | ``` 661 | ##### Options Object 662 | - name - string - Required. The name for the new playlist, for example "Your Coolest Playlist". This name does not need to be unique; a user may have several playlists with the same name. 663 | - public - boolean - Optional, default true. If true the playlist will be public, if false it will be private. To be able to create private playlists, the user must have granted the playlist-modify-private scope. 664 | 665 | 666 | Example: 667 | ```js 668 | Spotify 669 | .createPlaylist('1176458919', { name: 'Awesome Mix Vol. 1' }) 670 | .then(function (data) { 671 | console.log('playlist created'); 672 | }); 673 | ``` 674 | 675 | 676 | #### Add Tracks to a Playlist 677 | Add one or more tracks to a user’s playlist. Adding tracks to a public playlist requires the ```playlist-modify-public``` scope. Adding tracks to a private playlist requires the ```playlist-modify-private``` scope. 678 | ```js 679 | Spotify.addPlaylistTracks('user_id', 'playlist_id', 'comma separated string or array of spotify track uris'); 680 | ``` 681 | ##### Options Object (Optional) 682 | - position - integer - Optional. The position to insert the tracks, a zero-based index. For example, to insert the tracks in the first position: position=0; to insert the tracks in the third position: position=2. If omitted, the tracks will be appended to the playlist. Tracks are added in the order they are listed in the query string or request body. 683 | 684 | 685 | Example: 686 | ```js 687 | Spotify 688 | .addPlaylistTracks('1176458919', '2TkWjGCu8jurholsfdWtG4', 'spotify:track:4iV5W9uYEdYUVa79Axb7Rh, spotify:track:1301WleyT98MSxVHPZCA6M') 689 | .then(function (data) { 690 | console.log('tracks added to playlist'); 691 | }); 692 | ``` 693 | 694 | 695 | #### Remove Tracks from a Playlist 696 | Remove one or more tracks from a user’s playlist. Removing tracks from a public playlist requires the ```playlist-modify-public``` scope. Removing tracks from a private playlist requires the ```playlist-modify-private``` scope. 697 | ```js 698 | Spotify.removePlaylistTracks('user_id', 'playlist_id', 'comma separated string or array of spotify track ids or uris'); 699 | ``` 700 | Example: 701 | ```js 702 | Spotify 703 | .removePlaylistTracks('1176458919', '2TkWjGCu8jurholsfdWtG4', 'spotify:track:4iV5W9uYEdYUVa79Axb7Rh, spotify:track:1301WleyT98MSxVHPZCA6M') 704 | .then(function (data) { 705 | console.log('tracks removed from playlist'); 706 | }); 707 | ``` 708 | 709 | #### Reorder a Playlist's Tracks 710 | Reorder a track or a group of tracks in a playlist. 711 | ```js 712 | Spotify.reorderPlaylistTracks('user_id', 'playlist_id', options); 713 | ``` 714 | ##### Options Object (Required) 715 | - range_start - integer - Required. The position of the first track to be reordered. 716 | - range_length - integer - Optional. The amount of tracks to be reordered. Defaults to 1 if not set. 717 | - insert_before - integer - Required. The position where the tracks should be inserted. 718 | - snapshot_id - string - Optional. The playlist's snapshot ID against which you want to make the changes. 719 | 720 | 721 | Example: 722 | ```js 723 | Spotify.reorderPlaylistTracks('1176458919', '2TkWjGCu8jurholsfdWtG4', { 724 | range_start: 8, 725 | range_length: 5, 726 | insert_before: 0 727 | }).then(function (data) { 728 | console.log(data); 729 | }); 730 | ``` 731 | 732 | 733 | #### Replace a Playlist’s Tracks 734 | Replace all the tracks in a playlist, overwriting its existing tracks. This powerful request can be useful for replacing tracks, re-ordering existing tracks, or clearing the playlist. Replacing tracks in a public playlist requires the ```playlist-modify-public``` scope. Replacing tracks in a private playlist requires the ```playlist-modify-private``` scope. 735 | ```js 736 | Spotify.replacePlaylistTracks('user_id', 'playlist_id', 'comma separated string or array of spotify track ids or uris'); 737 | ``` 738 | Example: 739 | ```js 740 | Spotify 741 | .replacePlaylistTracks('1176458919', '2TkWjGCu8jurholsfdWtG4', 'spotify:track:4iV5W9uYEdYUVa79Axb7Rh, spotify:track:1301WleyT98MSxVHPZCA6M') 742 | .then(function (data) { 743 | console.log('tracks removed from playlist'); 744 | }); 745 | ``` 746 | 747 | 748 | #### Change a Playlist’s Details 749 | Change a playlist’s name and public/private state. (The user must, of course, own the playlist.) Changing a public playlist requires the ```playlist-modify-public``` scope. Changing a private playlist requires the ```playlist-modify-private``` scope. 750 | ```js 751 | Spotify.updatePlaylistDetails('user_id', 'playlist_id', options); 752 | ``` 753 | ##### Options Object (Optional) 754 | - name - string - Optional. The new name for the playlist, for example "My New Playlist Title". 755 | - public - Boolean - Optional. If true the playlist will be public, if false it will be private. 756 | 757 | 758 | Example: 759 | ```js 760 | Spotify 761 | .updatePlaylistDetails('1176458919', '2TkWjGCu8jurholsfdWtG4', { name: 'Updated Playlist Title' }) 762 | .then(function (data) { 763 | console.log('Updated playlist details'); 764 | }); 765 | ``` 766 | 767 | 768 | ### User Profiles 769 | User needs to be logged in to gain access to user profiles 770 | 771 | #### Get a User’s Profile 772 | Get public profile information about a Spotify user. 773 | ```js 774 | Spotify.getUser('user_id'); 775 | ``` 776 | Example: 777 | ```js 778 | Spotify.getUser('wizzler').then(function (data) { 779 | console.log(data); 780 | }); 781 | ``` 782 | 783 | 784 | #### Get Current User’s Profile 785 | Get detailed profile information about the current user (including the current user’s username). 786 | ```js 787 | Spotify.getCurrentUser(); 788 | ``` 789 | Example: 790 | ```js 791 | Spotify.getCurrentUser().then(function (data) { 792 | console.log(data); 793 | }); 794 | ``` 795 | 796 | 797 | ### Search 798 | #### Search for an Item 799 | Get Spotify catalog information about artists, albums, or tracks that match a keyword string. 800 | ```js 801 | Spotify.search('Search Query', 'type', options); 802 | ``` 803 | - type - Required. A comma-separated list of item types to search across. Valid types are: album, artist, playlist, and track. 804 | 805 | ##### Options Object (Optional) 806 | - limit - Optional. The maximum number of objects to return. Default: 20. Minimum: 1. Maximum: 50. 807 | - offset - Optional. The index of the first object to return. Default: 0 (i.e., the first object). Use with limit to get the next set of objects. 808 | 809 | 810 | Example: 811 | ```js 812 | Spotify.search('Nirvana', 'artist').then(function (data) { 813 | console.log(data); 814 | }); 815 | ``` 816 | 817 | 818 | ### Tracks 819 | #### Get a Track 820 | Get Spotify catalog information for a single track identified by its unique Spotify ID or Spotify URI. 821 | ```js 822 | Spotify.getTrack('Track Id or Spotify Track URI'); 823 | ``` 824 | Example: 825 | ```js 826 | Spotify.getTrack('0eGsygTp906u18L0Oimnem').then(function (data) { 827 | console.log(data); 828 | }); 829 | ``` 830 | 831 | #### Get Several Tracks 832 | Get Spotify catalog information for multiple tracks based on their Spotify IDs. 833 | ```js 834 | Spotify.getTracks('Comma separated list or array of Track Ids'); 835 | ``` 836 | Example: 837 | ```js 838 | Spotify.getTracks('0eGsygTp906u18L0Oimnem,1lDWb6b6ieDQ2xT7ewTC3G').then(function (data) { 839 | console.log(data); 840 | }); 841 | ``` 842 | 843 | #### Get Audio Features for a Track 844 | Get audio feature information for a single track identified by its unique Spotify ID. 845 | 846 | ```js 847 | Spotify.getTrackAudioFeatures('Track Id or Spotify Track URI'); 848 | ``` 849 | Example: 850 | ```js 851 | Spotify.getTrackAudioFeatures('0eGsygTp906u18L0Oimnem').then(function (data) { 852 | console.log(data); 853 | }); 854 | ``` 855 | 856 | #### Get Audio Features for Several Tracks 857 | Get audio features for multiple tracks based on their Spotify IDs. 858 | 859 | ```js 860 | Spotify.getTracksAudioFeatures('Comma separated list or array of Track Ids'); 861 | ``` 862 | Example: 863 | ```js 864 | Spotify.getTracksAudioFeatures('0eGsygTp906u18L0Oimnem,1lDWb6b6ieDQ2xT7ewTC3G').then(function (data) { 865 | console.log(data); 866 | }); 867 | ``` 868 | 869 | ### Authentication 870 | #### Login 871 | Will open login window. Requires user to initiate as it will open a pop up window. 872 | Requires client id, callback uri and scope to be set in config. 873 | ```js 874 | Spotify.login(); 875 | ``` 876 | 877 | Example: 878 | ```js 879 | $scope.login = function () { 880 | Spotify.login(); 881 | }; 882 | ``` 883 | 884 | #### Example callback html 885 | ```html 886 | 887 | 888 | 889 | 890 | 891 | 892 | 905 | 906 | 907 | 908 | 909 | 910 | ``` 911 | 912 | 913 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/eddiemoore/angular-spotify/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 914 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-spotify", 3 | "version": "1.5.1", 4 | "authors": [ 5 | "Ed Moore " 6 | ], 7 | "description": "Angular Service to connect with Spotify Web API", 8 | "keywords": [ 9 | "spotify", 10 | "angular", 11 | "angularjs" 12 | ], 13 | "license": "MIT", 14 | "homepage": "http://github.com/eddiemoore/angular-spotify", 15 | "ignore": [ 16 | "**/.*", 17 | "node_modules", 18 | "bower_components", 19 | "test", 20 | "tests" 21 | ], 22 | "main": "./dist/angular-spotify.min.js", 23 | "dependencies": { 24 | "angular": "~1.6" 25 | }, 26 | "devDependencies": { 27 | "angular-mocks": "~1.6", 28 | "angular-scenario": "~1.6" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /dist/angular-spotify.min.js: -------------------------------------------------------------------------------- 1 | /*! angular-spotify v1.5.1 2017-06-05 */ 2 | 3 | !function(a,b,c){"use strict";b.module("spotify",[]).provider("Spotify",function(){var c={};c.clientId=null,c.redirectUri=null,c.scope=null,c.authToken=null,this.setClientId=function(a){return c.clientId=a,c.clientId},this.getClientId=function(){return c.clientId},this.setAuthToken=function(a){return c.authToken=a,c.authToken},this.setRedirectUri=function(a){return c.redirectUri=a,c.redirectUri},this.getRedirectUri=function(){return c.redirectUri},this.setScope=function(a){return c.scope=a,c.scope};var d={};d.toQueryString=function(a){var c=[];return b.forEach(a,function(a,b){this.push(encodeURIComponent(b)+"="+encodeURIComponent(a))},c),c.join("&")},c.apiBase="https://api.spotify.com/v1",this.$get=["$q","$http","$window",function(e,f,g){function h(){this.clientId=c.clientId,this.redirectUri=c.redirectUri,this.apiBase=c.apiBase,this.scope=c.scope,this.authToken=c.authToken,this.toQueryString=d.toQueryString}function i(b,c,d,e){var f=a.open(b,c,d),g=a.setInterval(function(){try{f&&!f.closed||(a.clearInterval(g),e(f))}catch(a){}},1e3);return f}return h.prototype={api:function(a,b,c,d,g){var h=e.defer();return f({url:this.apiBase+a,method:b||"GET",params:c,data:d,headers:g,withCredentials:!1}).then(function(a){h.resolve(a)}).catch(function(a){h.reject(a)}),h.promise},_auth:function(a){var b={Authorization:"Bearer "+this.authToken};return a&&(b["Content-Type"]="application/json"),b},getAlbum:function(a){return a=-1===a.indexOf("spotify:")?a:a.split(":")[2],this.api("/albums/"+a,"GET",null,null,this._auth())},getAlbums:function(a){return a=b.isString(a)?a.split(","):a,b.forEach(a,function(b,c){a[c]=b.indexOf("spotify:")>-1?b.split(":")[2]:b}),this.api("/albums","GET",{ids:a?a.toString():""},null,this._auth())},getAlbumTracks:function(a,b){return a=-1===a.indexOf("spotify:")?a:a.split(":")[2],this.api("/albums/"+a+"/tracks","GET",b,null,this._auth())},getArtist:function(a){return a=-1===a.indexOf("spotify:")?a:a.split(":")[2],this.api("/artists/"+a,"GET",null,null,this._auth())},getArtists:function(a){return a=b.isString(a)?a.split(","):a,b.forEach(a,function(b,c){a[c]=b.indexOf("spotify:")>-1?b.split(":")[2]:b}),this.api("/artists/","GET",{ids:a?a.toString():""},null,this._auth())},getArtistAlbums:function(a,b){return a=-1===a.indexOf("spotify:")?a:a.split(":")[2],this.api("/artists/"+a+"/albums","GET",b,null,this._auth())},getArtistTopTracks:function(a,b){return a=-1===a.indexOf("spotify:")?a:a.split(":")[2],this.api("/artists/"+a+"/top-tracks","GET",{country:b},null,this._auth())},getRelatedArtists:function(a){return a=-1===a.indexOf("spotify:")?a:a.split(":")[2],this.api("/artists/"+a+"/related-artists","GET",null,null,this._auth())},getFeaturedPlaylists:function(a){return this.api("/browse/featured-playlists","GET",a,null,this._auth())},getNewReleases:function(a){return this.api("/browse/new-releases","GET",a,null,this._auth())},getCategories:function(a){return this.api("/browse/categories","GET",a,null,this._auth())},getCategory:function(a,b){return this.api("/browse/categories/"+a,"GET",b,null,this._auth())},getCategoryPlaylists:function(a,b){return this.api("/browse/categories/"+a+"/playlists","GET",b,null,this._auth())},getRecommendations:function(a){return this.api("/recommendations","GET",a,null,this._auth())},getAvailableGenreSeeds:function(){return this.api("/recommendations/available-genre-seeds","GET",null,null,this._auth())},following:function(a,b){return b=b||{},b.type=a,this.api("/me/following","GET",b,null,this._auth())},follow:function(a,b){return this.api("/me/following","PUT",{type:a,ids:b},null,this._auth())},unfollow:function(a,b){return this.api("/me/following","DELETE",{type:a,ids:b},null,this._auth())},userFollowingContains:function(a,b){return this.api("/me/following/contains","GET",{type:a,ids:b},null,this._auth())},followPlaylist:function(a,b,c){return this.api("/users/"+a+"/playlists/"+b+"/followers","PUT",null,{public:c||null},this._auth(!0))},unfollowPlaylist:function(a,b){return this.api("/users/"+a+"/playlists/"+b+"/followers","DELETE",null,null,this._auth())},playlistFollowingContains:function(a,b,c){return this.api("/users/"+a+"/playlists/"+b+"/followers/contains","GET",{ids:c.toString()},null,this._auth())},getSavedUserTracks:function(a){return this.api("/me/tracks","GET",a,null,this._auth())},userTracksContains:function(a){return a=b.isString(a)?a.split(","):a,b.forEach(a,function(b,c){a[c]=b.indexOf("spotify:")>-1?b.split(":")[2]:b}),this.api("/me/tracks/contains","GET",{ids:a.toString()},null,this._auth())},saveUserTracks:function(a){return a=b.isString(a)?a.split(","):a,b.forEach(a,function(b,c){a[c]=b.indexOf("spotify:")>-1?b.split(":")[2]:b}),this.api("/me/tracks","PUT",{ids:a.toString()},null,this._auth())},removeUserTracks:function(a){return a=b.isString(a)?a.split(","):a,b.forEach(a,function(b,c){a[c]=b.indexOf("spotify:")>-1?b.split(":")[2]:b}),this.api("/me/tracks","DELETE",{ids:a.toString()},null,this._auth(!0))},saveUserAlbums:function(a){return a=b.isString(a)?a.split(","):a,b.forEach(a,function(b,c){a[c]=b.indexOf("spotify:")>-1?b.split(":")[2]:b}),this.api("/me/albums","PUT",{ids:a.toString()},null,this._auth())},getSavedUserAlbums:function(a){return this.api("/me/albums","GET",a,null,this._auth())},removeUserAlbums:function(a){return a=b.isString(a)?a.split(","):a,b.forEach(a,function(b,c){a[c]=b.indexOf("spotify:")>-1?b.split(":")[2]:b}),this.api("/me/albums","DELETE",{ids:a.toString()},null,this._auth(!0))},userAlbumsContains:function(a){return a=b.isString(a)?a.split(","):a,b.forEach(a,function(b,c){a[c]=b.indexOf("spotify:")>-1?b.split(":")[2]:b}),this.api("/me/albums/contains","GET",{ids:a.toString()},null,this._auth())},getUserTopArtists:function(a){return a=a||{},this.api("/me/top/artists","GET",a,null,this._auth())},getUserTopTracks:function(a){return a=a||{},this.api("/me/top/tracks","GET",a,null,this._auth())},getUserPlaylists:function(a,b){return this.api("/users/"+a+"/playlists","GET",b,null,this._auth())},getPlaylist:function(a,b,c){return this.api("/users/"+a+"/playlists/"+b,"GET",c,null,this._auth())},getPlaylistTracks:function(a,b,c){return this.api("/users/"+a+"/playlists/"+b+"/tracks","GET",c,null,this._auth())},createPlaylist:function(a,b){return this.api("/users/"+a+"/playlists","POST",null,b,this._auth(!0))},addPlaylistTracks:function(a,c,d,e){return d=b.isArray(d)?d:d.split(","),b.forEach(d,function(a,b){d[b]=-1===a.indexOf("spotify:")?"spotify:track:"+a:a}),this.api("/users/"+a+"/playlists/"+c+"/tracks","POST",{uris:d.toString(),position:e?e.position:null},null,this._auth(!0))},removePlaylistTracks:function(a,c,d){d=b.isArray(d)?d:d.split(",");var e;return b.forEach(d,function(a,b){e=d[b],d[b]={uri:-1===e.indexOf("spotify:")?"spotify:track:"+e:e}}),this.api("/users/"+a+"/playlists/"+c+"/tracks","DELETE",null,{tracks:d},this._auth(!0))},reorderPlaylistTracks:function(a,b,c){return this.api("/users/"+a+"/playlists/"+b+"/tracks","PUT",null,c,this._auth(!0))},replacePlaylistTracks:function(a,c,d){d=b.isArray(d)?d:d.split(",");var e;return b.forEach(d,function(a,b){e=d[b],d[b]=-1===e.indexOf("spotify:")?"spotify:track:"+e:e}),this.api("/users/"+a+"/playlists/"+c+"/tracks","PUT",{uris:d.toString()},null,this._auth(!0))},updatePlaylistDetails:function(a,b,c){return this.api("/users/"+a+"/playlists/"+b,"PUT",null,c,this._auth(!0))},getUser:function(a){return this.api("/users/"+a,"GET",null,null,this._auth())},getCurrentUser:function(){return this.api("/me","GET",null,null,this._auth())},search:function(a,b,c){return c=c||{},c.q=a,c.type=b,this.api("/search","GET",c,null,this._auth())},getTrack:function(a){return a=-1===a.indexOf("spotify:")?a:a.split(":")[2],this.api("/tracks/"+a,"GET",null,null,this._auth())},getTracks:function(a){return a=b.isString(a)?a.split(","):a,b.forEach(a,function(b,c){a[c]=b.indexOf("spotify:")>-1?b.split(":")[2]:b}),this.api("/tracks/","GET",{ids:a?a.toString():""},null,this._auth())},getTrackAudioFeatures:function(a){return a=-1===a.indexOf("spotify:")?a:a.split(":")[2],this.api("/audio-features/"+a,"GET",null,null,this._auth())},getTracksAudioFeatures:function(a){return a=b.isString(a)?a.split(","):a,b.forEach(a,function(b,c){a[c]=b.indexOf("spotify:")>-1?b.split(":")[2]:b}),this.api("/audio-features/","GET",{ids:a?a.toString():""},null,this._auth())},setAuthToken:function(a){return this.authToken=a,this.authToken},login:function(){function a(d){"spotify-token"===d.key&&(k&&k.close(),j=!0,c.setAuthToken(d.newValue),g.removeEventListener("storage",a,!1),b.resolve(d.newValue))}var b=e.defer(),c=this,d=screen.width/2-200,f=screen.height/2-250,h={client_id:this.clientId,redirect_uri:this.redirectUri,scope:this.scope||"",response_type:"token"},j=!1,k=i("https://accounts.spotify.com/authorize?"+this.toQueryString(h),"Spotify","menubar=no,location=no,resizable=yes,scrollbars=yes,status=no,width=400,height=500,top="+f+",left="+d,function(){j||b.reject()});return g.addEventListener("storage",a,!1),b.promise}},new h}]})}(window,angular); -------------------------------------------------------------------------------- /examples/callback.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | angular-spotify demo! 5 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /examples/main.controller.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('exampleApp', ['spotify']) 3 | .config(function (SpotifyProvider) { 4 | SpotifyProvider.setClientId('123456789123456789'); 5 | SpotifyProvider.setRedirectUri('http://example.com/callback.html'); 6 | SpotifyProvider.setScope('playlist-read-private'); 7 | }) 8 | .controller('MainController', ['$scope', 'Spotify', function ($scope, Spotify) { 9 | 10 | $scope.searchArtist = function () { 11 | Spotify.search($scope.searchartist, 'artist').then(function (data) { 12 | $scope.artists = data.artists.items; 13 | }); 14 | }; 15 | 16 | $scope.login = function () { 17 | Spotify.login().then(function (data) { 18 | console.log(data); 19 | alert("You are now logged in"); 20 | }, function () { 21 | console.log('didn\'t log in'); 22 | }) 23 | }; 24 | 25 | // Gets an album 26 | Spotify.getAlbum('0sNOF9WDwhWunNAHPD3Baj').then(function (data){ 27 | console.log('=================== Album - ID ==================='); 28 | console.log(data); 29 | }); 30 | // Works with Spotify uri too 31 | Spotify.getAlbum('spotify:album:0sNOF9WDwhWunNAHPD3Baj').then(function (data){ 32 | console.log('=================== Album - Spotify URI ==================='); 33 | console.log(data); 34 | }); 35 | 36 | //Get multiple Albums 37 | Spotify.getAlbums('41MnTivkwTO3UUJ8DrqEJJ,6JWc4iAiJ9FjyK0B59ABb4,6UXCm6bOO4gFlDQZV5yL37').then(function (data) { 38 | console.log('=================== Albums - Ids ==================='); 39 | console.log(data); 40 | }); 41 | Spotify.getAlbums(['41MnTivkwTO3UUJ8DrqEJJ','6JWc4iAiJ9FjyK0B59ABb4','6UXCm6bOO4gFlDQZV5yL37']).then(function (data) { 42 | console.log('=================== Albums - Array ==================='); 43 | console.log(data); 44 | }); 45 | 46 | 47 | Spotify.getAlbumTracks('41MnTivkwTO3UUJ8DrqEJJ').then(function (data) { 48 | console.log('=================== Album Tracks - ID ==================='); 49 | console.log(data); 50 | }); 51 | Spotify.getAlbumTracks('spotify:album:41MnTivkwTO3UUJ8DrqEJJ').then(function (data) { 52 | console.log('=================== Album Tracks - Spotify URI ==================='); 53 | console.log(data); 54 | }); 55 | 56 | 57 | 58 | //Artist 59 | Spotify.getArtist('0LcJLqbBmaGUft1e9Mm8HV').then(function (data) { 60 | console.log('=================== Artist - Id ==================='); 61 | console.log(data); 62 | }); 63 | Spotify.getArtist('spotify:artist:0LcJLqbBmaGUft1e9Mm8HV').then(function (data) { 64 | console.log('=================== Artist - Spotify URI ==================='); 65 | console.log(data); 66 | }); 67 | 68 | Spotify.getArtistAlbums('0LcJLqbBmaGUft1e9Mm8HV').then(function (data) { 69 | console.log('=================== Artist Albums - Id ==================='); 70 | console.log(data); 71 | }); 72 | 73 | Spotify.getArtistAlbums('spotify:artist:0LcJLqbBmaGUft1e9Mm8HV').then(function (data) { 74 | console.log('=================== Artist Albums - Spotify URI ==================='); 75 | console.log(data); 76 | }); 77 | 78 | Spotify.getArtistTopTracks('0LcJLqbBmaGUft1e9Mm8HV', 'AU').then(function (data) { 79 | console.log('=================== Artist Top Tracks Australia ==================='); 80 | console.log(data); 81 | }); 82 | 83 | Spotify.getRelatedArtists('0LcJLqbBmaGUft1e9Mm8HV').then(function (data) { 84 | console.log('=================== Get Releated Artists ==================='); 85 | console.log(data); 86 | }); 87 | 88 | 89 | //Tracks 90 | Spotify.getTrack('0eGsygTp906u18L0Oimnem').then(function (data) { 91 | console.log('=================== Track ==================='); 92 | console.log(data); 93 | }); 94 | 95 | Spotify.getTracks('0eGsygTp906u18L0Oimnem,1lDWb6b6ieDQ2xT7ewTC3G').then(function (data) { 96 | console.log('=================== Tracks - String ==================='); 97 | console.log(data); 98 | }); 99 | 100 | Spotify.getTracks(['0eGsygTp906u18L0Oimnem','1lDWb6b6ieDQ2xT7ewTC3G']).then(function (data) { 101 | console.log('=================== Tracks - Array ==================='); 102 | console.log(data); 103 | }); 104 | 105 | }]); 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-spotify", 3 | "version": "1.5.1", 4 | "description": "Angular Service to connect to Spotify Web API", 5 | "main": "src/angular-spotify.js", 6 | "devDependencies": { 7 | "codecov": "^3.0.0", 8 | "coveralls": "^3.0.0", 9 | "grunt": "~1.6.1", 10 | "grunt-contrib-clean": "~2.0.0", 11 | "grunt-contrib-concat": "~1.0.0", 12 | "grunt-contrib-jshint": "~2.0.0", 13 | "grunt-contrib-uglify": "^3.0.1", 14 | "grunt-contrib-watch": "~1.1.0", 15 | "grunt-karma": "^2.0.0", 16 | "jasmine-core": "^2.4.1", 17 | "jasmine-node": "^1.14.5", 18 | "jshint": "^2.9.1", 19 | "jshint-stylish": "^2.1.0", 20 | "karma": "~1.7.1", 21 | "karma-coverage": "^1.1.1", 22 | "karma-jasmine": "^1.1.0", 23 | "karma-phantomjs-launcher": "^1.0.0", 24 | "karma-spec-reporter": "0.0.36", 25 | "load-grunt-tasks": "^3.4.1", 26 | "phantomjs-prebuilt": "2.1.15", 27 | "time-grunt": "^1.3.0" 28 | }, 29 | "engines": { 30 | "node": ">=0.10.0" 31 | }, 32 | "scripts": { 33 | "test": "grunt test", 34 | "coveralls": "cat ./test/coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", 35 | "codecov": "codecov", 36 | "prepublish": "grunt" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/eddiemoore/angular-spotify.git" 41 | }, 42 | "keywords": [ 43 | "spotify", 44 | "angular", 45 | "angularjs" 46 | ], 47 | "author": "Ed Moore", 48 | "license": "MIT", 49 | "bugs": { 50 | "url": "https://github.com/eddiemoore/angular-spotify/issues" 51 | }, 52 | "homepage": "https://github.com/eddiemoore/angular-spotify" 53 | } 54 | -------------------------------------------------------------------------------- /src/angular-spotify.js: -------------------------------------------------------------------------------- 1 | (function (window, angular, undefined) { 2 | 'use strict'; 3 | 4 | angular 5 | .module('spotify', []) 6 | .provider('Spotify', function () { 7 | 8 | // Module global settings. 9 | var settings = {}; 10 | settings.clientId = null; 11 | settings.redirectUri = null; 12 | settings.scope = null; 13 | settings.authToken = null; 14 | 15 | this.setClientId = function (clientId) { 16 | settings.clientId = clientId; 17 | return settings.clientId; 18 | }; 19 | 20 | this.getClientId = function () { 21 | return settings.clientId; 22 | }; 23 | 24 | this.setAuthToken = function (authToken) { 25 | settings.authToken = authToken; 26 | return settings.authToken; 27 | }; 28 | 29 | this.setRedirectUri = function (redirectUri) { 30 | settings.redirectUri = redirectUri; 31 | return settings.redirectUri; 32 | }; 33 | 34 | this.getRedirectUri = function () { 35 | return settings.redirectUri; 36 | }; 37 | 38 | this.setScope = function (scope) { 39 | settings.scope = scope; 40 | return settings.scope; 41 | }; 42 | 43 | var utils = {}; 44 | utils.toQueryString = function (obj) { 45 | var parts = []; 46 | angular.forEach(obj, function (value, key) { 47 | this.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); 48 | }, parts); 49 | return parts.join('&'); 50 | }; 51 | 52 | /** 53 | * API Base URL 54 | */ 55 | settings.apiBase = 'https://api.spotify.com/v1'; 56 | 57 | this.$get = ['$q', '$http', '$window', function ($q, $http, $window) { 58 | 59 | function NgSpotify () { 60 | this.clientId = settings.clientId; 61 | this.redirectUri = settings.redirectUri; 62 | this.apiBase = settings.apiBase; 63 | this.scope = settings.scope; 64 | this.authToken = settings.authToken; 65 | this.toQueryString = utils.toQueryString; 66 | } 67 | 68 | function openDialog (uri, name, options, cb) { 69 | var win = window.open(uri, name, options); 70 | var interval = window.setInterval(function () { 71 | try { 72 | if (!win || win.closed) { 73 | window.clearInterval(interval); 74 | cb(win); 75 | } 76 | } catch (e) {} 77 | }, 1000); 78 | return win; 79 | } 80 | 81 | NgSpotify.prototype = { 82 | api: function (endpoint, method, params, data, headers) { 83 | var deferred = $q.defer(); 84 | 85 | $http({ 86 | url: this.apiBase + endpoint, 87 | method: method ? method : 'GET', 88 | params: params, 89 | data: data, 90 | headers: headers, 91 | withCredentials: false 92 | }) 93 | .then(function (data) { 94 | deferred.resolve(data); 95 | }) 96 | .catch(function (data) { 97 | deferred.reject(data); 98 | }); 99 | return deferred.promise; 100 | }, 101 | 102 | _auth: function (isJson) { 103 | var auth = { 104 | 'Authorization': 'Bearer ' + this.authToken 105 | }; 106 | if (isJson) { 107 | auth['Content-Type'] = 'application/json'; 108 | } 109 | return auth; 110 | }, 111 | 112 | /** 113 | ====================== Albums ===================== 114 | */ 115 | 116 | /** 117 | * Gets an album 118 | * Pass in album id or spotify uri 119 | */ 120 | getAlbum: function (album) { 121 | album = album.indexOf('spotify:') === -1 ? album : album.split(':')[2]; 122 | 123 | return this.api('/albums/' + album, 'GET', null, null, this._auth()); 124 | }, 125 | 126 | /** 127 | * Gets an album 128 | * Pass in comma separated string or array of album ids 129 | */ 130 | getAlbums: function (albums) { 131 | albums = angular.isString(albums) ? albums.split(',') : albums; 132 | angular.forEach(albums, function (value, index) { 133 | albums[index] = value.indexOf('spotify:') > -1 ? value.split(':')[2] : value; 134 | }); 135 | return this.api('/albums', 'GET', { 136 | ids: albums ? albums.toString() : '' 137 | }, null, this._auth()); 138 | }, 139 | 140 | /** 141 | * Get Album Tracks 142 | * Pass in album id or spotify uri 143 | */ 144 | getAlbumTracks: function (album, options) { 145 | album = album.indexOf('spotify:') === -1 ? album : album.split(':')[2]; 146 | 147 | return this.api('/albums/' + album + '/tracks', 'GET', options, null, this._auth()); 148 | }, 149 | 150 | 151 | /** 152 | ====================== Artists ===================== 153 | */ 154 | 155 | /** 156 | * Get an Artist 157 | */ 158 | getArtist: function (artist) { 159 | artist = artist.indexOf('spotify:') === -1 ? artist : artist.split(':')[2]; 160 | 161 | return this.api('/artists/' + artist, 'GET', null, null, this._auth()); 162 | }, 163 | 164 | /** 165 | * Get multiple artists 166 | */ 167 | getArtists: function (artists) { 168 | artists = angular.isString(artists) ? artists.split(',') : artists; 169 | angular.forEach(artists, function (value, index) { 170 | artists[index] = value.indexOf('spotify:') > -1 ? value.split(':')[2] : value; 171 | }); 172 | return this.api('/artists/', 'GET', { 173 | ids: artists ? artists.toString() : '' 174 | }, null, this._auth()); 175 | }, 176 | 177 | //Artist Albums 178 | getArtistAlbums: function (artist, options) { 179 | artist = artist.indexOf('spotify:') === -1 ? artist : artist.split(':')[2]; 180 | 181 | return this.api('/artists/' + artist + '/albums', 'GET', options, null, this._auth()); 182 | }, 183 | 184 | /** 185 | * Get Artist Top Tracks 186 | * The country: an ISO 3166-1 alpha-2 country code. 187 | */ 188 | getArtistTopTracks: function (artist, country) { 189 | artist = artist.indexOf('spotify:') === -1 ? artist : artist.split(':')[2]; 190 | 191 | return this.api('/artists/' + artist + '/top-tracks', 'GET', { 192 | country: country 193 | }, null, this._auth()); 194 | }, 195 | 196 | getRelatedArtists: function (artist) { 197 | artist = artist.indexOf('spotify:') === -1 ? artist : artist.split(':')[2]; 198 | 199 | return this.api('/artists/' + artist + '/related-artists', 'GET', null, null, this._auth()); 200 | }, 201 | 202 | 203 | /** 204 | ====================== Browse ===================== 205 | */ 206 | getFeaturedPlaylists: function (options) { 207 | return this.api('/browse/featured-playlists', 'GET', options, null, this._auth()); 208 | }, 209 | 210 | getNewReleases: function (options) { 211 | return this.api('/browse/new-releases', 'GET', options, null, this._auth()); 212 | }, 213 | 214 | getCategories: function (options) { 215 | return this.api('/browse/categories', 'GET', options, null, this._auth()); 216 | }, 217 | 218 | getCategory: function (category_id, options) { 219 | return this.api('/browse/categories/' + category_id, 'GET', options, null, this._auth()); 220 | }, 221 | 222 | getCategoryPlaylists: function (category_id, options) { 223 | return this.api('/browse/categories/' + category_id + '/playlists', 'GET', options, null, this._auth()); 224 | }, 225 | 226 | getRecommendations: function (options) { 227 | return this.api('/recommendations', 'GET', options, null, this._auth()); 228 | }, 229 | 230 | getAvailableGenreSeeds: function () { 231 | return this.api('/recommendations/available-genre-seeds', 'GET', null, null, this._auth()); 232 | }, 233 | 234 | 235 | /** 236 | ====================== Following ===================== 237 | */ 238 | following: function (type, options) { 239 | options = options || {}; 240 | options.type = type; 241 | return this.api('/me/following', 'GET', options, null, this._auth()); 242 | }, 243 | 244 | follow: function (type, ids) { 245 | return this.api('/me/following', 'PUT', { type: type, ids: ids }, null, this._auth()); 246 | }, 247 | 248 | unfollow: function (type, ids) { 249 | return this.api('/me/following', 'DELETE', { type: type, ids: ids }, null, this._auth()); 250 | }, 251 | 252 | userFollowingContains: function (type, ids) { 253 | return this.api('/me/following/contains', 'GET', { type: type, ids: ids }, null, this._auth()); 254 | }, 255 | 256 | followPlaylist: function (userId, playlistId, isPublic) { 257 | return this.api('/users/' + userId + '/playlists/' + playlistId + '/followers', 'PUT', null, { 258 | public: isPublic || null 259 | }, this._auth(true)); 260 | }, 261 | 262 | unfollowPlaylist: function (userId, playlistId) { 263 | return this.api('/users/' + userId + '/playlists/' + playlistId + '/followers', 'DELETE', null, null, this._auth()); 264 | }, 265 | 266 | playlistFollowingContains: function(userId, playlistId, ids) { 267 | return this.api('/users/' + userId + '/playlists/' + playlistId + '/followers/contains', 'GET', { 268 | ids: ids.toString() 269 | }, null, this._auth()); 270 | }, 271 | 272 | 273 | /** 274 | ====================== Library ===================== 275 | */ 276 | getSavedUserTracks: function (options) { 277 | return this.api('/me/tracks', 'GET', options, null, this._auth()); 278 | }, 279 | 280 | userTracksContains: function (tracks) { 281 | tracks = angular.isString(tracks) ? tracks.split(',') : tracks; 282 | angular.forEach(tracks, function (value, index) { 283 | tracks[index] = value.indexOf('spotify:') > -1 ? value.split(':')[2] : value; 284 | }); 285 | return this.api('/me/tracks/contains', 'GET', { 286 | ids: tracks.toString() 287 | }, null, this._auth()); 288 | }, 289 | 290 | saveUserTracks: function (tracks) { 291 | tracks = angular.isString(tracks) ? tracks.split(',') : tracks; 292 | angular.forEach(tracks, function (value, index) { 293 | tracks[index] = value.indexOf('spotify:') > -1 ? value.split(':')[2] : value; 294 | }); 295 | return this.api('/me/tracks', 'PUT', { 296 | ids: tracks.toString() 297 | }, null, this._auth()); 298 | }, 299 | 300 | removeUserTracks: function (tracks) { 301 | tracks = angular.isString(tracks) ? tracks.split(',') : tracks; 302 | angular.forEach(tracks, function (value, index) { 303 | tracks[index] = value.indexOf('spotify:') > -1 ? value.split(':')[2] : value; 304 | }); 305 | return this.api('/me/tracks', 'DELETE', { 306 | ids: tracks.toString() 307 | }, null, this._auth(true)); 308 | }, 309 | 310 | saveUserAlbums: function (albums) { 311 | albums = angular.isString(albums) ? albums.split(',') : albums; 312 | angular.forEach(albums, function (value, index) { 313 | albums[index] = value.indexOf('spotify:') > -1 ? value.split(':')[2] : value; 314 | }); 315 | return this.api('/me/albums', 'PUT', { 316 | ids: albums.toString() 317 | }, null, this._auth()); 318 | }, 319 | 320 | getSavedUserAlbums: function (options) { 321 | return this.api('/me/albums', 'GET', options, null, this._auth()); 322 | }, 323 | 324 | removeUserAlbums: function (albums) { 325 | albums = angular.isString(albums) ? albums.split(',') : albums; 326 | angular.forEach(albums, function (value, index) { 327 | albums[index] = value.indexOf('spotify:') > -1 ? value.split(':')[2] : value; 328 | }); 329 | return this.api('/me/albums', 'DELETE', { 330 | ids: albums.toString() 331 | }, null, this._auth(true)); 332 | }, 333 | 334 | userAlbumsContains: function (albums) { 335 | albums = angular.isString(albums) ? albums.split(',') : albums; 336 | angular.forEach(albums, function (value, index) { 337 | albums[index] = value.indexOf('spotify:') > -1 ? value.split(':')[2] : value; 338 | }); 339 | return this.api('/me/albums/contains', 'GET', { 340 | ids: albums.toString() 341 | }, null, this._auth()); 342 | }, 343 | 344 | 345 | /** 346 | ====================== Personalization ===================== 347 | */ 348 | getUserTopArtists: function (options) { 349 | options = options || {}; 350 | return this.api('/me/top/artists', 'GET', options, null, this._auth()); 351 | }, 352 | 353 | getUserTopTracks: function (options) { 354 | options = options || {}; 355 | return this.api('/me/top/tracks', 'GET', options, null, this._auth()); 356 | }, 357 | 358 | 359 | /** 360 | ====================== Playlists ===================== 361 | */ 362 | getUserPlaylists: function (userId, options) { 363 | return this.api('/users/' + userId + '/playlists', 'GET', options, null, this._auth()); 364 | }, 365 | 366 | getPlaylist: function (userId, playlistId, options) { 367 | return this.api('/users/' + userId + '/playlists/' + playlistId, 'GET', options, null, this._auth()); 368 | }, 369 | 370 | getPlaylistTracks: function (userId, playlistId, options) { 371 | return this.api('/users/' + userId + '/playlists/' + playlistId + '/tracks', 'GET', options, null, this._auth()); 372 | }, 373 | 374 | createPlaylist: function (userId, options) { 375 | return this.api('/users/' + userId + '/playlists', 'POST', null, options, this._auth(true)); 376 | }, 377 | 378 | addPlaylistTracks: function (userId, playlistId, tracks, options) { 379 | tracks = angular.isArray(tracks) ? tracks : tracks.split(','); 380 | angular.forEach(tracks, function (value, index) { 381 | tracks[index] = value.indexOf('spotify:') === -1 ? 'spotify:track:' + value : value; 382 | }); 383 | return this.api('/users/' + userId + '/playlists/' + playlistId + '/tracks', 'POST', { 384 | uris: tracks.toString(), 385 | position: options ? options.position : null 386 | }, null, this._auth(true)); 387 | }, 388 | 389 | removePlaylistTracks: function (userId, playlistId, tracks) { 390 | tracks = angular.isArray(tracks) ? tracks : tracks.split(','); 391 | var track; 392 | angular.forEach(tracks, function (value, index) { 393 | track = tracks[index]; 394 | tracks[index] = { 395 | uri: track.indexOf('spotify:') === -1 ? 'spotify:track:' + track : track 396 | }; 397 | }); 398 | return this.api('/users/' + userId + '/playlists/' + playlistId + '/tracks', 'DELETE', null, { 399 | tracks: tracks 400 | }, this._auth(true)); 401 | }, 402 | 403 | reorderPlaylistTracks: function (userId, playlistId, options) { 404 | return this.api('/users/' + userId + '/playlists/' + playlistId + '/tracks', 'PUT', null, options, this._auth(true)); 405 | }, 406 | 407 | replacePlaylistTracks: function (userId, playlistId, tracks) { 408 | tracks = angular.isArray(tracks) ? tracks : tracks.split(','); 409 | var track; 410 | angular.forEach(tracks, function (value, index) { 411 | track = tracks[index]; 412 | tracks[index] = track.indexOf('spotify:') === -1 ? 'spotify:track:' + track : track; 413 | }); 414 | return this.api('/users/' + userId + '/playlists/' + playlistId + '/tracks', 'PUT', { 415 | uris: tracks.toString() 416 | }, null, this._auth(true)); 417 | }, 418 | 419 | updatePlaylistDetails: function (userId, playlistId, options) { 420 | return this.api('/users/' + userId + '/playlists/' + playlistId, 'PUT', null, options, this._auth(true)); 421 | }, 422 | 423 | /** 424 | ====================== Profiles ===================== 425 | */ 426 | 427 | getUser: function (userId) { 428 | return this.api('/users/' + userId, 'GET', null, null, this._auth()); 429 | }, 430 | 431 | getCurrentUser: function () { 432 | return this.api('/me', 'GET', null, null, this._auth()); 433 | }, 434 | 435 | 436 | 437 | /** 438 | * Search Spotify 439 | * q = search query 440 | * type = artist, album or track 441 | */ 442 | search: function (q, type, options) { 443 | options = options || {}; 444 | options.q = q; 445 | options.type = type; 446 | 447 | return this.api('/search', 'GET', options, null, this._auth()); 448 | }, 449 | 450 | 451 | /** 452 | ====================== Tracks ===================== 453 | */ 454 | getTrack: function (track) { 455 | track = track.indexOf('spotify:') === -1 ? track : track.split(':')[2]; 456 | 457 | return this.api('/tracks/' + track, 'GET', null, null, this._auth()); 458 | }, 459 | 460 | getTracks: function (tracks) { 461 | tracks = angular.isString(tracks) ? tracks.split(',') : tracks; 462 | angular.forEach(tracks, function (value, index) { 463 | tracks[index] = value.indexOf('spotify:') > -1 ? value.split(':')[2] : value; 464 | }); 465 | return this.api('/tracks/', 'GET', { 466 | ids: tracks ? tracks.toString() : '' 467 | }, null, this._auth()); 468 | }, 469 | 470 | getTrackAudioFeatures: function (track) { 471 | track = track.indexOf('spotify:') === -1 ? track : track.split(':')[2]; 472 | return this.api('/audio-features/' + track, 'GET', null, null, this._auth()); 473 | }, 474 | 475 | getTracksAudioFeatures: function (tracks) { 476 | tracks = angular.isString(tracks) ? tracks.split(',') : tracks; 477 | angular.forEach(tracks, function (value, index) { 478 | tracks[index] = value.indexOf('spotify:') > -1 ? value.split(':')[2] : value; 479 | }); 480 | return this.api('/audio-features/', 'GET', { 481 | ids: tracks ? tracks.toString() : '' 482 | }, null, this._auth()); 483 | }, 484 | 485 | 486 | /** 487 | ====================== Login ===================== 488 | */ 489 | setAuthToken: function (authToken) { 490 | this.authToken = authToken; 491 | return this.authToken; 492 | }, 493 | 494 | login: function () { 495 | var deferred = $q.defer(); 496 | var that = this; 497 | 498 | var w = 400, 499 | h = 500, 500 | left = (screen.width / 2) - (w / 2), 501 | top = (screen.height / 2) - (h / 2); 502 | 503 | var params = { 504 | client_id: this.clientId, 505 | redirect_uri: this.redirectUri, 506 | scope: this.scope || '', 507 | response_type: 'token' 508 | }; 509 | 510 | var authCompleted = false; 511 | var authWindow = openDialog( 512 | 'https://accounts.spotify.com/authorize?' + this.toQueryString(params), 513 | 'Spotify', 514 | 'menubar=no,location=no,resizable=yes,scrollbars=yes,status=no,width=' + w + ',height=' + h + ',top=' + top + ',left=' + left, 515 | function () { 516 | if (!authCompleted) { 517 | deferred.reject(); 518 | } 519 | } 520 | ); 521 | 522 | function storageChanged (e) { 523 | if (e.key === 'spotify-token') { 524 | if (authWindow) { authWindow.close(); } 525 | authCompleted = true; 526 | 527 | that.setAuthToken(e.newValue); 528 | $window.removeEventListener('storage', storageChanged, false); 529 | 530 | deferred.resolve(e.newValue); 531 | } 532 | } 533 | 534 | $window.addEventListener('storage', storageChanged, false); 535 | 536 | return deferred.promise; 537 | } 538 | }; 539 | 540 | return new NgSpotify(); 541 | }]; 542 | 543 | }); 544 | 545 | }(window, angular)); 546 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": false, 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": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "after": false, 23 | "afterEach": false, 24 | "angular": false, 25 | "before": false, 26 | "beforeEach": false, 27 | "browser": false, 28 | "describe": false, 29 | "expect": false, 30 | "inject": false, 31 | "it": false, 32 | "jasmine": false, 33 | "spyOn": false 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.12/config/configuration-file.html 3 | // Generated on 2014-09-01 using 4 | // generator-karma 0.8.3 5 | 6 | module.exports = function(config) { 7 | 'use strict'; 8 | 9 | config.set({ 10 | // enable / disable watching file and executing tests whenever any file changes 11 | autoWatch: true, 12 | 13 | // base path, that will be used to resolve files and exclude 14 | basePath: '../', 15 | 16 | // testing framework to use (jasmine/mocha/qunit/...) 17 | frameworks: ['jasmine'], 18 | 19 | // list of files / patterns to load in the browser 20 | files: [ 21 | // angular 22 | 'bower_components/angular/angular.js', 23 | 'bower_components/angular-mocks/angular-mocks.js', 24 | // jasmine jquery helper 25 | 'test/vendor/jquery-1.11.1.js', 26 | 'test/vendor/jasmine-jquery.js', 27 | // app 28 | 'src/**/*.js', 29 | // tests 30 | // 'test/mock/**/*.js', 31 | 'test/spec/angular-spotify.spec.js', 32 | // fixtures 33 | {pattern: 'test/mock/*.json', watched: true, served: true, included: false} 34 | ], 35 | 36 | // list of files / patterns to exclude 37 | exclude: [], 38 | 39 | // web server port 40 | port: 8080, 41 | 42 | // Start these browsers, currently available: 43 | // - Chrome 44 | // - ChromeCanary 45 | // - Firefox 46 | // - Opera 47 | // - Safari (only Mac) 48 | // - PhantomJS 49 | // - IE (only Windows) 50 | browsers: [ 51 | 'PhantomJS' 52 | ], 53 | 54 | reporters: ['coverage', 'spec'], 55 | 56 | preprocessors: { 57 | // source files, that you wanna generate coverage for 58 | // do not include tests or libraries 59 | // (these files will be instrumented by Istanbul) 60 | 'src/*.js': ['coverage'] 61 | }, 62 | 63 | // optionally, configure the reporter 64 | coverageReporter: { 65 | type : 'lcovonly', 66 | dir : 'test/coverage/', 67 | subdir: '.', 68 | file : 'lcov.info' 69 | }, 70 | 71 | // Which plugins to enable 72 | plugins: [ 73 | 'karma-phantomjs-launcher', 74 | 'karma-jasmine', 75 | 'karma-coverage', 76 | 'karma-spec-reporter' 77 | ], 78 | 79 | // Continuous Integration mode 80 | // if true, it capture browsers, run tests and exit 81 | singleRun: false, 82 | 83 | colors: true, 84 | 85 | // level of logging 86 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 87 | logLevel: config.LOG_INFO, 88 | 89 | // Uncomment the following lines if you are using grunt's server to run the tests 90 | // proxies: { 91 | // '/': 'http://localhost:9000/' 92 | // }, 93 | // URL root prevent conflicts with the site root 94 | // urlRoot: '_karma_' 95 | }); 96 | }; 97 | -------------------------------------------------------------------------------- /test/mock/album.error.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "status": 404, 4 | "message": "non existing id" 5 | } 6 | } -------------------------------------------------------------------------------- /test/mock/album.json: -------------------------------------------------------------------------------- 1 | { 2 | "album_type" : "album", 3 | "artists" : [ { 4 | "external_urls" : { 5 | "spotify" : "https://open.spotify.com/artist/08td7MxkoHQkXnWAYD8d6Q" 6 | }, 7 | "href" : "https://api.spotify.com/v1/artists/08td7MxkoHQkXnWAYD8d6Q", 8 | "id" : "08td7MxkoHQkXnWAYD8d6Q", 9 | "name" : "Tania Bowra", 10 | "type" : "artist", 11 | "uri" : "spotify:artist:08td7MxkoHQkXnWAYD8d6Q" 12 | } ], 13 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 14 | "external_ids" : { 15 | "upc" : "9324690012824" 16 | }, 17 | "external_urls" : { 18 | "spotify" : "https://open.spotify.com/album/6akEvsycLGftJxYudPjmqK" 19 | }, 20 | "genres" : [ ], 21 | "href" : "https://api.spotify.com/v1/albums/6akEvsycLGftJxYudPjmqK", 22 | "id" : "6akEvsycLGftJxYudPjmqK", 23 | "images" : [ { 24 | "height" : 640, 25 | "url" : "https://i.scdn.co/image/f2798ddab0c7b76dc2d270b65c4f67ddef7f6718", 26 | "width" : 640 27 | }, { 28 | "height" : 300, 29 | "url" : "https://i.scdn.co/image/b414091165ea0f4172089c2fc67bb35aa37cfc55", 30 | "width" : 300 31 | }, { 32 | "height" : 64, 33 | "url" : "https://i.scdn.co/image/8522fc78be4bf4e83fea8e67bb742e7d3dfe21b4", 34 | "width" : 64 35 | } ], 36 | "name" : "Place In The Sun", 37 | "popularity" : 6, 38 | "release_date" : "2004-02-02", 39 | "release_date_precision" : "day", 40 | "tracks" : { 41 | "href" : "https://api.spotify.com/v1/albums/6akEvsycLGftJxYudPjmqK/tracks?offset=0&limit=50", 42 | "items" : [ { 43 | "artists" : [ { 44 | "external_urls" : { 45 | "spotify" : "https://open.spotify.com/artist/08td7MxkoHQkXnWAYD8d6Q" 46 | }, 47 | "href" : "https://api.spotify.com/v1/artists/08td7MxkoHQkXnWAYD8d6Q", 48 | "id" : "08td7MxkoHQkXnWAYD8d6Q", 49 | "name" : "Tania Bowra", 50 | "type" : "artist", 51 | "uri" : "spotify:artist:08td7MxkoHQkXnWAYD8d6Q" 52 | } ], 53 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 54 | "disc_number" : 1, 55 | "duration_ms" : 276773, 56 | "explicit" : false, 57 | "external_urls" : { 58 | "spotify" : "https://open.spotify.com/track/2TpxZ7JUBn3uw46aR7qd6V" 59 | }, 60 | "href" : "https://api.spotify.com/v1/tracks/2TpxZ7JUBn3uw46aR7qd6V", 61 | "id" : "2TpxZ7JUBn3uw46aR7qd6V", 62 | "name" : "All I Want", 63 | "preview_url" : "https://p.scdn.co/mp3-preview/6d00206e32194d15df329d4770e4fa1f2ced3f57", 64 | "track_number" : 1, 65 | "type" : "track", 66 | "uri" : "spotify:track:2TpxZ7JUBn3uw46aR7qd6V" 67 | }, { 68 | "artists" : [ { 69 | "external_urls" : { 70 | "spotify" : "https://open.spotify.com/artist/08td7MxkoHQkXnWAYD8d6Q" 71 | }, 72 | "href" : "https://api.spotify.com/v1/artists/08td7MxkoHQkXnWAYD8d6Q", 73 | "id" : "08td7MxkoHQkXnWAYD8d6Q", 74 | "name" : "Tania Bowra", 75 | "type" : "artist", 76 | "uri" : "spotify:artist:08td7MxkoHQkXnWAYD8d6Q" 77 | } ], 78 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 79 | "disc_number" : 1, 80 | "duration_ms" : 247680, 81 | "explicit" : false, 82 | "external_urls" : { 83 | "spotify" : "https://open.spotify.com/track/4PjcfyZZVE10TFd9EKA72r" 84 | }, 85 | "href" : "https://api.spotify.com/v1/tracks/4PjcfyZZVE10TFd9EKA72r", 86 | "id" : "4PjcfyZZVE10TFd9EKA72r", 87 | "name" : "Someday", 88 | "preview_url" : "https://p.scdn.co/mp3-preview/2b15de922bf4f4b8cfe09c8448079b8ff7a45a5f", 89 | "track_number" : 2, 90 | "type" : "track", 91 | "uri" : "spotify:track:4PjcfyZZVE10TFd9EKA72r" 92 | }, { 93 | "artists" : [ { 94 | "external_urls" : { 95 | "spotify" : "https://open.spotify.com/artist/08td7MxkoHQkXnWAYD8d6Q" 96 | }, 97 | "href" : "https://api.spotify.com/v1/artists/08td7MxkoHQkXnWAYD8d6Q", 98 | "id" : "08td7MxkoHQkXnWAYD8d6Q", 99 | "name" : "Tania Bowra", 100 | "type" : "artist", 101 | "uri" : "spotify:artist:08td7MxkoHQkXnWAYD8d6Q" 102 | } ], 103 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 104 | "disc_number" : 1, 105 | "duration_ms" : 204093, 106 | "explicit" : false, 107 | "external_urls" : { 108 | "spotify" : "https://open.spotify.com/track/2tniUNfN0hmqavFuJ2NodO" 109 | }, 110 | "href" : "https://api.spotify.com/v1/tracks/2tniUNfN0hmqavFuJ2NodO", 111 | "id" : "2tniUNfN0hmqavFuJ2NodO", 112 | "name" : "Wet Wood", 113 | "preview_url" : "https://p.scdn.co/mp3-preview/330aa73c34f81c164f00f7cf4e3de1368e59ff20", 114 | "track_number" : 3, 115 | "type" : "track", 116 | "uri" : "spotify:track:2tniUNfN0hmqavFuJ2NodO" 117 | }, { 118 | "artists" : [ { 119 | "external_urls" : { 120 | "spotify" : "https://open.spotify.com/artist/08td7MxkoHQkXnWAYD8d6Q" 121 | }, 122 | "href" : "https://api.spotify.com/v1/artists/08td7MxkoHQkXnWAYD8d6Q", 123 | "id" : "08td7MxkoHQkXnWAYD8d6Q", 124 | "name" : "Tania Bowra", 125 | "type" : "artist", 126 | "uri" : "spotify:artist:08td7MxkoHQkXnWAYD8d6Q" 127 | } ], 128 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 129 | "disc_number" : 1, 130 | "duration_ms" : 240253, 131 | "explicit" : false, 132 | "external_urls" : { 133 | "spotify" : "https://open.spotify.com/track/1irrhbi54Uwsrky0s3SrPz" 134 | }, 135 | "href" : "https://api.spotify.com/v1/tracks/1irrhbi54Uwsrky0s3SrPz", 136 | "id" : "1irrhbi54Uwsrky0s3SrPz", 137 | "name" : "Give It 2 Me", 138 | "preview_url" : "https://p.scdn.co/mp3-preview/2f0bd7efcd2addda3330990366b81df3fdc37c8c", 139 | "track_number" : 4, 140 | "type" : "track", 141 | "uri" : "spotify:track:1irrhbi54Uwsrky0s3SrPz" 142 | }, { 143 | "artists" : [ { 144 | "external_urls" : { 145 | "spotify" : "https://open.spotify.com/artist/08td7MxkoHQkXnWAYD8d6Q" 146 | }, 147 | "href" : "https://api.spotify.com/v1/artists/08td7MxkoHQkXnWAYD8d6Q", 148 | "id" : "08td7MxkoHQkXnWAYD8d6Q", 149 | "name" : "Tania Bowra", 150 | "type" : "artist", 151 | "uri" : "spotify:artist:08td7MxkoHQkXnWAYD8d6Q" 152 | } ], 153 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 154 | "disc_number" : 1, 155 | "duration_ms" : 220173, 156 | "explicit" : false, 157 | "external_urls" : { 158 | "spotify" : "https://open.spotify.com/track/4JAJ6ujlF593H31kA4UhjR" 159 | }, 160 | "href" : "https://api.spotify.com/v1/tracks/4JAJ6ujlF593H31kA4UhjR", 161 | "id" : "4JAJ6ujlF593H31kA4UhjR", 162 | "name" : "Be Mine", 163 | "preview_url" : "https://p.scdn.co/mp3-preview/271590224d1b24af07b777ffc744191cda49c9b4", 164 | "track_number" : 5, 165 | "type" : "track", 166 | "uri" : "spotify:track:4JAJ6ujlF593H31kA4UhjR" 167 | }, { 168 | "artists" : [ { 169 | "external_urls" : { 170 | "spotify" : "https://open.spotify.com/artist/08td7MxkoHQkXnWAYD8d6Q" 171 | }, 172 | "href" : "https://api.spotify.com/v1/artists/08td7MxkoHQkXnWAYD8d6Q", 173 | "id" : "08td7MxkoHQkXnWAYD8d6Q", 174 | "name" : "Tania Bowra", 175 | "type" : "artist", 176 | "uri" : "spotify:artist:08td7MxkoHQkXnWAYD8d6Q" 177 | } ], 178 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 179 | "disc_number" : 1, 180 | "duration_ms" : 281946, 181 | "explicit" : false, 182 | "external_urls" : { 183 | "spotify" : "https://open.spotify.com/track/6UbiYwgZJrwqB5ILDVk1e5" 184 | }, 185 | "href" : "https://api.spotify.com/v1/tracks/6UbiYwgZJrwqB5ILDVk1e5", 186 | "id" : "6UbiYwgZJrwqB5ILDVk1e5", 187 | "name" : "Desire", 188 | "preview_url" : "https://p.scdn.co/mp3-preview/22ae385689d8ac4f3919ad26057edf215a70e912", 189 | "track_number" : 6, 190 | "type" : "track", 191 | "uri" : "spotify:track:6UbiYwgZJrwqB5ILDVk1e5" 192 | }, { 193 | "artists" : [ { 194 | "external_urls" : { 195 | "spotify" : "https://open.spotify.com/artist/08td7MxkoHQkXnWAYD8d6Q" 196 | }, 197 | "href" : "https://api.spotify.com/v1/artists/08td7MxkoHQkXnWAYD8d6Q", 198 | "id" : "08td7MxkoHQkXnWAYD8d6Q", 199 | "name" : "Tania Bowra", 200 | "type" : "artist", 201 | "uri" : "spotify:artist:08td7MxkoHQkXnWAYD8d6Q" 202 | } ], 203 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 204 | "disc_number" : 1, 205 | "duration_ms" : 189240, 206 | "explicit" : false, 207 | "external_urls" : { 208 | "spotify" : "https://open.spotify.com/track/0WIfmQdGqTbZQjjoSxM8hB" 209 | }, 210 | "href" : "https://api.spotify.com/v1/tracks/0WIfmQdGqTbZQjjoSxM8hB", 211 | "id" : "0WIfmQdGqTbZQjjoSxM8hB", 212 | "name" : "Eastside", 213 | "preview_url" : "https://p.scdn.co/mp3-preview/46b2b6e73675680a136a640fad9b96adb4db37ae", 214 | "track_number" : 7, 215 | "type" : "track", 216 | "uri" : "spotify:track:0WIfmQdGqTbZQjjoSxM8hB" 217 | }, { 218 | "artists" : [ { 219 | "external_urls" : { 220 | "spotify" : "https://open.spotify.com/artist/08td7MxkoHQkXnWAYD8d6Q" 221 | }, 222 | "href" : "https://api.spotify.com/v1/artists/08td7MxkoHQkXnWAYD8d6Q", 223 | "id" : "08td7MxkoHQkXnWAYD8d6Q", 224 | "name" : "Tania Bowra", 225 | "type" : "artist", 226 | "uri" : "spotify:artist:08td7MxkoHQkXnWAYD8d6Q" 227 | } ], 228 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 229 | "disc_number" : 1, 230 | "duration_ms" : 230440, 231 | "explicit" : false, 232 | "external_urls" : { 233 | "spotify" : "https://open.spotify.com/track/1Xan3mf1E173v2n4wbQFeO" 234 | }, 235 | "href" : "https://api.spotify.com/v1/tracks/1Xan3mf1E173v2n4wbQFeO", 236 | "id" : "1Xan3mf1E173v2n4wbQFeO", 237 | "name" : "Easy Way", 238 | "preview_url" : "https://p.scdn.co/mp3-preview/1926686f64b709f529d94b8af39398a7d5551fc8", 239 | "track_number" : 8, 240 | "type" : "track", 241 | "uri" : "spotify:track:1Xan3mf1E173v2n4wbQFeO" 242 | }, { 243 | "artists" : [ { 244 | "external_urls" : { 245 | "spotify" : "https://open.spotify.com/artist/08td7MxkoHQkXnWAYD8d6Q" 246 | }, 247 | "href" : "https://api.spotify.com/v1/artists/08td7MxkoHQkXnWAYD8d6Q", 248 | "id" : "08td7MxkoHQkXnWAYD8d6Q", 249 | "name" : "Tania Bowra", 250 | "type" : "artist", 251 | "uri" : "spotify:artist:08td7MxkoHQkXnWAYD8d6Q" 252 | } ], 253 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 254 | "disc_number" : 1, 255 | "duration_ms" : 281320, 256 | "explicit" : false, 257 | "external_urls" : { 258 | "spotify" : "https://open.spotify.com/track/5cBLGuA6AFlZISzkRiE7IT" 259 | }, 260 | "href" : "https://api.spotify.com/v1/tracks/5cBLGuA6AFlZISzkRiE7IT", 261 | "id" : "5cBLGuA6AFlZISzkRiE7IT", 262 | "name" : "Sympathy", 263 | "preview_url" : "https://p.scdn.co/mp3-preview/b55ab54cf457eb8a178fefbbbf32c52f64be9d5d", 264 | "track_number" : 9, 265 | "type" : "track", 266 | "uri" : "spotify:track:5cBLGuA6AFlZISzkRiE7IT" 267 | }, { 268 | "artists" : [ { 269 | "external_urls" : { 270 | "spotify" : "https://open.spotify.com/artist/08td7MxkoHQkXnWAYD8d6Q" 271 | }, 272 | "href" : "https://api.spotify.com/v1/artists/08td7MxkoHQkXnWAYD8d6Q", 273 | "id" : "08td7MxkoHQkXnWAYD8d6Q", 274 | "name" : "Tania Bowra", 275 | "type" : "artist", 276 | "uri" : "spotify:artist:08td7MxkoHQkXnWAYD8d6Q" 277 | } ], 278 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 279 | "disc_number" : 1, 280 | "duration_ms" : 262400, 281 | "explicit" : false, 282 | "external_urls" : { 283 | "spotify" : "https://open.spotify.com/track/3Wlw9nfntmKxVxLMzZeYLB" 284 | }, 285 | "href" : "https://api.spotify.com/v1/tracks/3Wlw9nfntmKxVxLMzZeYLB", 286 | "id" : "3Wlw9nfntmKxVxLMzZeYLB", 287 | "name" : "Pleasure And Pain", 288 | "preview_url" : "https://p.scdn.co/mp3-preview/df9af685d4481ca17218b351263562f4b774adea", 289 | "track_number" : 10, 290 | "type" : "track", 291 | "uri" : "spotify:track:3Wlw9nfntmKxVxLMzZeYLB" 292 | }, { 293 | "artists" : [ { 294 | "external_urls" : { 295 | "spotify" : "https://open.spotify.com/artist/08td7MxkoHQkXnWAYD8d6Q" 296 | }, 297 | "href" : "https://api.spotify.com/v1/artists/08td7MxkoHQkXnWAYD8d6Q", 298 | "id" : "08td7MxkoHQkXnWAYD8d6Q", 299 | "name" : "Tania Bowra", 300 | "type" : "artist", 301 | "uri" : "spotify:artist:08td7MxkoHQkXnWAYD8d6Q" 302 | } ], 303 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 304 | "disc_number" : 1, 305 | "duration_ms" : 240973, 306 | "explicit" : false, 307 | "external_urls" : { 308 | "spotify" : "https://open.spotify.com/track/6DonRyqagGQGdbjMKRy5f7" 309 | }, 310 | "href" : "https://api.spotify.com/v1/tracks/6DonRyqagGQGdbjMKRy5f7", 311 | "id" : "6DonRyqagGQGdbjMKRy5f7", 312 | "name" : "Peace Like A Summer Breeze", 313 | "preview_url" : "https://p.scdn.co/mp3-preview/77823e8af90501f950c324eb96ac600af8004a36", 314 | "track_number" : 11, 315 | "type" : "track", 316 | "uri" : "spotify:track:6DonRyqagGQGdbjMKRy5f7" 317 | } ], 318 | "limit" : 50, 319 | "next" : null, 320 | "offset" : 0, 321 | "previous" : null, 322 | "total" : 11 323 | }, 324 | "type" : "album", 325 | "uri" : "spotify:album:6akEvsycLGftJxYudPjmqK" 326 | } -------------------------------------------------------------------------------- /test/mock/albums.invalid-id.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "status": 400, 4 | "message": "invalid id" 5 | } 6 | } -------------------------------------------------------------------------------- /test/mock/albums.tracks.json: -------------------------------------------------------------------------------- 1 | { 2 | "href" : "https://api.spotify.com/v1/albums/0sNOF9WDwhWunNAHPD3Baj/tracks?offset=0&limit=50", 3 | "items" : [ { 4 | "artists" : [ { 5 | "external_urls" : { 6 | "spotify" : "https://open.spotify.com/artist/2BTZIqw0ntH9MvilQ3ewNY" 7 | }, 8 | "href" : "https://api.spotify.com/v1/artists/2BTZIqw0ntH9MvilQ3ewNY", 9 | "id" : "2BTZIqw0ntH9MvilQ3ewNY", 10 | "name" : "Cyndi Lauper", 11 | "type" : "artist", 12 | "uri" : "spotify:artist:2BTZIqw0ntH9MvilQ3ewNY" 13 | } ], 14 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TW", "UY" ], 15 | "disc_number" : 1, 16 | "duration_ms" : 305560, 17 | "explicit" : false, 18 | "external_urls" : { 19 | "spotify" : "https://open.spotify.com/track/3f9zqUnrnIq0LANhmnaF0V" 20 | }, 21 | "href" : "https://api.spotify.com/v1/tracks/3f9zqUnrnIq0LANhmnaF0V", 22 | "id" : "3f9zqUnrnIq0LANhmnaF0V", 23 | "name" : "Money Changes Everything", 24 | "preview_url" : "https://p.scdn.co/mp3-preview/01bb2a6c9a89c05a4300aea427241b1719a26b06", 25 | "track_number" : 1, 26 | "type" : "track", 27 | "uri" : "spotify:track:3f9zqUnrnIq0LANhmnaF0V" 28 | }, { 29 | "artists" : [ { 30 | "external_urls" : { 31 | "spotify" : "https://open.spotify.com/artist/2BTZIqw0ntH9MvilQ3ewNY" 32 | }, 33 | "href" : "https://api.spotify.com/v1/artists/2BTZIqw0ntH9MvilQ3ewNY", 34 | "id" : "2BTZIqw0ntH9MvilQ3ewNY", 35 | "name" : "Cyndi Lauper", 36 | "type" : "artist", 37 | "uri" : "spotify:artist:2BTZIqw0ntH9MvilQ3ewNY" 38 | } ], 39 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TW", "UY" ], 40 | "disc_number" : 1, 41 | "duration_ms" : 238266, 42 | "explicit" : false, 43 | "external_urls" : { 44 | "spotify" : "https://open.spotify.com/track/2joHDtKFVDDyWDHnOxZMAX" 45 | }, 46 | "href" : "https://api.spotify.com/v1/tracks/2joHDtKFVDDyWDHnOxZMAX", 47 | "id" : "2joHDtKFVDDyWDHnOxZMAX", 48 | "name" : "Girls Just Want to Have Fun", 49 | "preview_url" : "https://p.scdn.co/mp3-preview/4fb5dfb0f1d60d23d1072548847e26906951ead9", 50 | "track_number" : 2, 51 | "type" : "track", 52 | "uri" : "spotify:track:2joHDtKFVDDyWDHnOxZMAX" 53 | }, { 54 | "artists" : [ { 55 | "external_urls" : { 56 | "spotify" : "https://open.spotify.com/artist/2BTZIqw0ntH9MvilQ3ewNY" 57 | }, 58 | "href" : "https://api.spotify.com/v1/artists/2BTZIqw0ntH9MvilQ3ewNY", 59 | "id" : "2BTZIqw0ntH9MvilQ3ewNY", 60 | "name" : "Cyndi Lauper", 61 | "type" : "artist", 62 | "uri" : "spotify:artist:2BTZIqw0ntH9MvilQ3ewNY" 63 | } ], 64 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TW", "UY" ], 65 | "disc_number" : 1, 66 | "duration_ms" : 306706, 67 | "explicit" : false, 68 | "external_urls" : { 69 | "spotify" : "https://open.spotify.com/track/6ClztHzretmPHCeiNqR5wD" 70 | }, 71 | "href" : "https://api.spotify.com/v1/tracks/6ClztHzretmPHCeiNqR5wD", 72 | "id" : "6ClztHzretmPHCeiNqR5wD", 73 | "name" : "When You Were Mine", 74 | "preview_url" : "https://p.scdn.co/mp3-preview/7ce74522fabed32d7310084541bf38f906bb33bc", 75 | "track_number" : 3, 76 | "type" : "track", 77 | "uri" : "spotify:track:6ClztHzretmPHCeiNqR5wD" 78 | }, { 79 | "artists" : [ { 80 | "external_urls" : { 81 | "spotify" : "https://open.spotify.com/artist/2BTZIqw0ntH9MvilQ3ewNY" 82 | }, 83 | "href" : "https://api.spotify.com/v1/artists/2BTZIqw0ntH9MvilQ3ewNY", 84 | "id" : "2BTZIqw0ntH9MvilQ3ewNY", 85 | "name" : "Cyndi Lauper", 86 | "type" : "artist", 87 | "uri" : "spotify:artist:2BTZIqw0ntH9MvilQ3ewNY" 88 | } ], 89 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TW", "UY" ], 90 | "disc_number" : 1, 91 | "duration_ms" : 241333, 92 | "explicit" : false, 93 | "external_urls" : { 94 | "spotify" : "https://open.spotify.com/track/2tVHvZK4YYzTloSCBPm2tg" 95 | }, 96 | "href" : "https://api.spotify.com/v1/tracks/2tVHvZK4YYzTloSCBPm2tg", 97 | "id" : "2tVHvZK4YYzTloSCBPm2tg", 98 | "name" : "Time After Time", 99 | "preview_url" : "https://p.scdn.co/mp3-preview/0b0b522f1412996c5c0c8d44f886d76d43f33f6c", 100 | "track_number" : 4, 101 | "type" : "track", 102 | "uri" : "spotify:track:2tVHvZK4YYzTloSCBPm2tg" 103 | }, { 104 | "artists" : [ { 105 | "external_urls" : { 106 | "spotify" : "https://open.spotify.com/artist/2BTZIqw0ntH9MvilQ3ewNY" 107 | }, 108 | "href" : "https://api.spotify.com/v1/artists/2BTZIqw0ntH9MvilQ3ewNY", 109 | "id" : "2BTZIqw0ntH9MvilQ3ewNY", 110 | "name" : "Cyndi Lauper", 111 | "type" : "artist", 112 | "uri" : "spotify:artist:2BTZIqw0ntH9MvilQ3ewNY" 113 | } ], 114 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TW", "UY" ], 115 | "disc_number" : 1, 116 | "duration_ms" : 229266, 117 | "explicit" : false, 118 | "external_urls" : { 119 | "spotify" : "https://open.spotify.com/track/6iLhMDtOr52OVXaZdha5M6" 120 | }, 121 | "href" : "https://api.spotify.com/v1/tracks/6iLhMDtOr52OVXaZdha5M6", 122 | "id" : "6iLhMDtOr52OVXaZdha5M6", 123 | "name" : "She Bop", 124 | "preview_url" : "https://p.scdn.co/mp3-preview/95bbe8eb0ea9d64c8bed6a95a0484250068ffd57", 125 | "track_number" : 5, 126 | "type" : "track", 127 | "uri" : "spotify:track:6iLhMDtOr52OVXaZdha5M6" 128 | }, { 129 | "artists" : [ { 130 | "external_urls" : { 131 | "spotify" : "https://open.spotify.com/artist/2BTZIqw0ntH9MvilQ3ewNY" 132 | }, 133 | "href" : "https://api.spotify.com/v1/artists/2BTZIqw0ntH9MvilQ3ewNY", 134 | "id" : "2BTZIqw0ntH9MvilQ3ewNY", 135 | "name" : "Cyndi Lauper", 136 | "type" : "artist", 137 | "uri" : "spotify:artist:2BTZIqw0ntH9MvilQ3ewNY" 138 | } ], 139 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TW", "UY" ], 140 | "disc_number" : 1, 141 | "duration_ms" : 272840, 142 | "explicit" : false, 143 | "external_urls" : { 144 | "spotify" : "https://open.spotify.com/track/3csiLr2B2wRj4lsExn6jLf" 145 | }, 146 | "href" : "https://api.spotify.com/v1/tracks/3csiLr2B2wRj4lsExn6jLf", 147 | "id" : "3csiLr2B2wRj4lsExn6jLf", 148 | "name" : "All Through the Night", 149 | "preview_url" : "https://p.scdn.co/mp3-preview/e2dfc024cc4653ecc07f9c9e14fd882f8150cdb7", 150 | "track_number" : 6, 151 | "type" : "track", 152 | "uri" : "spotify:track:3csiLr2B2wRj4lsExn6jLf" 153 | }, { 154 | "artists" : [ { 155 | "external_urls" : { 156 | "spotify" : "https://open.spotify.com/artist/2BTZIqw0ntH9MvilQ3ewNY" 157 | }, 158 | "href" : "https://api.spotify.com/v1/artists/2BTZIqw0ntH9MvilQ3ewNY", 159 | "id" : "2BTZIqw0ntH9MvilQ3ewNY", 160 | "name" : "Cyndi Lauper", 161 | "type" : "artist", 162 | "uri" : "spotify:artist:2BTZIqw0ntH9MvilQ3ewNY" 163 | } ], 164 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TW", "UY" ], 165 | "disc_number" : 1, 166 | "duration_ms" : 220333, 167 | "explicit" : false, 168 | "external_urls" : { 169 | "spotify" : "https://open.spotify.com/track/4mRAnuBGYsW4WGbpW0QUkp" 170 | }, 171 | "href" : "https://api.spotify.com/v1/tracks/4mRAnuBGYsW4WGbpW0QUkp", 172 | "id" : "4mRAnuBGYsW4WGbpW0QUkp", 173 | "name" : "Witness", 174 | "preview_url" : "https://p.scdn.co/mp3-preview/9be78ca9a07914db151f1e970c068970769d2731", 175 | "track_number" : 7, 176 | "type" : "track", 177 | "uri" : "spotify:track:4mRAnuBGYsW4WGbpW0QUkp" 178 | }, { 179 | "artists" : [ { 180 | "external_urls" : { 181 | "spotify" : "https://open.spotify.com/artist/2BTZIqw0ntH9MvilQ3ewNY" 182 | }, 183 | "href" : "https://api.spotify.com/v1/artists/2BTZIqw0ntH9MvilQ3ewNY", 184 | "id" : "2BTZIqw0ntH9MvilQ3ewNY", 185 | "name" : "Cyndi Lauper", 186 | "type" : "artist", 187 | "uri" : "spotify:artist:2BTZIqw0ntH9MvilQ3ewNY" 188 | } ], 189 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TW", "UY" ], 190 | "disc_number" : 1, 191 | "duration_ms" : 252626, 192 | "explicit" : false, 193 | "external_urls" : { 194 | "spotify" : "https://open.spotify.com/track/3AIeUnffkLQaUaX1pkHyeD" 195 | }, 196 | "href" : "https://api.spotify.com/v1/tracks/3AIeUnffkLQaUaX1pkHyeD", 197 | "id" : "3AIeUnffkLQaUaX1pkHyeD", 198 | "name" : "I'll Kiss You", 199 | "preview_url" : "https://p.scdn.co/mp3-preview/9031f915fbe2d62cbac7e22eb4e17c69bbcc72b8", 200 | "track_number" : 8, 201 | "type" : "track", 202 | "uri" : "spotify:track:3AIeUnffkLQaUaX1pkHyeD" 203 | }, { 204 | "artists" : [ { 205 | "external_urls" : { 206 | "spotify" : "https://open.spotify.com/artist/2BTZIqw0ntH9MvilQ3ewNY" 207 | }, 208 | "href" : "https://api.spotify.com/v1/artists/2BTZIqw0ntH9MvilQ3ewNY", 209 | "id" : "2BTZIqw0ntH9MvilQ3ewNY", 210 | "name" : "Cyndi Lauper", 211 | "type" : "artist", 212 | "uri" : "spotify:artist:2BTZIqw0ntH9MvilQ3ewNY" 213 | } ], 214 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TW", "UY" ], 215 | "disc_number" : 1, 216 | "duration_ms" : 45933, 217 | "explicit" : false, 218 | "external_urls" : { 219 | "spotify" : "https://open.spotify.com/track/53eCpAFNbA9MQNfLilN3CH" 220 | }, 221 | "href" : "https://api.spotify.com/v1/tracks/53eCpAFNbA9MQNfLilN3CH", 222 | "id" : "53eCpAFNbA9MQNfLilN3CH", 223 | "name" : "He's so Unusual", 224 | "preview_url" : "https://p.scdn.co/mp3-preview/3b86ffe73440aeb1dca36ab9edb4245e0d7124c7", 225 | "track_number" : 9, 226 | "type" : "track", 227 | "uri" : "spotify:track:53eCpAFNbA9MQNfLilN3CH" 228 | }, { 229 | "artists" : [ { 230 | "external_urls" : { 231 | "spotify" : "https://open.spotify.com/artist/2BTZIqw0ntH9MvilQ3ewNY" 232 | }, 233 | "href" : "https://api.spotify.com/v1/artists/2BTZIqw0ntH9MvilQ3ewNY", 234 | "id" : "2BTZIqw0ntH9MvilQ3ewNY", 235 | "name" : "Cyndi Lauper", 236 | "type" : "artist", 237 | "uri" : "spotify:artist:2BTZIqw0ntH9MvilQ3ewNY" 238 | } ], 239 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TW", "UY" ], 240 | "disc_number" : 1, 241 | "duration_ms" : 196373, 242 | "explicit" : false, 243 | "external_urls" : { 244 | "spotify" : "https://open.spotify.com/track/51JS0KXziu9U1T8EBdRTUF" 245 | }, 246 | "href" : "https://api.spotify.com/v1/tracks/51JS0KXziu9U1T8EBdRTUF", 247 | "id" : "51JS0KXziu9U1T8EBdRTUF", 248 | "name" : "Yeah Yeah", 249 | "preview_url" : "https://p.scdn.co/mp3-preview/113b4d4b560cddd344fe9ea7d3704d8f8f1600a5", 250 | "track_number" : 10, 251 | "type" : "track", 252 | "uri" : "spotify:track:51JS0KXziu9U1T8EBdRTUF" 253 | }, { 254 | "artists" : [ { 255 | "external_urls" : { 256 | "spotify" : "https://open.spotify.com/artist/2BTZIqw0ntH9MvilQ3ewNY" 257 | }, 258 | "href" : "https://api.spotify.com/v1/artists/2BTZIqw0ntH9MvilQ3ewNY", 259 | "id" : "2BTZIqw0ntH9MvilQ3ewNY", 260 | "name" : "Cyndi Lauper", 261 | "type" : "artist", 262 | "uri" : "spotify:artist:2BTZIqw0ntH9MvilQ3ewNY" 263 | } ], 264 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TW", "UY" ], 265 | "disc_number" : 1, 266 | "duration_ms" : 275560, 267 | "explicit" : false, 268 | "external_urls" : { 269 | "spotify" : "https://open.spotify.com/track/2BGJvRarwOa2kiIGpLjIXT" 270 | }, 271 | "href" : "https://api.spotify.com/v1/tracks/2BGJvRarwOa2kiIGpLjIXT", 272 | "id" : "2BGJvRarwOa2kiIGpLjIXT", 273 | "name" : "Money Changes Everything", 274 | "preview_url" : "https://p.scdn.co/mp3-preview/76ac67ccd4f27eb0deaf9d789436def22def2304", 275 | "track_number" : 11, 276 | "type" : "track", 277 | "uri" : "spotify:track:2BGJvRarwOa2kiIGpLjIXT" 278 | }, { 279 | "artists" : [ { 280 | "external_urls" : { 281 | "spotify" : "https://open.spotify.com/artist/2BTZIqw0ntH9MvilQ3ewNY" 282 | }, 283 | "href" : "https://api.spotify.com/v1/artists/2BTZIqw0ntH9MvilQ3ewNY", 284 | "id" : "2BTZIqw0ntH9MvilQ3ewNY", 285 | "name" : "Cyndi Lauper", 286 | "type" : "artist", 287 | "uri" : "spotify:artist:2BTZIqw0ntH9MvilQ3ewNY" 288 | } ], 289 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TW", "UY" ], 290 | "disc_number" : 1, 291 | "duration_ms" : 320400, 292 | "explicit" : false, 293 | "external_urls" : { 294 | "spotify" : "https://open.spotify.com/track/5ggatiDTbCIJsUAa7IUP65" 295 | }, 296 | "href" : "https://api.spotify.com/v1/tracks/5ggatiDTbCIJsUAa7IUP65", 297 | "id" : "5ggatiDTbCIJsUAa7IUP65", 298 | "name" : "She Bop - Live", 299 | "preview_url" : "https://p.scdn.co/mp3-preview/ea6b57b35d2bcf5caddc7e6e3655fc3215eec8a4", 300 | "track_number" : 12, 301 | "type" : "track", 302 | "uri" : "spotify:track:5ggatiDTbCIJsUAa7IUP65" 303 | }, { 304 | "artists" : [ { 305 | "external_urls" : { 306 | "spotify" : "https://open.spotify.com/artist/2BTZIqw0ntH9MvilQ3ewNY" 307 | }, 308 | "href" : "https://api.spotify.com/v1/artists/2BTZIqw0ntH9MvilQ3ewNY", 309 | "id" : "2BTZIqw0ntH9MvilQ3ewNY", 310 | "name" : "Cyndi Lauper", 311 | "type" : "artist", 312 | "uri" : "spotify:artist:2BTZIqw0ntH9MvilQ3ewNY" 313 | } ], 314 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TW", "UY" ], 315 | "disc_number" : 1, 316 | "duration_ms" : 288240, 317 | "explicit" : false, 318 | "external_urls" : { 319 | "spotify" : "https://open.spotify.com/track/5ZBxoa2kBrBah3qNIV4rm7" 320 | }, 321 | "href" : "https://api.spotify.com/v1/tracks/5ZBxoa2kBrBah3qNIV4rm7", 322 | "id" : "5ZBxoa2kBrBah3qNIV4rm7", 323 | "name" : "All Through The Night - Live", 324 | "preview_url" : "https://p.scdn.co/mp3-preview/cc8d13b1ce2e41f915a126bc6723a9cf7c491e08", 325 | "track_number" : 13, 326 | "type" : "track", 327 | "uri" : "spotify:track:5ZBxoa2kBrBah3qNIV4rm7" 328 | } ], 329 | "limit" : 50, 330 | "next" : null, 331 | "offset" : 0, 332 | "previous" : null, 333 | "total" : 13 334 | } -------------------------------------------------------------------------------- /test/mock/featured-playlists.json: -------------------------------------------------------------------------------- 1 | { 2 | "message" : "Coffee and music – a perfect pairing!", 3 | "playlists" : { 4 | "href" : "https://api.spotify.com/v1/browse/featured-playlists?timestamp=2014-12-22T11:24:34&offset=0&limit=20", 5 | "items" : [ { 6 | "collaborative" : false, 7 | "external_urls" : { 8 | "spotify" : "http://open.spotify.com/user/spotify/playlist/4BKT5olNFqLB1FAa8OtC8k" 9 | }, 10 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/4BKT5olNFqLB1FAa8OtC8k", 11 | "id" : "4BKT5olNFqLB1FAa8OtC8k", 12 | "images" : [ { 13 | "url" : "https://i.scdn.co/image/248213a045c2f086f427692085827fb1feb70645" 14 | } ], 15 | "name" : "Your Favorite Coffeehouse", 16 | "owner" : { 17 | "external_urls" : { 18 | "spotify" : "http://open.spotify.com/user/spotify" 19 | }, 20 | "href" : "https://api.spotify.com/v1/users/spotify", 21 | "id" : "spotify", 22 | "type" : "user", 23 | "uri" : "spotify:user:spotify" 24 | }, 25 | "public" : null, 26 | "tracks" : { 27 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/4BKT5olNFqLB1FAa8OtC8k/tracks", 28 | "total" : 84 29 | }, 30 | "type" : "playlist", 31 | "uri" : "spotify:user:spotify:playlist:4BKT5olNFqLB1FAa8OtC8k" 32 | }, { 33 | "collaborative" : false, 34 | "external_urls" : { 35 | "spotify" : "http://open.spotify.com/user/spotify/playlist/5FJXhjdILmRA2z5bvz4nzf" 36 | }, 37 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/5FJXhjdILmRA2z5bvz4nzf", 38 | "id" : "5FJXhjdILmRA2z5bvz4nzf", 39 | "images" : [ { 40 | "url" : "https://i.scdn.co/image/3618cc1fa353d0dc720ecbd902de23c980fc1b31" 41 | } ], 42 | "name" : "Today's Top Hits", 43 | "owner" : { 44 | "external_urls" : { 45 | "spotify" : "http://open.spotify.com/user/spotify" 46 | }, 47 | "href" : "https://api.spotify.com/v1/users/spotify", 48 | "id" : "spotify", 49 | "type" : "user", 50 | "uri" : "spotify:user:spotify" 51 | }, 52 | "public" : null, 53 | "tracks" : { 54 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/5FJXhjdILmRA2z5bvz4nzf/tracks", 55 | "total" : 50 56 | }, 57 | "type" : "playlist", 58 | "uri" : "spotify:user:spotify:playlist:5FJXhjdILmRA2z5bvz4nzf" 59 | }, { 60 | "collaborative" : false, 61 | "external_urls" : { 62 | "spotify" : "http://open.spotify.com/user/spotify/playlist/2ujjMpFriZ2nayLmrD1Jgl" 63 | }, 64 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/2ujjMpFriZ2nayLmrD1Jgl", 65 | "id" : "2ujjMpFriZ2nayLmrD1Jgl", 66 | "images" : [ { 67 | "url" : "https://i.scdn.co/image/c6ae992ff7b6b9e49bea3b76e3d739ef2f1b1fb2" 68 | } ], 69 | "name" : "Deep Focus", 70 | "owner" : { 71 | "external_urls" : { 72 | "spotify" : "http://open.spotify.com/user/spotify" 73 | }, 74 | "href" : "https://api.spotify.com/v1/users/spotify", 75 | "id" : "spotify", 76 | "type" : "user", 77 | "uri" : "spotify:user:spotify" 78 | }, 79 | "public" : null, 80 | "tracks" : { 81 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/2ujjMpFriZ2nayLmrD1Jgl/tracks", 82 | "total" : 98 83 | }, 84 | "type" : "playlist", 85 | "uri" : "spotify:user:spotify:playlist:2ujjMpFriZ2nayLmrD1Jgl" 86 | }, { 87 | "collaborative" : false, 88 | "external_urls" : { 89 | "spotify" : "http://open.spotify.com/user/spotify/playlist/5UFRIQ89nVPcVsrWjLyjqI" 90 | }, 91 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/5UFRIQ89nVPcVsrWjLyjqI", 92 | "id" : "5UFRIQ89nVPcVsrWjLyjqI", 93 | "images" : [ { 94 | "url" : "https://i.scdn.co/image/9c320a0e9f484ac3668678ae9ee1338fd31e1a7b" 95 | } ], 96 | "name" : "Caffeine Rush", 97 | "owner" : { 98 | "external_urls" : { 99 | "spotify" : "http://open.spotify.com/user/spotify" 100 | }, 101 | "href" : "https://api.spotify.com/v1/users/spotify", 102 | "id" : "spotify", 103 | "type" : "user", 104 | "uri" : "spotify:user:spotify" 105 | }, 106 | "public" : null, 107 | "tracks" : { 108 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/5UFRIQ89nVPcVsrWjLyjqI/tracks", 109 | "total" : 71 110 | }, 111 | "type" : "playlist", 112 | "uri" : "spotify:user:spotify:playlist:5UFRIQ89nVPcVsrWjLyjqI" 113 | }, { 114 | "collaborative" : false, 115 | "external_urls" : { 116 | "spotify" : "http://open.spotify.com/user/spotify/playlist/0ExbFrTy6ypLj9YYNMTnmd" 117 | }, 118 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/0ExbFrTy6ypLj9YYNMTnmd", 119 | "id" : "0ExbFrTy6ypLj9YYNMTnmd", 120 | "images" : [ { 121 | "url" : "https://i.scdn.co/image/52a250248c75034207a34a56248c6686ab2b70eb" 122 | } ], 123 | "name" : "Intense Studying", 124 | "owner" : { 125 | "external_urls" : { 126 | "spotify" : "http://open.spotify.com/user/spotify" 127 | }, 128 | "href" : "https://api.spotify.com/v1/users/spotify", 129 | "id" : "spotify", 130 | "type" : "user", 131 | "uri" : "spotify:user:spotify" 132 | }, 133 | "public" : null, 134 | "tracks" : { 135 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/0ExbFrTy6ypLj9YYNMTnmd/tracks", 136 | "total" : 142 137 | }, 138 | "type" : "playlist", 139 | "uri" : "spotify:user:spotify:playlist:0ExbFrTy6ypLj9YYNMTnmd" 140 | }, { 141 | "collaborative" : false, 142 | "external_urls" : { 143 | "spotify" : "http://open.spotify.com/user/spotify/playlist/74lsbDpc3vMNHHwD0w07ke" 144 | }, 145 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/74lsbDpc3vMNHHwD0w07ke", 146 | "id" : "74lsbDpc3vMNHHwD0w07ke", 147 | "images" : [ { 148 | "url" : "https://i.scdn.co/image/6684e0debbbe7ca94d7eb96c1a186d7bbb2d366e" 149 | } ], 150 | "name" : "Country Coffeehouse", 151 | "owner" : { 152 | "external_urls" : { 153 | "spotify" : "http://open.spotify.com/user/spotify" 154 | }, 155 | "href" : "https://api.spotify.com/v1/users/spotify", 156 | "id" : "spotify", 157 | "type" : "user", 158 | "uri" : "spotify:user:spotify" 159 | }, 160 | "public" : null, 161 | "tracks" : { 162 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/74lsbDpc3vMNHHwD0w07ke/tracks", 163 | "total" : 32 164 | }, 165 | "type" : "playlist", 166 | "uri" : "spotify:user:spotify:playlist:74lsbDpc3vMNHHwD0w07ke" 167 | }, { 168 | "collaborative" : false, 169 | "external_urls" : { 170 | "spotify" : "http://open.spotify.com/user/spotify/playlist/19uVLpMdgv0Dy3LvpYx4LA" 171 | }, 172 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/19uVLpMdgv0Dy3LvpYx4LA", 173 | "id" : "19uVLpMdgv0Dy3LvpYx4LA", 174 | "images" : [ { 175 | "url" : "https://i.scdn.co/image/7feba1395174bd9bc16f1babafffe8570a5ef3c7" 176 | } ], 177 | "name" : "Workday – Lounge", 178 | "owner" : { 179 | "external_urls" : { 180 | "spotify" : "http://open.spotify.com/user/spotify" 181 | }, 182 | "href" : "https://api.spotify.com/v1/users/spotify", 183 | "id" : "spotify", 184 | "type" : "user", 185 | "uri" : "spotify:user:spotify" 186 | }, 187 | "public" : null, 188 | "tracks" : { 189 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/19uVLpMdgv0Dy3LvpYx4LA/tracks", 190 | "total" : 95 191 | }, 192 | "type" : "playlist", 193 | "uri" : "spotify:user:spotify:playlist:19uVLpMdgv0Dy3LvpYx4LA" 194 | }, { 195 | "collaborative" : false, 196 | "external_urls" : { 197 | "spotify" : "http://open.spotify.com/user/spotify/playlist/65y98W0UItf73DJKVgylTP" 198 | }, 199 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/65y98W0UItf73DJKVgylTP", 200 | "id" : "65y98W0UItf73DJKVgylTP", 201 | "images" : [ { 202 | "url" : "https://i.scdn.co/image/223eaea3d44938b833a86651267db608bea1c5a3" 203 | } ], 204 | "name" : "ESM | Electronic Study Music", 205 | "owner" : { 206 | "external_urls" : { 207 | "spotify" : "http://open.spotify.com/user/spotify" 208 | }, 209 | "href" : "https://api.spotify.com/v1/users/spotify", 210 | "id" : "spotify", 211 | "type" : "user", 212 | "uri" : "spotify:user:spotify" 213 | }, 214 | "public" : null, 215 | "tracks" : { 216 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/65y98W0UItf73DJKVgylTP/tracks", 217 | "total" : 53 218 | }, 219 | "type" : "playlist", 220 | "uri" : "spotify:user:spotify:playlist:65y98W0UItf73DJKVgylTP" 221 | }, { 222 | "collaborative" : false, 223 | "external_urls" : { 224 | "spotify" : "http://open.spotify.com/user/spotify/playlist/2SSEozHbLRKueJBM7LAlAo" 225 | }, 226 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/2SSEozHbLRKueJBM7LAlAo", 227 | "id" : "2SSEozHbLRKueJBM7LAlAo", 228 | "images" : [ { 229 | "url" : "https://i.scdn.co/image/8a6deaff6a3af92a9b9fd923e98e22c5acb65e56" 230 | } ], 231 | "name" : "Workday – Pop", 232 | "owner" : { 233 | "external_urls" : { 234 | "spotify" : "http://open.spotify.com/user/spotify" 235 | }, 236 | "href" : "https://api.spotify.com/v1/users/spotify", 237 | "id" : "spotify", 238 | "type" : "user", 239 | "uri" : "spotify:user:spotify" 240 | }, 241 | "public" : null, 242 | "tracks" : { 243 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/2SSEozHbLRKueJBM7LAlAo/tracks", 244 | "total" : 128 245 | }, 246 | "type" : "playlist", 247 | "uri" : "spotify:user:spotify:playlist:2SSEozHbLRKueJBM7LAlAo" 248 | }, { 249 | "collaborative" : false, 250 | "external_urls" : { 251 | "spotify" : "http://open.spotify.com/user/spotify/playlist/7cOO30bzxMm4tO34C9UalD" 252 | }, 253 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/7cOO30bzxMm4tO34C9UalD", 254 | "id" : "7cOO30bzxMm4tO34C9UalD", 255 | "images" : [ { 256 | "url" : "https://i.scdn.co/image/acf2d1cdd5fa27b589262aba943e8c3b170e014e" 257 | } ], 258 | "name" : "Cardio", 259 | "owner" : { 260 | "external_urls" : { 261 | "spotify" : "http://open.spotify.com/user/spotify" 262 | }, 263 | "href" : "https://api.spotify.com/v1/users/spotify", 264 | "id" : "spotify", 265 | "type" : "user", 266 | "uri" : "spotify:user:spotify" 267 | }, 268 | "public" : null, 269 | "tracks" : { 270 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/7cOO30bzxMm4tO34C9UalD/tracks", 271 | "total" : 45 272 | }, 273 | "type" : "playlist", 274 | "uri" : "spotify:user:spotify:playlist:7cOO30bzxMm4tO34C9UalD" 275 | }, { 276 | "collaborative" : false, 277 | "external_urls" : { 278 | "spotify" : "http://open.spotify.com/user/spotify/playlist/5O2ERf8kAYARVVdfCKZ9G7" 279 | }, 280 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/5O2ERf8kAYARVVdfCKZ9G7", 281 | "id" : "5O2ERf8kAYARVVdfCKZ9G7", 282 | "images" : [ { 283 | "url" : "https://i.scdn.co/image/34baf4085738a1e2ad1b426d1fa2d1e64a5bd856" 284 | } ], 285 | "name" : "Coffee Table Jazz", 286 | "owner" : { 287 | "external_urls" : { 288 | "spotify" : "http://open.spotify.com/user/spotify" 289 | }, 290 | "href" : "https://api.spotify.com/v1/users/spotify", 291 | "id" : "spotify", 292 | "type" : "user", 293 | "uri" : "spotify:user:spotify" 294 | }, 295 | "public" : null, 296 | "tracks" : { 297 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/5O2ERf8kAYARVVdfCKZ9G7/tracks", 298 | "total" : 57 299 | }, 300 | "type" : "playlist", 301 | "uri" : "spotify:user:spotify:playlist:5O2ERf8kAYARVVdfCKZ9G7" 302 | }, { 303 | "collaborative" : false, 304 | "external_urls" : { 305 | "spotify" : "http://open.spotify.com/user/spotify/playlist/67nMZWgcUxNa5uaiyLDR2x" 306 | }, 307 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/67nMZWgcUxNa5uaiyLDR2x", 308 | "id" : "67nMZWgcUxNa5uaiyLDR2x", 309 | "images" : [ { 310 | "url" : "https://i.scdn.co/image/a69b2bd382b17654cb57c42ab276097c9a7367da" 311 | } ], 312 | "name" : "Brain Food", 313 | "owner" : { 314 | "external_urls" : { 315 | "spotify" : "http://open.spotify.com/user/spotify" 316 | }, 317 | "href" : "https://api.spotify.com/v1/users/spotify", 318 | "id" : "spotify", 319 | "type" : "user", 320 | "uri" : "spotify:user:spotify" 321 | }, 322 | "public" : null, 323 | "tracks" : { 324 | "href" : "https://api.spotify.com/v1/users/spotify/playlists/67nMZWgcUxNa5uaiyLDR2x/tracks", 325 | "total" : 90 326 | }, 327 | "type" : "playlist", 328 | "uri" : "spotify:user:spotify:playlist:67nMZWgcUxNa5uaiyLDR2x" 329 | } ], 330 | "limit" : 20, 331 | "next" : null, 332 | "offset" : 0, 333 | "previous" : null, 334 | "total" : 12 335 | } 336 | } -------------------------------------------------------------------------------- /test/mock/new-releases.json: -------------------------------------------------------------------------------- 1 | { 2 | "albums" : { 3 | "href" : "https://api.spotify.com/v1/browse/new-releases?offset=0&limit=20", 4 | "items" : [ { 5 | "album_type" : "single", 6 | "available_markets" : [ "AD", "AR", "AT", "AU", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "ES", "FR", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "MC", "MT", "MX", "MY", "NI", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SG", "SI", "SK", "SV", "TR", "TW", "UY" ], 7 | "external_urls" : { 8 | "spotify" : "https://open.spotify.com/album/60mvULtYiNSRmpVvoa3RE4" 9 | }, 10 | "href" : "https://api.spotify.com/v1/albums/60mvULtYiNSRmpVvoa3RE4", 11 | "id" : "60mvULtYiNSRmpVvoa3RE4", 12 | "images" : [ { 13 | "height" : 640, 14 | "url" : "https://i.scdn.co/image/8642802d13a53541e313781c34521a0d33099aac", 15 | "width" : 640 16 | }, { 17 | "height" : 300, 18 | "url" : "https://i.scdn.co/image/631ee4d5160303af86751587457b1b00957e0519", 19 | "width" : 300 20 | }, { 21 | "height" : 64, 22 | "url" : "https://i.scdn.co/image/d7b7140400d985d1294d7b044da1b5b4bfc0ae69", 23 | "width" : 64 24 | } ], 25 | "name" : "We Are One (Ole Ola) [The Official 2014 FIFA World Cup Song]", 26 | "type" : "album", 27 | "uri" : "spotify:album:60mvULtYiNSRmpVvoa3RE4" 28 | }, { 29 | "album_type" : "album", 30 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 31 | "external_urls" : { 32 | "spotify" : "https://open.spotify.com/album/4JXziyWNlxM4oPw34PMjVj" 33 | }, 34 | "href" : "https://api.spotify.com/v1/albums/4JXziyWNlxM4oPw34PMjVj", 35 | "id" : "4JXziyWNlxM4oPw34PMjVj", 36 | "images" : [ { 37 | "height" : 640, 38 | "url" : "https://i.scdn.co/image/47c6249a3d514752fc783e64a2f47611bce66a4b", 39 | "width" : 640 40 | }, { 41 | "height" : 300, 42 | "url" : "https://i.scdn.co/image/a22dbdd2595f1144890eb269bb93cc79142f2767", 43 | "width" : 300 44 | }, { 45 | "height" : 64, 46 | "url" : "https://i.scdn.co/image/c1bd45015d6a1245603a88e03d336992f5d653a2", 47 | "width" : 64 48 | } ], 49 | "name" : "A13", 50 | "type" : "album", 51 | "uri" : "spotify:album:4JXziyWNlxM4oPw34PMjVj" 52 | }, { 53 | "album_type" : "album", 54 | "available_markets" : [ "AD", "AR", "AU", "BE", "BG", "BO", "BR", "CA", "CL", "CO", "CR", "CY", "CZ", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 55 | "external_urls" : { 56 | "spotify" : "https://open.spotify.com/album/1SIpLwZu1R69coxKMH06kw" 57 | }, 58 | "href" : "https://api.spotify.com/v1/albums/1SIpLwZu1R69coxKMH06kw", 59 | "id" : "1SIpLwZu1R69coxKMH06kw", 60 | "images" : [ { 61 | "height" : 640, 62 | "url" : "https://i.scdn.co/image/c65ef9bb11bcd9d06aa710866fb3440291c5f9ea", 63 | "width" : 640 64 | }, { 65 | "height" : 300, 66 | "url" : "https://i.scdn.co/image/8db5eeba539b7eabe7599e13e7bd83b8ec9f98bb", 67 | "width" : 300 68 | }, { 69 | "height" : 64, 70 | "url" : "https://i.scdn.co/image/abfb50b9844def2e40afac4059da71d658845ab2", 71 | "width" : 64 72 | } ], 73 | "name" : "May Death Never Stop You", 74 | "type" : "album", 75 | "uri" : "spotify:album:1SIpLwZu1R69coxKMH06kw" 76 | }, { 77 | "album_type" : "single", 78 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 79 | "external_urls" : { 80 | "spotify" : "https://open.spotify.com/album/5qZNAZ5xJCUAiyYoETU0aj" 81 | }, 82 | "href" : "https://api.spotify.com/v1/albums/5qZNAZ5xJCUAiyYoETU0aj", 83 | "id" : "5qZNAZ5xJCUAiyYoETU0aj", 84 | "images" : [ { 85 | "height" : 640, 86 | "url" : "https://i.scdn.co/image/86c15f11c44726c52df044882e39e6c4d168f8a8", 87 | "width" : 640 88 | }, { 89 | "height" : 300, 90 | "url" : "https://i.scdn.co/image/f7e52154d6ea994ea9e78dd8b1ab0d37417934ef", 91 | "width" : 300 92 | }, { 93 | "height" : 64, 94 | "url" : "https://i.scdn.co/image/2a5c218cb7e524c5bd95b938b3553905311705da", 95 | "width" : 64 96 | } ], 97 | "name" : "Keep Watch", 98 | "type" : "album", 99 | "uri" : "spotify:album:5qZNAZ5xJCUAiyYoETU0aj" 100 | }, { 101 | "album_type" : "album", 102 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 103 | "external_urls" : { 104 | "spotify" : "https://open.spotify.com/album/5ZzFFF7wSMmGaIWjAHElnW" 105 | }, 106 | "href" : "https://api.spotify.com/v1/albums/5ZzFFF7wSMmGaIWjAHElnW", 107 | "id" : "5ZzFFF7wSMmGaIWjAHElnW", 108 | "images" : [ { 109 | "height" : 640, 110 | "url" : "https://i.scdn.co/image/aa1d1cf0d70fb29b4ae575f10d2ac8acc88413f5", 111 | "width" : 640 112 | }, { 113 | "height" : 300, 114 | "url" : "https://i.scdn.co/image/aed60ab4e13c6cd781df8dcd2375c2b186ae1991", 115 | "width" : 300 116 | }, { 117 | "height" : 64, 118 | "url" : "https://i.scdn.co/image/058e4170c906814ee70fb51ae1d84d09d5244979", 119 | "width" : 64 120 | } ], 121 | "name" : "By Any Means", 122 | "type" : "album", 123 | "uri" : "spotify:album:5ZzFFF7wSMmGaIWjAHElnW" 124 | }, { 125 | "album_type" : "single", 126 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 127 | "external_urls" : { 128 | "spotify" : "https://open.spotify.com/album/0LKHTm8YOJH9ygGm8DWv40" 129 | }, 130 | "href" : "https://api.spotify.com/v1/albums/0LKHTm8YOJH9ygGm8DWv40", 131 | "id" : "0LKHTm8YOJH9ygGm8DWv40", 132 | "images" : [ { 133 | "height" : 640, 134 | "url" : "https://i.scdn.co/image/6b2eea62c1a6f986824e25ff4e0f072c515a9988", 135 | "width" : 640 136 | }, { 137 | "height" : 300, 138 | "url" : "https://i.scdn.co/image/f9a46fb4f6f4ca0117c2d1ab530a36958921720b", 139 | "width" : 300 140 | }, { 141 | "height" : 64, 142 | "url" : "https://i.scdn.co/image/06e3464983b24b9957828d10b6c2a34c6dddae9c", 143 | "width" : 64 144 | } ], 145 | "name" : "Depth of My Soul (feat. Shana Halligan)", 146 | "type" : "album", 147 | "uri" : "spotify:album:0LKHTm8YOJH9ygGm8DWv40" 148 | }, { 149 | "album_type" : "album", 150 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 151 | "external_urls" : { 152 | "spotify" : "https://open.spotify.com/album/1dKh4z5Aayt8FFDWjO5FDh" 153 | }, 154 | "href" : "https://api.spotify.com/v1/albums/1dKh4z5Aayt8FFDWjO5FDh", 155 | "id" : "1dKh4z5Aayt8FFDWjO5FDh", 156 | "images" : [ { 157 | "height" : 640, 158 | "url" : "https://i.scdn.co/image/b2602ba2bd35dca1cc2903d58429a9379b342bf3", 159 | "width" : 640 160 | }, { 161 | "height" : 300, 162 | "url" : "https://i.scdn.co/image/8b86d8c65c01dacc92305003559db960e36a9614", 163 | "width" : 300 164 | }, { 165 | "height" : 64, 166 | "url" : "https://i.scdn.co/image/3eb58e564cc6ede9ac234c293e905e166cefa1b2", 167 | "width" : 64 168 | } ], 169 | "name" : "Singles", 170 | "type" : "album", 171 | "uri" : "spotify:album:1dKh4z5Aayt8FFDWjO5FDh" 172 | }, { 173 | "album_type" : "album", 174 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 175 | "external_urls" : { 176 | "spotify" : "https://open.spotify.com/album/33jUyJOx4j6BWJ7VkzWoth" 177 | }, 178 | "href" : "https://api.spotify.com/v1/albums/33jUyJOx4j6BWJ7VkzWoth", 179 | "id" : "33jUyJOx4j6BWJ7VkzWoth", 180 | "images" : [ { 181 | "height" : 640, 182 | "url" : "https://i.scdn.co/image/e35451b38b162c4a8665b77e729c53a2437d6f74", 183 | "width" : 640 184 | }, { 185 | "height" : 300, 186 | "url" : "https://i.scdn.co/image/f5baa64ff1928f27163516658b8301b92e54c80a", 187 | "width" : 300 188 | }, { 189 | "height" : 64, 190 | "url" : "https://i.scdn.co/image/41f4c456d66bc81d4e10d502d707fb778c431798", 191 | "width" : 64 192 | } ], 193 | "name" : "Out Among The Stars", 194 | "type" : "album", 195 | "uri" : "spotify:album:33jUyJOx4j6BWJ7VkzWoth" 196 | }, { 197 | "album_type" : "album", 198 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 199 | "external_urls" : { 200 | "spotify" : "https://open.spotify.com/album/4kE1w1zgP6Ag6Ojbuxzk7l" 201 | }, 202 | "href" : "https://api.spotify.com/v1/albums/4kE1w1zgP6Ag6Ojbuxzk7l", 203 | "id" : "4kE1w1zgP6Ag6Ojbuxzk7l", 204 | "images" : [ { 205 | "height" : 640, 206 | "url" : "https://i.scdn.co/image/c9eca97fd2817f349737dfe2aa672929759a556b", 207 | "width" : 640 208 | }, { 209 | "height" : 300, 210 | "url" : "https://i.scdn.co/image/b131664fc60fc781b5bfe41a41763d4176abfdcb", 211 | "width" : 300 212 | }, { 213 | "height" : 64, 214 | "url" : "https://i.scdn.co/image/5d2a8eeb9732f68c09c8c582ee5d028091fe1abf", 215 | "width" : 64 216 | } ], 217 | "name" : "Underneath the Rainbow (Bonus Track Version)", 218 | "type" : "album", 219 | "uri" : "spotify:album:4kE1w1zgP6Ag6Ojbuxzk7l" 220 | }, { 221 | "album_type" : "album", 222 | "available_markets" : [ "AD", "AR", "BE", "BG", "BO", "BR", "CA", "CL", "CO", "CR", "CY", "CZ", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 223 | "external_urls" : { 224 | "spotify" : "https://open.spotify.com/album/6RCOAR93Gi157qwW771xFG" 225 | }, 226 | "href" : "https://api.spotify.com/v1/albums/6RCOAR93Gi157qwW771xFG", 227 | "id" : "6RCOAR93Gi157qwW771xFG", 228 | "images" : [ { 229 | "height" : 640, 230 | "url" : "https://i.scdn.co/image/590d34233a0ef309d2c83213fb20cdef7e7804c6", 231 | "width" : 640 232 | }, { 233 | "height" : 300, 234 | "url" : "https://i.scdn.co/image/5bceabff51f4e0366fa7e05bfc92e3f8a73e157f", 235 | "width" : 300 236 | }, { 237 | "height" : 64, 238 | "url" : "https://i.scdn.co/image/32d480d47b3f0d0687c7d6cb362f7246b35fb62e", 239 | "width" : 64 240 | } ], 241 | "name" : "Kiss Me Once (Special Edition)", 242 | "type" : "album", 243 | "uri" : "spotify:album:6RCOAR93Gi157qwW771xFG" 244 | }, { 245 | "album_type" : "single", 246 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 247 | "external_urls" : { 248 | "spotify" : "https://open.spotify.com/album/14GhiNSb8mH3sOnersqp28" 249 | }, 250 | "href" : "https://api.spotify.com/v1/albums/14GhiNSb8mH3sOnersqp28", 251 | "id" : "14GhiNSb8mH3sOnersqp28", 252 | "images" : [ { 253 | "height" : 640, 254 | "url" : "https://i.scdn.co/image/ceabefd7b5c7c1a4a4a550de823289810b14b7dd", 255 | "width" : 640 256 | }, { 257 | "height" : 300, 258 | "url" : "https://i.scdn.co/image/398e5dda368c86337c8240dd2e6fa015ae576ce2", 259 | "width" : 300 260 | }, { 261 | "height" : 64, 262 | "url" : "https://i.scdn.co/image/e73d5b0fb22ae7e0ceb66155a7dc82f5512f9005", 263 | "width" : 64 264 | } ], 265 | "name" : "FALLINLOVE2NITE", 266 | "type" : "album", 267 | "uri" : "spotify:album:14GhiNSb8mH3sOnersqp28" 268 | }, { 269 | "album_type" : "album", 270 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 271 | "external_urls" : { 272 | "spotify" : "https://open.spotify.com/album/7rf1qZJ6hGSlPN7K9ShsVV" 273 | }, 274 | "href" : "https://api.spotify.com/v1/albums/7rf1qZJ6hGSlPN7K9ShsVV", 275 | "id" : "7rf1qZJ6hGSlPN7K9ShsVV", 276 | "images" : [ { 277 | "height" : 640, 278 | "url" : "https://i.scdn.co/image/9ab61dfa896d1431af5cddcb2bb9dba471103bb0", 279 | "width" : 640 280 | }, { 281 | "height" : 300, 282 | "url" : "https://i.scdn.co/image/fd6fe6aa26f835e85fc8b555f2ed86a36d62ca33", 283 | "width" : 300 284 | }, { 285 | "height" : 64, 286 | "url" : "https://i.scdn.co/image/c00728e445db587f47d8f4161319d83e5b2fe33c", 287 | "width" : 64 288 | } ], 289 | "name" : "Recess", 290 | "type" : "album", 291 | "uri" : "spotify:album:7rf1qZJ6hGSlPN7K9ShsVV" 292 | }, { 293 | "album_type" : "album", 294 | "available_markets" : [ "AD", "AR", "AT", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 295 | "external_urls" : { 296 | "spotify" : "https://open.spotify.com/album/1YPlEB0kZ4SOyT2kBLgINR" 297 | }, 298 | "href" : "https://api.spotify.com/v1/albums/1YPlEB0kZ4SOyT2kBLgINR", 299 | "id" : "1YPlEB0kZ4SOyT2kBLgINR", 300 | "images" : [ { 301 | "height" : 640, 302 | "url" : "https://i.scdn.co/image/baca3f284d82c0b10286dfdd0727b96b7744caf5", 303 | "width" : 640 304 | }, { 305 | "height" : 300, 306 | "url" : "https://i.scdn.co/image/f0cea1f04f0a442049aad5047d06a4948752d94e", 307 | "width" : 300 308 | }, { 309 | "height" : 64, 310 | "url" : "https://i.scdn.co/image/233aafd9f55291c1103ed69b821af3521d4f97ce", 311 | "width" : 64 312 | } ], 313 | "name" : "Love Letters", 314 | "type" : "album", 315 | "uri" : "spotify:album:1YPlEB0kZ4SOyT2kBLgINR" 316 | }, { 317 | "album_type" : "album", 318 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "UY" ], 319 | "external_urls" : { 320 | "spotify" : "https://open.spotify.com/album/2BQejnIGjuFugsT71hhOG3" 321 | }, 322 | "href" : "https://api.spotify.com/v1/albums/2BQejnIGjuFugsT71hhOG3", 323 | "id" : "2BQejnIGjuFugsT71hhOG3", 324 | "images" : [ { 325 | "height" : 640, 326 | "url" : "https://i.scdn.co/image/b02ee9ec0e8eade076bc2358cba1e22921de956d", 327 | "width" : 640 328 | }, { 329 | "height" : 300, 330 | "url" : "https://i.scdn.co/image/1e92081774a2150269dc23ee02bd643c8e5c1bb5", 331 | "width" : 300 332 | }, { 333 | "height" : 64, 334 | "url" : "https://i.scdn.co/image/113ff50a0e7d0a52de712314e1bf4e66e1460598", 335 | "width" : 64 336 | } ], 337 | "name" : "The Take Off And Landing Of Everything", 338 | "type" : "album", 339 | "uri" : "spotify:album:2BQejnIGjuFugsT71hhOG3" 340 | }, { 341 | "album_type" : "single", 342 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "UY" ], 343 | "external_urls" : { 344 | "spotify" : "https://open.spotify.com/album/1RUwY4InqOgnkrH1zcl9AI" 345 | }, 346 | "href" : "https://api.spotify.com/v1/albums/1RUwY4InqOgnkrH1zcl9AI", 347 | "id" : "1RUwY4InqOgnkrH1zcl9AI", 348 | "images" : [ { 349 | "height" : 640, 350 | "url" : "https://i.scdn.co/image/36946cbaa32098e4a3716a18a3b2eb2b4c2d07b0", 351 | "width" : 640 352 | }, { 353 | "height" : 300, 354 | "url" : "https://i.scdn.co/image/264664cfb24f659e478a768722668dd5658e8ca5", 355 | "width" : 300 356 | }, { 357 | "height" : 64, 358 | "url" : "https://i.scdn.co/image/b34d0694f1c24489ba862ebb1a32006015e7e1b0", 359 | "width" : 64 360 | } ], 361 | "name" : "Guilty All The Same (feat. Rakim)", 362 | "type" : "album", 363 | "uri" : "spotify:album:1RUwY4InqOgnkrH1zcl9AI" 364 | }, { 365 | "album_type" : "album", 366 | "available_markets" : [ "CA", "US" ], 367 | "external_urls" : { 368 | "spotify" : "https://open.spotify.com/album/13xRSfodlL3UtG3xSyL8u2" 369 | }, 370 | "href" : "https://api.spotify.com/v1/albums/13xRSfodlL3UtG3xSyL8u2", 371 | "id" : "13xRSfodlL3UtG3xSyL8u2", 372 | "images" : [ { 373 | "height" : 640, 374 | "url" : "https://i.scdn.co/image/2ab5e967f8979027fbe7ae7508aaa216f98ebbd9", 375 | "width" : 640 376 | }, { 377 | "height" : 300, 378 | "url" : "https://i.scdn.co/image/0b87d1fbca3b4bf8a1df3bff69c49083bc0363ba", 379 | "width" : 300 380 | }, { 381 | "height" : 64, 382 | "url" : "https://i.scdn.co/image/221626b9dc42c9a8e75d53a277c601824f173c49", 383 | "width" : 64 384 | } ], 385 | "name" : "No Mythologies to Follow (Deluxe)", 386 | "type" : "album", 387 | "uri" : "spotify:album:13xRSfodlL3UtG3xSyL8u2" 388 | }, { 389 | "album_type" : "album", 390 | "available_markets" : [ "AD", "AR", "AT", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 391 | "external_urls" : { 392 | "spotify" : "https://open.spotify.com/album/7lAYJiypiPbCDvjKOBX1TV" 393 | }, 394 | "href" : "https://api.spotify.com/v1/albums/7lAYJiypiPbCDvjKOBX1TV", 395 | "id" : "7lAYJiypiPbCDvjKOBX1TV", 396 | "images" : [ { 397 | "height" : 640, 398 | "url" : "https://i.scdn.co/image/915c9f12ef6479d885ae3f6de2dedafb70494d20", 399 | "width" : 640 400 | }, { 401 | "height" : 300, 402 | "url" : "https://i.scdn.co/image/aba9cf39c84a1444420b230f0e6b851ec4db8f08", 403 | "width" : 300 404 | }, { 405 | "height" : 64, 406 | "url" : "https://i.scdn.co/image/107b06d55ea161a15a21dec478d5aa1d0befaa40", 407 | "width" : 64 408 | } ], 409 | "name" : "Atlas", 410 | "type" : "album", 411 | "uri" : "spotify:album:7lAYJiypiPbCDvjKOBX1TV" 412 | }, { 413 | "album_type" : "single", 414 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 415 | "external_urls" : { 416 | "spotify" : "https://open.spotify.com/album/4cCfFozyo6JC8acN8uIP7u" 417 | }, 418 | "href" : "https://api.spotify.com/v1/albums/4cCfFozyo6JC8acN8uIP7u", 419 | "id" : "4cCfFozyo6JC8acN8uIP7u", 420 | "images" : [ { 421 | "height" : 640, 422 | "url" : "https://i.scdn.co/image/95f5cbdb03db43c16046562c5f85cc2e3f77b596", 423 | "width" : 640 424 | }, { 425 | "height" : 300, 426 | "url" : "https://i.scdn.co/image/c950057b00130fb061e801b45aea6cc45dba1bc3", 427 | "width" : 300 428 | }, { 429 | "height" : 64, 430 | "url" : "https://i.scdn.co/image/53c5c7fbda7527abb8635e7af36af4e333f01e22", 431 | "width" : 64 432 | } ], 433 | "name" : "Magic", 434 | "type" : "album", 435 | "uri" : "spotify:album:4cCfFozyo6JC8acN8uIP7u" 436 | }, { 437 | "album_type" : "album", 438 | "available_markets" : [ "AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI", "SK", "SV", "TR", "TW", "US", "UY" ], 439 | "external_urls" : { 440 | "spotify" : "https://open.spotify.com/album/4JHtCtKG5CXJAXYLJtKUEE" 441 | }, 442 | "href" : "https://api.spotify.com/v1/albums/4JHtCtKG5CXJAXYLJtKUEE", 443 | "id" : "4JHtCtKG5CXJAXYLJtKUEE", 444 | "images" : [ { 445 | "height" : 640, 446 | "url" : "https://i.scdn.co/image/c61a960d6110e259eb26a8a48d39b5b43b61bcdf", 447 | "width" : 640 448 | }, { 449 | "height" : 300, 450 | "url" : "https://i.scdn.co/image/e504411141d5dd4742d41245e9821d6da9079bc5", 451 | "width" : 300 452 | }, { 453 | "height" : 64, 454 | "url" : "https://i.scdn.co/image/b4af49db137f7862cef31d64b4ab9c7f14475467", 455 | "width" : 64 456 | } ], 457 | "name" : "Somebody's Party - EP", 458 | "type" : "album", 459 | "uri" : "spotify:album:4JHtCtKG5CXJAXYLJtKUEE" 460 | }, { 461 | "album_type" : "album", 462 | "available_markets" : [ "AR", "AU", "BO", "BR", "CA", "CL", "CO", "CR", "DO", "EC", "GT", "HK", "HN", "MX", "MY", "NI", "NZ", "PA", "PE", "PH", "PY", "SG", "SV", "TW", "US", "UY" ], 463 | "external_urls" : { 464 | "spotify" : "https://open.spotify.com/album/0Cvy3SH2exFvz8WIX68HSZ" 465 | }, 466 | "href" : "https://api.spotify.com/v1/albums/0Cvy3SH2exFvz8WIX68HSZ", 467 | "id" : "0Cvy3SH2exFvz8WIX68HSZ", 468 | "images" : [ { 469 | "height" : 640, 470 | "url" : "https://i.scdn.co/image/3d0b40dbf4ed6318b2bbe87d83a9bcbf39fcc45d", 471 | "width" : 640 472 | }, { 473 | "height" : 300, 474 | "url" : "https://i.scdn.co/image/bc2bd24632e69303590c47a60c0d3b7e73ad641a", 475 | "width" : 300 476 | }, { 477 | "height" : 64, 478 | "url" : "https://i.scdn.co/image/79b3b1df664940f0352a7214e1260603124cc590", 479 | "width" : 64 480 | } ], 481 | "name" : "Spotify Sessions - Live at Warped Tour 2013", 482 | "type" : "album", 483 | "uri" : "spotify:album:0Cvy3SH2exFvz8WIX68HSZ" 484 | } ], 485 | "limit" : 20, 486 | "next" : "https://api.spotify.com/v1/browse/new-releases?offset=20&limit=20", 487 | "offset" : 0, 488 | "previous" : null, 489 | "total" : 120 490 | } 491 | } -------------------------------------------------------------------------------- /test/mock/search.artist.json: -------------------------------------------------------------------------------- 1 | { 2 | "artists" : { 3 | "href" : "https://api.spotify.com/v1/search?query=Nirvana&offset=0&limit=20&type=artist", 4 | "items" : [ { 5 | "external_urls" : { 6 | "spotify" : "https://open.spotify.com/artist/6olE6TJLqED3rqDCT0FyPh" 7 | }, 8 | "genres" : [ "alternative rock", "grunge", "hot", "permanent wave", "rock" ], 9 | "href" : "https://api.spotify.com/v1/artists/6olE6TJLqED3rqDCT0FyPh", 10 | "id" : "6olE6TJLqED3rqDCT0FyPh", 11 | "images" : [ { 12 | "height" : 1057, 13 | "url" : "https://i.scdn.co/image/5d66307bbf73337bb073bfb2bf242e099a47e219", 14 | "width" : 1000 15 | }, { 16 | "height" : 677, 17 | "url" : "https://i.scdn.co/image/695867a7c6a0df25c5bddf0b00cc2cfde35b3ffc", 18 | "width" : 640 19 | }, { 20 | "height" : 211, 21 | "url" : "https://i.scdn.co/image/3a9c12e86ad8e2bbecb0e919b80bb5ece6f1dbe3", 22 | "width" : 200 23 | }, { 24 | "height" : 68, 25 | "url" : "https://i.scdn.co/image/5ae6f10633adc53210cd18a4f216f27de330f19d", 26 | "width" : 64 27 | } ], 28 | "name" : "Nirvana", 29 | "popularity" : 66, 30 | "type" : "artist", 31 | "uri" : "spotify:artist:6olE6TJLqED3rqDCT0FyPh" 32 | }, { 33 | "external_urls" : { 34 | "spotify" : "https://open.spotify.com/artist/3sS2Q1UZuUXL7TZSbQumDI" 35 | }, 36 | "genres" : [ ], 37 | "href" : "https://api.spotify.com/v1/artists/3sS2Q1UZuUXL7TZSbQumDI", 38 | "id" : "3sS2Q1UZuUXL7TZSbQumDI", 39 | "images" : [ { 40 | "height" : 640, 41 | "url" : "https://i.scdn.co/image/3a6e6796d8918759ab2c020f6727e6cf145bee31", 42 | "width" : 640 43 | }, { 44 | "height" : 300, 45 | "url" : "https://i.scdn.co/image/621c0f2ff8a77bd0c2df3ddcefd0b56ff8207850", 46 | "width" : 300 47 | }, { 48 | "height" : 64, 49 | "url" : "https://i.scdn.co/image/6f27a6df6d6f8d8ad3eeaacd40a16cc8dc0fedcf", 50 | "width" : 64 51 | } ], 52 | "name" : "Approaching Nirvana", 53 | "popularity" : 38, 54 | "type" : "artist", 55 | "uri" : "spotify:artist:3sS2Q1UZuUXL7TZSbQumDI" 56 | }, { 57 | "external_urls" : { 58 | "spotify" : "https://open.spotify.com/artist/7dIxU1XgxBIa3KJAWzaFAC" 59 | }, 60 | "genres" : [ ], 61 | "href" : "https://api.spotify.com/v1/artists/7dIxU1XgxBIa3KJAWzaFAC", 62 | "id" : "7dIxU1XgxBIa3KJAWzaFAC", 63 | "images" : [ { 64 | "height" : 640, 65 | "url" : "https://i.scdn.co/image/27aba4e27ca575477a863b3bbd08ec7b7f5860a9", 66 | "width" : 640 67 | }, { 68 | "height" : 300, 69 | "url" : "https://i.scdn.co/image/28fc19d914747265c8988125142508225ff8f908", 70 | "width" : 300 71 | }, { 72 | "height" : 64, 73 | "url" : "https://i.scdn.co/image/8dc55983a3c95a9b27a6bfd6376ae2a9fdb1e772", 74 | "width" : 64 75 | } ], 76 | "name" : "Nirvana", 77 | "popularity" : 11, 78 | "type" : "artist", 79 | "uri" : "spotify:artist:7dIxU1XgxBIa3KJAWzaFAC" 80 | }, { 81 | "external_urls" : { 82 | "spotify" : "https://open.spotify.com/artist/4VWpDDJrQ45G5XMazt13NY" 83 | }, 84 | "genres" : [ ], 85 | "href" : "https://api.spotify.com/v1/artists/4VWpDDJrQ45G5XMazt13NY", 86 | "id" : "4VWpDDJrQ45G5XMazt13NY", 87 | "images" : [ { 88 | "height" : 640, 89 | "url" : "https://i.scdn.co/image/04bf904293cf1e8129a6511d279294cc88710d58", 90 | "width" : 640 91 | }, { 92 | "height" : 300, 93 | "url" : "https://i.scdn.co/image/62bb4310560ed96fbc5dac2782fae44980fa2ee8", 94 | "width" : 300 95 | }, { 96 | "height" : 64, 97 | "url" : "https://i.scdn.co/image/cd8ae9365bbeea96177169c9aafaf2b9a770937c", 98 | "width" : 64 99 | } ], 100 | "name" : "Dylan Nirvana", 101 | "popularity" : 0, 102 | "type" : "artist", 103 | "uri" : "spotify:artist:4VWpDDJrQ45G5XMazt13NY" 104 | }, { 105 | "external_urls" : { 106 | "spotify" : "https://open.spotify.com/artist/6Mb3w5zGtJrZfOiTsRg9sK" 107 | }, 108 | "genres" : [ ], 109 | "href" : "https://api.spotify.com/v1/artists/6Mb3w5zGtJrZfOiTsRg9sK", 110 | "id" : "6Mb3w5zGtJrZfOiTsRg9sK", 111 | "images" : [ ], 112 | "name" : "Nirvana Lopez", 113 | "popularity" : 0, 114 | "type" : "artist", 115 | "uri" : "spotify:artist:6Mb3w5zGtJrZfOiTsRg9sK" 116 | }, { 117 | "external_urls" : { 118 | "spotify" : "https://open.spotify.com/artist/35FtUNfeF80o8d2ZsBiL1q" 119 | }, 120 | "genres" : [ ], 121 | "href" : "https://api.spotify.com/v1/artists/35FtUNfeF80o8d2ZsBiL1q", 122 | "id" : "35FtUNfeF80o8d2ZsBiL1q", 123 | "images" : [ ], 124 | "name" : "Nirvana 2", 125 | "popularity" : 0, 126 | "type" : "artist", 127 | "uri" : "spotify:artist:35FtUNfeF80o8d2ZsBiL1q" 128 | }, { 129 | "external_urls" : { 130 | "spotify" : "https://open.spotify.com/artist/7trg2KlfOzVbPBHfMtHNVX" 131 | }, 132 | "genres" : [ ], 133 | "href" : "https://api.spotify.com/v1/artists/7trg2KlfOzVbPBHfMtHNVX", 134 | "id" : "7trg2KlfOzVbPBHfMtHNVX", 135 | "images" : [ ], 136 | "name" : "Nirvana 2002", 137 | "popularity" : 0, 138 | "type" : "artist", 139 | "uri" : "spotify:artist:7trg2KlfOzVbPBHfMtHNVX" 140 | }, { 141 | "external_urls" : { 142 | "spotify" : "https://open.spotify.com/artist/0Rd5mpn0dgnYj3amvWJcJT" 143 | }, 144 | "genres" : [ ], 145 | "href" : "https://api.spotify.com/v1/artists/0Rd5mpn0dgnYj3amvWJcJT", 146 | "id" : "0Rd5mpn0dgnYj3amvWJcJT", 147 | "images" : [ ], 148 | "name" : "Nirvana Savoury", 149 | "popularity" : 0, 150 | "type" : "artist", 151 | "uri" : "spotify:artist:0Rd5mpn0dgnYj3amvWJcJT" 152 | }, { 153 | "external_urls" : { 154 | "spotify" : "https://open.spotify.com/artist/6D1zhuR7mE86MMv92Z98FA" 155 | }, 156 | "genres" : [ ], 157 | "href" : "https://api.spotify.com/v1/artists/6D1zhuR7mE86MMv92Z98FA", 158 | "id" : "6D1zhuR7mE86MMv92Z98FA", 159 | "images" : [ { 160 | "height" : 640, 161 | "url" : "https://i.scdn.co/image/c9c111b423b208108ce5e69bf0426c0642c40a65", 162 | "width" : 640 163 | }, { 164 | "height" : 300, 165 | "url" : "https://i.scdn.co/image/116199ba9875fdeb15a7742d64dcbddb97d2594e", 166 | "width" : 300 167 | }, { 168 | "height" : 64, 169 | "url" : "https://i.scdn.co/image/564a103baf59d0e6293818f5eca0046cd1e070fe", 170 | "width" : 64 171 | } ], 172 | "name" : "Frets Nirvana", 173 | "popularity" : 0, 174 | "type" : "artist", 175 | "uri" : "spotify:artist:6D1zhuR7mE86MMv92Z98FA" 176 | }, { 177 | "external_urls" : { 178 | "spotify" : "https://open.spotify.com/artist/2PXPU5KedzYVI2iBpnNyHT" 179 | }, 180 | "genres" : [ ], 181 | "href" : "https://api.spotify.com/v1/artists/2PXPU5KedzYVI2iBpnNyHT", 182 | "id" : "2PXPU5KedzYVI2iBpnNyHT", 183 | "images" : [ { 184 | "height" : 640, 185 | "url" : "https://i.scdn.co/image/ae7861c5cd0960ffd419a36e041da14f08ec687b", 186 | "width" : 640 187 | }, { 188 | "height" : 300, 189 | "url" : "https://i.scdn.co/image/f017fe098fed8d24e6e482c7e36c523446743322", 190 | "width" : 300 191 | }, { 192 | "height" : 64, 193 | "url" : "https://i.scdn.co/image/8359ebfae60849f2c17ae6b36e8fc54437a84163", 194 | "width" : 64 195 | } ], 196 | "name" : "Nicole Nirvana", 197 | "popularity" : 0, 198 | "type" : "artist", 199 | "uri" : "spotify:artist:2PXPU5KedzYVI2iBpnNyHT" 200 | }, { 201 | "external_urls" : { 202 | "spotify" : "https://open.spotify.com/artist/72ME0qtrjmv9j6oXSzmJlR" 203 | }, 204 | "genres" : [ ], 205 | "href" : "https://api.spotify.com/v1/artists/72ME0qtrjmv9j6oXSzmJlR", 206 | "id" : "72ME0qtrjmv9j6oXSzmJlR", 207 | "images" : [ { 208 | "height" : 640, 209 | "url" : "https://i.scdn.co/image/92f8ca3a870eb37cfcd07c77f6adbdd11aa2e758", 210 | "width" : 640 211 | }, { 212 | "height" : 300, 213 | "url" : "https://i.scdn.co/image/f123f11c63769eaceffb094168686f279ed1ea32", 214 | "width" : 300 215 | }, { 216 | "height" : 64, 217 | "url" : "https://i.scdn.co/image/3c45daefe1ea091680accf6cce539f9e8772cf7c", 218 | "width" : 64 219 | } ], 220 | "name" : "Nirvana Singh", 221 | "popularity" : 0, 222 | "type" : "artist", 223 | "uri" : "spotify:artist:72ME0qtrjmv9j6oXSzmJlR" 224 | }, { 225 | "external_urls" : { 226 | "spotify" : "https://open.spotify.com/artist/5jdyUplSQGWfRzefm3Ybeb" 227 | }, 228 | "genres" : [ ], 229 | "href" : "https://api.spotify.com/v1/artists/5jdyUplSQGWfRzefm3Ybeb", 230 | "id" : "5jdyUplSQGWfRzefm3Ybeb", 231 | "images" : [ { 232 | "height" : 640, 233 | "url" : "https://i.scdn.co/image/70051d8f6657f4acebee8652bce1365d5e3fafb5", 234 | "width" : 640 235 | }, { 236 | "height" : 300, 237 | "url" : "https://i.scdn.co/image/9cdc261e76ee668ea3034cde3b2497086cf92a07", 238 | "width" : 300 239 | }, { 240 | "height" : 64, 241 | "url" : "https://i.scdn.co/image/96baf3550d52705355e4ea3fa114e37385103b20", 242 | "width" : 64 243 | } ], 244 | "name" : "Black Nirvana", 245 | "popularity" : 0, 246 | "type" : "artist", 247 | "uri" : "spotify:artist:5jdyUplSQGWfRzefm3Ybeb" 248 | }, { 249 | "external_urls" : { 250 | "spotify" : "https://open.spotify.com/artist/7n8JJgvxvIYxtPL7KAWxQ9" 251 | }, 252 | "genres" : [ ], 253 | "href" : "https://api.spotify.com/v1/artists/7n8JJgvxvIYxtPL7KAWxQ9", 254 | "id" : "7n8JJgvxvIYxtPL7KAWxQ9", 255 | "images" : [ ], 256 | "name" : "Raptile feat. Nirvana Savoury", 257 | "popularity" : 0, 258 | "type" : "artist", 259 | "uri" : "spotify:artist:7n8JJgvxvIYxtPL7KAWxQ9" 260 | }, { 261 | "external_urls" : { 262 | "spotify" : "https://open.spotify.com/artist/7o0DbCO8JrLSb3Q8c8rFpk" 263 | }, 264 | "genres" : [ ], 265 | "href" : "https://api.spotify.com/v1/artists/7o0DbCO8JrLSb3Q8c8rFpk", 266 | "id" : "7o0DbCO8JrLSb3Q8c8rFpk", 267 | "images" : [ { 268 | "height" : 640, 269 | "url" : "https://i.scdn.co/image/7f06c00e0ca8918d99e6e902a838ccd79b8cd251", 270 | "width" : 640 271 | }, { 272 | "height" : 300, 273 | "url" : "https://i.scdn.co/image/cc5dce9a9288111ff3342384c082570eb19a1838", 274 | "width" : 300 275 | }, { 276 | "height" : 64, 277 | "url" : "https://i.scdn.co/image/399b6f4b8e907be4472e0f2c19b1869b97204c82", 278 | "width" : 64 279 | } ], 280 | "name" : "Nirvana Meditation Orchestra", 281 | "popularity" : 0, 282 | "type" : "artist", 283 | "uri" : "spotify:artist:7o0DbCO8JrLSb3Q8c8rFpk" 284 | }, { 285 | "external_urls" : { 286 | "spotify" : "https://open.spotify.com/artist/3fWjSLaMhzh6afo0wxu7sE" 287 | }, 288 | "genres" : [ ], 289 | "href" : "https://api.spotify.com/v1/artists/3fWjSLaMhzh6afo0wxu7sE", 290 | "id" : "3fWjSLaMhzh6afo0wxu7sE", 291 | "images" : [ ], 292 | "name" : "Nirvana Oasis Spa", 293 | "popularity" : 0, 294 | "type" : "artist", 295 | "uri" : "spotify:artist:3fWjSLaMhzh6afo0wxu7sE" 296 | }, { 297 | "external_urls" : { 298 | "spotify" : "https://open.spotify.com/artist/5x0skLD8uDinbbosepFm7S" 299 | }, 300 | "genres" : [ ], 301 | "href" : "https://api.spotify.com/v1/artists/5x0skLD8uDinbbosepFm7S", 302 | "id" : "5x0skLD8uDinbbosepFm7S", 303 | "images" : [ { 304 | "height" : 640, 305 | "url" : "https://i.scdn.co/image/f572519f97bc9729c80571732a07284c98d66c81", 306 | "width" : 640 307 | }, { 308 | "height" : 300, 309 | "url" : "https://i.scdn.co/image/2ad9a6d40470b2264097959445feda8895894d7a", 310 | "width" : 300 311 | }, { 312 | "height" : 64, 313 | "url" : "https://i.scdn.co/image/df19e91d29f6d4fdafaa39f63bf7cac160a388bb", 314 | "width" : 64 315 | } ], 316 | "name" : "Capital Cities Nirvana", 317 | "popularity" : 1, 318 | "type" : "artist", 319 | "uri" : "spotify:artist:5x0skLD8uDinbbosepFm7S" 320 | }, { 321 | "external_urls" : { 322 | "spotify" : "https://open.spotify.com/artist/5NBv1IbgzUWfhlMCuQpm4q" 323 | }, 324 | "genres" : [ ], 325 | "href" : "https://api.spotify.com/v1/artists/5NBv1IbgzUWfhlMCuQpm4q", 326 | "id" : "5NBv1IbgzUWfhlMCuQpm4q", 327 | "images" : [ { 328 | "height" : 640, 329 | "url" : "https://i.scdn.co/image/437542885690aa4cfb9f39d1817028e5e4dfff8a", 330 | "width" : 640 331 | }, { 332 | "height" : 300, 333 | "url" : "https://i.scdn.co/image/8050135ebee9536b6239db2aa29c13f7ca05b10b", 334 | "width" : 300 335 | }, { 336 | "height" : 64, 337 | "url" : "https://i.scdn.co/image/5bd43ae15bd58750db06be212872a7d083ba6abc", 338 | "width" : 64 339 | } ], 340 | "name" : "Dylan Nirvana & the Bad Flowers", 341 | "popularity" : 0, 342 | "type" : "artist", 343 | "uri" : "spotify:artist:5NBv1IbgzUWfhlMCuQpm4q" 344 | }, { 345 | "external_urls" : { 346 | "spotify" : "https://open.spotify.com/artist/424VCRr8HdYc1YqRfLWRs6" 347 | }, 348 | "genres" : [ ], 349 | "href" : "https://api.spotify.com/v1/artists/424VCRr8HdYc1YqRfLWRs6", 350 | "id" : "424VCRr8HdYc1YqRfLWRs6", 351 | "images" : [ { 352 | "height" : 600, 353 | "url" : "https://i.scdn.co/image/0021a280ea1c5fa3305bb8c9abb1549c9fe54cbf", 354 | "width" : 600 355 | }, { 356 | "height" : 300, 357 | "url" : "https://i.scdn.co/image/6dc9528eec17531eb0e253a0c8fefae51c2b3215", 358 | "width" : 300 359 | }, { 360 | "height" : 64, 361 | "url" : "https://i.scdn.co/image/7cd44de77919eae3c31211d425386d74f625598d", 362 | "width" : 64 363 | } ], 364 | "name" : "Nirvana", 365 | "popularity" : 1, 366 | "type" : "artist", 367 | "uri" : "spotify:artist:424VCRr8HdYc1YqRfLWRs6" 368 | }, { 369 | "external_urls" : { 370 | "spotify" : "https://open.spotify.com/artist/5bV14Dh6Wq4yCgx6FfIClA" 371 | }, 372 | "genres" : [ ], 373 | "href" : "https://api.spotify.com/v1/artists/5bV14Dh6Wq4yCgx6FfIClA", 374 | "id" : "5bV14Dh6Wq4yCgx6FfIClA", 375 | "images" : [ { 376 | "height" : 640, 377 | "url" : "https://i.scdn.co/image/cdff27d3d99fdbfdd4c5227dc352feb6728c8540", 378 | "width" : 640 379 | }, { 380 | "height" : 300, 381 | "url" : "https://i.scdn.co/image/16dc8fe0226340a93fa020e6b627fbd9f29a11f1", 382 | "width" : 300 383 | }, { 384 | "height" : 64, 385 | "url" : "https://i.scdn.co/image/1631cebb5cc142b3ff030bb7492ea1c3bdd387b1", 386 | "width" : 64 387 | } ], 388 | "name" : "Nirvana's Dreams", 389 | "popularity" : 0, 390 | "type" : "artist", 391 | "uri" : "spotify:artist:5bV14Dh6Wq4yCgx6FfIClA" 392 | }, { 393 | "external_urls" : { 394 | "spotify" : "https://open.spotify.com/artist/4kalcpdOZ3speoV6Vuh0h0" 395 | }, 396 | "genres" : [ ], 397 | "href" : "https://api.spotify.com/v1/artists/4kalcpdOZ3speoV6Vuh0h0", 398 | "id" : "4kalcpdOZ3speoV6Vuh0h0", 399 | "images" : [ ], 400 | "name" : "Karaoke - Nirvana", 401 | "popularity" : 0, 402 | "type" : "artist", 403 | "uri" : "spotify:artist:4kalcpdOZ3speoV6Vuh0h0" 404 | } ], 405 | "limit" : 20, 406 | "next" : "https://api.spotify.com/v1/search?query=Nirvana&offset=20&limit=1&type=artist", 407 | "offset" : 0, 408 | "previous" : null, 409 | "total" : 21 410 | } 411 | } -------------------------------------------------------------------------------- /test/mock/search.missing-type.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "status": 400, 4 | "message": "Missing parameter type" 5 | } 6 | } -------------------------------------------------------------------------------- /test/vendor/jasmine-jquery.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Jasmine-jQuery: a set of jQuery helpers for Jasmine tests. 3 | 4 | Version 2.0.5 5 | 6 | https://github.com/velesin/jasmine-jquery 7 | 8 | Copyright (c) 2010-2014 Wojciech Zawistowski, Travis Jeffery 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining 11 | a copy of this software and associated documentation files (the 12 | "Software"), to deal in the Software without restriction, including 13 | without limitation the rights to use, copy, modify, merge, publish, 14 | distribute, sublicense, and/or sell copies of the Software, and to 15 | permit persons to whom the Software is furnished to do so, subject to 16 | the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be 19 | included in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 25 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 26 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 27 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | */ 29 | 30 | +function (window, jasmine, $) { "use strict"; 31 | 32 | jasmine.spiedEventsKey = function (selector, eventName) { 33 | return [$(selector).selector, eventName].toString() 34 | } 35 | 36 | jasmine.getFixtures = function () { 37 | return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures() 38 | } 39 | 40 | jasmine.getStyleFixtures = function () { 41 | return jasmine.currentStyleFixtures_ = jasmine.currentStyleFixtures_ || new jasmine.StyleFixtures() 42 | } 43 | 44 | jasmine.Fixtures = function () { 45 | this.containerId = 'jasmine-fixtures' 46 | this.fixturesCache_ = {} 47 | this.fixturesPath = 'spec/javascripts/fixtures' 48 | } 49 | 50 | jasmine.Fixtures.prototype.set = function (html) { 51 | this.cleanUp() 52 | return this.createContainer_(html) 53 | } 54 | 55 | jasmine.Fixtures.prototype.appendSet= function (html) { 56 | this.addToContainer_(html) 57 | } 58 | 59 | jasmine.Fixtures.prototype.preload = function () { 60 | this.read.apply(this, arguments) 61 | } 62 | 63 | jasmine.Fixtures.prototype.load = function () { 64 | this.cleanUp() 65 | this.createContainer_(this.read.apply(this, arguments)) 66 | } 67 | 68 | jasmine.Fixtures.prototype.appendLoad = function () { 69 | this.addToContainer_(this.read.apply(this, arguments)) 70 | } 71 | 72 | jasmine.Fixtures.prototype.read = function () { 73 | var htmlChunks = [] 74 | , fixtureUrls = arguments 75 | 76 | for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) { 77 | htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex])) 78 | } 79 | 80 | return htmlChunks.join('') 81 | } 82 | 83 | jasmine.Fixtures.prototype.clearCache = function () { 84 | this.fixturesCache_ = {} 85 | } 86 | 87 | jasmine.Fixtures.prototype.cleanUp = function () { 88 | $('#' + this.containerId).remove() 89 | } 90 | 91 | jasmine.Fixtures.prototype.sandbox = function (attributes) { 92 | var attributesToSet = attributes || {} 93 | return $('
').attr(attributesToSet) 94 | } 95 | 96 | jasmine.Fixtures.prototype.createContainer_ = function (html) { 97 | var container = $('
') 98 | .attr('id', this.containerId) 99 | .html(html) 100 | 101 | $(document.body).append(container) 102 | return container 103 | } 104 | 105 | jasmine.Fixtures.prototype.addToContainer_ = function (html){ 106 | var container = $(document.body).find('#'+this.containerId).append(html) 107 | 108 | if (!container.length) { 109 | this.createContainer_(html) 110 | } 111 | } 112 | 113 | jasmine.Fixtures.prototype.getFixtureHtml_ = function (url) { 114 | if (typeof this.fixturesCache_[url] === 'undefined') { 115 | this.loadFixtureIntoCache_(url) 116 | } 117 | return this.fixturesCache_[url] 118 | } 119 | 120 | jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function (relativeUrl) { 121 | var self = this 122 | , url = this.makeFixtureUrl_(relativeUrl) 123 | , htmlText = '' 124 | , request = $.ajax({ 125 | async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded 126 | cache: false, 127 | url: url, 128 | success: function (data, status, $xhr) { 129 | htmlText = $xhr.responseText 130 | } 131 | }).fail(function ($xhr, status, err) { 132 | throw new Error('Fixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + err.message + ')') 133 | }) 134 | 135 | var scripts = $($.parseHTML(htmlText, true)).find('script[src]') || []; 136 | 137 | scripts.each(function(){ 138 | $.ajax({ 139 | async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded 140 | cache: false, 141 | dataType: 'script', 142 | url: $(this).attr('src'), 143 | success: function (data, status, $xhr) { 144 | htmlText += '' 145 | }, 146 | error: function ($xhr, status, err) { 147 | throw new Error('Script could not be loaded: ' + scriptSrc + ' (status: ' + status + ', message: ' + err.message + ')') 148 | } 149 | }); 150 | }) 151 | 152 | self.fixturesCache_[relativeUrl] = htmlText; 153 | } 154 | 155 | jasmine.Fixtures.prototype.makeFixtureUrl_ = function (relativeUrl){ 156 | return this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl 157 | } 158 | 159 | jasmine.Fixtures.prototype.proxyCallTo_ = function (methodName, passedArguments) { 160 | return this[methodName].apply(this, passedArguments) 161 | } 162 | 163 | 164 | jasmine.StyleFixtures = function () { 165 | this.fixturesCache_ = {} 166 | this.fixturesNodes_ = [] 167 | this.fixturesPath = 'spec/javascripts/fixtures' 168 | } 169 | 170 | jasmine.StyleFixtures.prototype.set = function (css) { 171 | this.cleanUp() 172 | this.createStyle_(css) 173 | } 174 | 175 | jasmine.StyleFixtures.prototype.appendSet = function (css) { 176 | this.createStyle_(css) 177 | } 178 | 179 | jasmine.StyleFixtures.prototype.preload = function () { 180 | this.read_.apply(this, arguments) 181 | } 182 | 183 | jasmine.StyleFixtures.prototype.load = function () { 184 | this.cleanUp() 185 | this.createStyle_(this.read_.apply(this, arguments)) 186 | } 187 | 188 | jasmine.StyleFixtures.prototype.appendLoad = function () { 189 | this.createStyle_(this.read_.apply(this, arguments)) 190 | } 191 | 192 | jasmine.StyleFixtures.prototype.cleanUp = function () { 193 | while(this.fixturesNodes_.length) { 194 | this.fixturesNodes_.pop().remove() 195 | } 196 | } 197 | 198 | jasmine.StyleFixtures.prototype.createStyle_ = function (html) { 199 | var styleText = $('
').html(html).text() 200 | , style = $('') 201 | 202 | this.fixturesNodes_.push(style) 203 | $('head').append(style) 204 | } 205 | 206 | jasmine.StyleFixtures.prototype.clearCache = jasmine.Fixtures.prototype.clearCache 207 | jasmine.StyleFixtures.prototype.read_ = jasmine.Fixtures.prototype.read 208 | jasmine.StyleFixtures.prototype.getFixtureHtml_ = jasmine.Fixtures.prototype.getFixtureHtml_ 209 | jasmine.StyleFixtures.prototype.loadFixtureIntoCache_ = jasmine.Fixtures.prototype.loadFixtureIntoCache_ 210 | jasmine.StyleFixtures.prototype.makeFixtureUrl_ = jasmine.Fixtures.prototype.makeFixtureUrl_ 211 | jasmine.StyleFixtures.prototype.proxyCallTo_ = jasmine.Fixtures.prototype.proxyCallTo_ 212 | 213 | jasmine.getJSONFixtures = function () { 214 | return jasmine.currentJSONFixtures_ = jasmine.currentJSONFixtures_ || new jasmine.JSONFixtures() 215 | } 216 | 217 | jasmine.JSONFixtures = function () { 218 | this.fixturesCache_ = {} 219 | this.fixturesPath = 'spec/javascripts/fixtures/json' 220 | } 221 | 222 | jasmine.JSONFixtures.prototype.load = function () { 223 | this.read.apply(this, arguments) 224 | return this.fixturesCache_ 225 | } 226 | 227 | jasmine.JSONFixtures.prototype.read = function () { 228 | var fixtureUrls = arguments 229 | 230 | for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) { 231 | this.getFixtureData_(fixtureUrls[urlIndex]) 232 | } 233 | 234 | return this.fixturesCache_ 235 | } 236 | 237 | jasmine.JSONFixtures.prototype.clearCache = function () { 238 | this.fixturesCache_ = {} 239 | } 240 | 241 | jasmine.JSONFixtures.prototype.getFixtureData_ = function (url) { 242 | if (!this.fixturesCache_[url]) this.loadFixtureIntoCache_(url) 243 | return this.fixturesCache_[url] 244 | } 245 | 246 | jasmine.JSONFixtures.prototype.loadFixtureIntoCache_ = function (relativeUrl) { 247 | var self = this 248 | , url = this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl 249 | 250 | $.ajax({ 251 | async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded 252 | cache: false, 253 | dataType: 'json', 254 | url: url, 255 | success: function (data) { 256 | self.fixturesCache_[relativeUrl] = data 257 | }, 258 | error: function ($xhr, status, err) { 259 | throw new Error('JSONFixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + err.message + ')') 260 | } 261 | }) 262 | } 263 | 264 | jasmine.JSONFixtures.prototype.proxyCallTo_ = function (methodName, passedArguments) { 265 | return this[methodName].apply(this, passedArguments) 266 | } 267 | 268 | jasmine.jQuery = function () {} 269 | 270 | jasmine.jQuery.browserTagCaseIndependentHtml = function (html) { 271 | return $('
').append(html).html() 272 | } 273 | 274 | jasmine.jQuery.elementToString = function (element) { 275 | return $(element).map(function () { return this.outerHTML; }).toArray().join(', ') 276 | } 277 | 278 | var data = { 279 | spiedEvents: {} 280 | , handlers: [] 281 | } 282 | 283 | jasmine.jQuery.events = { 284 | spyOn: function (selector, eventName) { 285 | var handler = function (e) { 286 | data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] = jasmine.util.argsToArray(arguments) 287 | } 288 | 289 | $(selector).on(eventName, handler) 290 | data.handlers.push(handler) 291 | 292 | return { 293 | selector: selector, 294 | eventName: eventName, 295 | handler: handler, 296 | reset: function (){ 297 | delete data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] 298 | } 299 | } 300 | }, 301 | 302 | args: function (selector, eventName) { 303 | var actualArgs = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] 304 | 305 | if (!actualArgs) { 306 | throw "There is no spy for " + eventName + " on " + selector.toString() + ". Make sure to create a spy using spyOnEvent." 307 | } 308 | 309 | return actualArgs 310 | }, 311 | 312 | wasTriggered: function (selector, eventName) { 313 | return !!(data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]) 314 | }, 315 | 316 | wasTriggeredWith: function (selector, eventName, expectedArgs, util, customEqualityTesters) { 317 | var actualArgs = jasmine.jQuery.events.args(selector, eventName).slice(1) 318 | 319 | if (Object.prototype.toString.call(expectedArgs) !== '[object Array]') 320 | actualArgs = actualArgs[0] 321 | 322 | return util.equals(expectedArgs, actualArgs, customEqualityTesters) 323 | }, 324 | 325 | wasPrevented: function (selector, eventName) { 326 | var args = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] 327 | , e = args ? args[0] : undefined 328 | 329 | return e && e.isDefaultPrevented() 330 | }, 331 | 332 | wasStopped: function (selector, eventName) { 333 | var args = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] 334 | , e = args ? args[0] : undefined 335 | return e && e.isPropagationStopped() 336 | }, 337 | 338 | cleanUp: function () { 339 | data.spiedEvents = {} 340 | data.handlers = [] 341 | } 342 | } 343 | 344 | var hasProperty = function (actualValue, expectedValue) { 345 | if (expectedValue === undefined) 346 | return actualValue !== undefined 347 | 348 | return actualValue === expectedValue 349 | } 350 | 351 | beforeEach(function () { 352 | jasmine.addMatchers({ 353 | toHaveClass: function () { 354 | return { 355 | compare: function (actual, className) { 356 | return { pass: $(actual).hasClass(className) } 357 | } 358 | } 359 | }, 360 | 361 | toHaveCss: function () { 362 | return { 363 | compare: function (actual, css) { 364 | for (var prop in css){ 365 | var value = css[prop] 366 | // see issue #147 on gh 367 | ;if (value === 'auto' && $(actual).get(0).style[prop] === 'auto') continue 368 | if ($(actual).css(prop) !== value) return { pass: false } 369 | } 370 | return { pass: true } 371 | } 372 | } 373 | }, 374 | 375 | toBeVisible: function () { 376 | return { 377 | compare: function (actual) { 378 | return { pass: $(actual).is(':visible') } 379 | } 380 | } 381 | }, 382 | 383 | toBeHidden: function () { 384 | return { 385 | compare: function (actual) { 386 | return { pass: $(actual).is(':hidden') } 387 | } 388 | } 389 | }, 390 | 391 | toBeSelected: function () { 392 | return { 393 | compare: function (actual) { 394 | return { pass: $(actual).is(':selected') } 395 | } 396 | } 397 | }, 398 | 399 | toBeChecked: function () { 400 | return { 401 | compare: function (actual) { 402 | return { pass: $(actual).is(':checked') } 403 | } 404 | } 405 | }, 406 | 407 | toBeEmpty: function () { 408 | return { 409 | compare: function (actual) { 410 | return { pass: $(actual).is(':empty') } 411 | } 412 | } 413 | }, 414 | 415 | toBeInDOM: function () { 416 | return { 417 | compare: function (actual) { 418 | return { pass: $.contains(document.documentElement, $(actual)[0]) } 419 | } 420 | } 421 | }, 422 | 423 | toExist: function () { 424 | return { 425 | compare: function (actual) { 426 | return { pass: $(actual).length } 427 | } 428 | } 429 | }, 430 | 431 | toHaveLength: function () { 432 | return { 433 | compare: function (actual, length) { 434 | return { pass: $(actual).length === length } 435 | } 436 | } 437 | }, 438 | 439 | toHaveAttr: function () { 440 | return { 441 | compare: function (actual, attributeName, expectedAttributeValue) { 442 | return { pass: hasProperty($(actual).attr(attributeName), expectedAttributeValue) } 443 | } 444 | } 445 | }, 446 | 447 | toHaveProp: function () { 448 | return { 449 | compare: function (actual, propertyName, expectedPropertyValue) { 450 | return { pass: hasProperty($(actual).prop(propertyName), expectedPropertyValue) } 451 | } 452 | } 453 | }, 454 | 455 | toHaveId: function () { 456 | return { 457 | compare: function (actual, id) { 458 | return { pass: $(actual).attr('id') == id } 459 | } 460 | } 461 | }, 462 | 463 | toHaveHtml: function () { 464 | return { 465 | compare: function (actual, html) { 466 | return { pass: $(actual).html() == jasmine.jQuery.browserTagCaseIndependentHtml(html) } 467 | } 468 | } 469 | }, 470 | 471 | toContainHtml: function () { 472 | return { 473 | compare: function (actual, html) { 474 | var actualHtml = $(actual).html() 475 | , expectedHtml = jasmine.jQuery.browserTagCaseIndependentHtml(html) 476 | 477 | return { pass: (actualHtml.indexOf(expectedHtml) >= 0) } 478 | } 479 | } 480 | }, 481 | 482 | toHaveText: function () { 483 | return { 484 | compare: function (actual, text) { 485 | var actualText = $(actual).text() 486 | var trimmedText = $.trim(actualText) 487 | 488 | if (text && $.isFunction(text.test)) { 489 | return { pass: text.test(actualText) || text.test(trimmedText) } 490 | } else { 491 | return { pass: (actualText == text || trimmedText == text) } 492 | } 493 | } 494 | } 495 | }, 496 | 497 | toContainText: function () { 498 | return { 499 | compare: function (actual, text) { 500 | var trimmedText = $.trim($(actual).text()) 501 | 502 | if (text && $.isFunction(text.test)) { 503 | return { pass: text.test(trimmedText) } 504 | } else { 505 | return { pass: trimmedText.indexOf(text) != -1 } 506 | } 507 | } 508 | } 509 | }, 510 | 511 | toHaveValue: function () { 512 | return { 513 | compare: function (actual, value) { 514 | return { pass: $(actual).val() === value } 515 | } 516 | } 517 | }, 518 | 519 | toHaveData: function () { 520 | return { 521 | compare: function (actual, key, expectedValue) { 522 | return { pass: hasProperty($(actual).data(key), expectedValue) } 523 | } 524 | } 525 | }, 526 | 527 | toContainElement: function () { 528 | return { 529 | compare: function (actual, selector) { 530 | if (window.debug) debugger 531 | return { pass: $(actual).find(selector).length } 532 | } 533 | } 534 | }, 535 | 536 | toBeMatchedBy: function () { 537 | return { 538 | compare: function (actual, selector) { 539 | return { pass: $(actual).filter(selector).length } 540 | } 541 | } 542 | }, 543 | 544 | toBeDisabled: function () { 545 | return { 546 | compare: function (actual, selector) { 547 | return { pass: $(actual).is(':disabled') } 548 | } 549 | } 550 | }, 551 | 552 | toBeFocused: function (selector) { 553 | return { 554 | compare: function (actual, selector) { 555 | return { pass: $(actual)[0] === $(actual)[0].ownerDocument.activeElement } 556 | } 557 | } 558 | }, 559 | 560 | toHandle: function () { 561 | return { 562 | compare: function (actual, event) { 563 | var events = $._data($(actual).get(0), "events") 564 | 565 | if (!events || !event || typeof event !== "string") { 566 | return { pass: false } 567 | } 568 | 569 | var namespaces = event.split(".") 570 | , eventType = namespaces.shift() 571 | , sortedNamespaces = namespaces.slice(0).sort() 572 | , namespaceRegExp = new RegExp("(^|\\.)" + sortedNamespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") 573 | 574 | if (events[eventType] && namespaces.length) { 575 | for (var i = 0; i < events[eventType].length; i++) { 576 | var namespace = events[eventType][i].namespace 577 | 578 | if (namespaceRegExp.test(namespace)) 579 | return { pass: true } 580 | } 581 | } else { 582 | return { pass: (events[eventType] && events[eventType].length > 0) } 583 | } 584 | 585 | return { pass: false } 586 | } 587 | } 588 | }, 589 | 590 | toHandleWith: function () { 591 | return { 592 | compare: function (actual, eventName, eventHandler) { 593 | var normalizedEventName = eventName.split('.')[0] 594 | , stack = $._data($(actual).get(0), "events")[normalizedEventName] 595 | 596 | for (var i = 0; i < stack.length; i++) { 597 | if (stack[i].handler == eventHandler) return { pass: true } 598 | } 599 | 600 | return { pass: false } 601 | } 602 | } 603 | }, 604 | 605 | toHaveBeenTriggeredOn: function () { 606 | return { 607 | compare: function (actual, selector) { 608 | var result = { pass: jasmine.jQuery.events.wasTriggered(selector, actual) } 609 | 610 | result.message = result.pass ? 611 | "Expected event " + $(actual) + " not to have been triggered on " + selector : 612 | "Expected event " + $(actual) + " to have been triggered on " + selector 613 | 614 | return result; 615 | } 616 | } 617 | }, 618 | 619 | toHaveBeenTriggered: function (){ 620 | return { 621 | compare: function (actual) { 622 | var eventName = actual.eventName 623 | , selector = actual.selector 624 | , result = { pass: jasmine.jQuery.events.wasTriggered(selector, eventName) } 625 | 626 | result.message = result.pass ? 627 | "Expected event " + eventName + " not to have been triggered on " + selector : 628 | "Expected event " + eventName + " to have been triggered on " + selector 629 | 630 | return result 631 | } 632 | } 633 | }, 634 | 635 | toHaveBeenTriggeredOnAndWith: function (j$, customEqualityTesters) { 636 | return { 637 | compare: function (actual, selector, expectedArgs) { 638 | var wasTriggered = jasmine.jQuery.events.wasTriggered(selector, actual) 639 | , result = { pass: wasTriggered && jasmine.jQuery.events.wasTriggeredWith(selector, actual, expectedArgs, j$, customEqualityTesters) } 640 | 641 | if (wasTriggered) { 642 | var actualArgs = jasmine.jQuery.events.args(selector, actual, expectedArgs)[1] 643 | result.message = result.pass ? 644 | "Expected event " + actual + " not to have been triggered with " + jasmine.pp(expectedArgs) + " but it was triggered with " + jasmine.pp(actualArgs) : 645 | "Expected event " + actual + " to have been triggered with " + jasmine.pp(expectedArgs) + " but it was triggered with " + jasmine.pp(actualArgs) 646 | 647 | } else { 648 | // todo check on this 649 | result.message = result.pass ? 650 | "Expected event " + actual + " not to have been triggered on " + selector : 651 | "Expected event " + actual + " to have been triggered on " + selector 652 | } 653 | 654 | return result 655 | } 656 | } 657 | }, 658 | 659 | toHaveBeenPreventedOn: function () { 660 | return { 661 | compare: function (actual, selector) { 662 | var result = { pass: jasmine.jQuery.events.wasPrevented(selector, actual) } 663 | 664 | result.message = result.pass ? 665 | "Expected event " + actual + " not to have been prevented on " + selector : 666 | "Expected event " + actual + " to have been prevented on " + selector 667 | 668 | return result 669 | } 670 | } 671 | }, 672 | 673 | toHaveBeenPrevented: function () { 674 | return { 675 | compare: function (actual) { 676 | var eventName = actual.eventName 677 | , selector = actual.selector 678 | , result = { pass: jasmine.jQuery.events.wasPrevented(selector, eventName) } 679 | 680 | result.message = result.pass ? 681 | "Expected event " + eventName + " not to have been prevented on " + selector : 682 | "Expected event " + eventName + " to have been prevented on " + selector 683 | 684 | return result 685 | } 686 | } 687 | }, 688 | 689 | toHaveBeenStoppedOn: function () { 690 | return { 691 | compare: function (actual, selector) { 692 | var result = { pass: jasmine.jQuery.events.wasStopped(selector, actual) } 693 | 694 | result.message = result.pass ? 695 | "Expected event " + actual + " not to have been stopped on " + selector : 696 | "Expected event " + actual + " to have been stopped on " + selector 697 | 698 | return result; 699 | } 700 | } 701 | }, 702 | 703 | toHaveBeenStopped: function () { 704 | return { 705 | compare: function (actual) { 706 | var eventName = actual.eventName 707 | , selector = actual.selector 708 | , result = { pass: jasmine.jQuery.events.wasStopped(selector, eventName) } 709 | 710 | result.message = result.pass ? 711 | "Expected event " + eventName + " not to have been stopped on " + selector : 712 | "Expected event " + eventName + " to have been stopped on " + selector 713 | 714 | return result 715 | } 716 | } 717 | } 718 | }) 719 | 720 | jasmine.getEnv().addCustomEqualityTester(function(a, b) { 721 | if (a && b) { 722 | if (a instanceof $ || jasmine.isDomNode(a)) { 723 | var $a = $(a) 724 | 725 | if (b instanceof $) 726 | return $a.length == b.length && a.is(b) 727 | 728 | return $a.is(b); 729 | } 730 | 731 | if (b instanceof $ || jasmine.isDomNode(b)) { 732 | var $b = $(b) 733 | 734 | if (a instanceof $) 735 | return a.length == $b.length && $b.is(a) 736 | 737 | return $(b).is(a); 738 | } 739 | } 740 | }) 741 | 742 | jasmine.getEnv().addCustomEqualityTester(function (a, b) { 743 | if (a instanceof $ && b instanceof $ && a.size() == b.size()) 744 | return a.is(b) 745 | }) 746 | }) 747 | 748 | afterEach(function () { 749 | jasmine.getFixtures().cleanUp() 750 | jasmine.getStyleFixtures().cleanUp() 751 | jasmine.jQuery.events.cleanUp() 752 | }) 753 | 754 | window.readFixtures = function () { 755 | return jasmine.getFixtures().proxyCallTo_('read', arguments) 756 | } 757 | 758 | window.preloadFixtures = function () { 759 | jasmine.getFixtures().proxyCallTo_('preload', arguments) 760 | } 761 | 762 | window.loadFixtures = function () { 763 | jasmine.getFixtures().proxyCallTo_('load', arguments) 764 | } 765 | 766 | window.appendLoadFixtures = function () { 767 | jasmine.getFixtures().proxyCallTo_('appendLoad', arguments) 768 | } 769 | 770 | window.setFixtures = function (html) { 771 | return jasmine.getFixtures().proxyCallTo_('set', arguments) 772 | } 773 | 774 | window.appendSetFixtures = function () { 775 | jasmine.getFixtures().proxyCallTo_('appendSet', arguments) 776 | } 777 | 778 | window.sandbox = function (attributes) { 779 | return jasmine.getFixtures().sandbox(attributes) 780 | } 781 | 782 | window.spyOnEvent = function (selector, eventName) { 783 | return jasmine.jQuery.events.spyOn(selector, eventName) 784 | } 785 | 786 | window.preloadStyleFixtures = function () { 787 | jasmine.getStyleFixtures().proxyCallTo_('preload', arguments) 788 | } 789 | 790 | window.loadStyleFixtures = function () { 791 | jasmine.getStyleFixtures().proxyCallTo_('load', arguments) 792 | } 793 | 794 | window.appendLoadStyleFixtures = function () { 795 | jasmine.getStyleFixtures().proxyCallTo_('appendLoad', arguments) 796 | } 797 | 798 | window.setStyleFixtures = function (html) { 799 | jasmine.getStyleFixtures().proxyCallTo_('set', arguments) 800 | } 801 | 802 | window.appendSetStyleFixtures = function (html) { 803 | jasmine.getStyleFixtures().proxyCallTo_('appendSet', arguments) 804 | } 805 | 806 | window.loadJSONFixtures = function () { 807 | return jasmine.getJSONFixtures().proxyCallTo_('load', arguments) 808 | } 809 | 810 | window.getJSONFixture = function (url) { 811 | return jasmine.getJSONFixtures().proxyCallTo_('read', arguments)[url] 812 | } 813 | }(window, window.jasmine, window.jQuery); 814 | --------------------------------------------------------------------------------