text: 'use strict';
  
  var AuthenticationRequest = require('./authentication-request'),
    WebApiRequest = require('./webapi-request'),
    HttpManager = require('./http-manager');
  
  function SpotifyWebApi(credentials) {
    this._credentials = credentials || {};
  }
  
  SpotifyWebApi.prototype = {
    setCredentials: function(credentials) {
      for (var key in credentials) {
        if (credentials.hasOwnProperty(key)) {
          this._credentials[key] = credentials[key];
        }
      }
    },
  
    getCredentials: function() {
      return this._credentials;
    },
  
    resetCredentials: function() {
      this._credentials = null;
    },
  
    setClientId: function(clientId) {
      this._setCredential('clientId', clientId);
    },
  
    setClientSecret: function(clientSecret) {
      this._setCredential('clientSecret', clientSecret);
    },
  
    setAccessToken: function(accessToken) {
      this._setCredential('accessToken', accessToken);
    },
  
    setRefreshToken: function(refreshToken) {
      this._setCredential('refreshToken', refreshToken);
    },
  
    setRedirectURI: function(redirectUri) {
      this._setCredential('redirectUri', redirectUri);
    },
  
    getRedirectURI: function() {
      return this._getCredential('redirectUri');
    },
  
    getClientId: function() {
      return this._getCredential('clientId');
    },
  
    getClientSecret: function() {
      return this._getCredential('clientSecret');
    },
  
    getAccessToken: function() {
      return this._getCredential('accessToken');
    },
  
    getRefreshToken: function() {
      return this._getCredential('refreshToken');
    },
  
    resetClientId: function() {
      this._resetCredential('clientId');
    },
  
    resetClientSecret: function() {
      this._resetCredential('clientSecret');
    },
  
    resetAccessToken: function() {
      this._resetCredential('accessToken');
    },
  
    resetRefreshToken: function() {
      this._resetCredential('refreshToken');
    },
  
    resetRedirectURI: function() {
      this._resetCredential('redirectUri');
    },
  
    _setCredential: function(credentialKey, value) {
      this._credentials = this._credentials || {};
      this._credentials[credentialKey] = value;
    },
  
    _getCredential: function(credentialKey) {
      if (!this._credentials) {
        return;
      } else {
        return this._credentials[credentialKey];
      }
    },
  
    _resetCredential: function(credentialKey) {
      if (!this._credentials) {
        return;
      } else {
        this._credentials[credentialKey] = null;
      }
    },
  
    /**
     * Look up a track.
     * @param {string} trackId The track's ID.
     * @param {Object} [options] The possible options, currently only market.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example getTrack('3Qm86XLflmIXVm1wcwkgDK').then(...)
     * @returns {Promise|undefined} A promise that if successful, returns an object containing information
     *          about the track. Not returned if a callback is given.
     */
    getTrack: function(trackId, options, callback) {
      // In case someone is using a version where options parameter did not exist.
      var actualCallback, actualOptions;
      if (typeof options === 'function' && !callback) {
        actualCallback = options;
        actualOptions = {};
      } else {
        actualCallback = callback;
        actualOptions = options;
      }
  
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/tracks/' + trackId)
        .withQueryParameters(actualOptions)
        .build()
        .execute(HttpManager.get, actualCallback);
    },
  
    /**
     * Look up several tracks.
     * @param {string[]} trackIds The IDs of the artists.
     * @param {Object} [options] The possible options, currently only market.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example getArtists(['0oSGxfWSnnOXhD2fKuz2Gy', '3dBVyJ7JuOMt4GE9607Qin']).then(...)
     * @returns {Promise|undefined} A promise that if successful, returns an object containing information
     *          about the artists. Not returned if a callback is given.
     */
    getTracks: function(trackIds, options, callback) {
      // In case someone is using a version where options parameter did not exist.
      var actualCallback, actualOptions;
      if (typeof options === 'function' && !callback) {
        actualCallback = options;
        actualOptions = {};
      } else {
        actualCallback = callback;
        actualOptions = options;
      }
  
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/tracks')
        .withQueryParameters(
          {
            ids: trackIds.join(',')
          },
          actualOptions
        )
        .build()
        .execute(HttpManager.get, actualCallback);
    },
  
    /**
     * Look up an album.
     * @param {string} albumId The album's ID.
     * @param {Object} [options] The possible options, currently only market.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example getAlbum('0sNOF9WDwhWunNAHPD3Baj').then(...)
     * @returns {Promise|undefined} A promise that if successful, returns an object containing information
     *          about the album. Not returned if a callback is given.
     */
    getAlbum: function(albumId, options, callback) {
      // In case someone is using a version where options parameter did not exist.
      var actualCallback, actualOptions;
      if (typeof options === 'function' && !callback) {
        actualCallback = options;
        actualOptions = {};
      } else {
        actualCallback = callback;
        actualOptions = options;
      }
  
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/albums/' + albumId)
        .withQueryParameters(actualOptions)
        .build()
        .execute(HttpManager.get, actualCallback);
    },
  
    /**
     * Look up several albums.
     * @param {string[]} albumIds The IDs of the albums.
     * @param {Object} [options] The possible options, currently only market.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example getAlbums(['0oSGxfWSnnOXhD2fKuz2Gy', '3dBVyJ7JuOMt4GE9607Qin']).then(...)
     * @returns {Promise|undefined} A promise that if successful, returns an object containing information
     *          about the albums. Not returned if a callback is given.
     */
    getAlbums: function(albumIds, options, callback) {
      // In case someone is using a version where options parameter did not exist.
      var actualCallback, actualOptions;
      if (typeof options === 'function' && !callback) {
        actualCallback = options;
        actualOptions = {};
      } else {
        actualCallback = callback;
        actualOptions = options;
      }
  
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/albums')
        .withQueryParameters(
          {
            ids: albumIds.join(',')
          },
          actualOptions
        )
        .build()
        .execute(HttpManager.get, actualCallback);
    },
  
    /**
     * Look up an artist.
     * @param {string} artistId The artist's ID.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example api.getArtist('1u7kkVrr14iBvrpYnZILJR').then(...)
     * @returns {Promise|undefined} A promise that if successful, returns an object containing information
     *          about the artist. Not returned if a callback is given.
     */
    getArtist: function(artistId, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/artists/' + artistId)
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Look up several artists.
     * @param {string[]} artistIds The IDs of the artists.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example getArtists(['0oSGxfWSnnOXhD2fKuz2Gy', '3dBVyJ7JuOMt4GE9607Qin']).then(...)
     * @returns {Promise|undefined} A promise that if successful, returns an object containing information
     *          about the artists. Not returned if a callback is given.
     */
    getArtists: function(artistIds, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/artists')
        .withQueryParameters({
          ids: artistIds.join(',')
        })
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Search for music entities of certain types.
     * @param {string} query The search query.
     * @param {string[]} types An array of item types to search across.
     * Valid types are: 'album', 'artist', 'playlist', and 'track'.
     * @param {Object} [options] The possible options, e.g. limit, offset.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example search('Abba', ['track', 'playlist'], { limit : 5, offset : 1 }).then(...)
     * @returns {Promise|undefined} A promise that if successful, returns an object containing the
     *          search results. The result is paginated. If the promise is rejected,
     *          it contains an error object. Not returned if a callback is given.
     */
    search: function(query, types, options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/search/')
        .withQueryParameters(
          {
            type: types.join(','),
            q: query
          },
          options
        )
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Search for an album.
     * @param {string} query The search query.
     * @param {Object} [options] The possible options, e.g. limit, offset.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example searchAlbums('Space Oddity', { limit : 5, offset : 1 }).then(...)
     * @returns {Promise|undefined} A promise that if successful, returns an object containing the
     *          search results. The result is paginated. If the promise is rejected,
     *          it contains an error object. Not returned if a callback is given.
     */
    searchAlbums: function(query, options, callback) {
      return this.search(query, ['album'], options, callback);
    },
  
    /**
     * Search for an artist.
     * @param {string} query The search query.
     * @param {Object} [options] The possible options, e.g. limit, offset.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example searchArtists('David Bowie', { limit : 5, offset : 1 }).then(...)
     * @returns {Promise|undefined} A promise that if successful, returns an object containing the
     *          search results. The result is paginated. If the promise is rejected,
     *          it contains an error object. Not returned if a callback is given.
     */
    searchArtists: function(query, options, callback) {
      return this.search(query, ['artist'], options, callback);
    },
  
    /**
     * Search for a track.
     * @param {string} query The search query.
     * @param {Object} [options] The possible options, e.g. limit, offset.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example searchTracks('Mr. Brightside', { limit : 3, offset : 2 }).then(...)
     * @returns {Promise|undefined} A promise that if successful, returns an object containing the
     *          search results. The result is paginated. If the promise is rejected,
     *          it contains an error object. Not returned if a callback is given.
     */
    searchTracks: function(query, options, callback) {
      return this.search(query, ['track'], options, callback);
    },
  
    /**
     * Search for playlists.
     * @param {string} query The search query.
     * @param {Object} options The possible options.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example searchPlaylists('workout', { limit : 1, offset : 0 }).then(...)
     * @returns {Promise|undefined} A promise that if successful, returns an object containing the
     *          search results. The result is paginated. If the promise is rejected,
     *          it contains an error object. Not returned if a callback is given.
     */
    searchPlaylists: function(query, options, callback) {
      return this.search(query, ['playlist'], options, callback);
    },
  
    /**
     * Get an artist's albums.
     * @param {string} artistId The artist's ID.
     * @options {Object} [options] The possible options, e.g. limit, offset.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example getArtistAlbums('0oSGxfWSnnOXhD2fKuz2Gy', { album_type : 'album', country : 'GB', limit : 2, offset : 5 }).then(...)
     * @returns {Promise|undefined} A promise that if successful, returns an object containing the albums
     *          for the given artist. The result is paginated. If the promise is rejected,
     *          it contains an error object. Not returned if a callback is given.
     */
    getArtistAlbums: function(artistId, options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/artists/' + artistId + '/albums')
        .withQueryParameters(options)
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Get the tracks of an album.
     * @param albumId the album's ID.
     * @options {Object} [options] The possible options, e.g. limit.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example getAlbumTracks('41MnTivkwTO3UUJ8DrqEJJ', { limit : 5, offset : 1 }).then(...)
     * @returns {Promise|undefined} A promise that if successful, returns an object containing the
     *                    tracks in the album. The result is paginated. If the promise is rejected.
     *                    it contains an error object. Not returned if a callback is given.
     */
    getAlbumTracks: function(albumId, options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/albums/' + albumId + '/tracks')
        .withQueryParameters(options)
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Get an artist's top tracks.
     * @param {string} artistId The artist's ID.
     * @param {string} country The country/territory where the tracks are most popular. (format: ISO 3166-1 alpha-2)
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example getArtistTopTracks('0oSGxfWSnnOXhD2fKuz2Gy', 'GB').then(...)
     * @returns {Promise|undefined} A promise that if successful, returns an object containing the
     *          artist's top tracks in the given country. If the promise is rejected,
     *          it contains an error object. Not returned if a callback is given.
     */
    getArtistTopTracks: function(artistId, country, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/artists/' + artistId + '/top-tracks')
        .withQueryParameters({
          country: country
        })
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Get related artists.
     * @param {string} artistId The artist's ID.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example getArtistRelatedArtists('0oSGxfWSnnOXhD2fKuz2Gy').then(...)
     * @returns {Promise|undefined} A promise that if successful, returns an object containing the
     *          related artists. If the promise is rejected, it contains an error object. Not returned if a callback is given.
     */
    getArtistRelatedArtists: function(artistId, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/artists/' + artistId + '/related-artists')
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Get information about a user.
     * @param userId The user ID.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example getUser('thelinmichael').then(...)
     * @returns {Promise|undefined} A promise that if successful, resolves to an object
     *          containing information about the user. If the promise is
     *          rejected, it contains an error object. Not returned if a callback is given.
     */
    getUser: function(userId, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/users/' + encodeURIComponent(userId))
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Get information about the user that has signed in (the current user).
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example getMe().then(...)
     * @returns {Promise|undefined} A promise that if successful, resolves to an object
     *          containing information about the user. The amount of information
     *          depends on the permissions given by the user. If the promise is
     *          rejected, it contains an error object. Not returned if a callback is given.
     */
    getMe: function(callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me')
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Get a user's playlists.
     * @param {string} userId An optional id of the user. If you know the Spotify URI it is easy
     * to find the id (e.g. spotify:user:<here_is_the_id>). If not provided, the id of the user that granted
     * the permissions will be used.
     * @param {Object} [options] The options supplied to this request.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example getUserPlaylists('thelinmichael').then(...)
     * @returns {Promise|undefined} A promise that if successful, resolves to an object containing
     *          a list of playlists. If rejected, it contains an error object. Not returned if a callback is given.
     */
    getUserPlaylists: function(userId, options, callback) {
      var path;
      if (typeof userId === 'string') {
        path = '/v1/users/' + encodeURIComponent(userId) + '/playlists';
      } else if (typeof userId === 'object') {
        callback = options;
        options = userId;
        path = '/v1/me/playlists';
      } /* undefined */ else {
        path = '/v1/me/playlists';
      }
  
      return WebApiRequest.builder(this.getAccessToken())
        .withPath(path)
        .withQueryParameters(options)
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Get a playlist.
     * @param {string} playlistId The playlist's ID.
     * @param {Object} [options] The options supplied to this request.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example getPlaylist('thelinmichael', '3EsfV6XzCHU8SPNdbnFogK').then(...)
     * @returns {Promise|undefined} A promise that if successful, resolves to an object containing
     *          the playlist. If rejected, it contains an error object. Not returned if a callback is given.
     */
    getPlaylist: function(playlistId, options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/playlists/' + playlistId)
        .withQueryParameters(options)
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Get tracks in a playlist.
     * @param {string} playlistId The playlist's ID.
     * @param {Object} [options] Optional options, such as fields.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example getPlaylistTracks('thelinmichael', '3ktAYNcRHpazJ9qecm3ptn').then(...)
     * @returns {Promise|undefined} A promise that if successful, resolves to an object that containing
     * the tracks in the playlist. If rejected, it contains an error object. Not returned if a callback is given.
     */
    getPlaylistTracks: function(playlistId, options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/playlists/' + playlistId + '/tracks')
        .withQueryParameters(options)
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Create a playlist.
     * @param {string} userId The playlist's owner's user ID.
     * @param {string} playlistName The name of the playlist.
     * @param {Object} [options] The possible options, currently only public.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example createPlaylist('thelinmichael', 'My cool playlist!', { public : false }).then(...)
     * @returns {Promise|undefined} A promise that if successful, resolves to an object containing information about the
     *          created playlist. If rejected, it contains an error object. Not returned if a callback is given.
     */
    createPlaylist: function(userId, playlistName, options, callback) {
      // In case someone is using a version where options parameter did not exist.
      var actualCallback;
      if (typeof options === 'function' && !callback) {
        actualCallback = options;
      } else {
        actualCallback = callback;
      }
  
      var actualOptions = { name: playlistName };
      if (typeof options === 'object') {
        Object.keys(options).forEach(function(key) {
          actualOptions[key] = options[key];
        });
      }
  
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/users/' + encodeURIComponent(userId) + '/playlists')
        .withHeaders({ 'Content-Type': 'application/json' })
        .withBodyParameters(actualOptions)
        .build()
        .execute(HttpManager.post, actualCallback);
    },
  
    /**
     * Follow a playlist.
     * @param {string} playlistId The playlist's ID
     * @param {Object} [options] The possible options, currently only public.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected,
     * it contains an error object. Not returned if a callback is given.
     */
    followPlaylist: function(playlistId, options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/playlists/' + playlistId + '/followers')
        .withHeaders({ 'Content-Type': 'application/json' })
        .withBodyParameters(options)
        .build()
        .execute(HttpManager.put, callback);
    },
  
    /**
     * Unfollow a playlist.
     * @param {string} playlistId The playlist's ID
     * @param {Object} [options] The possible options, currently only public.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected,
     * it contains an error object. Not returned if a callback is given.
     */
    unfollowPlaylist: function(playlistId, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/playlists/' + playlistId + '/followers')
        .withHeaders({ 'Content-Type': 'application/json' })
        .build()
        .execute(HttpManager.del, callback);
    },
  
    /**
     * Change playlist details.
     * @param {string} playlistId The playlist's ID
     * @param {Object} [options] The possible options, e.g. name, public.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example changePlaylistDetails('thelinmichael', '3EsfV6XzCHU8SPNdbnFogK', {name: 'New name', public: true}).then(...)
     * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected,
     * it contains an error object. Not returned if a callback is given.
     */
    changePlaylistDetails: function(playlistId, options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/playlists/' + playlistId)
        .withHeaders({ 'Content-Type': 'application/json' })
        .withBodyParameters(options)
        .build()
        .execute(HttpManager.put, callback);
    },
  
    /**
     * Replace the image used to represent a specific playlist.
     * @param {string} playlistId The playlist's ID
     * @param {string} base64URI Base64 encoded JPEG image data, maximum payload size is 256 KB
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example uploadCustomPlaylistCoverImage('thelinmichael', '3EsfV6XzCHU8SPNdbnFogK', 'longbase64uri').then(...)
     * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected,
     * it contains an error object. Not returned if a callback is given.
     */
    uploadCustomPlaylistCoverImage: function(playlistId, base64URI, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/playlists/' + playlistId + '/images')
        .withHeaders({ 'Content-Type': 'image/jpeg' })
        .withBodyParameters(base64URI)
        .build()
        .execute(HttpManager.put, callback);
    },
  
    /**
     * Add tracks to a playlist.
     * @param {string} playlistId The playlist's ID
     * @param {string[]} tracks URIs of the tracks to add to the playlist.
     * @param {Object} [options] Options, position being the only one.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example addTracksToPlaylist('thelinmichael', '3EsfV6XzCHU8SPNdbnFogK',
                '["spotify:track:4iV5W9uYEdYUVa79Axb7Rh", "spotify:track:1301WleyT98MSxVHPZCA6M"]').then(...)
     * @returns {Promise|undefined} A promise that if successful returns an object containing a snapshot_id. If rejected,
     * it contains an error object. Not returned if a callback is given.
     */
    addTracksToPlaylist: function(playlistId, tracks, options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/playlists/' + playlistId + '/tracks')
        .withHeaders({ 'Content-Type': 'application/json' })
        .withQueryParameters(options)
        .withBodyParameters({
          uris: tracks
        })
        .build()
        .execute(HttpManager.post, callback);
    },
  
    /**
     * Remove tracks from a playlist.
     * @param {string} playlistId The playlist's ID
     * @param {Object[]} tracks An array of objects containing a property called uri with the track URI (String), and
     * a an optional property called positions (int[]), e.g. { uri : "spotify:track:491rM2JN8KvmV6p0oDDuJT", positions : [0, 15] }
     * @param {Object} options Options, snapshot_id being the only one.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful returns an object containing a snapshot_id. If rejected,
     * it contains an error object. Not returned if a callback is given.
     */
    removeTracksFromPlaylist: function(playlistId, tracks, options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/playlists/' + playlistId + '/tracks')
        .withHeaders({ 'Content-Type': 'application/json' })
        .withBodyParameters(
          {
            tracks: tracks
          },
          options
        )
        .build()
        .execute(HttpManager.del, callback);
    },
  
    /**
     * Remove tracks from a playlist by position instead of specifying the tracks' URIs.
     * @param {string} playlistId The playlist's ID
     * @param {int[]} positions The positions of the tracks in the playlist that should be removed
     * @param {string} snapshot_id The snapshot ID, or version, of the playlist. Required
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful returns an object containing a snapshot_id. If rejected,
     * it contains an error object. Not returned if a callback is given.
     */
    removeTracksFromPlaylistByPosition: function(
      playlistId,
      positions,
      snapshotId,
      callback
    ) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/playlists/' + playlistId + '/tracks')
        .withHeaders({ 'Content-Type': 'application/json' })
        .withBodyParameters({
          positions: positions,
          snapshot_id: snapshotId
        })
        .build()
        .execute(HttpManager.del, callback);
    },
  
    /**
     * Replace tracks in a playlist.
     * @param {string} playlistId The playlist's ID
     * @param {Object[]} uris An array of track URIs (strings)
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful returns an empty object. If rejected,
     * it contains an error object. Not returned if a callback is given.
     */
    replaceTracksInPlaylist: function(playlistId, uris, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/playlists/' + playlistId + '/tracks')
        .withHeaders({ 'Content-Type': 'application/json' })
        .withBodyParameters({
          uris: uris
        })
        .build()
        .execute(HttpManager.put, callback);
    },
  
    /**
     * Reorder tracks in a playlist.
     * @param {string} playlistId The playlist's ID
     * @param {int} rangeStart The position of the first track to be reordered.
     * @param {int} insertBefore The position where the tracks should be inserted.
     * @param {Object} options Optional parameters, i.e. range_length and snapshot_id.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful returns an object containing a snapshot_id. If rejected,
     * it contains an error object. Not returned if a callback is given.
     */
    reorderTracksInPlaylist: function(
      playlistId,
      rangeStart,
      insertBefore,
      options,
      callback
    ) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/playlists/' + playlistId + '/tracks')
        .withHeaders({ 'Content-Type': 'application/json' })
        .withBodyParameters(
          {
            range_start: rangeStart,
            insert_before: insertBefore
          },
          options
        )
        .build()
        .execute(HttpManager.put, callback);
    },
  
    /**
     * Get audio features for a single track identified by its unique Spotify ID.
     * @param {string} trackId The track ID
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example getAudioFeaturesForTrack('38P3Q4QcdjQALGF2Z92BmR').then(...)
     * @returns {Promise|undefined} A promise that if successful, resolves to an object
     *          containing information about the audio features. If the promise is
     *          rejected, it contains an error object. Not returned if a callback is given.
     */
    getAudioFeaturesForTrack: function(trackId, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/audio-features/' + trackId)
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Get audio analysis for a single track identified by its unique Spotify ID.
     * @param {string} trackId The track ID
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example getAudioAnalysisForTrack('38P3Q4QcdjQALGF2Z92BmR').then(...)
     * @returns {Promise|undefined} A promise that if successful, resolves to an object
     *          containing information about the audio analysis. If the promise is
     *          rejected, it contains an error object. Not returned if a callback is given.
     */
    getAudioAnalysisForTrack: function(trackId, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/audio-analysis/' + trackId)
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Get audio features for multiple tracks identified by their unique Spotify ID.
     * @param {string[]} trackIds The track IDs
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example getAudioFeaturesForTracks(['38P3Q4QcdjQALGF2Z92BmR', '2HO2bnoMrpnZUbUqiilLHi']).then(...)
     * @returns {Promise|undefined} A promise that if successful, resolves to an object
     *          containing information about the audio features for the tracks. If the promise is
     *          rejected, it contains an error object. Not returned if a callback is given.
     */
    getAudioFeaturesForTracks: function(trackIds, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/audio-features')
        .withQueryParameters({
          ids: trackIds.join(',')
        })
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Create a playlist-style listening experience based on seed artists, tracks and genres.
     * @param {Object} [options] The options supplied to this request.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example getRecommendations({ min_energy: 0.4, seed_artists: ['6mfK6Q2tzLMEchAr0e9Uzu', '4DYFVNKZ1uixa6SQTvzQwJ'], min_popularity: 50 }).then(...)
     * @returns {Promise|undefined} A promise that if successful, resolves to an object containing
     *          a list of tracks and a list of seeds. If rejected, it contains an error object. Not returned if a callback is given.
     */
    getRecommendations: function(options, callback) {
      var _opts = {};
      var optionsOfTypeArray = ['seed_artists', 'seed_genres', 'seed_tracks'];
      for (var option in options) {
        if (options.hasOwnProperty(option)) {
          if (
            optionsOfTypeArray.indexOf(option) !== -1 &&
            Object.prototype.toString.call(options[option]) === '[object Array]'
          ) {
            _opts[option] = options[option].join(',');
          } else {
            _opts[option] = options[option];
          }
        }
      }
  
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/recommendations')
        .withQueryParameters(_opts)
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Retrieve a list of available genres seed parameter values for recommendations.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example getAvailableGenreSeeds().then(...)
     * @returns {Promise|undefined} A promise that if successful, resolves to an object containing
     *          a list of available genres to be used as seeds for recommendations.
     *          If rejected, it contains an error object. Not returned if a callback is given.
     */
    getAvailableGenreSeeds: function(callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/recommendations/available-genre-seeds')
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Retrieve a URL where the user can give the application permissions.
     * @param {string[]} scopes The scopes corresponding to the permissions the application needs.
     * @param {string} state A parameter that you can use to maintain a value between the request and the callback to redirect_uri.It is useful to prevent CSRF exploits.
     * @param {boolean} showDialog A parameter that you can use to force the user to approve the app on each login rather than being automatically redirected.
     * @returns {string} The URL where the user can give application permissions.
     */
    createAuthorizeURL: function(scopes, state, showDialog) {
      return AuthenticationRequest.builder()
        .withPath('/authorize')
        .withQueryParameters({
          client_id: this.getClientId(),
          response_type: 'code',
          redirect_uri: this.getRedirectURI(),
          scope: scopes.join('%20'),
          state: state,
          show_dialog: showDialog && !!showDialog
        })
        .build()
        .getURL();
    },
  
    /**
     * Retrieve the tracks that are saved to the authenticated users Your Music library.
     * @param {Object} [options] Options, being market, limit, and/or offset.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object which in turn contains
     *          playlist track objects. Not returned if a callback is given.
     */
    getMySavedTracks: function(options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/tracks')
        .withQueryParameters(options)
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Check if one or more tracks is already saved in the current Spotify user’s “Your Music” library.
     * @param {string[]} trackIds The track IDs
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful, resolves into an array of booleans. The order
     * of the returned array's elements correspond to the track ID in the request.
     * The boolean value of true indicates that the track is part of the user's library, otherwise false.
     * Not returned if a callback is given.
     */
    containsMySavedTracks: function(trackIds, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/tracks/contains')
        .withQueryParameters({
          ids: trackIds.join(',')
        })
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Remove a track from the authenticated user's Your Music library.
     * @param {string[]} trackIds The track IDs
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful returns null, otherwise an error.
     * Not returned if a callback is given.
     */
    removeFromMySavedTracks: function(trackIds, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/tracks')
        .withHeaders({ 'Content-Type': 'application/json' })
        .withBodyParameters({ ids: trackIds })
        .build()
        .execute(HttpManager.del, callback);
    },
  
    /**
     * Add a track from the authenticated user's Your Music library.
     * @param {string[]} trackIds The track IDs
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful returns null, otherwise an error. Not returned if a callback is given.
     */
    addToMySavedTracks: function(trackIds, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/tracks')
        .withHeaders({ 'Content-Type': 'application/json' })
        .withBodyParameters({ ids: trackIds })
        .build()
        .execute(HttpManager.put, callback);
    },
  
    /**
     * Remove an album from the authenticated user's Your Music library.
     * @param {string[]} albumIds The album IDs
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful returns null, otherwise an error.
     * Not returned if a callback is given.
     */
    removeFromMySavedAlbums: function(albumIds, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/albums')
        .withHeaders({ 'Content-Type': 'application/json' })
        .withBodyParameters(albumIds)
        .build()
        .execute(HttpManager.del, callback);
    },
  
    /**
     * Add an album from the authenticated user's Your Music library.
     * @param {string[]} albumIds The track IDs
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful returns null, otherwise an error. Not returned if a callback is given.
     */
    addToMySavedAlbums: function(albumIds, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/albums')
        .withHeaders({ 'Content-Type': 'application/json' })
        .withBodyParameters(albumIds)
        .build()
        .execute(HttpManager.put, callback);
    },
  
    /**
     * Retrieve the albums that are saved to the authenticated users Your Music library.
     * @param {Object} [options] Options, being market, limit, and/or offset.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object which in turn contains
     *          playlist album objects. Not returned if a callback is given.
     */
    getMySavedAlbums: function(options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/albums')
        .withQueryParameters(options)
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Check if one or more albums is already saved in the current Spotify user’s “Your Music” library.
     * @param {string[]} albumIds The album IDs
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful, resolves into an array of booleans. The order
     * of the returned array's elements correspond to the album ID in the request.
     * The boolean value of true indicates that the album is part of the user's library, otherwise false.
     * Not returned if a callback is given.
     */
    containsMySavedAlbums: function(albumIds, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/albums/contains')
        .withQueryParameters({
          ids: albumIds.join(',')
        })
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Get the current user's top artists based on calculated affinity.
     * @param {Object} [options] Options, being time_range, limit, offset.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of artists,
     *          otherwise an error. Not returned if a callback is given.
     */
    getMyTopArtists: function(options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/top/artists')
        .withQueryParameters(options)
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Get the current user's top tracks based on calculated affinity.
     * @param {Object} [options] Options, being time_range, limit, offset.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks,
     *          otherwise an error. Not returned if a callback is given.
     */
    getMyTopTracks: function(options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/top/tracks')
        .withQueryParameters(options)
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Get the Current User's Recently Played Tracks
     * @param {Object} [options] Options, being type, after, limit, before.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks,
     *          otherwise an error. Not returned if a callback is given.
     */
    getMyRecentlyPlayedTracks: function(options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/player/recently-played')
        .withQueryParameters(options)
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Get the Current User's Connect Devices
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks,
     *          otherwise an error. Not returned if a callback is given.
     */
    getMyDevices: function(callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/player/devices')
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Get the Current User's Currently Playing Track.
     * @param {Object} [options] Options, being market.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks,
     *          otherwise an error. Not returned if a callback is given.
     */
    getMyCurrentPlayingTrack: function(options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/player/currently-playing')
        .withQueryParameters(options)
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Get the Current User's Current Playback State
     * @param {Object} [options] Options, being market.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks,
     *          otherwise an error. Not returned if a callback is given.
     */
    getMyCurrentPlaybackState: function(options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/player')
        .withQueryParameters(options)
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Transfer a User's Playback
     * @param {Object} [options] Options, being market.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks,
     *          otherwise an error. Not returned if a callback is given.
     */
    transferMyPlayback: function(options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/player')
        .withHeaders({ 'Content-Type': 'application/json' })
        .withBodyParameters({
          device_ids: options.deviceIds,
          play: !!options.play
        })
        .build()
        .execute(HttpManager.put, callback);
    },
  
    /**
     * Starts o Resumes the Current User's Playback
     * @param {Object} [options] Options, being device_id, context_uri, offset, uris, position_ms.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example playbackResume({context_uri: 'spotify:album:5ht7ItJgpBH7W6vJ5BqpPr'}).then(...)
     * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks,
     *          otherwise an error. Not returned if a callback is given.
     */
    play: function(options, callback) {
      /*jshint camelcase: false */
      var _options = options || {};
      var queryParams = _options.device_id
        ? { device_id: _options.device_id }
        : null;
      var postData = {};
      ['context_uri', 'uris', 'offset', 'position_ms'].forEach(function(field) {
        if (field in _options) {
          postData[field] = _options[field];
        }
      });
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/player/play')
        .withQueryParameters(queryParams)
        .withHeaders({ 'Content-Type': 'application/json' })
        .withBodyParameters(postData)
        .build()
        .execute(HttpManager.put, callback);
    },
  
    /**
     * Pauses the Current User's Playback
     * @param {Object} [options] Options, for now device_id,
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example playbackPause().then(...)
     * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks,
     *          otherwise an error. Not returned if a callback is given.
     */
    pause: function(options, callback) {
      return (
        WebApiRequest.builder(this.getAccessToken())
          .withPath('/v1/me/player/pause')
          /*jshint camelcase: false */
          .withQueryParameters(
            options && options.device_id ? { device_id: options.device_id } : null
          )
          .withHeaders({ 'Content-Type': 'application/json' })
          .build()
          .execute(HttpManager.put, callback)
      );
    },
  
    /**
     * Skip the Current User's Playback To Previous Track
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example playbackPrevious().then(...)
     * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks,
     *          otherwise an error. Not returned if a callback is given.
     */
    skipToPrevious: function(callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/player/previous')
        .withHeaders({ 'Content-Type': 'application/json' })
        .build()
        .execute(HttpManager.post, callback);
    },
  
    /**
     * Skip the Current User's Playback To Next Track
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example playbackNext().then(...)
     * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks,
     *          otherwise an error. Not returned if a callback is given.
     */
    skipToNext: function(callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/player/next')
        .withHeaders({ 'Content-Type': 'application/json' })
        .build()
        .execute(HttpManager.post, callback);
    },
  
    /**
     * Seeks to the given position in the user’s currently playing track.
     *
     * @param {number} positionMs The position in milliseconds to seek to. Must be a positive number.
     * @param {Object} options A JSON object with options that can be passed.
     * @param {function(Object,Object)} callback An optional callback that receives 2 parameters. The first
     * one is the error object (null if no error), and the second is the value if the request succeeded.
     * @return {Object} Null if a callback is provided, a `Promise` object otherwise
     */
    seek: function(positionMs, options, callback) {
      var params = {
        /* jshint camelcase: false */
        position_ms: positionMs
      };
      if (options && 'device_id' in options) {
        /* jshint camelcase: false */
        params.device_id = options.device_id;
      }
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/player/seek')
        .withQueryParameters(params)
        .build()
        .execute(HttpManager.put, callback);
    },
  
    /**
     * Set Repeat Mode On The Current User's Playback
     * @param {Object} [options] Options, being state (track, context, off).
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example playbackRepeat({state: 'context'}).then(...)
     * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks,
     *          otherwise an error. Not returned if a callback is given.
     */
    setRepeat: function(options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/player/repeat')
        .withQueryParameters({
          state: options.state || 'off'
        })
        .build()
        .execute(HttpManager.put, callback);
    },
  
    /**
     * Set Shuffle Mode On The Current User's Playback
     * @param {Object} [options] Options, being state (true, false).
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example playbackShuffle({state: 'false'}).then(...)
     * @returns {Promise|undefined} A promise that if successful, resolves into a paging object of tracks,
     *          otherwise an error. Not returned if a callback is given.
     */
    setShuffle: function(options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/player/shuffle')
        .withQueryParameters({
          state: options.state || 'false'
        })
        .build()
        .execute(HttpManager.put, callback);
    },
  
    /**
     * Set the volume for the user’s current playback device.
     *
     * @param {number} volumePercent The volume to set. Must be a value from 0 to 100 inclusive.
     * @param {Object} options A JSON object with options that can be passed.
     * @param {function(Object,Object)} callback An optional callback that receives 2 parameters. The first
     * one is the error object (null if no error), and the second is the value if the request succeeded.
     * @return {Object} Null if a callback is provided, a `Promise` object otherwise
     */
    setVolume: function(volumePercent, options, callback) {
      var params = {
        /* jshint camelcase: false */
        volume_percent: volumePercent
      };
      if (options && 'device_id' in options) {
        /* jshint camelcase: false */
        params.device_id = options.device_id;
      }
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/player/volume')
        .withQueryParameters(params)
        .build()
        .execute(HttpManager.put, callback);
    },
  
    /**
     * Add the current user as a follower of one or more other Spotify users.
     * @param {string[]} userIds The IDs of the users to be followed.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example followUsers(['thelinmichael', 'wizzler']).then(...)
     * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected,
     *          it contains an error object. Not returned if a callback is given.
     */
    followUsers: function(userIds, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/following')
        .withQueryParameters({
          ids: userIds.join(','),
          type: 'user'
        })
        .build()
        .execute(HttpManager.put, callback);
    },
  
    /**
     * Add the current user as a follower of one or more artists.
     * @param {string[]} artistIds The IDs of the artists to be followed.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example followArtists(['0LcJLqbBmaGUft1e9Mm8HV', '3gqv1kgivAc92KnUm4elKv']).then(...)
     * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected,
     *          it contains an error object. Not returned if a callback is given.
     */
    followArtists: function(artistIds, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/following')
        .withQueryParameters({
          ids: artistIds.join(','),
          type: 'artist'
        })
        .build()
        .execute(HttpManager.put, callback);
    },
  
    /**
     * Remove the current user as a follower of one or more other Spotify users.
     * @param {string[]} userIds The IDs of the users to be unfollowed.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example unfollowUsers(['thelinmichael', 'wizzler']).then(...)
     * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected,
     *          it contains an error object. Not returned if a callback is given.
     */
    unfollowUsers: function(userIds, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/following')
        .withQueryParameters({
          ids: userIds.join(','),
          type: 'user'
        })
        .build()
        .execute(HttpManager.del, callback);
    },
  
    /**
     * Remove the current user as a follower of one or more artists.
     * @param {string[]} artistIds The IDs of the artists to be unfollowed.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example unfollowArtists(['0LcJLqbBmaGUft1e9Mm8HV', '3gqv1kgivAc92KnUm4elKv']).then(...)
     * @returns {Promise|undefined} A promise that if successful, simply resolves to an empty object. If rejected,
     *          it contains an error object. Not returned if a callback is given.
     */
    unfollowArtists: function(artistIds, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/following')
        .withQueryParameters({
          ids: artistIds.join(','),
          type: 'artist'
        })
        .build()
        .execute(HttpManager.del, callback);
    },
  
    /**
     * Check to see if the current user is following one or more other Spotify users.
     * @param {string[]} userIds The IDs of the users to check if are followed by the current user.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example isFollowingUsers(['thelinmichael', 'wizzler']).then(...)
     * @returns {Promise|undefined} A promise that if successful, resolves into an array of booleans. The order
     *          of the returned array's elements correspond to the users IDs in the request.
     *          The boolean value of true indicates that the user is following that user, otherwise is not.
     *          Not returned if a callback is given.
     */
    isFollowingUsers: function(userIds, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/following/contains')
        .withQueryParameters({
          ids: userIds.join(','),
          type: 'user'
        })
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Get the current user's followed artists.
     * @param {Object} [options] Options, being after and limit.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object which contains
     * album objects. Not returned if a callback is given.
     */
    getFollowedArtists: function(options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/following')
        .withHeaders({ 'Content-Type': 'application/json' })
        .withQueryParameters(
          {
            type: 'artist'
          },
          options
        )
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Check if users are following a playlist.
     * @param {string} userId The playlist's owner's user ID
     * @param {string} playlistId The playlist's ID
     * @param {String[]} User IDs of the following users
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful returns an array of booleans. If rejected,
     * it contains an error object. Not returned if a callback is given.
     */
    areFollowingPlaylist: function(userId, playlistId, followerIds, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath(
          '/v1/users/' +
            encodeURIComponent(userId) +
            '/playlists/' +
            playlistId +
            '/followers/contains'
        )
        .withQueryParameters({
          ids: followerIds.join(',')
        })
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Check to see if the current user is following one or more artists.
     * @param {string[]} artistIds The IDs of the artists to check if are followed by the current user.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @example isFollowingArtists(['0LcJLqbBmaGUft1e9Mm8HV', '3gqv1kgivAc92KnUm4elKv']).then(...)
     * @returns {Promise|undefined} A promise that if successful, resolves into an array of booleans. The order
     *          of the returned array's elements correspond to the artists IDs in the request.
     *          The boolean value of true indicates that the user is following that artist, otherwise is not.
     *          Not returned if a callback is given.
     */
    isFollowingArtists: function(artistIds, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/me/following/contains')
        .withQueryParameters({
          ids: artistIds.join(','),
          type: 'artist'
        })
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Retrieve new releases
     * @param {Object} [options] Options, being country, limit and/or offset.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object which contains
     * album objects. Not returned if a callback is given.
     */
    getNewReleases: function(options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/browse/new-releases')
        .withHeaders({ 'Content-Type': 'application/json' })
        .withQueryParameters(options)
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Retrieve featured playlists
     * @param {Object} [options] Options, being country, locale, timestamp, limit, offset.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object which contains
     * featured playlists. Not returned if a callback is given.
     */
    getFeaturedPlaylists: function(options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/browse/featured-playlists')
        .withHeaders({ 'Content-Type': 'application/json' })
        .withQueryParameters(options)
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Retrieve a list of categories used to tag items in Spotify (e.g. in the 'Browse' tab)
     * @param {Object} [options] Options, being country, locale, limit, offset.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a paging object of categories.
     * Not returned if a callback is given.
     */
    getCategories: function(options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/browse/categories')
        .withQueryParameters(options)
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Retrieve a category.
     * @param {string} categoryId The id of the category to retrieve.
     * @param {Object} [options] Options, being country, locale.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful, resolves to an object containing a category object.
     * Not returned if a callback is given.
     */
    getCategory: function(categoryId, options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/browse/categories/' + categoryId)
        .withQueryParameters(options)
        .build()
        .execute(HttpManager.get, callback);
    },
  
    /**
     * Retrieve playlists for a category.
     * @param {string} categoryId The id of the category to retrieve playlists for.
     * @param {Object} [options] Options, being country, limit, offset.
     * @param {requestCallback} [callback] Optional callback method to be called instead of the promise.
     * @returns {Promise|undefined} A promise that if successful, resolves to a paging object containing simple playlists.
     * Not returned if a callback is given.
     */
    getPlaylistsForCategory: function(categoryId, options, callback) {
      return WebApiRequest.builder(this.getAccessToken())
        .withPath('/v1/browse/categories/' + categoryId + '/playlists')
        .withQueryParameters(options)
        .build()
        .execute(HttpManager.get, callback);
    }
  };
  
  SpotifyWebApi._addMethods = function(methods) {
    for (var i in methods) {
      if (methods.hasOwnProperty(i)) {
        this.prototype[i] = methods[i];
      }
    }
  };
  
  module.exports = SpotifyWebApi;
  