cody-music
Version:
mac osx spotify and itunes music player controller, spotify audio features, itunes and spotify genre, and playlist control
723 lines (626 loc) • 20.5 kB
text/typescript
import { MusicClient, SPOTIFY_ROOT_API } from "./client";
import {
CodyResponse,
CodyResponseType,
PlaylistItem,
Track,
PaginationItem,
PlayerType,
} from "./models";
import { MusicStore } from "./store";
import { UserProfile } from "./profile";
import { MusicUtil } from "./util";
const musicClient = MusicClient.getInstance();
const musicStore = MusicStore.getInstance();
const userProfile = UserProfile.getInstance();
const musicUtil = new MusicUtil();
export class PlaylistService {
private static instance: PlaylistService;
private constructor() {
//
}
static getInstance() {
if (!PlaylistService.instance) {
PlaylistService.instance = new PlaylistService();
}
return PlaylistService.instance;
}
async removeFromSpotifyLiked(trackIds: string[]): Promise<CodyResponse> {
trackIds = musicUtil.createTrackIdsFromUris(trackIds);
const api = `/v1/me/tracks`;
/**
* ["4iV5W9uYEdYUVa79Axb7Rh", "1301WleyT98MSxVHPZCA6M"]
*/
const qsOptions = { ids: trackIds.join(",") };
let codyResp: CodyResponse = 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: string[]): Promise<CodyResponse> {
trackIds = musicUtil.createTrackIdsFromUris(trackIds);
const api = `/v1/me/tracks`;
/**
* {ids:["4iV5W9uYEdYUVa79Axb7Rh", "1301WleyT98MSxVHPZCA6M"]}
*/
const qsOptions = {};
const payload = {
ids: trackIds,
};
let codyResp: CodyResponse = 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: any = {}) {
let tracks: Track[] = [];
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: CodyResponse = 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: any[] = 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: 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(
SPOTIFY_ROOT_API.length
);
codyResp = await musicClient.spotifyApiGet(nextApi, {});
} else {
break;
}
}
}
return tracks;
}
async getPlaylists(qsOptions: any = {}): Promise<PlaylistItem[]> {
let playlists: PlaylistItem[] = [];
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: PlaylistItem) => {
playlist.playerType = 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: PlaylistItem) => {
playlist.playerType = PlayerType.WebSpotify;
playlist.type = "playlist";
playlists.push(playlist);
});
}
threshold = codyResp.data.limit + codyResp.data.offset;
total = codyResp.data.total;
}
}
}
}
return playlists;
}
async getPlaylistsForUser(
spotifyUserId: string,
limit: number,
offset: number
): Promise<CodyResponse> {
limit = limit || 50;
offset = offset || 0;
const qsOptions = {
limit,
offset,
};
const api = `/v1/users/${spotifyUserId}/playlists`;
let codyResp: CodyResponse = 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: string): Promise<PlaylistItem> {
let playlistItem: PlaylistItem = new PlaylistItem();
// make sure the ID is not the URI
playlist_id = musicUtil.createSpotifyIdFromUri(playlist_id);
const api = `/v1/playlists/${playlist_id}`;
let codyResp: CodyResponse = 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: string, qsOptions: any = {}) {
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: PaginationItem = new PaginationItem();
let tracks: Track[] = [];
while (true) {
if (
codyResp &&
codyResp.status === 200 &&
codyResp.data &&
codyResp.data.items
) {
let trackContainers: any[] = codyResp.data.items;
// ensure the playerType is set
trackContainers.forEach((item: any) => {
if (item.track) {
const track: 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(
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: any = {}): Promise<string[]> {
let names: string[] = [];
let playlistNames = await this.getPlaylists(qsOptions);
if (playlistNames) {
names = playlistNames.map((playlistItem: PlaylistItem) => {
return playlistItem.name;
});
}
return names;
}
/**
* Create a new playlist
* @param name
* @param isPublic
*/
async createPlaylist(
name: string,
isPublic: boolean,
description: string = ""
): Promise<CodyResponse> {
// get the profile if we don't have it
if (!musicStore.spotifyUserId) {
await userProfile.getUserProfile();
}
const spotifyUserId = musicStore.spotifyUserId;
let playlists: PlaylistItem[] = await this.getPlaylists();
// check if it's already in the playlist
const existingPlaylist: PlaylistItem[] = playlists.length
? playlists.filter((n: PlaylistItem) => n.name === name)
: [];
if (existingPlaylist.length > 0) {
// already exists, return it
const failedCreate: CodyResponse = new CodyResponse();
failedCreate.status = 500;
failedCreate.state = 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: CodyResponse = await musicClient.spotifyApiPost(
api,
{},
JSON.stringify(payload)
);
if (resp && resp.state === CodyResponseType.Success) {
// fetch this playlist to add it to "playlists"
const playlistId = resp.data.id;
const createdPlaylistItem: PlaylistItem = await this.getSpotifyPlaylist(
playlistId
);
if (createdPlaylistItem) {
playlists.push(createdPlaylistItem);
}
}
return resp;
}
const failedCreate: CodyResponse = new CodyResponse();
failedCreate.status = 500;
failedCreate.state = CodyResponseType.Failed;
failedCreate.message = "Unable to fetch the user ID";
return failedCreate;
}
async deletePlaylist(playlist_id: string): Promise<CodyResponse> {
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: string, q: string, limit: number = 50) {
limit = limit < 1 ? 1 : limit > 50 ? 50 : limit;
q = q.trim();
let qryObj: any = {
type,
q,
limit,
};
// concat the key/value filterObjects
const api = `/v1/search`;
let codyResp: CodyResponse = 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: any = {};
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: string,
track_ids: string[],
position: number = 0
) {
let codyResp = new CodyResponse();
if (!track_ids) {
codyResp.status = 500;
codyResp.state = 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: string, track_ids: string[]) {
let codyResp = new CodyResponse();
if (!track_ids) {
codyResp.status = 500;
codyResp.state = 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: string,
track_ids: string[]
): Promise<CodyResponse> {
playlist_id = musicUtil.createSpotifyIdFromUri(playlist_id);
let codyResp = new CodyResponse();
if (!track_ids) {
codyResp.status = 500;
codyResp.state = CodyResponseType.Failed;
codyResp.message = "No track URIs provided to remove from playlist";
return codyResp;
}
// returns list of URIs
let payload: any = {};
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: Track[] = [];
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: string): Promise<CodyResponse> {
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;
}
}