cody-music
Version:
mac osx spotify and itunes music player controller, spotify audio features, itunes and spotify genre, and playlist control
566 lines • 22.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.PlaylistService = void 0;
const client_1 = require("./client");
const models_1 = require("./models");
const store_1 = require("./store");
const profile_1 = require("./profile");
const util_1 = require("./util");
const musicClient = client_1.MusicClient.getInstance();
const musicStore = store_1.MusicStore.getInstance();
const userProfile = profile_1.UserProfile.getInstance();
const musicUtil = new util_1.MusicUtil();
class PlaylistService {
constructor() {
//
}
static getInstance() {
if (!PlaylistService.instance) {
PlaylistService.instance = new PlaylistService();
}
return PlaylistService.instance;
}
async removeFromSpotifyLiked(trackIds) {
trackIds = musicUtil.createTrackIdsFromUris(trackIds);
const api = `/v1/me/tracks`;
/**
* ["4iV5W9uYEdYUVa79Axb7Rh", "1301WleyT98MSxVHPZCA6M"]
*/
const qsOptions = { ids: trackIds.join(",") };
let codyResp = await musicClient.spotifyApiDelete(api, qsOptions);
// check if the token needs to be refreshed
if (codyResp.status === 401) {
// refresh the token
await musicClient.refreshSpotifyToken();
// try again
codyResp = await musicClient.spotifyApiDelete(api, qsOptions);
}
return codyResp;
}
async saveToSpotifyLiked(trackIds) {
trackIds = musicUtil.createTrackIdsFromUris(trackIds);
const api = `/v1/me/tracks`;
/**
* {ids:["4iV5W9uYEdYUVa79Axb7Rh", "1301WleyT98MSxVHPZCA6M"]}
*/
const qsOptions = {};
const payload = {
ids: trackIds,
};
let codyResp = await musicClient.spotifyApiPut(api, qsOptions, payload);
// check if the token needs to be refreshed
if (codyResp.status === 401) {
// refresh the token
await musicClient.refreshSpotifyToken();
// try again
codyResp = await musicClient.spotifyApiPut(api, qsOptions, payload);
}
return codyResp;
}
async getSavedTracks(qsOptions = {}) {
let tracks = [];
const totalTracksToFetch = !qsOptions.limit || qsOptions.limit === -1 ? -1 : qsOptions.limit;
if (!qsOptions.limit) {
qsOptions["limit"] = 50;
}
else if (qsOptions.limit < 1) {
qsOptions.limit = 1;
}
if (!qsOptions.offset) {
qsOptions["offset"] = 0;
}
const api = `/v1/me/tracks`;
let codyResp = await musicClient.spotifyApiGet(api, qsOptions);
// check if the token needs to be refreshed
if (codyResp.status === 401) {
// refresh the token
await musicClient.refreshSpotifyToken();
// try again
codyResp = await musicClient.spotifyApiGet(api, qsOptions);
}
if (musicUtil.isResponseOkWithData(codyResp) && codyResp.data.items) {
while (true) {
let trackContainers = codyResp.data.items;
// ensure the playerType is set
let fetchedLimit = false;
for (let x = 0; x < trackContainers.length; x++) {
const item = trackContainers[x];
if (item.track) {
const track = musicUtil.buildTrack(item.track);
tracks.push(track);
}
if (totalTracksToFetch > 0 &&
tracks.length >= totalTracksToFetch) {
fetchedLimit = true;
break;
}
}
if (fetchedLimit) {
break;
}
if (codyResp.data.next) {
// fetch the next set (remove the root)
let nextApi = codyResp.data.next.substring(client_1.SPOTIFY_ROOT_API.length);
codyResp = await musicClient.spotifyApiGet(nextApi, {});
}
else {
break;
}
}
}
return tracks;
}
async getPlaylists(qsOptions = {}) {
let playlists = [];
if (!musicStore.spotifyUserId) {
await userProfile.getUserProfile();
}
if (musicStore.spotifyUserId) {
const spotifyUserId = musicStore.spotifyUserId;
const fetchAll = qsOptions.all ? true : false;
let limit = qsOptions.limit ? qsOptions.limit : 50;
limit = limit < 1 ? 1 : limit;
let offset = qsOptions.offset ? qsOptions.offset : 0;
let codyResp = await this.getPlaylistsForUser(spotifyUserId, limit, offset);
if (musicUtil.isItemsResponseOk(codyResp)) {
let playlistItems = codyResp.data.items;
// ensure the playerType is set
playlistItems.forEach((playlist) => {
playlist.playerType = models_1.PlayerType.WebSpotify;
playlist.type = "playlist";
playlists.push(playlist);
});
// check if we need to fetch every playlist
if (fetchAll) {
let threshold = codyResp.data.limit + codyResp.data.offset;
let total = codyResp.data.total;
while (total > threshold) {
// update the next offset and fetch the next set
offset = threshold;
codyResp = await this.getPlaylistsForUser(musicStore.spotifyUserId, limit, offset);
if (musicUtil.isItemsResponseOk(codyResp)) {
playlistItems = codyResp.data.items;
// ensure the playerType is set
playlistItems.forEach((playlist) => {
playlist.playerType = models_1.PlayerType.WebSpotify;
playlist.type = "playlist";
playlists.push(playlist);
});
}
threshold = codyResp.data.limit + codyResp.data.offset;
total = codyResp.data.total;
}
}
}
}
return playlists;
}
async getPlaylistsForUser(spotifyUserId, limit, offset) {
limit = limit || 50;
offset = offset || 0;
const qsOptions = {
limit,
offset,
};
const api = `/v1/users/${spotifyUserId}/playlists`;
let codyResp = await musicClient.spotifyApiGet(api, qsOptions);
// check if the token needs to be refreshed
if (codyResp.status === 401) {
// refresh the token
await musicClient.refreshSpotifyToken();
// try again
codyResp = await musicClient.spotifyApiGet(api, qsOptions);
}
return codyResp;
}
async getSpotifyPlaylist(playlist_id) {
let playlistItem = new models_1.PlaylistItem();
// make sure the ID is not the URI
playlist_id = musicUtil.createSpotifyIdFromUri(playlist_id);
const api = `/v1/playlists/${playlist_id}`;
let codyResp = await musicClient.spotifyApiGet(api, {});
// check if the token needs to be refreshed
if (codyResp.status === 401) {
// refresh the token
await musicClient.refreshSpotifyToken();
// try again
codyResp = await musicClient.spotifyApiGet(api, {});
}
if (musicUtil.isResponseOk(codyResp)) {
playlistItem = {
...codyResp.data,
};
}
return playlistItem;
}
async getPlaylistTracks(playlist_id, qsOptions = {}) {
if (!qsOptions.limit) {
// maximum is 100 at a time
qsOptions["limit"] = 100;
}
else if (qsOptions.limit < 1) {
qsOptions.limit = 1;
}
if (!qsOptions.offset) {
qsOptions["offset"] = 0;
}
// fields to return for the present moment
// TODO: allow options to update this
qsOptions["fields"] =
"href,limit,next,offset,previous,total,items(track(name,id,album(id,name),artists,popularity))";
const api = `/v1/playlists/${playlist_id}/tracks`;
let codyResp = await musicClient.spotifyApiGet(api, qsOptions);
// check if the token needs to be refreshed
if (codyResp.status === 401) {
// refresh the token
await musicClient.refreshSpotifyToken();
// try again
codyResp = await musicClient.spotifyApiPost(api, qsOptions);
}
const paginationItem = new models_1.PaginationItem();
let tracks = [];
while (true) {
if (codyResp &&
codyResp.status === 200 &&
codyResp.data &&
codyResp.data.items) {
let trackContainers = codyResp.data.items;
// ensure the playerType is set
trackContainers.forEach((item) => {
if (item.track) {
const track = musicUtil.buildTrack(item.track);
tracks.push(track);
}
});
if (codyResp.data.next) {
// fetch the next set (remove the root)
let nextApi = codyResp.data.next.substring(client_1.SPOTIFY_ROOT_API.length);
codyResp = await musicClient.spotifyApiGet(nextApi, {});
}
else {
break;
}
}
else {
break;
}
}
delete codyResp.data;
paginationItem.items = tracks;
paginationItem.total = tracks.length;
codyResp["data"] = paginationItem;
return codyResp;
}
async getPlaylistNames(qsOptions = {}) {
let names = [];
let playlistNames = await this.getPlaylists(qsOptions);
if (playlistNames) {
names = playlistNames.map((playlistItem) => {
return playlistItem.name;
});
}
return names;
}
/**
* Create a new playlist
* @param name
* @param isPublic
*/
async createPlaylist(name, isPublic, description = "") {
// get the profile if we don't have it
if (!musicStore.spotifyUserId) {
await userProfile.getUserProfile();
}
const spotifyUserId = musicStore.spotifyUserId;
let playlists = await this.getPlaylists();
// check if it's already in the playlist
const existingPlaylist = playlists.length
? playlists.filter((n) => n.name === name)
: [];
if (existingPlaylist.length > 0) {
// already exists, return it
const failedCreate = new models_1.CodyResponse();
failedCreate.status = 500;
failedCreate.state = models_1.CodyResponseType.Failed;
failedCreate.message = `The playlist '${name}' already exists`;
return failedCreate;
}
if (spotifyUserId) {
/**
* --data "{\"name\":\"A New Playlist\", \"public\":false}
*/
const payload = {
name,
public: isPublic,
description,
};
const api = `/v1/users/${spotifyUserId}/playlists`;
const resp = await musicClient.spotifyApiPost(api, {}, JSON.stringify(payload));
if (resp && resp.state === models_1.CodyResponseType.Success) {
// fetch this playlist to add it to "playlists"
const playlistId = resp.data.id;
const createdPlaylistItem = await this.getSpotifyPlaylist(playlistId);
if (createdPlaylistItem) {
playlists.push(createdPlaylistItem);
}
}
return resp;
}
const failedCreate = new models_1.CodyResponse();
failedCreate.status = 500;
failedCreate.state = models_1.CodyResponseType.Failed;
failedCreate.message = "Unable to fetch the user ID";
return failedCreate;
}
async deletePlaylist(playlist_id) {
playlist_id = musicUtil.createSpotifyIdFromUri(playlist_id);
const api = `/v1/playlists/${playlist_id}/followers`;
let codyResp = await musicClient.spotifyApiDelete(api, {}, {});
// check if the token needs to be refreshed
if (codyResp.status === 401) {
// refresh the token
await musicClient.refreshSpotifyToken();
// try again
codyResp = await musicClient.spotifyApiDelete(api, {}, {});
}
return codyResp;
}
/**
* type: Valid types are: album , artist, playlist, and track
* q: can have a filter and keywords, or just keywords. You
* can have a wildcard as well. The query will search against
* the name and description if a specific filter isn't specified.
* examples:
* 1) search for a track by name "what a time to be alive"
* query string: ?q=name:what%20a%20time&type=track
* result: this should return tracks matching the track name
* 2) search for a track using a wildcard in the name
* query string: ?q=name:what*&type=track&limit=50
* result: will return all tracks with "what" in the name
* 3) search for an artist in name or description
* query string: ?tania%20bowra&type=artist
* result: will return all artists where tania bowra is in
* the name or description
* limit: max of 50
* @param type
* @param q
*/
async search(type, q, limit = 50) {
limit = limit < 1 ? 1 : limit > 50 ? 50 : limit;
q = q.trim();
let qryObj = {
type,
q,
limit,
};
// concat the key/value filterObjects
const api = `/v1/search`;
let codyResp = await musicClient.spotifyApiGet(api, qryObj);
// check if the token needs to be refreshed
if (codyResp.status === 401) {
// refresh the token
await musicClient.refreshSpotifyToken();
// try again
codyResp = await musicClient.spotifyApiGet(api, qryObj);
}
let hasData = codyResp &&
codyResp.data &&
codyResp.data.tracks &&
codyResp.data.tracks.items &&
codyResp.data.tracks.items.length > 0
? true
: false;
// empty result example (and the basic result structure)
/**
* {"status":200,"state":"success","statusText":"OK","message":"",
* "data":{
* "tracks":{
* "href":"https://api.spotify.com/v1/search?query=track%3AEl+Perd%C3%B3n+artist%3ANicky+Jam+%26+Enrique+Iglesias&type=track&market=US&offset=0&limit=1",
* "items":[],
* "limit":1,
* "next":null,
* "offset":0,
* "previous":null,
* "total":0
* }
* },
* "error":{}
* }
*/
// If the search doesn't return anything for a track and the search
// included "track:" and "artist:", try again with just the "track:"
if (type === "track" && !hasData) {
// create a new query with just the track
if (q.includes("track:") && q.includes("artist:")) {
const trackIdx = q.indexOf("track:");
const artistIdx = q.indexOf("artist:");
if (artistIdx > trackIdx) {
// grab everything up until the artistIdx
q = q.substring(0, artistIdx);
}
else {
// grab everything start from the trackIdx
q = q.substring(trackIdx);
}
q = q.trim();
qryObj = {
type,
q,
limit,
};
codyResp = await musicClient.spotifyApiGet(api, qryObj);
}
}
hasData = codyResp?.data?.tracks?.items?.length;
let emptyResult = {};
if (!hasData) {
if (type === "track") {
emptyResult["tracks"] = { items: [] };
}
else if (type === "album") {
emptyResult["albums"] = { items: [] };
}
else if (type === "artist") {
emptyResult["artists"] = { items: [] };
}
else {
emptyResult["playlists"] = { items: [] };
}
}
const searchResult = hasData ? codyResp.data : emptyResult;
return searchResult;
}
/**
* Add tracks to a given playlist
* @param playlist_id
* @param track_ids
* @param position
*/
async addTracksToPlaylist(playlist_id, track_ids, position = 0) {
let codyResp = new models_1.CodyResponse();
if (!track_ids) {
codyResp.status = 500;
codyResp.state = models_1.CodyResponseType.Failed;
codyResp.message = "No track URIs provided to add to playlist";
return codyResp;
}
const tracks = musicUtil.createUrisFromTrackIds(track_ids);
let payload = {
uris: tracks,
position,
};
const api = `/v1/playlists/${playlist_id}/tracks`;
codyResp = await musicClient.spotifyApiPost(api, {}, payload);
// check if the token needs to be refreshed
if (codyResp.status === 401) {
// refresh the token
await musicClient.refreshSpotifyToken();
// try again
codyResp = await musicClient.spotifyApiPost(api, {}, payload);
}
return codyResp;
}
/**
* Replace tracks of a given playlist. This will wipe out
* the current set of tracks.
* @param playlist_id
* @param track_ids
*/
async replacePlaylistTracks(playlist_id, track_ids) {
let codyResp = new models_1.CodyResponse();
if (!track_ids) {
codyResp.status = 500;
codyResp.state = models_1.CodyResponseType.Failed;
codyResp.message = "No track URIs provided to remove from playlist";
return codyResp;
}
const tracks = musicUtil.createUrisFromTrackIds(track_ids);
let payload = {
uris: tracks,
};
const api = `/v1/playlists/${playlist_id}/tracks`;
codyResp = await musicClient.spotifyApiPut(api, {}, payload);
// check if the token needs to be refreshed
if (codyResp.status === 401) {
// refresh the token
await musicClient.refreshSpotifyToken();
// try again
codyResp = await musicClient.spotifyApiPut(api, {}, payload);
}
return codyResp;
}
/**
* Track IDs should be the uri (i.e. "spotify:track:4iV5W9uYEdYUVa79Axb7Rh")
* but if it's only the id (i.e. "4iV5W9uYEdYUVa79Axb7Rh") this will add
* the uri part "spotify:track:"
* @param playlist_id
* @param trackIds
*/
async removeTracksFromPlaylist(playlist_id, track_ids) {
playlist_id = musicUtil.createSpotifyIdFromUri(playlist_id);
let codyResp = new models_1.CodyResponse();
if (!track_ids) {
codyResp.status = 500;
codyResp.state = models_1.CodyResponseType.Failed;
codyResp.message = "No track URIs provided to remove from playlist";
return codyResp;
}
// returns list of URIs
let payload = {};
payload["tracks"] = musicUtil.createUrisFromTrackIds(track_ids, true /*addUriObj*/);
codyResp = await musicClient.spotifyApiDelete(`/v1/playlists/${playlist_id}/tracks`, {}, payload);
// check if the token needs to be refreshed
if (codyResp.status === 401) {
// refresh the token
await musicClient.refreshSpotifyToken();
// try again
codyResp = await musicClient.spotifyApiDelete(`/v1/playlists/${playlist_id}/tracks`, {}, payload);
}
return codyResp;
}
async getTopSpotifyTracks() {
let tracks = [];
const api = `/v1/me/top/tracks`;
// add to the api to prevent the querystring from escaping the comma
const qsOptions = {
time_range: "medium_term",
limit: 50,
};
let response = await musicClient.spotifyApiGet(api, qsOptions);
// check if the token needs to be refreshed
if (response.status === 401) {
// refresh the token
await musicClient.refreshSpotifyToken();
// try again
response = await musicClient.spotifyApiGet(api, qsOptions);
}
if (musicUtil.isResponseOk(response)) {
tracks = response.data.items;
}
if (tracks && tracks.length > 0) {
tracks = tracks.map((track) => {
return musicUtil.copySpotifyTrackToCodyTrack(track);
});
}
return tracks;
}
/**
* follow a playlist
* @param playlist_id
*/
async followPlaylist(playlist_id) {
playlist_id = musicUtil.createSpotifyIdFromUri(playlist_id);
const api = `/v1/playlists/${playlist_id}/followers`;
let codyResp = await musicClient.spotifyApiPut(api, {}, {});
// check if the token needs to be refreshed
if (codyResp.status === 401) {
// refresh the token
await musicClient.refreshSpotifyToken();
// try again
codyResp = await musicClient.spotifyApiPut(api, {}, {});
}
return codyResp;
}
}
exports.PlaylistService = PlaylistService;
//# sourceMappingURL=playlist.service.js.map