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
text/typescript
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);
}
}