UNPKG

cody-music

Version:

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

752 lines (684 loc) 26.5 kB
import { MusicUtil } from "./util"; import { MusicClient } from "./client"; import { PlayerName } from "./models"; import { MusicStore } from "./store"; const os = require("os"); const musicClient = MusicClient.getInstance(); const musicUtil = new MusicUtil(); export class MusicController { static readonly WINDOWS_SPOTIFY_TRACK_FIND: string = 'tasklist /fi "imagename eq Spotify.exe" /fo list /v | find " - "'; private scriptsPath: string = __dirname + "/scripts/"; private lastVolumeLevel: any = null; // applscript music commands and scripts private scripts: any = { 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', }; private static instance: MusicController; private constructor() { // } static getInstance() { if (!MusicController.instance) { MusicController.instance = new MusicController(); } return MusicController.instance; } async isMusicPlayerActive(player: PlayerName) { player = musicUtil.getPlayerName(player); if (!musicUtil.isMac()) { return false; } let appName = "Spotify.app"; if (player === PlayerName.ItunesDesktop) { appName = "iTunes.app"; } let command = `ps -ef | grep "${appName}" | grep -v grep`; if (player === 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: PlayerName) { 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 === 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: string, options: any = {}): Promise<any> { let launchResult: any = "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 === PlayerName.SpotifyDesktop || player === 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: string, options: any = {}) { 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: string) { let result = await musicUtil.execCmd(command); if (result === null || result === undefined || result === "") { result = "ok"; } return result; } async execScript( player: string, scriptName: string, params: any = null, argv: any = 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: any) => { 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( 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 === 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: PlayerName, scriptName: string, params: any = null, argv: any = null ) { player = musicUtil.getPlayerName(player); if (player === 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 === 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 === 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 === PlayerName.ItunesDesktop) { MusicStore.getInstance().itunesAccessGranted = true; } result = "ok"; } return result; } ); } setVolume(player: string, volume: number) { this.lastVolumeLevel = volume; return this.execScript(player, "setVolume", [volume]).then((result) => { if (result === null || result === undefined || result === "") { result = "ok"; } return result; }); } setItunesLoved(loved: boolean) { return this.execScript(PlayerName.ItunesDesktop, "setItunesLoved", [ loved, ]) .then((result) => { if (result === null || result === undefined || result === "") { result = "ok"; } return result; }) .catch((err) => { return false; }); } playSpotifyDesktopTrack(trackId: string = "", playlistId: string = "") { trackId = musicUtil.createUriFromTrackId(trackId); if (playlistId) { playlistId = musicUtil.createPlaylistUriFromPlaylistId(playlistId); } if (playlistId) { this.playTrackInContext(PlayerName.SpotifyDesktop, [ trackId, playlistId, ]); } else { this.run(PlayerName.SpotifyDesktop, "playTrack", [trackId]); } } playTrackInContext(player: string, params: any[]) { return this.execScript(player, "playTrackInContext", params).then( (result) => { if (result === null || result === undefined || result === "") { result = "ok"; } return result; } ); } public async playPauseSpotifyDevice(device_id: string, play: boolean) { const payload = { device_ids: [device_id], play, }; return musicClient.spotifyApiPut("/v1/me/player", {}, payload); } public async spotifyWebPlayPlaylist( playlistId: string, startingTrackId: string = "", deviceId: string = "" ) { 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: any = { 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; } public async spotifyWebPlayTrack(trackId: string, deviceId: string = "") { /** * 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; } public async spotifyWebPlay(options: any) { const qsOptions = options.device_id ? { device_id: options.device_id } : {}; let payload: any = {}; 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; } public async spotifyWebPause(options: any) { const qsOptions = options.device_id ? { device_id: options.device_id } : {}; let payload: any = {}; 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; } public async spotifyWebPrevious(options: any) { const qsOptions = options.device_id ? { device_id: options.device_id } : {}; let payload: any = {}; 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; } public async spotifyWebNext(options: any) { const qsOptions = options.device_id ? { device_id: options.device_id } : {}; let payload: any = {}; 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; } public async getGenre( artist: string, songName: string = "", spotifyArtistId: string = "" ): Promise<string> { let genre = await musicClient.getGenreFromItunes(artist, songName); if (!genre || genre === "") { genre = await this.getGenreFromSpotify( artist, spotifyArtistId ).catch((e) => { return ""; }); } return genre; } public getHighestFrequencySpotifyGenre(genreList: string[]) { return musicClient.getHighestFrequencySpotifyGenre(genreList); } public async getGenreFromSpotify( artist: string, spotifyArtistId: string = "" ): Promise<string> { 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} */ public quitApp(player: PlayerName) { if (player === PlayerName.ItunesDesktop) { return this.stopPlayer(PlayerName.ItunesDesktop); } else { return this.stopPlayer(PlayerName.SpotifyDesktop); } } /** * Launches the desktop player * @param player {spotify|spotify-web|itunes} */ public launchApp(player: PlayerName) { if (player === PlayerName.ItunesDesktop) { return this.startPlayer(PlayerName.ItunesDesktop); } return this.startPlayer(PlayerName.SpotifyDesktop); } }