cody-music
Version:
mac osx spotify and itunes music player controller, spotify audio features, itunes and spotify genre, and playlist control
780 lines • 32 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MusicPlayerState = exports.SPOTIFY_LIKED_SONGS_PLAYLIST_NAME = void 0;
const util_1 = require("./util");
const controller_1 = require("./controller");
const store_1 = require("./store");
const client_1 = require("./client");
const models_1 = require("./models");
const audiostat_1 = require("./audiostat");
const date_fns_1 = require("date-fns");
const musicStore = store_1.MusicStore.getInstance();
const musicClient = client_1.MusicClient.getInstance();
const audioStat = audiostat_1.AudioStat.getInstance();
const musicController = controller_1.MusicController.getInstance();
const musicUtil = new util_1.MusicUtil();
exports.SPOTIFY_LIKED_SONGS_PLAYLIST_NAME = "Liked Songs";
class MusicPlayerState {
constructor() {
//
}
static getInstance() {
if (!MusicPlayerState.instance) {
MusicPlayerState.instance = new MusicPlayerState();
}
return MusicPlayerState.instance;
}
async isWindowsSpotifyRunning() {
/**
* /tasklist /fi "imagename eq Spotify.exe" /fo list /v |find " - "
* Window Title: Dexys Midnight Runners - Come On Eileen
*/
let result = await musicUtil
.execCmd(controller_1.MusicController.WINDOWS_SPOTIFY_TRACK_FIND)
.catch((e) => {
return null;
});
if (result && result.toLowerCase().includes("title")) {
return true;
}
return false;
}
async isSpotifyWebRunning() {
let accessToken = musicStore.spotifyAccessToken;
if (accessToken) {
let spotifyDevices = await this.getSpotifyDevices();
if (spotifyDevices.length > 0) {
return true;
}
}
return false;
}
/**
* returns...
* {
"devices" : [ {
"id" : "5fbb3ba6aa454b5534c4ba43a8c7e8e45a63ad0e",
"is_active" : false,
"is_private_session": true,
"is_restricted" : false,
"name" : "My fridge",
"type" : "Computer",
"volume_percent" : 100
} ]
}
*/
async getSpotifyDevices() {
let devices = [];
const accessToken = musicStore.spotifyAccessToken;
if (!accessToken) {
return [];
}
const api = "/v1/me/player/devices";
let response = await musicClient.spotifyApiGet(api);
// 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);
}
if (response.data && response.data.devices) {
devices = response.data.devices;
}
return devices || [];
}
/**
* returns i.e.
* track = {
artist: 'Bob Dylan',
album: 'Highway 61 Revisited',
disc_number: 1,
duration: 370,
played count: 0,
track_number: 1,
starred: false,
popularity: 71,
id: 'spotify:track:3AhXZa8sUQht0UEdBJgpGc',
name: 'Like A Rolling Stone',
album_artist: 'Bob Dylan',
artwork_url: 'http://images.spotify.com/image/e3d720410b4a0770c1fc84bc8eb0f0b76758a358',
spotify_url: 'spotify:track:3AhXZa8sUQht0UEdBJgpGc' }
}
*/
async getWindowsSpotifyTrackInfo() {
let windowTitleStr = "Window Title:";
// get the artist - song name from the command result, then get the rest of the info from spotify
let songInfo = await musicUtil
.execCmd(controller_1.MusicController.WINDOWS_SPOTIFY_TRACK_FIND)
.catch((e) => {
return null;
});
if (!songInfo || !songInfo.includes(windowTitleStr)) {
// it must have paused, or an ad, or it was closed
return null;
}
// fetch it from spotify
// result will be something like: "Window Title: Dexys Midnight Runners - Come On Eileen"
songInfo = songInfo.substring(windowTitleStr.length);
let artistSong = songInfo.split("-");
let artist = artistSong[0].trim();
let song = artistSong[1].trim();
const qParam = encodeURIComponent(`artist:${artist} track:${song}`);
const qryStr = `q=${qParam}&type=track&limit=2&offset=0`;
let api = `/v1/search?${qryStr}`;
let resp = await musicClient.spotifyApiGet(api);
let trackInfo = null;
if (musicUtil.isResponseOk(resp) &&
resp.data &&
resp.data.tracks &&
resp.data.tracks.items) {
trackInfo = resp.data.tracks.items[0];
// set the other attributes like start and type
trackInfo["type"] = "spotify";
trackInfo["state"] = "playing";
trackInfo["start"] = 0;
trackInfo["end"] = 0;
trackInfo["genre"] = "";
}
return trackInfo;
}
async getSpotifyTracks(ids, includeArtistData = false, includeAudioFeaturesData = false, includeGenre = false) {
const finalIds = [];
ids.forEach((id) => {
id = musicUtil.createSpotifyIdFromUri(id);
if (id) {
finalIds.push(id);
}
});
const tracksToReturn = [];
const api = `/v1/tracks`;
const qsOptions = { ids: finalIds.join(",") };
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);
}
// get the features if the flag is set to true
let spotifyAudioFeaturesP = null;
if (includeAudioFeaturesData) {
// async - call the spotify api to fetch the audio features
spotifyAudioFeaturesP = audioStat
.getSpotifyAudioFeatures(ids)
.catch((e) => {
return null;
});
}
if (response && response.status === 200 && response.data) {
// get the tracks
let artistIdMap = {};
const tracks = response.data.tracks || [];
tracks.forEach((trackData) => {
const track = musicUtil.copySpotifyTrackToCodyTrack(trackData);
track.progress_ms = response.data.progress_ms
? response.data.progress_ms
: 0;
if (track.artists) {
track.artists.forEach((artist) => {
artistIdMap[artist.id] = artist.id;
});
}
tracksToReturn.push(track);
});
// fetch the artists from spotify if this flag is set to true
if (includeArtistData) {
let artistIds = Object.keys(artistIdMap).map(key => key);
// fetch the artists all at once or in batches
let artists = [];
if (artistIds) {
// spotify's limit is 50, so batch if it's greater than 50
if (artistIds.length > 50) {
while (artistIds.length) {
// keep removing from the artistIds 50 at a time
let splicedArtistIds = artistIds.splice(0, 50);
const batchedArtists = await this.getSpotifyArtistsByIds(splicedArtistIds);
if (batchedArtists && batchedArtists.length) {
artists.push(...batchedArtists);
}
}
}
else {
artists = await this.getSpotifyArtistsByIds(artistIds);
}
}
// populate the artists into the existing tracks
if (artists && artists.length) {
tracksToReturn.forEach((t) => {
// get the artist IDs from the tracks to return shallow artists array
const trackArtistIds = t.artists.map((artist) => artist.id);
// filter out the full artists found in the artists response
// based on the artist IDs that were in the tracksToReturn
const artistsForTrack = artists.filter((n) => trackArtistIds.includes(n.id));
if (artistsForTrack && artistsForTrack.length) {
// replace the shallow artists with the full artists
t.artists = artistsForTrack;
}
if (!t.genre && includeGenre) {
// first check if we have an artist in artists
let genre = "";
if (t.artists && t.artists.length) {
for (let artistCandidate of t.artists) {
if (artistCandidate.genres &&
artistCandidate.genres.length) {
try {
genre = musicClient.getHighestFrequencySpotifyGenre(artistCandidate.genres);
}
catch (e) {
//
}
break;
}
}
}
if (genre) {
t.genre = genre;
}
}
});
}
}
// get the features if the flag is set to true
if (includeAudioFeaturesData) {
// await for the call we made earlier
const spotifyAudioFeatures = await spotifyAudioFeaturesP;
if (spotifyAudioFeatures && spotifyAudioFeatures.length) {
// "id": "4JpKVNYnVcJ8tuMKjAj50A",
// "uri": "spotify:track:4JpKVNYnVcJ8tuMKjAj50A",
// track.features = spotifyAudioFeatures[0];
spotifyAudioFeatures.forEach((feature) => {
const uri = feature.uri;
const foundTrack = tracksToReturn.find((t) => t.uri === uri);
if (foundTrack) {
foundTrack.features = feature;
}
});
}
}
}
return tracksToReturn;
}
async getSpotifyTrackById(id, includeArtistData = false, includeAudioFeaturesData = false, includeGenre = false) {
id = musicUtil.createSpotifyIdFromUri(id);
let track;
let api = `/v1/tracks/${id}`;
let response = await musicClient.spotifyApiGet(api);
// 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);
}
if (response && response.status === 200 && response.data) {
track = musicUtil.copySpotifyTrackToCodyTrack(response.data);
track.progress_ms = response.data.progress_ms
? response.data.progress_ms
: 0;
// get the arist data
if (includeArtistData && track.artists) {
let artists = [];
for (let i = 0; i < track.artists.length; i++) {
const artist = track.artists[i];
const artistData = await this.getSpotifyArtistById(artist.id);
artists.push(artistData);
}
if (artists.length > 0) {
track.artists = artists;
}
else {
track.artists = [];
}
}
if (!track.genre && includeGenre) {
// first check if we have an artist in artists
// artists[0].genres[0]
let genre = "";
if (track.artists &&
track.artists.length > 0 &&
track.artists[0].genres) {
// make sure we use the highest frequency genre
genre = musicClient.getHighestFrequencySpotifyGenre(track.artists[0].genres);
}
if (!genre) {
// get the genre
genre = await musicController.getGenre(track.artist, track.name);
}
if (genre) {
track.genre = genre;
}
}
// get the features
if (includeAudioFeaturesData) {
const spotifyAudioFeatures = await audioStat.getSpotifyAudioFeatures([id]);
if (spotifyAudioFeatures && spotifyAudioFeatures.length > 0) {
track.features = spotifyAudioFeatures[0];
}
}
}
else {
track = new models_1.Track();
}
return track;
}
async getSpotifyArtistsByIds(ids) {
let artists = [];
ids = musicUtil.createSpotifyIdsFromUris(ids);
let api = `/v1/artists`;
// const qParam = { ids };
// just create a comma separated list of these
if (ids && ids.length) {
api = `${api}?ids=${ids.join(",")}`;
}
let response = await musicClient.spotifyApiGet(api);
// 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);
}
if (response && response.status === 200 && response.data) {
artists = response.data.artists || [];
}
return artists;
}
async getSpotifyArtistById(id) {
let artist = new models_1.Artist();
id = musicUtil.createSpotifyIdFromUri(id);
let api = `/v1/artists/${id}`;
let response = await musicClient.spotifyApiGet(api);
// 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);
}
if (response && response.status === 200 && response.data) {
const artistData = response.data;
// delete external_urls
delete artistData.external_urls;
artist = artistData;
}
return artist;
}
async getSpotifyWebCurrentTrack() {
let track;
let api = "/v1/me/player/currently-playing";
let response = await musicClient.spotifyApiGet(api);
// 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);
}
if (response &&
response.status === 200 &&
response.data &&
response.data.item) {
const data = response.data;
track = musicUtil.copySpotifyTrackToCodyTrack(data.item);
track.progress_ms = data.progress_ms ? data.progress_ms : 0;
// set the actions ("actions": {"disallows": {"resuming": true}})
track.actions = data.actions;
// set whether this track is playing or not
/**
* data: {
context:null
currently_playing_type:"track"
is_playing:true
item:Object {album: Object, artists: Array(1), available_markets: Array(79), …}
progress_ms:153583
timestamp:1583797755729
}
*/
const isPlaying = data.is_playing !== undefined && data.is_playing !== null
? data.is_playing
: false;
if (track.uri && track.uri.includes("spotify:ad:")) {
track.state = models_1.TrackStatus.Advertisement;
}
else {
track.state = isPlaying
? models_1.TrackStatus.Playing
: models_1.TrackStatus.Paused;
}
}
else {
track = new models_1.Track();
track.state = models_1.TrackStatus.NotAssigned;
track.httpStatus = response.status;
}
return track;
}
async getSpotifyRecentlyPlayedTracksBefore(limit = 50, before = 0) {
return this.fetchSpotifyReentlyPlayedTracksData(limit, 0, before);
}
async getSpotifyRecentlyPlayedTracksAfter(limit = 50, after = 0) {
return this.fetchSpotifyReentlyPlayedTracksData(limit, after, 0);
}
async getSpotifyRecentlyPlayedTracks(limit = 50, after = 0, before = 0) {
const resp = await this.fetchSpotifyReentlyPlayedTracksData(limit, after, before);
if (resp && resp.data && resp.data.tracks) {
return resp.data.tracks;
}
return [];
}
/**
* Fetch the recently played tracks data
* @param limit (max of 1000)
* @param after
* @param before
*/
async fetchSpotifyReentlyPlayedTracksData(limit = 50, after = 0, before = 0) {
let api = "/v1/me/player/recently-played";
const qsOptions = {};
// max # of tracks for pagination
let trackLimit = limit;
if (trackLimit <= 0 || trackLimit > 1000) {
trackLimit = 1000;
}
// spotify api limit is 50
let apiLimit = limit;
if (apiLimit <= 0 || apiLimit > 50) {
apiLimit = 50;
}
// set the spotify per api limit
qsOptions["limit"] = apiLimit;
if (after && after > 0) {
qsOptions["after"] = after;
}
else if (before && before > 0) {
qsOptions["before"] = before;
}
let resp = await musicClient.spotifyApiGet(api, qsOptions);
// check if the token needs to be refreshed
if (resp.status === 401) {
// refresh the token
await musicClient.refreshSpotifyToken();
// try again
resp = await musicClient.spotifyApiGet(api, qsOptions);
}
let tracks = [];
if (musicUtil.isItemsResponseOk(resp)) {
resp.data.items.forEach((item) => {
const track = musicUtil.copySpotifyTrackToCodyTrack(item.track);
// set the context info
if (item.context) {
track.context_type = item.context.type;
track.context_uri = item.context.uri;
}
track.played_at = item.played_at;
track.played_at_utc_seconds = (0, date_fns_1.getUnixTime)(item.played_at);
tracks.push(track);
});
let cursors = resp.data.cursors;
if (cursors) {
cursors.before = parseInt(cursors.before, 10);
cursors.after = parseInt(cursors.after, 10);
}
else {
cursors = {
before: 0,
after: 0,
};
}
if (resp.data.next && tracks.length < trackLimit) {
// continue fetching until we've reached the limit
let reachedLimit = false;
let nextApi = resp.data.next;
while (!reachedLimit) {
let nextCursors = resp.data.cursors;
if (nextCursors && cursors) {
// update the before and after
const prevBefore = cursors.before;
const before = parseInt(nextCursors.before, 10);
if (before < prevBefore || prevBefore === 0) {
cursors.before = before;
}
const prevAfter = cursors.after;
const after = parseInt(nextCursors.after, 10);
if (after > prevAfter || prevAfter === 0) {
cursors.after = after;
}
}
if (!nextApi) {
reachedLimit = true;
break;
}
// get the next api
api = nextApi.substring(nextApi.indexOf("/v1"), nextApi.length);
const nextResults = await musicClient.spotifyApiGet(api);
if (musicUtil.isItemsResponseOk(nextResults)) {
nextApi = nextResults.data.next;
if (nextResults.data.items.length > 0) {
nextResults.data.items.forEach((item) => {
let spotifyTrack = item.track;
const track = musicUtil.copySpotifyTrackToCodyTrack(spotifyTrack);
track.played_at = item.played_at;
track.played_at_utc_seconds = (0, date_fns_1.getUnixTime)(item.played_at);
tracks.push(track);
});
}
else {
reachedLimit = true;
}
}
else {
reachedLimit = true;
}
if (tracks.length >= trackLimit) {
reachedLimit = true;
}
}
}
// update the cursors
resp.data.cursors = cursors;
}
if (resp.data) {
delete resp.data.items;
delete resp.data.href;
delete resp.data.next;
delete resp.data.limit;
// add tracks to the response
resp.data["tracks"] = tracks;
}
return resp;
}
async getRecommendationsForTracks(seed_tracks = [], limit = 40, market = "", min_popularity = 20, target_popularity = 90, seed_genres = [], seed_artists = [], features = {}) {
let tracks = [];
// change the trackIds to non-uri ids
seed_tracks = musicUtil.createTrackIdsFromUris(seed_tracks);
// the create trackIds will create normal artist ids as well
seed_artists = musicUtil.createTrackIdsFromUris(seed_artists);
// it can only take up to 5, remove the rest
if (seed_tracks.length > 5) {
seed_tracks.length = 5;
}
if (seed_genres.length > 5) {
seed_genres.length = 5;
}
if (seed_artists.length > 5) {
seed_artists.length = 5;
}
const qsOptions = {
limit,
min_popularity,
target_popularity,
};
if (seed_genres.length) {
qsOptions["seed_genres"] = seed_genres.join(",");
}
if (seed_tracks.length) {
qsOptions["seed_tracks"] = seed_tracks.join(",");
}
if (seed_artists.length) {
qsOptions["seed_artists"] = seed_artists.join(",");
}
if (market) {
qsOptions["market"] = market;
}
const featureKeys = Object.keys(features);
if (featureKeys.length) {
featureKeys.forEach((key) => {
qsOptions[key] = features[key];
});
}
const api = `/v1/recommendations`;
// add to the api to prevent the querystring from escaping the comma
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.tracks;
}
return tracks;
}
async setMute(mute, device_id = "") {
const api = `/v1/me/player/volume`;
if (musicStore.prevVolumePercent === 0) {
// get the previous volume
const devices = await this.getSpotifyDevices();
const playerContext = await this.getSpotifyPlayerContext();
if (playerContext && playerContext.device) {
musicStore.prevVolumePercent =
playerContext.device.volume_percent;
}
if (playerContext.device.volume_percent === 0) {
musicStore.prevVolumePercent = 45;
}
}
let qsOptions = {
volume_percent: mute ? 0 : musicStore.prevVolumePercent,
};
if (device_id) {
qsOptions["device_id"] = device_id;
}
let codyResp = await musicClient.spotifyApiPut(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.spotifyApiPut(api, qsOptions, {});
}
return codyResp;
}
async setShuffle(shuffle, device_id = "") {
const api = `/v1/me/player/shuffle`;
let qsOptions = {
state: shuffle,
};
if (device_id) {
qsOptions["device_id"] = device_id;
}
let codyResp = await musicClient.spotifyApiPut(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.spotifyApiPut(api, qsOptions, {});
}
return codyResp;
}
async setRepeatOff(device_id = "") {
return await this.setRepeat("off", device_id);
}
async setTrackRepeat(device_id = "") {
return await this.setRepeat("track", device_id);
}
async setPlaylistRepeat(device_id = "") {
return await this.setRepeat("context", device_id);
}
async updateRepeatMode(setToOn, device_id = "") {
const state = setToOn ? "track" : "off";
return await this.setRepeat(state, device_id);
}
async setRepeat(state, device_id = "") {
const api = `/v1/me/player/repeat`;
let qsOptions = {
state,
};
if (device_id) {
qsOptions["device_id"] = device_id;
}
let codyResp = await musicClient.spotifyApiPut(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.spotifyApiPut(api, qsOptions, {});
}
return codyResp;
}
async getSpotifyPlayerContext() {
let playerContext = new models_1.PlayerContext();
let api = "/v1/me/player";
let response = await musicClient.spotifyApiGet(api);
// 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);
}
if (response &&
response.status === 200 &&
response.data &&
response.data.item) {
// override "type" with "spotify"
response.data.item["type"] = "spotify";
response.data.item["playerType"] = models_1.PlayerType.WebSpotify;
musicUtil.extractAristFromSpotifyTrack(response.data.item);
playerContext = response.data;
}
return playerContext;
}
async launchAndPlaySpotifyTrack(trackId = "", playlistId = "", playerName = models_1.PlayerName.SpotifyWeb) {
// check if there's any spotify devices
const spotifyDevices = await this.getSpotifyDevices();
if (!spotifyDevices || spotifyDevices.length === 0) {
// no spotify devices found, lets launch the web player with the track
// launch it
await this.launchWebPlayer(playerName);
// now select it from within the playlist within 2 seconds
await setTimeout(() => {
this.playSpotifyTrackFromPlaylist(trackId, musicStore.spotifyUserId, playlistId);
}, 5000);
}
else {
// a device is found, play using the device
await this.playSpotifyTrackFromPlaylist(trackId, musicStore.spotifyUserId, playlistId);
}
}
async playSpotifyTrackFromPlaylist(trackId, spotifyUserId, playlistId = "") {
const spotifyUserUri = musicUtil.createSpotifyUserUriFromId(spotifyUserId);
if (playlistId === exports.SPOTIFY_LIKED_SONGS_PLAYLIST_NAME) {
playlistId = "";
}
const spotifyDevices = await this.getSpotifyDevices();
const deviceId = spotifyDevices.length > 0 ? spotifyDevices[0].id : "";
let options = {};
if (deviceId) {
options["device_id"] = deviceId;
}
if (trackId) {
options["track_ids"] = [trackId];
}
else {
options["offset"] = { position: 0 };
}
if (playlistId) {
const playlistUri = `${spotifyUserUri}:playlist:${playlistId}`;
options["context_uri"] = playlistUri;
}
/**
* to play a track without the play list id
* curl -X "PUT" "https://api.spotify.com/v1/me/player/play?device_id=4f38ae14f61b3a2e4ed97d537a5cb3d09cf34ea1"
* --data "{\"uris\":[\"spotify:track:2j5hsQvApottzvTn4pFJWF\"]}"
*/
if (!playlistId) {
// just play by track id
await musicController.spotifyWebPlayTrack(trackId, deviceId);
}
else {
// we have playlist id within the options, use that
await musicController.spotifyWebPlayPlaylist(playlistId, trackId, deviceId);
}
}
launchWebPlayer(options) {
if (options.album_id) {
const albumId = musicUtil.createSpotifyIdFromUri(options.album_id);
return musicUtil.launchWebUrl(`https://open.spotify.com/album/${albumId}`);
}
else if (options.track_id) {
const trackId = musicUtil.createSpotifyIdFromUri(options.track_id);
return musicUtil.launchWebUrl(`https://open.spotify.com/track/${trackId}`);
}
else if (options.playlist_id) {
const playlistId = musicUtil.createSpotifyIdFromUri(options.playlist_id);
return musicUtil.launchWebUrl(`https://open.spotify.com/playlist/${playlistId}`);
}
return musicUtil.launchWebUrl("https://open.spotify.com");
}
updateSpotifyLoved(loved) {
//
}
}
exports.MusicPlayerState = MusicPlayerState;
//# sourceMappingURL=playerstate.js.map