UNPKG

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
"use strict"; 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