UNPKG

cody-music

Version:

mac osx spotify and itunes music player controller, spotify audio features, itunes and spotify genre, and playlist control

621 lines 25.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MusicController = void 0; const util_1 = require("./util"); const client_1 = require("./client"); const models_1 = require("./models"); const store_1 = require("./store"); const os = require("os"); const musicClient = client_1.MusicClient.getInstance(); const musicUtil = new util_1.MusicUtil(); class MusicController { constructor() { this.scriptsPath = __dirname + "/scripts/"; this.lastVolumeLevel = null; // applscript music commands and scripts this.scripts = { state: { file: "get_state.{0}.applescript", requiresArgv: false, }, checkPlayerRunningState: { file: "check_state.{0}.applescript", requiresArgv: false, }, firstTrackState: { file: "get_first_track_state.{0}.applescript", requiresArgv: false, }, volumeUp: { file: "volume_up.{0}.applescript", requiresArgv: false, }, volumeDown: { file: "volume_down.{0}.applescript", requiresArgv: false, }, playTrackInContext: 'tell application "{0}" to play track "{1}" {2} "{3}"', playTrackNumberInPlaylist: { file: "play_track_number_in_playlist.{0}.applescript", requiresArgv: true, }, play: 'tell application "{0}" to play', playFromLibrary: 'tell application "{0}" to play of playlist "{1}"', playSongFromLibrary: 'tell application "{0}" to play track "{1}" of playlist "{2}"', playTrack: 'tell application "{0}" to play track "{1}"', pause: 'tell application "{0}" to pause', playPause: 'tell application "{0}" to playpause', next: 'tell application "{0}" to play (next track)', previous: 'tell application "{0}" to play (previous track)', repeatOn: 'tell application "{0}" to set {1} to {2}', repeatOff: 'tell application "{0}" to set {1} to {2}', isRepeating: 'tell application "{0}" to return {1}', setVolume: 'tell application "{0}" to set sound volume to {1}', mute: 'tell application "{0}" to set sound volume to 0', unMute: 'tell application "{0}" to set sound volume to {1}', setShuffling: 'tell application "{0}" to set {1} to {2}', isShuffling: 'tell application "{0}" to {1}', playlistNames: { file: "get_playlist_names.{0}.applescript", }, playTrackOfPlaylist: { file: "play_track_of_playlist.{0}.applescript", }, playlistTracksOfPlaylist: { file: "get_playlist_songs.{0}.applescript", requiresArgv: true, }, setItunesLoved: 'tell application "{0}" to set loved of current track to {1}', playlistTrackCounts: { file: "get_playlist_count.{0}.applescript", requiresArgv: false, }, activate: 'tell application "{0}" to activate', }; // } static getInstance() { if (!MusicController.instance) { MusicController.instance = new MusicController(); } return MusicController.instance; } async isMusicPlayerActive(player) { player = musicUtil.getPlayerName(player); if (!musicUtil.isMac()) { return false; } let appName = "Spotify.app"; if (player === models_1.PlayerName.ItunesDesktop) { appName = "iTunes.app"; } let command = `ps -ef | grep "${appName}" | grep -v grep`; if (player === models_1.PlayerName.ItunesDesktop) { // make sure it's not the cache extension process command = `${command} | grep -i "visualizer"`; } command = `${command} | awk '{print $2}'`; // this returns the PID of the requested player const result = await musicUtil.execCmd(command); if (result && !result.error) { return true; } return false; } async stopPlayer(player) { if (!musicUtil.isMac()) { return ""; } /** * ps -ef | grep "Spotify.app" | grep -v grep | awk '{print $2}' | xargs kill * ps -ef | grep "iTunes.app" | grep -v grep | awk '{print $2}' | xargs kill */ let appName = "Spotify.app"; if (player === models_1.PlayerName.ItunesDesktop) { appName = "iTunes.app"; } const command = `ps -ef | grep "${appName}" | grep -v grep | awk '{print $2}' | xargs kill`; let result = await musicUtil.execCmd(command); if (result === null || result === undefined || result === "") { result = "ok"; } return result; } async startPlayer(player, options = {}) { let launchResult = "ok"; if (musicUtil.isWindows()) { launchResult = await this.launchPlayerWithCommand("cmd /c spotify.exe"); if (launchResult && launchResult.error) { // try using the %APPDATA%/Spotify/Spotify.exe command launchResult = await this.launchPlayerWithCommand("%APPDATA%/Spotify/Spotify.exe"); if (launchResult && launchResult.error) { // try with roaming/spotify launchResult = await this.launchPlayerWithCommand("%APPDATA%/Roaming/Spotify/Spotify.exe"); if (launchResult && launchResult.error) { // try it with START launchResult = await this.launchPlayerWithCommand("START SPOTIFY"); if (launchResult && launchResult.error) { // try with just spotify launchResult = await this.launchPlayerWithCommand("spotify"); if (launchResult && launchResult.error) { // try with spotify exe launchResult = await this.launchPlayerWithCommand("spotify.exe"); if (launchResult && launchResult.error) { const homedir = os.homedir(); const cmd = `${homedir}/AppData/Roaming/Spotify/Spotify.exe`; launchResult = await this.launchPlayerWithCommand(cmd); } } } } } } return launchResult; } else if (musicUtil.isLinux()) { launchResult = await this.launchPlayerWithCommand("snap install spotify"); if (launchResult && launchResult.error) { launchResult = await this.launchPlayerWithCommand("flatpak run com.spotify.Client"); if (launchResult && launchResult.error) { // try with just spotify launchResult = await this.launchPlayerWithCommand("spotify"); if (launchResult && launchResult.error) { launchResult = await this.launchPlayerWithCommand("/usr/bin/spotify"); } } } return launchResult; } if (player === models_1.PlayerName.SpotifyDesktop || player === models_1.PlayerName.ItunesDesktop) { launchResult = await this.run(player, "activate"); if (launchResult && launchResult.error) { // try launching with the start command return await this.startMacPlayer(player, options); } } return await this.startMacPlayer(player, options); } async startMacPlayer(player, options = {}) { player = musicUtil.getPlayerName(player); let quietly = true; if (options && options.quietly !== undefined && options.quietly !== null) { quietly = options.quietly; } const command = quietly ? `open -a ${player} -gj` : `open -a ${player}`; let result = await musicUtil.execCmd(command); if (result === null || result === undefined || result === "") { result = "ok"; } return result; } async launchPlayerWithCommand(command) { let result = await musicUtil.execCmd(command); if (result === null || result === undefined || result === "") { result = "ok"; } return result; } async execScript(player, scriptName, params = null, argv = null) { player = musicUtil.getPlayerName(player); let script = this.scripts[scriptName]; if (!params) { // set player to the params params = [player]; } else { // push the player to the front of the params array params.unshift(player); } let command = ""; // get the script file if the attribut has one if (script.file) { // apply the params (which should only have the player name) const scriptFile = musicUtil.formatString(script.file, params); let file = `${this.scriptsPath}${scriptFile}`; if (argv) { // make sure they have quotes around the argv argv = argv.map((val) => { return `"${val}"`; }); const argvOptions = argv.join(" "); command = `osascript ${file} ${argvOptions}`; } else { command = `osascript ${file}`; } } else { if (scriptName === "play" && player.toLowerCase() === "itunes") { // if itunes is not currently running, default to play from the // user's default playlist let itunesTrack = await this.run(models_1.PlayerName.ItunesDesktop, "state"); if (itunesTrack) { // make it an object try { itunesTrack = JSON.parse(itunesTrack); if (!itunesTrack || !itunesTrack.id) { // play from the user's default playlist script = this.scripts.playFromLibrary; params.push("Library"); } } catch (err) { // play from the user's default playlist script = this.scripts.playFromLibrary; params.push("Library"); } } } else if (scriptName === "playTrackInContext") { if (player === models_1.PlayerName.ItunesDesktop) { params.splice(2, 0, "of playlist"); } else { params.splice(2, 0, "in context"); } } // apply the params to the one line script script = musicUtil.formatString(script, params); command = `osascript -e \'${script}\'`; } let result = await musicUtil.execCmd(command); if (result === null || result === undefined || result === "") { result = "ok"; } return result; } async run(player, scriptName, params = null, argv = null) { player = musicUtil.getPlayerName(player); if (player === models_1.PlayerName.SpotifyDesktop) { if (scriptName === "repeatOn") { params = ["repeating", "true"]; } else if (scriptName === "repeatOff") { params = ["repeating", "false"]; } else if (scriptName === "isRepeating") { params = ["repeating"]; } else if (scriptName === "setShuffling") { // this will already have params params.unshift("shuffling"); } else if (scriptName === "isShuffling") { params = ["return shuffling"]; } } else if (player === models_1.PlayerName.ItunesDesktop) { if (scriptName === "repeatOn") { // repeat one for itunes params = ["song repeat", "one"]; } else if (scriptName === "repeatOff") { // repeat off for itunes params = ["song repeat", "off"]; } else if (scriptName === "isRepeating") { // get the song repeat value params = ["song repeat"]; } else if (scriptName === "setShuffling") { params.unshift("shuffle enabled"); } else if (scriptName === "isShuffling") { params = ["get shuffle enabled"]; } } if (scriptName === "mute") { // get the current volume state let stateResult = await this.execScript(player, "state"); let json = JSON.parse(stateResult); this.lastVolumeLevel = json.volume; } else if (scriptName === "unMute") { params = [this.lastVolumeLevel]; } else if (scriptName === "next" || scriptName === "previous") { // make sure it's not on repeat if (player === models_1.PlayerName.SpotifyDesktop) { await this.execScript(player, "state", ["repeating", "false"]); } else { await this.execScript(player, "state", ["song repeat", "off"]); } } return this.execScript(player, scriptName, params, argv).then(async (result) => { if (result && result.error && result.error.toLowerCase().includes("not authorized")) { // reset the apple events to show the request access again // await musicUtil.execCmd("tccutil reset AppleEvents"); // result = await this.execScript( // player, // scriptName, // params, // argv // ); return "[GRANT_ERROR] Desktop Player Access Not Authorized"; } if (result === null || result === undefined || result === "") { if (player === models_1.PlayerName.ItunesDesktop) { store_1.MusicStore.getInstance().itunesAccessGranted = true; } result = "ok"; } return result; }); } setVolume(player, volume) { this.lastVolumeLevel = volume; return this.execScript(player, "setVolume", [volume]).then((result) => { if (result === null || result === undefined || result === "") { result = "ok"; } return result; }); } setItunesLoved(loved) { return this.execScript(models_1.PlayerName.ItunesDesktop, "setItunesLoved", [ loved, ]) .then((result) => { if (result === null || result === undefined || result === "") { result = "ok"; } return result; }) .catch((err) => { return false; }); } playSpotifyDesktopTrack(trackId = "", playlistId = "") { trackId = musicUtil.createUriFromTrackId(trackId); if (playlistId) { playlistId = musicUtil.createPlaylistUriFromPlaylistId(playlistId); } if (playlistId) { this.playTrackInContext(models_1.PlayerName.SpotifyDesktop, [ trackId, playlistId, ]); } else { this.run(models_1.PlayerName.SpotifyDesktop, "playTrack", [trackId]); } } playTrackInContext(player, params) { return this.execScript(player, "playTrackInContext", params).then((result) => { if (result === null || result === undefined || result === "") { result = "ok"; } return result; }); } async playPauseSpotifyDevice(device_id, play) { const payload = { device_ids: [device_id], play, }; return musicClient.spotifyApiPut("/v1/me/player", {}, payload); } async spotifyWebPlayPlaylist(playlistId, startingTrackId = "", deviceId = "") { const qsOptions = deviceId ? { device_id: deviceId } : {}; playlistId = musicUtil.createPlaylistUriFromPlaylistId(playlistId); const trackUris = musicUtil.createUrisFromTrackIds([startingTrackId]); // play playlist needs body params... // {"context_uri":"spotify:playlist:<id>"} let payload = { offset: { position: 0 }, }; // playlistId is required payload["context_uri"] = playlistId; if (trackUris && trackUris.length > 0) { payload.offset = { uri: trackUris[0], }; } const api = "/v1/me/player/play"; let response = await musicClient.spotifyApiPut(api, qsOptions, payload); // check if the token needs to be refreshed if (response.status === 401) { // refresh the token await musicClient.refreshSpotifyToken(); // try again response = await musicClient.spotifyApiPut(api, qsOptions, payload); } return response; } async spotifyWebPlayTrack(trackId, deviceId = "") { /** * 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\"]}" */ const trackUris = musicUtil.createUrisFromTrackIds([trackId]); const qsOptions = deviceId ? { device_id: deviceId } : {}; const payload = { uris: trackUris, }; const api = "/v1/me/player/play"; let response = await musicClient.spotifyApiPut(api, qsOptions, payload); // check if the token needs to be refreshed if (response.status === 401) { // refresh the token await musicClient.refreshSpotifyToken(); // try again response = await musicClient.spotifyApiPut(api, qsOptions, payload); } return response; } async spotifyWebPlay(options) { const qsOptions = options.device_id ? { device_id: options.device_id } : {}; let payload = {}; if (options.uris) { payload["uris"] = options.uris; } else if (options.track_ids) { payload["uris"] = musicUtil.createUrisFromTrackIds(options.track_ids); } // "offset": {"position": 5} if (options.offset !== undefined && options.offset !== null) { // payload["offset"] = options.offset; payload["offset"] = { position: options.offset }; } if (options.context_uri) { payload["context_uri"] = options.context_uri; } // change the offset to the 1st uri if the // context uri is also used // context_uri refers to: album or playlist object if (payload.context_uri && payload.uris) { if (options.offset !== undefined && options.offset !== null) { payload["offset"] = { ...payload.offset, uri: payload.uris[0], }; } else { payload["offset"] = { uri: payload.uris[0], }; } delete payload.uris; } const api = "/v1/me/player/play"; let response = await musicClient.spotifyApiPut(api, qsOptions, payload); // check if the token needs to be refreshed if (response.status === 401) { // refresh the token await musicClient.refreshSpotifyToken(); // try again response = await musicClient.spotifyApiPut(api, qsOptions, payload); } return response; } async spotifyWebPause(options) { const qsOptions = options.device_id ? { device_id: options.device_id } : {}; let payload = {}; if (options.uris) { payload["uris"] = options.uris; } else if (options.track_ids) { payload["uris"] = musicUtil.createUrisFromTrackIds(options.track_ids); } const api = "/v1/me/player/pause"; 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 spotifyWebPrevious(options) { const qsOptions = options.device_id ? { device_id: options.device_id } : {}; let payload = {}; if (options.uris) { payload["uris"] = options.uris; } else if (options.track_ids) { payload["uris"] = musicUtil.createUrisFromTrackIds(options.track_ids); } const api = "/v1/me/player/previous"; let codyResp = await musicClient.spotifyApiPost(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.spotifyApiPost(api, qsOptions, payload); } return codyResp; } async spotifyWebNext(options) { const qsOptions = options.device_id ? { device_id: options.device_id } : {}; let payload = {}; if (options.uris) { payload["uris"] = options.uris; } else if (options.track_ids) { payload["uris"] = musicUtil.createUrisFromTrackIds(options.track_ids); } const api = "/v1/me/player/next"; let codyResp = await musicClient.spotifyApiPost(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.spotifyApiPost(api, qsOptions, payload); } return codyResp; } async getGenre(artist, songName = "", spotifyArtistId = "") { let genre = await musicClient.getGenreFromItunes(artist, songName); if (!genre || genre === "") { genre = await this.getGenreFromSpotify(artist, spotifyArtistId).catch((e) => { return ""; }); } return genre; } getHighestFrequencySpotifyGenre(genreList) { return musicClient.getHighestFrequencySpotifyGenre(genreList); } async getGenreFromSpotify(artist, spotifyArtistId = "") { let response = null; let genre = ""; if (spotifyArtistId) { // make sure it's just the ID part spotifyArtistId = musicUtil.createSpotifyIdFromUri(spotifyArtistId); } response = await musicClient.getGenreFromSpotify(artist, spotifyArtistId); // check if the token needs to be refreshed if (response.status === 401) { // refresh the token await musicClient.refreshSpotifyToken(); // try again response = await musicClient.getGenreFromSpotify(artist, spotifyArtistId); } genre = response.data; return genre; } /** * Kills the Spotify desktop player if it's running * @param player {spotify|spotify-web|itunes} */ quitApp(player) { if (player === models_1.PlayerName.ItunesDesktop) { return this.stopPlayer(models_1.PlayerName.ItunesDesktop); } else { return this.stopPlayer(models_1.PlayerName.SpotifyDesktop); } } /** * Launches the desktop player * @param player {spotify|spotify-web|itunes} */ launchApp(player) { if (player === models_1.PlayerName.ItunesDesktop) { return this.startPlayer(models_1.PlayerName.ItunesDesktop); } return this.startPlayer(models_1.PlayerName.SpotifyDesktop); } } exports.MusicController = MusicController; MusicController.WINDOWS_SPOTIFY_TRACK_FIND = 'tasklist /fi "imagename eq Spotify.exe" /fo list /v | find " - "'; //# sourceMappingURL=controller.js.map