Merge branch 'develop'
This commit is contained in:
commit
df2f7b7216
195 changed files with 62285 additions and 12895 deletions
342
src/main/plugins/chromecast.ts
Normal file
342
src/main/plugins/chromecast.ts
Normal file
|
@ -0,0 +1,342 @@
|
|||
import * as electron from 'electron';
|
||||
import * as os from 'os';
|
||||
import {resolve} from 'path';
|
||||
import * as CiderReceiver from '../base/castreceiver';
|
||||
|
||||
export default class ChromecastPlugin {
|
||||
|
||||
/**
|
||||
* Private variables for interaction in plugins
|
||||
*/
|
||||
private _win: any;
|
||||
private _app: any;
|
||||
private _lastfm: any;
|
||||
private _store: any;
|
||||
private _timer: any;
|
||||
private audioClient = require('castv2-client').Client;
|
||||
private mdns = require('mdns-js');
|
||||
|
||||
private devices : any = [];
|
||||
private castDevices : any = [];
|
||||
|
||||
// private GCRunning = false;
|
||||
// private GCBuffer: any;
|
||||
// private expectedConnections = 0;
|
||||
// private currentConnections = 0;
|
||||
private activeConnections : any = [];
|
||||
// private requests = [];
|
||||
// private GCstream = new Stream.PassThrough(),
|
||||
private connectedHosts : any = {};
|
||||
// private port = false;
|
||||
// private server = false;
|
||||
// private bufcount = 0;
|
||||
// private bufcount2 = 0;
|
||||
// private headerSent = false;
|
||||
|
||||
|
||||
private searchForGCDevices() {
|
||||
try {
|
||||
|
||||
let browser = this.mdns.createBrowser(this.mdns.tcp('googlecast'));
|
||||
browser.on('ready', browser.discover);
|
||||
|
||||
browser.on('update', (service :any) => {
|
||||
if (service.addresses && service.fullname && service.fullname.includes('_googlecast._tcp')) {
|
||||
this.ondeviceup(service.addresses[0], service.fullname.substring(0, service.fullname.indexOf("._googlecast")) + " " + (service.type[0].description ?? ""), '', 'googlecast');
|
||||
}
|
||||
});
|
||||
const Client = require('node-ssdp').Client;
|
||||
// also do a SSDP/UPnP search
|
||||
let ssdpBrowser = new Client();
|
||||
ssdpBrowser.on('response', (headers :any , statusCode : any, rinfo: any) => {
|
||||
var location = getLocation(headers);
|
||||
if (location != null) {
|
||||
this.getServiceDescription(location, rinfo.address);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function getLocation(headers: any) {
|
||||
let location = null;
|
||||
if (headers["LOCATION"] != null ){location = headers["LOCATION"]}
|
||||
else if (headers["Location"] != null ){location = headers["Location"]}
|
||||
return location;
|
||||
}
|
||||
|
||||
ssdpBrowser.search('urn:dial-multiscreen-org:device:dial:1');
|
||||
|
||||
// // actual upnp devices
|
||||
// if (app.cfg.get("audio.enableDLNA")) {
|
||||
// let ssdpBrowser2 = new Client();
|
||||
// ssdpBrowser2.on('response', (headers, statusCode, rinfo) => {
|
||||
// var location = getLocation(headers);
|
||||
// if (location != null) {
|
||||
// this.getServiceDescription(location, rinfo.address);
|
||||
// }
|
||||
|
||||
// });
|
||||
// ssdpBrowser2.search('urn:schemas-upnp-org:device:MediaRenderer:1');
|
||||
|
||||
// }
|
||||
|
||||
|
||||
} catch (e) {
|
||||
console.log('Search GC err', e);
|
||||
}
|
||||
}
|
||||
|
||||
private getServiceDescription(url:any, address:any) {
|
||||
const request = require('request');
|
||||
request.get(url, (error: any, response: any, body: any) => {
|
||||
if (!error && response.statusCode === 200) {
|
||||
this.parseServiceDescription(body, address, url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ondeviceup(host: any, name: any, location: any, type: any) {
|
||||
if (this.castDevices.findIndex((item:any) => item.host === host && item.name === name && item.location === location && item.type === type) === -1) {
|
||||
this.castDevices.push({
|
||||
name: name,
|
||||
host: host,
|
||||
location: location,
|
||||
type: type
|
||||
});
|
||||
if (this.devices.indexOf(host) === -1) {
|
||||
this.devices.push(host);
|
||||
}
|
||||
if (name) {
|
||||
this._win.webContents.executeJavaScript(`console.log('deviceFound','ip: ${host} name:${name}')`).catch((err: any) => console.error(err));
|
||||
console.log("deviceFound", host, name);
|
||||
}
|
||||
} else {
|
||||
this._win.webContents.executeJavaScript(`console.log('deviceFound (added)','ip: ${host} name:${name}')`).catch((err: any) => console.error(err));
|
||||
console.log("deviceFound (added)", host, name);
|
||||
}
|
||||
}
|
||||
|
||||
private parseServiceDescription(body: any, address: any, url: any) {
|
||||
const parseString = require('xml2js').parseString;
|
||||
parseString(body, (err: any, result: any) => {
|
||||
if (!err && result && result.root && result.root.device) {
|
||||
const device = result.root.device[0];
|
||||
console.log('device', device);
|
||||
let devicetype = 'googlecast';
|
||||
console.log()
|
||||
if (device.deviceType && device.deviceType.toString() === 'urn:schemas-upnp-org:device:MediaRenderer:1') {
|
||||
devicetype = 'upnp';
|
||||
}
|
||||
this.ondeviceup(address, device.friendlyName.toString(), url, devicetype);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private loadMedia(client: any, song: any, artist: any, album: any, albumart: any, cb?: any) {
|
||||
// const u = 'http://' + this.getIp() + ':' + server.address().port + '/';
|
||||
// const DefaultMediaReceiver : any = require('castv2-client').DefaultMediaReceiver;
|
||||
client.launch(CiderReceiver, (err: any, player: any) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
return;
|
||||
}
|
||||
let media = {
|
||||
// Here you can plug an URL to any mp4, webm, mp3 or jpg file with the proper contentType.
|
||||
contentId: 'http://' + this.getIp() + ':9000/audio.wav',
|
||||
contentType: 'audio/wav',
|
||||
streamType: 'LIVE', // or LIVE
|
||||
|
||||
// Title and cover displayed while buffering
|
||||
metadata: {
|
||||
type: 0,
|
||||
metadataType: 3,
|
||||
title: song ?? "",
|
||||
albumName: album ?? "",
|
||||
artist: artist ?? "",
|
||||
images: [
|
||||
{url: albumart ?? ""}]
|
||||
}
|
||||
};
|
||||
|
||||
player.on('status', (status: any) => {
|
||||
console.log('status broadcast playerState=%s', status);
|
||||
});
|
||||
|
||||
console.log('app "%s" launched, loading media %s ...', player, media);
|
||||
|
||||
player.load(media, {
|
||||
autoplay: true
|
||||
}, (err: any, status: any) => {
|
||||
console.log('media loaded playerState=%s', status);
|
||||
});
|
||||
|
||||
|
||||
client.getStatus((x: any, status: any) => {
|
||||
if (status && status.volume) {
|
||||
client.volume = status.volume.level;
|
||||
client.muted = status.volume.muted;
|
||||
client.stepInterval = status.volume.stepInterval;
|
||||
}
|
||||
})
|
||||
|
||||
// send websocket ip
|
||||
|
||||
player.sendIp("ws://"+this.getIp()+":26369");
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private getIp() {
|
||||
let ip = false;
|
||||
let alias = 0;
|
||||
let ifaces: any = os.networkInterfaces();
|
||||
for (var dev in ifaces) {
|
||||
ifaces[dev].forEach((details:any) => {
|
||||
if (details.family === 'IPv4') {
|
||||
if (!/(loopback|vmware|internal|hamachi|vboxnet|virtualbox)/gi.test(dev + (alias ? ':' + alias : ''))) {
|
||||
if (details.address.substring(0, 8) === '192.168.' ||
|
||||
details.address.substring(0, 7) === '172.16.' ||
|
||||
details.address.substring(0, 3) === '10.'
|
||||
) {
|
||||
ip = details.address;
|
||||
++alias;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
private stream(device: any, song: any, artist: any, album: any, albumart: any) {
|
||||
let castMode = 'googlecast';
|
||||
let UPNPDesc = '';
|
||||
castMode = device.type;
|
||||
UPNPDesc = device.location;
|
||||
|
||||
let client;
|
||||
if (castMode === 'googlecast') {
|
||||
let client = new this.audioClient();
|
||||
client.volume = 100;
|
||||
client.stepInterval = 0.5;
|
||||
client.muted = false;
|
||||
|
||||
client.connect(device.host, () => {
|
||||
// console.log('connected, launching app ...', 'http://' + this.getIp() + ':' + server.address().port + '/');
|
||||
if (!this.connectedHosts[device.host]) {
|
||||
this.connectedHosts[device.host] = client;
|
||||
this.activeConnections.push(client);
|
||||
}
|
||||
this.loadMedia(client, song, artist, album, albumart);
|
||||
});
|
||||
|
||||
client.on('close', () => {
|
||||
console.info("Client Closed");
|
||||
for (let i = this.activeConnections.length - 1; i >= 0; i--) {
|
||||
if (this.activeConnections[i] === client) {
|
||||
this.activeConnections.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
client.on('error', (err: any) => {
|
||||
console.log('Error: %s', err.message);
|
||||
client.close();
|
||||
delete this.connectedHosts[device.host];
|
||||
});
|
||||
|
||||
} else {
|
||||
// upnp devices
|
||||
//try {
|
||||
// client = new MediaRendererClient(UPNPDesc);
|
||||
// const options = {
|
||||
// autoplay: true,
|
||||
// contentType: 'audio/x-wav',
|
||||
// dlnaFeatures: 'DLNA.ORG_PN=-;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01700000000000000000000000000000',
|
||||
// metadata: {
|
||||
// title: 'Apple Music Electron',
|
||||
// creator: 'Streaming ...',
|
||||
// type: 'audio', // can be 'video', 'audio' or 'image'
|
||||
// // url: 'http://' + getIp() + ':' + server.address().port + '/',
|
||||
// // protocolInfo: 'DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000;
|
||||
// }
|
||||
// };
|
||||
|
||||
// client.load('http://' + getIp() + ':' + server.address().port + '/a.wav', options, function (err, _result) {
|
||||
// if (err) throw err;
|
||||
// console.log('playing ...');
|
||||
// });
|
||||
|
||||
// } catch (e) {
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
private async setupGCServer(){
|
||||
return ''
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Base Plugin Details (Eventually implemented into a GUI in settings)
|
||||
*/
|
||||
public name: string = 'Chromecast';
|
||||
public description: string = 'LastFM plugin for Cider';
|
||||
public version: string = '0.0.1';
|
||||
public author: string = 'vapormusic / Cider Collective';
|
||||
|
||||
/**
|
||||
* Runs on plugin load (Currently run on application start)
|
||||
*/
|
||||
constructor(app: any, store: any) {
|
||||
this._app = app;
|
||||
this._store = store
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on app ready
|
||||
*/
|
||||
onReady(win: any): void {
|
||||
this._win = win;
|
||||
electron.ipcMain.on('getKnownCastDevices', (event) => {
|
||||
event.returnValue = this.castDevices
|
||||
});
|
||||
|
||||
electron.ipcMain.on('performGCCast', (event, device, song, artist, album, albumart) => {
|
||||
// this.setupGCServer().then( () => {
|
||||
this._win.webContents.setAudioMuted(true);
|
||||
console.log(device);
|
||||
this.stream(device, song, artist, album, albumart);
|
||||
// })
|
||||
});
|
||||
|
||||
electron.ipcMain.on('getChromeCastDevices', (_event, _data) => {
|
||||
this.searchForGCDevices();
|
||||
});
|
||||
|
||||
electron.ipcMain.on('stopGCast', (_event) => {
|
||||
this._win.webContents.setAudioMuted(false);
|
||||
this.activeConnections = [];
|
||||
this.connectedHosts = {};
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on app stop
|
||||
*/
|
||||
onBeforeQuit(): void {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on song change
|
||||
* @param attributes Music Attributes
|
||||
*/
|
||||
onNowPlayingItemDidChange(attributes: any): void {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
import * as RPC from 'discord-rpc'
|
||||
import {ipcMain} from "electron";
|
||||
import fetch from 'electron-fetch'
|
||||
|
||||
export default class DiscordRichPresence {
|
||||
|
||||
|
@ -6,6 +8,8 @@ export default class DiscordRichPresence {
|
|||
* Private variables for interaction in plugins
|
||||
*/
|
||||
private static _store: any;
|
||||
private _app : any;
|
||||
private _attributes : any;
|
||||
private static _connection: boolean = false;
|
||||
|
||||
/**
|
||||
|
@ -29,6 +33,7 @@ export default class DiscordRichPresence {
|
|||
smallImageText: '',
|
||||
instance: false
|
||||
};
|
||||
|
||||
private _activityCache: RPC.Presence = {
|
||||
details: '',
|
||||
state: '',
|
||||
|
@ -58,7 +63,6 @@ export default class DiscordRichPresence {
|
|||
|
||||
// Create the client
|
||||
this._client = new RPC.Client({transport: "ipc"});
|
||||
|
||||
// Runs on Ready
|
||||
this._client.on('ready', () => {
|
||||
console.info(`[DiscordRPC][connect] Successfully Connected to Discord. Authed for user: ${this._client.user.id}.`);
|
||||
|
@ -90,6 +94,44 @@ export default class DiscordRichPresence {
|
|||
}).catch((e: any) => console.error(`[DiscordRPC][disconnect] ${e}`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the Discord activity object
|
||||
*/
|
||||
private filterActivity(activity: any, attributes: any): Object {
|
||||
|
||||
// Checks if the name is greater than 128 because some songs can be that long
|
||||
if (activity.details && activity.details.length > 128) {
|
||||
activity.details = activity.details.substring(0, 125) + '...'
|
||||
}
|
||||
|
||||
// Check large image
|
||||
if (activity.largeImageKey == null || activity.largeImageKey === "" || activity.largeImageKey.length > 256) {
|
||||
activity.largeImageKey = "cider";
|
||||
}
|
||||
|
||||
// Timestamp
|
||||
if (new Date(attributes.endTime).getTime() < 0) {
|
||||
delete activity.startTime
|
||||
delete activity.endTime
|
||||
}
|
||||
|
||||
// not sure
|
||||
if (!attributes.artistName) {
|
||||
delete activity.state;
|
||||
}
|
||||
|
||||
if (!activity.largeImageText || activity.largeImageText.length < 2) {
|
||||
delete activity.largeImageText
|
||||
}
|
||||
|
||||
activity.buttons.forEach((key: {label: string, url: string}, _v: Number) => {
|
||||
if (key.url.includes('undefined') || key.url.includes('no-id-found')) {
|
||||
activity.buttons.splice(key, 1);
|
||||
}
|
||||
})
|
||||
return activity
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the activity of the client
|
||||
* @param {object} attributes
|
||||
|
@ -105,23 +147,18 @@ export default class DiscordRichPresence {
|
|||
this._activity = {
|
||||
details: attributes.name,
|
||||
state: `${attributes.artistName ? `by ${attributes.artistName}` : ''}`,
|
||||
startTimestamp: ((new Date(attributes.endTime).getTime() < 0) ? null : attributes.startTime),
|
||||
endTimestamp: ((new Date(attributes.endTime).getTime() < 0) ? null : attributes.endTime),
|
||||
largeImageKey: (attributes.artwork.url.replace('{w}', '1024').replace('{h}', '1024')) ?? 'cider',
|
||||
startTimestamp: attributes.startTime,
|
||||
endTimestamp: attributes.endTime,
|
||||
largeImageKey: attributes?.artwork?.url?.replace('{w}', '1024').replace('{h}', '1024'),
|
||||
largeImageText: attributes.albumName,
|
||||
instance: false, // Whether the activity is in a game session
|
||||
|
||||
buttons: [
|
||||
{label: "Listen on Cider", url: attributes.url.cider},
|
||||
{label: "View on Apple Music", url: attributes.url.appleMusic},
|
||||
]
|
||||
] //To change attributes.url => preload/cider-preload.js
|
||||
};
|
||||
|
||||
|
||||
// Checks if the name is greater than 128 because some songs can be that long
|
||||
if (this._activity.details && this._activity.details.length > 128) {
|
||||
this._activity.details = this._activity.details.substring(0, 125) + '...'
|
||||
}
|
||||
this._activity = this.filterActivity(this._activity, attributes)
|
||||
|
||||
// Check if its pausing (false) or playing (true)
|
||||
if (!attributes.status) {
|
||||
|
@ -136,7 +173,6 @@ export default class DiscordRichPresence {
|
|||
this._client.setActivity(this._activity)
|
||||
.catch((e: any) => console.error(`[DiscordRichPresence][setActivity] ${e}`));
|
||||
}
|
||||
|
||||
} else if (this._activity && this._activityCache !== this._activity && this._activity.details) {
|
||||
if (!DiscordRichPresence._store.general.discord_rpc_clear_on_pause) {
|
||||
this._activity.smallImageKey = 'play';
|
||||
|
@ -157,17 +193,37 @@ export default class DiscordRichPresence {
|
|||
/**
|
||||
* Runs on plugin load (Currently run on application start)
|
||||
*/
|
||||
constructor(_app: any, store: any) {
|
||||
constructor(app: any, store: any) {
|
||||
DiscordRichPresence._store = store
|
||||
console.debug(`[Plugin][${this.name}] Loading Complete.`);
|
||||
this._app = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on app ready
|
||||
*/
|
||||
onReady(_win: any): void {
|
||||
let self = this
|
||||
this.connect((DiscordRichPresence._store.general.discord_rpc == 1) ? '911790844204437504' : '886578863147192350');
|
||||
console.debug(`[Plugin][${this.name}] Ready.`);
|
||||
ipcMain.on('updateRPCImage', (_event, imageurl) => {
|
||||
if (!DiscordRichPresence._store.general.privateEnabled){
|
||||
fetch('https://api.cider.sh/v1/images' ,{
|
||||
|
||||
method: 'POST',
|
||||
body: JSON.stringify({url : imageurl}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': _win.webContents.getUserAgent()
|
||||
},
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(function(json){
|
||||
self._attributes["artwork"]["url"] = json.url
|
||||
self.updateActivity(self._attributes)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -179,10 +235,12 @@ export default class DiscordRichPresence {
|
|||
|
||||
/**
|
||||
* Runs on playback State Change
|
||||
* @param attributes Music Attributes (attributes.state = current state)
|
||||
* @param attributes Music Attributes (attributes.status = current state)
|
||||
*/
|
||||
onPlaybackStateDidChange(attributes: object): void {
|
||||
this.updateActivity(attributes)
|
||||
if (!DiscordRichPresence._store.general.privateEnabled){
|
||||
this._attributes = attributes
|
||||
this.updateActivity(attributes)}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -190,6 +248,8 @@ export default class DiscordRichPresence {
|
|||
* @param attributes Music Attributes
|
||||
*/
|
||||
onNowPlayingItemDidChange(attributes: object): void {
|
||||
this.updateActivity(attributes)
|
||||
if (!DiscordRichPresence._store.general.privateEnabled){
|
||||
this._attributes = attributes
|
||||
this.updateActivity(attributes)}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ export default class LastFMPlugin {
|
|||
private _app: any;
|
||||
private _lastfm: any;
|
||||
private _store: any;
|
||||
private _timer: any;
|
||||
|
||||
private authenticateFromFile() {
|
||||
let sessionData = require(this.sessionPath)
|
||||
|
@ -77,64 +78,52 @@ export default class LastFMPlugin {
|
|||
}
|
||||
}
|
||||
|
||||
private async scrobbleSong(attributes: any) {
|
||||
await new Promise(resolve => setTimeout(resolve, Math.round(attributes.durationInMillis * (this._store.lastfm.scrobble_after / 100))));
|
||||
const currentAttributes = attributes;
|
||||
private scrobbleSong(attributes: any) {
|
||||
if (this._timer) clearTimeout(this._timer);
|
||||
var self = this;
|
||||
this._timer = setTimeout(async () => {
|
||||
const currentAttributes = attributes;
|
||||
|
||||
if (!this._lastfm || this._lastfm.cachedAttributes === attributes) {
|
||||
return
|
||||
}
|
||||
if (!self._lastfm || self._lastfm.cachedAttributes === attributes) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this._lastfm.cachedAttributes) {
|
||||
if (this._lastfm.cachedAttributes.playParams.id === attributes.playParams.id) return;
|
||||
}
|
||||
if (self._lastfm.cachedAttributes) {
|
||||
if (self._lastfm.cachedAttributes.playParams.id === attributes.playParams.id) return;
|
||||
}
|
||||
|
||||
if (currentAttributes.status && currentAttributes === attributes) {
|
||||
if (fs.existsSync(this.sessionPath)) {
|
||||
// Scrobble playing song.
|
||||
if (attributes.status === true) {
|
||||
this._lastfm.track.scrobble({
|
||||
'artist': this.filterArtistName(attributes.artistName),
|
||||
'track': attributes.name,
|
||||
'album': attributes.albumName,
|
||||
'albumArtist': this.filterArtistName(attributes.artistName),
|
||||
'timestamp': new Date().getTime() / 1000
|
||||
}, function (err: any, scrobbled: any) {
|
||||
if (err) {
|
||||
return console.error('[LastFM] An error occurred while scrobbling', err);
|
||||
}
|
||||
const artist = await this.getPrimaryArtist(attributes)
|
||||
const album = this.getAlbumName(attributes)
|
||||
|
||||
console.log('[LastFM] Successfully scrobbled: ', scrobbled);
|
||||
});
|
||||
this._lastfm.cachedAttributes = attributes
|
||||
if (currentAttributes.status && currentAttributes === attributes) {
|
||||
if (fs.existsSync(this.sessionPath)) {
|
||||
// Scrobble playing song.
|
||||
if (attributes.status === true) {
|
||||
self._lastfm.track.scrobble({
|
||||
'artist': artist,
|
||||
'track': attributes.name,
|
||||
'album': album,
|
||||
'albumArtist': artist,
|
||||
'timestamp': new Date().getTime() / 1000
|
||||
}, function (err: any, scrobbled: any) {
|
||||
if (err) {
|
||||
return console.error('[LastFM] An error occurred while scrobbling', err);
|
||||
}
|
||||
|
||||
console.log('[LastFM] Successfully scrobbled: ', scrobbled);
|
||||
});
|
||||
self._lastfm.cachedAttributes = attributes
|
||||
}
|
||||
} else {
|
||||
self.authenticate();
|
||||
}
|
||||
} else {
|
||||
this.authenticate();
|
||||
return console.log('[LastFM] Did not add ', attributes.name, '—', artist, 'because now playing a other song.');
|
||||
}
|
||||
} else {
|
||||
return console.log('[LastFM] Did not add ', attributes.name, '—', this.filterArtistName(attributes.artistName), 'because now playing a other song.');
|
||||
}
|
||||
}, Math.round(attributes.durationInMillis * Math.min((self._store.lastfm.scrobble_after / 100),0.8)));
|
||||
}
|
||||
|
||||
private filterArtistName(artist: any) {
|
||||
if (!this._store.lastfm.enabledRemoveFeaturingArtists) return artist;
|
||||
|
||||
artist = artist.split(' ');
|
||||
if (artist.includes('&')) {
|
||||
artist.length = artist.indexOf('&');
|
||||
}
|
||||
if (artist.includes('and')) {
|
||||
artist.length = artist.indexOf('and');
|
||||
}
|
||||
artist = artist.join(' ');
|
||||
if (artist.includes(',')) {
|
||||
artist = artist.split(',')
|
||||
artist = artist[0]
|
||||
}
|
||||
return artist.charAt(0).toUpperCase() + artist.slice(1);
|
||||
}
|
||||
|
||||
private updateNowPlayingSong(attributes: any) {
|
||||
private async updateNowPlayingSong(attributes: any) {
|
||||
if (!this._lastfm || this._lastfm.cachedNowPlayingAttributes === attributes || !this._store.lastfm.NowPlaying) {
|
||||
return
|
||||
}
|
||||
|
@ -144,13 +133,16 @@ export default class LastFMPlugin {
|
|||
}
|
||||
|
||||
if (fs.existsSync(this.sessionPath)) {
|
||||
const artist = await this.getPrimaryArtist(attributes)
|
||||
const album = this.getAlbumName(attributes)
|
||||
|
||||
// update Now Playing
|
||||
if (attributes.status === true) {
|
||||
this._lastfm.track.updateNowPlaying({
|
||||
'artist': this.filterArtistName(attributes.artistName),
|
||||
'artist': artist,
|
||||
'track': attributes.name,
|
||||
'album': attributes.albumName,
|
||||
'albumArtist': this.filterArtistName(attributes.artistName)
|
||||
'album': album,
|
||||
'albumArtist': artist
|
||||
}, function (err: any, nowPlaying: any) {
|
||||
if (err) {
|
||||
return console.error('[LastFM] An error occurred while updating nowPlayingSong', err);
|
||||
|
@ -166,6 +158,44 @@ export default class LastFMPlugin {
|
|||
}
|
||||
}
|
||||
|
||||
private getAlbumName(attributes: any): string {
|
||||
return attributes.albumName.replace(/ - Single| - EP/g, '');
|
||||
}
|
||||
|
||||
private async getPrimaryArtist(attributes: any) {
|
||||
const songId = attributes.playParams.catalogId || attributes.playParams.id
|
||||
|
||||
if (!this._store.lastfm.enabledRemoveFeaturingArtists || !songId) return attributes.artistName;
|
||||
|
||||
const res = await this._win.webContents.executeJavaScript(`
|
||||
(async () => {
|
||||
const subMk = await MusicKit.getInstance().api.v3.music("/v1/catalog/" + MusicKit.getInstance().storefrontId + "/songs/${songId}", {
|
||||
include: {
|
||||
songs: ["artists"]
|
||||
}
|
||||
})
|
||||
if (!subMk) console.error('[LastFM] Request failed: /v1/catalog/us/songs/${songId}')
|
||||
return subMk.data
|
||||
})()
|
||||
`).catch(console.error)
|
||||
if (!res) return attributes.artistName
|
||||
|
||||
const data = res.data
|
||||
if (!data.length) {
|
||||
console.error(`[LastFM] Unable to locate song with id of ${songId}`)
|
||||
return attributes.artistName;
|
||||
}
|
||||
|
||||
const artists = res.data[0].relationships.artists.data
|
||||
if (!artists.length) {
|
||||
console.error(`[LastFM] Unable to find artists related to the song with id of ${songId}`)
|
||||
return attributes.artistName;
|
||||
}
|
||||
|
||||
const primaryArtist = artists[0]
|
||||
return primaryArtist.attributes.name
|
||||
}
|
||||
|
||||
/**
|
||||
* Base Plugin Details (Eventually implemented into a GUI in settings)
|
||||
*/
|
||||
|
@ -229,25 +259,19 @@ export default class LastFMPlugin {
|
|||
console.log('Example plugin stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on playback State Change
|
||||
* @param attributes Music Attributes (attributes.state = current state)
|
||||
*/
|
||||
onPlaybackStateDidChange(attributes: object): void {
|
||||
this.scrobbleSong(attributes)
|
||||
this.updateNowPlayingSong(attributes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on song change
|
||||
* @param attributes Music Attributes
|
||||
*/
|
||||
onNowPlayingItemDidChange(attributes: object): void {
|
||||
if (!this._store.lastfm.filterLoop){
|
||||
nowPlayingItemDidChangeLastFM(attributes: any): void {
|
||||
if (!this._store.general.privateEnabled){
|
||||
attributes.status = true
|
||||
if (!this._store.lastfm.filterLoop) {
|
||||
this._lastfm.cachedNowPlayingAttributes = false;
|
||||
this._lastfm.cachedAttributes = false}
|
||||
this.scrobbleSong(attributes)
|
||||
this._lastfm.cachedAttributes = false
|
||||
}
|
||||
this.updateNowPlayingSong(attributes)
|
||||
this.scrobbleSong(attributes)}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
254
src/main/plugins/menubar.ts
Normal file
254
src/main/plugins/menubar.ts
Normal file
|
@ -0,0 +1,254 @@
|
|||
import {app, Menu, shell} from "electron";
|
||||
|
||||
export default class Thumbar {
|
||||
/**
|
||||
* Private variables for interaction in plugins
|
||||
*/
|
||||
private _win: any;
|
||||
private _app: any;
|
||||
private _store: any;
|
||||
|
||||
/**
|
||||
* Base Plugin Details (Eventually implemented into a GUI in settings)
|
||||
*/
|
||||
public name: string = 'Menubar Plugin';
|
||||
public description: string = 'Creates the menubar';
|
||||
public version: string = '1.0.0';
|
||||
public author: string = 'Core / Quacksire';
|
||||
|
||||
/**
|
||||
* Thumbnail Toolbar Assets
|
||||
* NATIVE-IMAGE DOESN'T SUPPORT SVG
|
||||
private icons: { [key: string]: Electron.NativeImage } = {
|
||||
remoteIcon: nativeImage.createFromPath(join(utils.getPath('rendererPath'), 'views/svg/smartphone.svg')).toPNG(),
|
||||
soundIcon: nativeImage.createFromPath(join(utils.getPath('rendererPath'), 'views/svg/headphones.svg')).toPNG(),
|
||||
aboutIcon: nativeImage.createFromPath(join(utils.getPath('rendererPath'), 'views/svg/info.svg')).toPNG(),
|
||||
settingsIcon: nativeImage.createFromPath(join(utils.getPath('rendererPath'), 'views/svg/settings.svg')).toPNG(),
|
||||
logoutIcon: nativeImage.createFromPath(join(utils.getPath('rendererPath'), 'views/svg/log-out.svg')).toPNG(),
|
||||
ciderIcon: nativeImage.createFromPath(join(utils.getPath('rendererPath'), 'assets/logocute.png')).toPNG(),
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Menubar Assets
|
||||
* @private
|
||||
*/
|
||||
private isMac: boolean = process.platform === 'darwin';
|
||||
private menubarTemplate: any = [
|
||||
{
|
||||
label: app.getName(),
|
||||
submenu: [
|
||||
{
|
||||
label: 'About',
|
||||
click: () => this._win.webContents.executeJavaScript(`app.appRoute('about')`)
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Settings',
|
||||
accelerator: 'CommandOrControl+,',
|
||||
click: () => this._win.webContents.executeJavaScript(`app.appRoute('settings')`)
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ role: 'services' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'hide' },
|
||||
{ role: 'hideOthers' },
|
||||
{ role: 'unhide' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'quit' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{role: 'reload'},
|
||||
{role: 'forceReload'},
|
||||
{role: 'toggleDevTools'},
|
||||
{type: 'separator'},
|
||||
{role: 'resetZoom'},
|
||||
{role: 'zoomIn'},
|
||||
{role: 'zoomOut'},
|
||||
{type: 'separator'},
|
||||
{role: 'togglefullscreen'},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Window',
|
||||
submenu: [
|
||||
{role: 'minimize'},
|
||||
{role: 'zoom'},
|
||||
...(this.isMac ? [
|
||||
{type: 'separator'},
|
||||
{role: 'front'},
|
||||
] : [
|
||||
{role: 'close'}
|
||||
]),
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{ role: 'undo' },
|
||||
{ role: 'redo' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'cut' },
|
||||
{ role: 'copy' },
|
||||
{ role: 'paste' },
|
||||
]
|
||||
},
|
||||
{type: 'separator'},
|
||||
{
|
||||
label: 'Web Remote',
|
||||
accelerator: 'CommandOrControl+Shift+W',
|
||||
sublabel: 'Opens in external window',
|
||||
click: () => this._win.webContents.executeJavaScript(`ipcRenderer.invoke('showQR')`)
|
||||
},
|
||||
{
|
||||
label: 'Audio Settings',
|
||||
accelerator: 'CommandOrControl+Shift+A',
|
||||
click: () => this._win.webContents.executeJavaScript(`app.modals.audioSettings = true`)
|
||||
},
|
||||
{
|
||||
label: 'Plug-in Menu',
|
||||
accelerator: 'CommandOrControl+Shift+P',
|
||||
click: () => this._win.webContents.executeJavaScript(`app.modals.pluginMenu = true`)
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Controls',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Pause / Play',
|
||||
accelerator: 'Space',
|
||||
click: () => this._win.webContents.executeJavaScript(`app.SpacePause()`)
|
||||
},
|
||||
{
|
||||
label: 'Next',
|
||||
accelerator: 'CommandOrControl+Right',
|
||||
click: () => this._win.webContents.executeJavaScript(`MusicKitInterop.next()`)
|
||||
},
|
||||
{
|
||||
label: 'Previous',
|
||||
accelerator: 'CommandOrControl+Left',
|
||||
click: () => this._win.webContents.executeJavaScript(`MusicKitInterop.previous()`)
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Volume Up',
|
||||
accelerator: 'CommandOrControl+Up',
|
||||
click: () => this._win.webContents.executeJavaScript(`app.volumeUp()`)
|
||||
},
|
||||
{
|
||||
label: 'Volume Down',
|
||||
accelerator: 'CommandOrControl+Down',
|
||||
click: () => this._win.webContents.executeJavaScript(`app.volumeDown()`)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Account',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Account Settings',
|
||||
click: () => this._win.webContents.executeJavaScript(`app.appRoute('apple-account-settings')`)
|
||||
},
|
||||
{
|
||||
label: 'Sign Out',
|
||||
click: () => this._win.webContents.executeJavaScript(`app.unauthorize()`)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Support',
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Discord',
|
||||
click: () => shell.openExternal("https://discord.gg/AppleMusic").catch(console.error)
|
||||
},
|
||||
{
|
||||
label: 'GitHub Wiki',
|
||||
click: () => shell.openExternal("https://github.com/ciderapp/Cider/wiki/Troubleshooting").catch(console.error)
|
||||
},
|
||||
{type: 'separator'},
|
||||
{
|
||||
label: 'Report a...',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Bug',
|
||||
click: () => shell.openExternal("https://github.com/ciderapp/Cider/issues/new?assignees=&labels=bug%2Ctriage&template=bug_report.yaml&title=%5BBug%5D%3A+").catch(console.error)
|
||||
},
|
||||
{
|
||||
label: 'Feature Request',
|
||||
click: () => shell.openExternal("https://github.com/ciderapp/Cider/issues/new?assignees=&labels=enhancement%2Ctriage&template=feature_request.yaml&title=%5BEnhancement%5D%3A+").catch(console.error)
|
||||
},
|
||||
{
|
||||
label: 'Translation Report/Request',
|
||||
click: () => shell.openExternal("https://github.com/ciderapp/Cider/issues/new?assignees=&labels=%F0%9F%8C%90+Translations&template=translation.yaml&title=%5BTranslation%5D%3A+").catch(console.error)
|
||||
},
|
||||
]
|
||||
},
|
||||
{type: 'separator'},
|
||||
{
|
||||
label: 'View License',
|
||||
click: () => shell.openExternal("https://github.com/ciderapp/Cider/blob/main/LICENSE").catch(console.error)
|
||||
},
|
||||
{type: 'separator'},
|
||||
{
|
||||
label: 'Toggle Developer Tools',
|
||||
accelerator: 'Option+CommandOrControl+I',
|
||||
click: () => this._win.webContents.openDevTools()
|
||||
},
|
||||
{
|
||||
label: 'Open Configuration File in Editor',
|
||||
click: () => this._store.openInEditor()
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
/*******************************************************************************************
|
||||
* Public Methods
|
||||
* ****************************************************************************************/
|
||||
|
||||
/**
|
||||
* Runs on plugin load (Currently run on application start)
|
||||
*/
|
||||
constructor(app: any, store: any) {
|
||||
this._app = app;
|
||||
this._store = store
|
||||
console.debug(`[Plugin][${this.name}] Loading Complete.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on app ready
|
||||
*/
|
||||
onReady(win: Electron.BrowserWindow): void {
|
||||
this._win = win;
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(this.menubarTemplate))
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on app stop
|
||||
*/
|
||||
onBeforeQuit(): void {
|
||||
console.debug(`[Plugin][${this.name}] Stopped.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on playback State Change
|
||||
* @param attributes Music Attributes (attributes.status = current state)
|
||||
*/
|
||||
onPlaybackStateDidChange(attributes: object): void {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on song change
|
||||
* @param attributes Music Attributes
|
||||
*/
|
||||
onNowPlayingItemDidChange(attributes: object): void {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -174,7 +174,11 @@ export default class MPRIS {
|
|||
*/
|
||||
onBeforeQuit(): void {
|
||||
console.debug(`[Plugin][${this.name}] Stopped.`);
|
||||
this.clearState()
|
||||
try {
|
||||
this.clearState()
|
||||
}catch(e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
133
src/main/plugins/thumbar.ts
Normal file
133
src/main/plugins/thumbar.ts
Normal file
|
@ -0,0 +1,133 @@
|
|||
import {nativeImage, nativeTheme} from "electron";
|
||||
import {utils} from "../base/utils";
|
||||
import {join} from "path";
|
||||
|
||||
export default class Thumbar {
|
||||
/**
|
||||
* Private variables for interaction in plugins
|
||||
*/
|
||||
private _win: any;
|
||||
private _app: any;
|
||||
|
||||
/**
|
||||
* Base Plugin Details (Eventually implemented into a GUI in settings)
|
||||
*/
|
||||
public name: string = 'Thumbnail Toolbar Plugin';
|
||||
public description: string = 'Creates and managed the thumbnail toolbar buttons and their events';
|
||||
public version: string = '1.0.0';
|
||||
public author: string = 'Core';
|
||||
|
||||
/**
|
||||
* Thumbnail Toolbar Assets
|
||||
*/
|
||||
private icons: { [key: string]: Electron.NativeImage } = {
|
||||
pause: nativeImage.createFromPath(join(utils.getPath('resourcePath'), 'icons/thumbar', `${nativeTheme.shouldUseDarkColors ? 'light' : 'dark'}_pause.png`)),
|
||||
play: nativeImage.createFromPath(join(utils.getPath('resourcePath'), 'icons/thumbar', `${nativeTheme.shouldUseDarkColors ? 'light' : 'dark'}_play.png`)),
|
||||
next: nativeImage.createFromPath(join(utils.getPath('resourcePath'), 'icons/thumbar', `${nativeTheme.shouldUseDarkColors ? 'light' : 'dark'}_next.png`)),
|
||||
previous: nativeImage.createFromPath(join(utils.getPath('resourcePath'), 'icons/thumbar', `${nativeTheme.shouldUseDarkColors ? 'light' : 'dark'}_previous.png`)),
|
||||
}
|
||||
|
||||
/*******************************************************************************************
|
||||
* Private Methods
|
||||
* ****************************************************************************************/
|
||||
|
||||
/**
|
||||
* Blocks non-windows systems from running this plugin
|
||||
* @private
|
||||
* @decorator
|
||||
*/
|
||||
private static windowsOnly(_target: any, _propertyKey: string, descriptor: PropertyDescriptor) {
|
||||
if (process.platform !== 'win32') {
|
||||
descriptor.value = function () {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the thumbnail toolbar
|
||||
*/
|
||||
@Thumbar.windowsOnly
|
||||
private updateButtons(attributes: any) {
|
||||
|
||||
console.log(attributes)
|
||||
|
||||
if (!attributes) {
|
||||
return
|
||||
}
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
tooltip: 'Previous',
|
||||
icon: this.icons.previous,
|
||||
click() {
|
||||
utils.playback.previous()
|
||||
}
|
||||
},
|
||||
{
|
||||
tooltip: attributes.status ? 'Pause' : 'Play',
|
||||
icon: attributes.status ? this.icons.pause : this.icons.play,
|
||||
click() {
|
||||
utils.playback.playPause()
|
||||
}
|
||||
},
|
||||
{
|
||||
tooltip: 'Next',
|
||||
icon: this.icons.next,
|
||||
click() {
|
||||
utils.playback.next()
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
if (!attributes.playParams || attributes.playParams.id === 'no-id-found') {
|
||||
this._win.setThumbarButtons([])
|
||||
} else {
|
||||
this._win.setThumbarButtons(buttons);
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************************
|
||||
* Public Methods
|
||||
* ****************************************************************************************/
|
||||
|
||||
/**
|
||||
* Runs on plugin load (Currently run on application start)
|
||||
*/
|
||||
constructor(app: any, _store: any) {
|
||||
this._app = app;
|
||||
console.debug(`[Plugin][${this.name}] Loading Complete.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on app ready
|
||||
*/
|
||||
onReady(win: Electron.BrowserWindow): void {
|
||||
this._win = win;
|
||||
console.debug(`[Plugin][${this.name}] Ready.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on app stop
|
||||
*/
|
||||
onBeforeQuit(): void {
|
||||
console.debug(`[Plugin][${this.name}] Stopped.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on playback State Change
|
||||
* @param attributes Music Attributes (attributes.status = current state)
|
||||
*/
|
||||
onPlaybackStateDidChange(attributes: object): void {
|
||||
this.updateButtons(attributes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on song change
|
||||
* @param attributes Music Attributes
|
||||
*/
|
||||
onNowPlayingItemDidChange(attributes: object): void {
|
||||
this.updateButtons(attributes)
|
||||
}
|
||||
|
||||
}
|
244
src/main/plugins/webNowPlaying.ts
Normal file
244
src/main/plugins/webNowPlaying.ts
Normal file
|
@ -0,0 +1,244 @@
|
|||
import * as WebSocket from 'ws';
|
||||
|
||||
/**
|
||||
* 0-pad a number.
|
||||
* @param {Number} number
|
||||
* @param {Number} length
|
||||
* @returns String
|
||||
*/
|
||||
const pad = (number: number, length: number) => String(number).padStart(length, '0');
|
||||
|
||||
/**
|
||||
* Convert seconds to a time string acceptable to Rainmeter
|
||||
* https://github.com/tjhrulz/WebNowPlaying-BrowserExtension/blob/master/WebNowPlaying.js#L50-L59
|
||||
* @param {Number} timeInSeconds
|
||||
* @returns String
|
||||
*/
|
||||
const convertTimeToString = (timeInSeconds: number) => {
|
||||
const timeInMinutes = Math.floor(timeInSeconds / 60);
|
||||
if (timeInMinutes < 60) {
|
||||
return timeInMinutes + ":" + pad(Math.floor(timeInSeconds % 60), 2);
|
||||
}
|
||||
return Math.floor(timeInMinutes / 60) + ":" + pad(Math.floor(timeInMinutes % 60), 2) + ":" + pad(Math.floor(timeInSeconds % 60), 2);
|
||||
}
|
||||
|
||||
export default class WebNowPlaying {
|
||||
/**
|
||||
* Base Plugin Details (Eventually implemented into a GUI in settings)
|
||||
*/
|
||||
public name: string = 'WebNowPlaying';
|
||||
public description: string = 'Song info and playback control for the Rainmeter WebNowPlaying plugin.';
|
||||
public version: string = '1.0.1';
|
||||
public author: string = 'Zennn <me@jozen.blue>';
|
||||
|
||||
private _win: any;
|
||||
private ws?: WebSocket;
|
||||
private wsapiConn?: WebSocket;
|
||||
private playerName: string = 'Cider';
|
||||
|
||||
constructor() {
|
||||
console.debug(`[Plugin][${this.name}] Loading Complete.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocks non-windows systems from running this plugin
|
||||
* @private
|
||||
* @decorator
|
||||
*/
|
||||
private static windowsOnly(_target: any, _propertyKey: string, descriptor: PropertyDescriptor) {
|
||||
if (process.platform !== 'win32') {
|
||||
descriptor.value = () => void 0;
|
||||
}
|
||||
}
|
||||
|
||||
private sendSongInfo(attributes: any) {
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
const fields = ['STATE', 'TITLE', 'ARTIST', 'ALBUM', 'COVER', 'DURATION', 'POSITION', 'VOLUME', 'REPEAT', 'SHUFFLE'];
|
||||
fields.forEach((field) => {
|
||||
try {
|
||||
let value: any = '';
|
||||
switch (field) {
|
||||
case 'STATE':
|
||||
value = attributes.status ? 1 : 2;
|
||||
break;
|
||||
case 'TITLE':
|
||||
value = attributes.name;
|
||||
break;
|
||||
case 'ARTIST':
|
||||
value = attributes.artistName;
|
||||
break;
|
||||
case 'ALBUM':
|
||||
value = attributes.albumName;
|
||||
break;
|
||||
case 'COVER':
|
||||
value = attributes.artwork.url.replace('{w}', attributes.artwork.width).replace('{h}', attributes.artwork.height);
|
||||
break;
|
||||
case 'DURATION':
|
||||
value = convertTimeToString(attributes.durationInMillis / 1000);
|
||||
break;
|
||||
case 'POSITION':
|
||||
value = convertTimeToString((attributes.durationInMillis - attributes.remainingTime) / 1000);
|
||||
break;
|
||||
case 'VOLUME':
|
||||
value = attributes.volume * 100;
|
||||
break;
|
||||
case 'REPEAT':
|
||||
value = attributes.repeatMode;
|
||||
break;
|
||||
case 'SHUFFLE':
|
||||
value = attributes.shuffleMode;
|
||||
break;
|
||||
}
|
||||
this.ws?.send(`${field}:${value}`);
|
||||
} catch (error) {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(`Error:Error updating ${field} for ${this.playerName}`);
|
||||
this.ws.send(`ErrorD:${error}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private fireEvent(evt: WebSocket.MessageEvent) {
|
||||
if (!evt.data) return;
|
||||
const data = <string>evt.data;
|
||||
|
||||
let value: string = '';
|
||||
if (data.split(/ (.+)/).length > 1) {
|
||||
value = data.split(/ (.+)/)[1];
|
||||
}
|
||||
const eventName = data.split(' ')[0].toLowerCase();
|
||||
|
||||
try {
|
||||
switch (eventName) {
|
||||
case 'playpause':
|
||||
this._win.webContents.executeJavaScript('MusicKitInterop.playPause()').catch(console.error);
|
||||
break;
|
||||
case 'next':
|
||||
this._win.webContents.executeJavaScript('MusicKitInterop.next()').catch(console.error);
|
||||
break;
|
||||
case 'previous':
|
||||
this._win.webContents.executeJavaScript('MusicKitInterop.previous()').catch(console.error);
|
||||
break;
|
||||
case 'setposition':
|
||||
this._win.webContents.executeJavaScript(`MusicKit.getInstance().seekToTime(${parseFloat(value)})`);
|
||||
break;
|
||||
case 'setvolume':
|
||||
this._win.webContents.executeJavaScript(`MusicKit.getInstance().volume = ${parseFloat(value) / 100}`);
|
||||
break;
|
||||
case 'repeat':
|
||||
this._win.webContents.executeJavaScript('wsapi.toggleRepeat()').catch(console.error);
|
||||
break;
|
||||
case 'shuffle':
|
||||
this._win.webContents.executeJavaScript('wsapi.toggleShuffle()').catch(console.error);
|
||||
break;
|
||||
case 'togglethumbsup':
|
||||
// not implemented
|
||||
break;
|
||||
case 'togglethumbsdown':
|
||||
// not implemented
|
||||
break;
|
||||
case 'rating':
|
||||
// not implemented
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug(error);
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(`Error:Error sending event to ${this.playerName}`);
|
||||
this.ws.send(`ErrorD:${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on app ready
|
||||
*/
|
||||
@WebNowPlaying.windowsOnly
|
||||
public onReady(win: any) {
|
||||
this._win = win;
|
||||
|
||||
// Connect to Rainmeter plugin and retry on disconnect.
|
||||
const init = () => {
|
||||
try {
|
||||
this.ws = new WebSocket('ws://127.0.0.1:8974/');
|
||||
let retry: NodeJS.Timeout;
|
||||
this.ws.onopen = () => {
|
||||
console.info('[WebNowPlaying] Connected to Rainmeter');
|
||||
this.ws?.send(`PLAYER:${this.playerName}`);
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
clearTimeout(retry);
|
||||
retry = setTimeout(init, 2000);
|
||||
};
|
||||
|
||||
this.ws.onerror = () => {
|
||||
clearTimeout(retry);
|
||||
this.ws?.close();
|
||||
};
|
||||
|
||||
this.ws.onmessage = this.fireEvent?.bind(this);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
// Connect to wsapi. Only used to update progress.
|
||||
try {
|
||||
this.wsapiConn = new WebSocket('ws://127.0.0.1:26369/');
|
||||
|
||||
this.wsapiConn.onopen = () => {
|
||||
console.info('[WebNowPlaying] Connected to wsapi');
|
||||
};
|
||||
|
||||
this.wsapiConn.onmessage = (evt: WebSocket.MessageEvent) => {
|
||||
const response = JSON.parse(<string>evt.data);
|
||||
if (response.type === 'playbackStateUpdate') {
|
||||
this.sendSongInfo(response.data);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
console.debug(`[Plugin][${this.name}] Ready.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on app stop
|
||||
*/
|
||||
@WebNowPlaying.windowsOnly
|
||||
public onBeforeQuit() {
|
||||
if (this.ws) {
|
||||
this.ws.send('STATE:0');
|
||||
this.ws.onclose = () => void 0; // disable onclose handler first to stop it from retrying
|
||||
this.ws.close();
|
||||
}
|
||||
if (this.wsapiConn) {
|
||||
this.wsapiConn.close();
|
||||
}
|
||||
console.debug(`[Plugin][${this.name}] Stopped.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on playback State Change
|
||||
* @param attributes Music Attributes (attributes.status = current state)
|
||||
*/
|
||||
@WebNowPlaying.windowsOnly
|
||||
public onPlaybackStateDidChange(attributes: any) {
|
||||
this.sendSongInfo(attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on song change
|
||||
* @param attributes Music Attributes
|
||||
*/
|
||||
@WebNowPlaying.windowsOnly
|
||||
public onNowPlayingItemDidChange(attributes: any) {
|
||||
this.sendSongInfo(attributes);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue