const qs = require('querystring');
const Track = require('../structures/Track.js');
const API = 'https://api.spotify.com/v1/me/player';
const HTTPError = require('../HTTPError.js');
const ApiError = require('../ApiError.js');
class PlayerManager {
/**
* Manages spotify playing.
* @param {Spotify} spotify - The spotify client.
*/
constructor(spotify) {
/**
* The spotify client.
* @type {Spotify}
*/
this.spotify = spotify;
}
/**
* Get information about the user's current playback state.
* @param {AdditionalTypes[]} [types=['track']] - The types that the client supports. (track, episode)
* @returns {Promise<State|HTTPError|ApiError>}
*/
state(types = ['track']) {
const options = qs.stringify({
additional_types: types.join(','),
});
const path = API + '?' + options;
return new Promise((resolve, reject) => {
this.spotify.util
.fetch({
path,
})
.then((response) => {
this.spotify.util.toJson(response).then((body) => {
if (body) {
if (response.status == 200) {
return resolve(body);
}
reject(new ApiError(body.error));
}
reject(new HTTPError(response));
});
});
});
}
/**
* Transfer playback to a new device and determine if it should start playing.
* @param {string} id - The id of the device to transfer the playback to.
* @param {boolean} [play=true] - The playback continues after transfer.
* @returns {Promise<Status|HTTPError|ApiError>}
*/
transfer(id, play = true) {
const body = {
device_ids: [id],
play,
};
return new Promise((resolve, reject) => {
this.spotify.util
.fetch({
path: API,
method: 'put',
body,
})
.then((response) => {
this.spotify.util.toJson(response).then((body) => {
if (response.status == 204) {
resolve({ status: response.status });
} else if (body) {
reject(new ApiError(body.error));
}
reject(new HTTPError(response));
});
});
});
}
/**
* Get information about a user’s available devices.
* @returns {Promise<Device[]|HTTPError|ApiError>}
*/
devices() {
const path = API + '/devices';
return new Promise((resolve, reject) => {
this.spotify.util
.fetch({
path,
})
.then((response) => {
this.spotify.util.toJson(response).then((body) => {
if (response.status == 204) {
resolve({ status: response.status });
} else if (body) {
reject(new ApiError(body.error));
}
reject(new HTTPError(response));
});
});
});
}
/**
* Get the object currently being played on the user's Spotify account.
* @param {AdditionalTypes[]} [types=['track']] - The types that the client supports.
* @returns {Promise<Track|HTTPError|ApiError>}
*/
current(types = ['track']) {
const options = qs.stringify({
additional_types: Array.isArray(types) ? types.join(',') : types,
});
const path = API + '/currently-playing?' + options;
return new Promise((resolve, reject) => {
this.spotify.util
.fetch({
path,
})
.then((response) => {
this.spotify.util.toJson(response).then((body) => {
if (body) {
if (response.status == 200) {
const track = new Track(this.spotify, body);
return resolve(track);
}
reject(new ApiError(body.error));
}
reject(new HTTPError(response));
});
});
});
}
/**
* Start a new context.
* @param {ContextURI} uri - The context uri to start playing.
* @param {StartOptions} [options]
* @returns {Promise<Status|HTTPError|ApiError>}
*/
start(uri, { device, offset = 0, ms = 0 } = {}) {
const options = qs.stringify({
device_id: device,
});
const body = {
context_uri: uri,
position_ms: ms,
offset: {
position: offset,
},
};
const path = API + '/play?' + (device ? options : '');
return new Promise((resolve, reject) => {
this.spotify.util
.fetch({
path,
method: 'put',
body,
})
.then((response) => {
this.spotify.util.toJson(response).then((body) => {
if (response.status == 204) {
resolve({ status: response.status });
} else if (body) {
reject(new ApiError(body.error));
}
reject(new HTTPError(response));
});
});
});
}
/**
* Resume current playback on the user's active device.
* @param {string} [device] - The id of the device this command is targeting. If not supplied, the user's currently active device is the target.
* @returns {Promise<Status|HTTPError|ApiError>}
*/
resume(device) {
const options = qs.stringify({
device_id: device,
});
const path = API + '/play?' + (device ? options : '');
return new Promise((resolve, reject) => {
this.spotify.util
.fetch({
path,
method: 'put',
})
.then((response) => {
this.spotify.util.toJson(response).then((body) => {
if (response.status == 204) {
resolve({ status: response.status });
} else if (body) {
reject(new ApiError(body.error));
}
reject(new HTTPError(response));
});
});
});
}
/**
* Pause playback on the user's account.
* @param {string} [device] - The id of the device this command is targeting. If not supplied, the user's currently active device is the target.
* @returns {Promise<Status|HTTPError|ApiError>}
*/
pause(device) {
const options = qs.stringify({
device_id: device,
});
const path = API + '/pause?' + (device ? options : '');
return new Promise((resolve, reject) => {
this.spotify.util
.fetch({
path,
method: 'put',
})
.then((response) => {
this.spotify.util.toJson(response).then((body) => {
if (response.status == 204) {
resolve({ status: response.status });
} else if (body) {
reject(new ApiError(body.error));
}
reject(new HTTPError(response));
});
});
});
}
/**
* Skips to next track in the user’s queue.
* @param {string} [device] - The id of the device this command is targeting. If not supplied, the user's currently active device is the target.
* @returns {Promise<Status|HTTPError|ApiError>}
*/
next(device) {
const options = qs.stringify({
device_id: device,
});
const path = API + '/next?' + (device ? options : '');
return new Promise((resolve, reject) => {
this.spotify.util
.fetch({
path,
method: 'post',
})
.then((response) => {
this.spotify.util.toJson(response).then((body) => {
if (response.status == 204) {
resolve({ status: response.status });
} else if (body) {
reject(new ApiError(body.error));
}
reject(new HTTPError(response));
});
});
});
}
/**
* Skips to previous track in the user’s queue.
* @param {string} [device] - The id of the device this command is targeting. If not supplied, the user's currently active device is the target.
* @returns {Promise<Status|HTTPError|ApiError>}
*/
back(device) {
const options = qs.stringify({
device_id: device,
});
const path = API + '/previous?' + (device ? options : '');
return new Promise((resolve, reject) => {
this.spotify.util
.fetch({
path,
method: 'post',
})
.then((response) => {
this.spotify.util.toJson(response).then((body) => {
if (response.status == 204) {
resolve({ status: response.status });
} else if (body) {
reject(new ApiError(body.error));
}
reject(new HTTPError(response));
});
});
});
}
/**
* Seeks to the given position in the user’s currently playing track.
* @param {number} ms - The position in milliseconds to seek to. Passing in a position that is greater than the length of the track will cause the player to start playing the next song.
* @param {string} [device] - The id of the device this command is targeting. If not supplied, the user's currently active device is the target.
* @returns {Promise<Status|HTTPError|ApiError>}
*/
seek(ms, device) {
const opts = {
position_ms: ms,
};
if (device) {
opts['device'] = device;
}
const options = qs.stringify(opts);
const path = API + '/seek?' + options;
return new Promise((resolve, reject) => {
this.spotify.util
.fetch({
path,
method: 'put',
})
.then((response) => {
this.spotify.util.toJson(response).then((body) => {
if (response.status == 204) {
resolve({ status: response.status });
} else if (body) {
reject(new ApiError(body.error));
}
reject(new HTTPError(response));
});
});
});
}
/**
*Set the repeat mode for the user's playback. Options are repeat-track, repeat-context, and off.
* @param {RepeatStates} state - The state to set the user's currently active device repeat mode to.
* @param {string} [device] - The id of the device this command is targeting. If not supplied, the user's currently active device is the target.
* @returns {Promise<Status|HTTPError|ApiError>}
*/
repeat(state, device) {
const opts = {
state,
};
if (device) {
opts['device'] = device;
}
const options = qs.stringify(opts);
const path = API + '/repeat?' + options;
return new Promise((resolve, reject) => {
this.spotify.util
.fetch({
path,
method: 'put',
})
.then((response) => {
this.spotify.util.toJson(response).then((body) => {
if (response.status == 204) {
resolve({ status: response.status });
} else if (body) {
reject(new ApiError(body.error));
}
reject(new HTTPError(response));
});
});
});
}
/**
* Set the volume for the user’s current playback device.
* @param {number} vol - The volume to set. Must be a value from 0 to 100 inclusive.
* @param {string} [device] - The id of the device this command is targeting. If not supplied, the user's currently active device is the target.
* @returns {Promise<Status|HTTPError|ApiError>}
*/
volume(vol, device) {
const opts = {
volume_percent: vol,
};
if (device) {
opts['device'] = device;
}
const options = qs.stringify(opts);
const path = API + '/volume?' + options;
return new Promise((resolve, reject) => {
this.spotify.util
.fetch({
path,
method: 'put',
})
.then((response) => {
this.spotify.util.toJson(response).then((body) => {
if (response.status == 204) {
resolve({ status: response.status });
} else if (body) {
reject(new ApiError(body.error));
}
reject(new HTTPError(response));
});
});
});
}
/**
* Toggle shuffle on or off for user’s playback.
* @param {boolean} state - To shuffle the user's playback.
* @param {string} [device] - The id of the device this command is targeting. If not supplied, the user's currently active device is the target.
* @returns {Promise<Status|HTTPError|ApiError>}
*/
shuffle(state, device) {
const opts = {
state,
};
if (device) {
opts['device'] = device;
}
const options = qs.stringify(opts);
const path = API + '/shuffle?' + options;
return new Promise((resolve, reject) => {
this.spotify.util
.fetch({
path,
method: 'put',
})
.then((response) => {
this.spotify.util.toJson(response).then((body) => {
if (response.status == 204) {
resolve({ status: response.status });
} else if (body) {
reject(new ApiError(body.error));
}
reject(new HTTPError(response));
});
});
});
}
/**
* Get tracks from the current user's recently played tracks. Note: Currently doesn't support podcast episodes.
* @param {RecentOptions} options
* @returns {Promise<Track[]|HTTPError|ApiError>}
*/
recent({ limit = 20, after, before } = {}) {
if (after && before) {
throw new Error('Only one of `after` or `before` can be provided.');
}
const opts = {
limit,
};
if (after) {
opts['after'] = after;
} else if (before) {
opts['before'] = before;
}
const options = qs.stringify(opts);
const path = API + '/recently-played?' + options;
return new Promise((resolve, reject) => {
this.spotify.util
.fetch({
path,
})
.then((response) => {
this.spotify.util.toJson(response).then((body) => {
if (body) {
if (response.status == 200) {
const tracks = body.items.map(
(t) => new Track(this.spotify, t.track)
);
return resolve(tracks);
}
reject(new ApiError(body.error));
}
reject(new HTTPError(response));
});
});
});
}
/**
* Add an item to the end of the user's current playback queue.
* @param {ContextURI} uri - The uri of the item to add to the queue. Must be a track or an episode uri.
* @param {string} [device] - The id of the device this command is targeting. If not supplied, the user's currently active device is the target.
* @returns {Promise<Status|HTTPError|ApiError>}
*/
queue(uri, device) {
const opts = {
uri,
};
if (device) {
opts['device'] = device;
}
const options = qs.stringify(opts);
const path = API + '/queue?' + options;
return new Promise((resolve, reject) => {
this.spotify.util
.fetch({
path,
method: 'post',
})
.then((response) => {
this.spotify.util.toJson(response).then((body) => {
if (response.status == 204) {
resolve({ status: response.status });
} else if (body) {
reject(new ApiError(body.error));
}
reject(new HTTPError(response));
});
});
});
}
}
module.exports = PlayerManager;
/**
* @typedef {Object} StartOptions
* @property {string} [device] - The device id to start the playback on - by default the current active device.
* @property {number} [offset=0] - The offset to start the playback.
* @property {number} [ms=0] - The position to start the playbak in milliseconds.
*/
/**
* @typedef {Object} RecentOptions
* @property {number} [limit=20] - The maximum number of items to return. Minimum: 1. Maximum: 50.
* @property {number} [after] - A Unix timestamp in milliseconds. Returns all items before (but not including) this cursor position.
* @property {number} [before] -A Unix timestamp in milliseconds. Returns all items after (but not including) this cursor position.
*/
/**
* Additional types for the client to support - by default 'track' is the suppiled type.
* - track
* - episode
* @typedef {string} AdditionalTypes
*/
/**
* track - will repeat the current track.
* context - will repeat the current context.
* off - will turn repeat off.
* @typedef {string} RepeatStates
*/