CHONKY BOY

This commit is contained in:
Core 2022-08-04 05:27:29 +01:00
parent 31ed921a1a
commit c15f55d0ee
No known key found for this signature in database
GPG key ID: FE9BF1B547F8F3C6
213 changed files with 64188 additions and 55736 deletions

View file

@ -1,8 +1,8 @@
import {app, Menu, nativeImage, Tray, ipcMain, clipboard, shell} from 'electron';
import {readFileSync} from "fs";
import * as path from 'path';
import * as log from 'electron-log';
import {utils} from './utils';
import { app, Menu, nativeImage, Tray, ipcMain, clipboard, shell } from "electron";
import { readFileSync } from "fs";
import * as path from "path";
import * as log from "electron-log";
import { utils } from "./utils";
/**
* @file Creates App instance
@ -11,317 +11,314 @@ import {utils} from './utils';
/** @namespace */
export class AppEvents {
private protocols: string[] = [
"ame",
"cider",
"itms",
"itmss",
"musics",
"music"
]
private plugin: any = undefined;
private tray: any = undefined;
private i18n: any = undefined;
private protocols: string[] = ["ame", "cider", "itms", "itmss", "musics", "music"];
private plugin: any = undefined;
private tray: any = undefined;
private i18n: any = undefined;
/** @constructor */
constructor() {
this.start();
/** @constructor */
constructor() {
this.start();
}
/**
* Handles all actions that occur for the app on start (Mainly commandline arguments)
* @returns {void}
*/
private start(): void {
AppEvents.initLogging();
console.info("[AppEvents] App started");
/**********************************************************************************************************************
* Startup arguments handling
**********************************************************************************************************************/
if (app.commandLine.hasSwitch("version") || app.commandLine.hasSwitch("v")) {
console.log(app.getVersion());
app.exit();
}
/**
* Handles all actions that occur for the app on start (Mainly commandline arguments)
* @returns {void}
*/
private start(): void {
AppEvents.initLogging()
console.info('[AppEvents] App started');
/**********************************************************************************************************************
* Startup arguments handling
**********************************************************************************************************************/
if (app.commandLine.hasSwitch('version') || app.commandLine.hasSwitch('v')) {
console.log(app.getVersion())
app.exit()
}
// Verbose Check
if (app.commandLine.hasSwitch('verbose')) {
console.log("[Cider] User has launched the application with --verbose");
}
// Log File Location
if (app.commandLine.hasSwitch('log') || app.commandLine.hasSwitch('l')) {
console.log(path.join(app.getPath('userData'), 'logs'))
app.exit()
}
// Try limiting JS memory to 350MB.
app.commandLine.appendSwitch('js-flags', '--max-old-space-size=350');
// Expose GC
app.commandLine.appendSwitch('js-flags', '--expose_gc')
if (process.platform === "win32") {
app.setAppUserModelId(app.getName()) // For notification name
}
/***********************************************************************************************************************
* Commandline arguments
**********************************************************************************************************************/
switch (utils.getStoreValue('visual.hw_acceleration') as string) {
default:
case "default":
app.commandLine.appendSwitch('enable-accelerated-mjpeg-decode')
app.commandLine.appendSwitch('enable-accelerated-video')
app.commandLine.appendSwitch('disable-gpu-driver-bug-workarounds')
app.commandLine.appendSwitch('ignore-gpu-blacklist')
app.commandLine.appendSwitch('enable-native-gpu-memory-buffers')
app.commandLine.appendSwitch('enable-accelerated-video-decode');
app.commandLine.appendSwitch('enable-gpu-rasterization');
app.commandLine.appendSwitch('enable-native-gpu-memory-buffers');
app.commandLine.appendSwitch('enable-oop-rasterization');
break;
case "webgpu":
console.info("WebGPU is enabled.");
app.commandLine.appendSwitch('enable-unsafe-webgpu')
break;
case "disabled":
console.info("Hardware acceleration is disabled.");
app.commandLine.appendSwitch('disable-gpu')
break;
}
if (process.platform === "linux") {
app.commandLine.appendSwitch('disable-features', 'MediaSessionService');
}
/***********************************************************************************************************************
* Protocols
**********************************************************************************************************************/
/** */
if (process.defaultApp) {
if (process.argv.length >= 2) {
this.protocols.forEach((protocol: string) => {
app.setAsDefaultProtocolClient(protocol, process.execPath, [path.resolve(process.argv[1])])
})
}
} else {
this.protocols.forEach((protocol: string) => {
app.setAsDefaultProtocolClient(protocol)
})
}
// Verbose Check
if (app.commandLine.hasSwitch("verbose")) {
console.log("[Cider] User has launched the application with --verbose");
}
public quit() {
console.log('[AppEvents] App quit');
// Log File Location
if (app.commandLine.hasSwitch("log") || app.commandLine.hasSwitch("l")) {
console.log(path.join(app.getPath("userData"), "logs"));
app.exit();
}
public ready(plug: any) {
this.plugin = plug
console.log('[AppEvents] App ready');
// Try limiting JS memory to 350MB.
app.commandLine.appendSwitch("js-flags", "--max-old-space-size=350");
AppEvents.setLoginSettings()
}
// Expose GC
app.commandLine.appendSwitch("js-flags", "--expose_gc");
public bwCreated() {
app.on('open-url', (event, url) => {
event.preventDefault()
if (this.protocols.some((protocol: string) => url.includes(protocol))) {
this.LinkHandler(url)
console.log(url)
}
})
if (process.platform === "darwin") {
app.setUserActivity('8R23J2835D.com.ciderapp.webremote.play', {
title: 'Web Remote',
description: 'Connect to your Web Remote',
}, "https://webremote.cider.sh")
}
this.InstanceHandler()
if (process.platform !== "darwin") {
this.InitTray()
}
if (process.platform === "win32") {
app.setAppUserModelId(app.getName()); // For notification name
}
/***********************************************************************************************************************
* Private methods
* Commandline arguments
**********************************************************************************************************************/
switch (utils.getStoreValue("visual.hw_acceleration") as string) {
default:
case "default":
app.commandLine.appendSwitch("enable-accelerated-mjpeg-decode");
app.commandLine.appendSwitch("enable-accelerated-video");
app.commandLine.appendSwitch("disable-gpu-driver-bug-workarounds");
app.commandLine.appendSwitch("ignore-gpu-blacklist");
app.commandLine.appendSwitch("enable-native-gpu-memory-buffers");
app.commandLine.appendSwitch("enable-accelerated-video-decode");
app.commandLine.appendSwitch("enable-gpu-rasterization");
app.commandLine.appendSwitch("enable-native-gpu-memory-buffers");
app.commandLine.appendSwitch("enable-oop-rasterization");
break;
/**
* Handles links (URI) and protocols for the application
* @param arg
*/
private LinkHandler(arg: string) {
if (!arg) return;
case "webgpu":
console.info("WebGPU is enabled.");
app.commandLine.appendSwitch("enable-unsafe-webgpu");
break;
// LastFM Auth URL
if (arg.includes('auth')) {
const authURI = arg.split('/auth/')[1]
if (authURI.startsWith('lastfm')) { // If we wanted more auth options
console.log('token: ', authURI.split('lastfm?token=')[1])
utils.getWindow().webContents.executeJavaScript(`ipcRenderer.send('lastfm:auth', "${authURI.split('lastfm?token=')[1]}")`).catch(console.error)
}
case "disabled":
console.info("Hardware acceleration is disabled.");
app.commandLine.appendSwitch("disable-gpu");
break;
}
if (process.platform === "linux") {
app.commandLine.appendSwitch("disable-features", "MediaSessionService");
}
/***********************************************************************************************************************
* Protocols
**********************************************************************************************************************/
/** */
if (process.defaultApp) {
if (process.argv.length >= 2) {
this.protocols.forEach((protocol: string) => {
app.setAsDefaultProtocolClient(protocol, process.execPath, [path.resolve(process.argv[1])]);
});
}
} else {
this.protocols.forEach((protocol: string) => {
app.setAsDefaultProtocolClient(protocol);
});
}
}
public quit() {
console.log("[AppEvents] App quit");
}
public ready(plug: any) {
this.plugin = plug;
console.log("[AppEvents] App ready");
AppEvents.setLoginSettings();
}
public bwCreated() {
app.on("open-url", (event, url) => {
event.preventDefault();
if (this.protocols.some((protocol: string) => url.includes(protocol))) {
this.LinkHandler(url);
console.log(url);
}
});
if (process.platform === "darwin") {
app.setUserActivity(
"8R23J2835D.com.ciderapp.webremote.play",
{
title: "Web Remote",
description: "Connect to your Web Remote",
},
"https://webremote.cider.sh"
);
}
this.InstanceHandler();
if (process.platform !== "darwin") {
this.InitTray();
}
}
/***********************************************************************************************************************
* Private methods
**********************************************************************************************************************/
/**
* Handles links (URI) and protocols for the application
* @param arg
*/
private LinkHandler(arg: string) {
if (!arg) return;
// LastFM Auth URL
if (arg.includes("auth")) {
const authURI = arg.split("/auth/")[1];
if (authURI.startsWith("lastfm")) {
// If we wanted more auth options
console.log("token: ", authURI.split("lastfm?token=")[1]);
utils
.getWindow()
.webContents.executeJavaScript(`ipcRenderer.send('lastfm:auth', "${authURI.split("lastfm?token=")[1]}")`)
.catch(console.error);
}
} else if (arg.includes("playpause")) {
//language=JS
utils.getWindow().webContents.executeJavaScript("MusicKitInterop.playPause()");
} else if (arg.includes("nextitem")) {
//language=JS
utils.getWindow().webContents.executeJavaScript("app.mk.skipToNextItem()");
}
// Play
else if (arg.includes("/play/")) {
//Steer away from protocol:// specific conditionals
const playParam = arg.split("/play/")[1];
const mediaType = {
"s/": "song",
"a/": "album",
"p/": "playlist",
};
for (const [key, value] of Object.entries(mediaType)) {
if (playParam.includes(key)) {
const id = playParam.split(key)[1];
utils.getWindow().webContents.send("play", value, id);
console.debug(`[LinkHandler] Attempting to load ${value} by id: ${id}`);
}
else if (arg.includes('playpause')) {
//language=JS
utils.getWindow().webContents.executeJavaScript('MusicKitInterop.playPause()')
}
else if (arg.includes('nextitem')) {
//language=JS
utils.getWindow().webContents.executeJavaScript('app.mk.skipToNextItem()')
}
// Play
else if (arg.includes('/play/')) { //Steer away from protocol:// specific conditionals
const playParam = arg.split('/play/')[1]
}
} else if (arg.includes("music.apple.com")) {
// URL (used with itms/itmss/music/musics uris)
console.log(arg);
let url = arg.split("//")[1];
console.warn(`[LinkHandler] Attempting to load url: ${url}`);
utils.getWindow().webContents.send("play", "url", url);
} else if (arg.includes("/debug/appdata")) {
shell.openPath(app.getPath("userData"));
} else if (arg.includes("/debug/logs")) {
shell.openPath(app.getPath("logs"));
} else if (arg.includes("/discord")) {
shell.openExternal("https://discord.gg/applemusic");
} else if (arg.includes("/github")) {
shell.openExternal("https://github.com/ciderapp/cider");
} else if (arg.includes("/donate")) {
shell.openExternal("https://opencollective.com/ciderapp");
} else if (arg.includes("/beep")) {
shell.beep();
} else {
utils.getWindow().webContents.executeJavaScript(`app.appRoute('${arg.split("//")[1]}')`);
}
}
const mediaType = {
"s/": "song",
"a/": "album",
"p/": "playlist"
}
/**
* Handles the creation of a new instance of the app
*/
private InstanceHandler() {
// Detects of an existing instance is running (So if the lock has been achieved, no existing instance has been found)
const gotTheLock = app.requestSingleInstanceLock();
for (const [key, value] of Object.entries(mediaType)) {
if (playParam.includes(key)) {
const id = playParam.split(key)[1]
utils.getWindow().webContents.send('play', value, id)
console.debug(`[LinkHandler] Attempting to load ${value} by id: ${id}`)
}
}
if (!gotTheLock) {
// Runs on the new instance if another instance has been found
console.log("[Cider] Another instance has been found, quitting.");
app.quit();
} else {
// Runs on the first instance if no other instance has been found
app.on("second-instance", (_event, startArgs) => {
console.log("[InstanceHandler] (second-instance) Instance started with " + startArgs.toString());
} else if (arg.includes('music.apple.com')) { // URL (used with itms/itmss/music/musics uris)
console.log(arg)
let url = arg.split('//')[1]
console.warn(`[LinkHandler] Attempting to load url: ${url}`);
utils.getWindow().webContents.send('play', 'url', url)
} else if (arg.includes('/debug/appdata')) {
shell.openPath(app.getPath('userData'))
} else if (arg.includes('/debug/logs')) {
shell.openPath(app.getPath('logs'))
} else if (arg.includes('/discord')) {
shell.openExternal('https://discord.gg/applemusic')
} else if (arg.includes('/github')) {
shell.openExternal('https://github.com/ciderapp/cider')
} else if (arg.includes('/donate')) {
shell.openExternal('https://opencollective.com/ciderapp')
} else if (arg.includes('/beep')) {
shell.beep()
startArgs.forEach((arg) => {
console.log(arg);
if (arg.includes("cider://")) {
console.debug("[InstanceHandler] (second-instance) Link detected with " + arg);
this.LinkHandler(arg);
} else if (arg.includes("--force-quit")) {
console.warn("[InstanceHandler] (second-instance) Force Quit found. Quitting App.");
app.quit();
} else if (utils.getWindow()) {
if (utils.getWindow().isMinimized()) utils.getWindow().restore();
utils.getWindow().show();
utils.getWindow().focus();
}
});
});
}
}
/**
* Initializes the applications tray
*/
private InitTray() {
const icons = {
win32: nativeImage.createFromPath(path.join(__dirname, `../../resources/icons/icon.ico`)).resize({
width: 32,
height: 32,
}),
linux: nativeImage.createFromPath(path.join(__dirname, `../../resources/icons/icon.png`)).resize({
width: 32,
height: 32,
}),
darwin: nativeImage.createFromPath(path.join(__dirname, `../../resources/icons/icon.png`)).resize({
width: 20,
height: 20,
}),
};
this.tray = new Tray(process.platform === "win32" ? icons.win32 : process.platform === "darwin" ? icons.darwin : icons.linux);
this.tray.setToolTip(app.getName());
this.setTray(false);
this.tray.on("double-click", () => {
if (utils.getWindow()) {
if (utils.getWindow().isVisible()) {
utils.getWindow().focus();
} else {
utils.getWindow().webContents.executeJavaScript(`app.appRoute('${arg.split('//')[1]}')`)
utils.getWindow().show();
}
}
}
});
/**
* Handles the creation of a new instance of the app
*/
private InstanceHandler() {
// Detects of an existing instance is running (So if the lock has been achieved, no existing instance has been found)
const gotTheLock = app.requestSingleInstanceLock()
utils.getWindow().on("show", () => {
this.setTray(true);
});
if (!gotTheLock) { // Runs on the new instance if another instance has been found
console.log('[Cider] Another instance has been found, quitting.')
app.quit()
} else { // Runs on the first instance if no other instance has been found
app.on('second-instance', (_event, startArgs) => {
console.log("[InstanceHandler] (second-instance) Instance started with " + startArgs.toString())
utils.getWindow().on("restore", () => {
this.setTray(true);
});
startArgs.forEach(arg => {
console.log(arg)
if (arg.includes("cider://")) {
console.debug('[InstanceHandler] (second-instance) Link detected with ' + arg)
this.LinkHandler(arg)
} else if (arg.includes("--force-quit")) {
console.warn('[InstanceHandler] (second-instance) Force Quit found. Quitting App.');
app.quit()
} else if (utils.getWindow()) {
if (utils.getWindow().isMinimized()) utils.getWindow().restore()
utils.getWindow().show()
utils.getWindow().focus()
}
})
})
}
utils.getWindow().on("hide", () => {
this.setTray(false);
});
}
utils.getWindow().on("minimize", () => {
this.setTray(false);
});
}
/**
* Initializes the applications tray
*/
private InitTray() {
const icons = {
"win32": nativeImage.createFromPath(path.join(__dirname, `../../resources/icons/icon.ico`)).resize({
width: 32,
height: 32
}),
"linux": nativeImage.createFromPath(path.join(__dirname, `../../resources/icons/icon.png`)).resize({
width: 32,
height: 32
}),
"darwin": nativeImage.createFromPath(path.join(__dirname, `../../resources/icons/icon.png`)).resize({
width: 20,
height: 20
}),
}
this.tray = new Tray(process.platform === 'win32' ? icons.win32 : (process.platform === 'darwin' ? icons.darwin : icons.linux))
this.tray.setToolTip(app.getName())
this.setTray(false)
/**
* Sets the tray context menu to a given state
* @param visible - BrowserWindow Visibility
*/
private setTray(visible: boolean = utils.getWindow().isVisible()) {
this.i18n = utils.getLocale(utils.getStoreValue("general.language"));
this.tray.on('double-click', () => {
if (utils.getWindow()) {
if (utils.getWindow().isVisible()) {
utils.getWindow().focus()
} else {
utils.getWindow().show()
}
}
})
const ciderIcon = nativeImage.createFromPath(path.join(__dirname, `../../resources/icons/icon.png`)).resize({
width: 24,
height: 24,
});
utils.getWindow().on('show', () => {
this.setTray(true)
})
const menu = Menu.buildFromTemplate([
{
label: app.getName(),
enabled: false,
icon: ciderIcon,
},
utils.getWindow().on('restore', () => {
this.setTray(true)
})
{ type: "separator" },
utils.getWindow().on('hide', () => {
this.setTray(false)
})
utils.getWindow().on('minimize', () => {
this.setTray(false)
})
}
/**
* Sets the tray context menu to a given state
* @param visible - BrowserWindow Visibility
*/
private setTray(visible: boolean = utils.getWindow().isVisible()) {
this.i18n = utils.getLocale(utils.getStoreValue('general.language'))
const ciderIcon = nativeImage.createFromPath(path.join(__dirname, `../../resources/icons/icon.png`)).resize({
width: 24,
height: 24
})
const menu = Menu.buildFromTemplate([
{
label: app.getName(),
enabled: false,
icon: ciderIcon,
},
{type: 'separator'},
/* For now only idea i dont know if posible to implement
/* For now only idea i dont know if posible to implement
this could be implemented in a plugin if you would like track info, it would be impractical to put listeners in this file. -Core
{
@ -337,90 +334,93 @@ export class AppEvents {
{type: 'separator'},
*/
{
visible: !visible,
label: this.i18n['term.playpause'],
click: () => {
utils.getWindow().webContents.executeJavaScript('MusicKitInterop.playPause()')
}
},
{
visible: !visible,
label: this.i18n['term.next'],
click: () => {
utils.getWindow().webContents.executeJavaScript(`MusicKitInterop.next()`)
}
},
{
visible: !visible,
label: this.i18n['term.previous'],
click: () => {
utils.getWindow().webContents.executeJavaScript(`MusicKitInterop.previous()`)
}
},
{type: 'separator', visible: !visible},
{
visible: !visible,
label: this.i18n["term.playpause"],
click: () => {
utils.getWindow().webContents.executeJavaScript("MusicKitInterop.playPause()");
},
},
{
label: (visible ? this.i18n['action.tray.minimize'] : `${this.i18n['action.tray.show']}`),
click: () => {
if (utils.getWindow()) {
if (visible) {
utils.getWindow().hide()
} else {
utils.getWindow().show()
}
}
}
},
{
label: this.i18n['term.quit'],
click: () => {
app.quit()
}
{
visible: !visible,
label: this.i18n["term.next"],
click: () => {
utils.getWindow().webContents.executeJavaScript(`MusicKitInterop.next()`);
},
},
{
visible: !visible,
label: this.i18n["term.previous"],
click: () => {
utils.getWindow().webContents.executeJavaScript(`MusicKitInterop.previous()`);
},
},
{ type: "separator", visible: !visible },
{
label: visible ? this.i18n["action.tray.minimize"] : `${this.i18n["action.tray.show"]}`,
click: () => {
if (utils.getWindow()) {
if (visible) {
utils.getWindow().hide();
} else {
utils.getWindow().show();
}
])
this.tray.setContextMenu(menu)
}
}
},
},
{
label: this.i18n["term.quit"],
click: () => {
app.quit();
},
},
]);
this.tray.setContextMenu(menu);
}
/**
* Initializes logging in the application
* @private
*/
private static initLogging() {
log.transports.console.format = '[{h}:{i}:{s}.{ms}] [{level}] {text}';
Object.assign(console, log.functions);
console.debug = function(...args: any[]) {
if (!app.isPackaged) {
log.debug(...args)
}
};
/**
* Initializes logging in the application
* @private
*/
private static initLogging() {
log.transports.console.format = "[{h}:{i}:{s}.{ms}] [{level}] {text}";
Object.assign(console, log.functions);
console.debug = function (...args: any[]) {
if (!app.isPackaged) {
log.debug(...args);
}
};
ipcMain.on('fetch-log', (_event) => {
const data = readFileSync(log.transports.file.getFile().path, {encoding: 'utf8', flag: 'r'});
clipboard.writeText(data)
})
}
ipcMain.on("fetch-log", (_event) => {
const data = readFileSync(log.transports.file.getFile().path, {
encoding: "utf8",
flag: "r",
});
clipboard.writeText(data);
});
}
/**
* Set login settings
* @private
*/
private static setLoginSettings() {
if (utils.getStoreValue('general.onStartup.enabled')) {
app.setLoginItemSettings({
openAtLogin: true,
path: app.getPath('exe'),
args: [`${utils.getStoreValue('general.onStartup.hidden') ? '--hidden' : ''}`]
})
} else {
app.setLoginItemSettings({
openAtLogin: false,
path: app.getPath('exe')
})
}
/**
* Set login settings
* @private
*/
private static setLoginSettings() {
if (utils.getStoreValue("general.onStartup.enabled")) {
app.setLoginItemSettings({
openAtLogin: true,
path: app.getPath("exe"),
args: [`${utils.getStoreValue("general.onStartup.hidden") ? "--hidden" : ""}`],
});
} else {
app.setLoginItemSettings({
openAtLogin: false,
path: app.getPath("exe"),
});
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,10 @@
var util = require('util');
var castv2Cli = require('castv2-client');
var util = require("util");
var castv2Cli = require("castv2-client");
var RequestResponseController = castv2Cli.RequestResponseController;
function CiderCastController(client, sourceId, destinationId) {
RequestResponseController.call(this, client, sourceId, destinationId, 'urn:x-cast:com.ciderapp.customdata');
this.once('close', onclose);
RequestResponseController.call(this, client, sourceId, destinationId, "urn:x-cast:com.ciderapp.customdata");
this.once("close", onclose);
var self = this;
function onclose() {
self.stop();
@ -13,20 +13,20 @@ function CiderCastController(client, sourceId, destinationId) {
util.inherits(CiderCastController, RequestResponseController);
CiderCastController.prototype.sendIp = function(ip) {
CiderCastController.prototype.sendIp = function (ip) {
// TODO: Implement Callback
let data = {
ip : ip
}
ip: ip,
};
this.request(data);
};
CiderCastController.prototype.kill = function() {
CiderCastController.prototype.kill = function () {
// TODO: Implement Callback
let data = {
action : "stop"
}
action: "stop",
};
this.request(data);
};
module.exports = CiderCastController;
module.exports = CiderCastController;

View file

@ -1,9 +1,9 @@
//@ts-nocheck
var util = require('util');
var util = require("util");
// var debug = require('debug')('castv2-client');
var Application = require('castv2-client').Application;
var MediaController = require('castv2-client').MediaController;
var CiderCastController = require('./castcontroller');
var Application = require("castv2-client").Application;
var MediaController = require("castv2-client").MediaController;
var CiderCastController = require("./castcontroller");
function CiderReceiver(client, session) {
Application.apply(this, arguments);
@ -11,70 +11,69 @@ function CiderReceiver(client, session) {
this.media = this.createController(MediaController);
this.mediaReceiver = this.createController(CiderCastController);
this.media.on('status', onstatus);
this.media.on("status", onstatus);
var self = this;
function onstatus(status) {
self.emit('status', status);
self.emit("status", status);
}
}
// FE96A351
// 27E1334F
CiderReceiver.APP_ID = 'FE96A351';
CiderReceiver.APP_ID = "FE96A351";
util.inherits(CiderReceiver, Application);
CiderReceiver.prototype.getStatus = function(callback) {
CiderReceiver.prototype.getStatus = function (callback) {
this.media.getStatus.apply(this.media, arguments);
};
CiderReceiver.prototype.load = function(media, options, callback) {
CiderReceiver.prototype.load = function (media, options, callback) {
this.media.load.apply(this.media, arguments);
};
CiderReceiver.prototype.play = function(callback) {
CiderReceiver.prototype.play = function (callback) {
this.media.play.apply(this.media, arguments);
};
CiderReceiver.prototype.pause = function(callback) {
CiderReceiver.prototype.pause = function (callback) {
this.media.pause.apply(this.media, arguments);
};
CiderReceiver.prototype.stop = function(callback) {
CiderReceiver.prototype.stop = function (callback) {
this.media.stop.apply(this.media, arguments);
};
CiderReceiver.prototype.seek = function(currentTime, callback) {
CiderReceiver.prototype.seek = function (currentTime, callback) {
this.media.seek.apply(this.media, arguments);
};
CiderReceiver.prototype.queueLoad = function(items, options, callback) {
CiderReceiver.prototype.queueLoad = function (items, options, callback) {
this.media.queueLoad.apply(this.media, arguments);
};
CiderReceiver.prototype.queueInsert = function(items, options, callback) {
CiderReceiver.prototype.queueInsert = function (items, options, callback) {
this.media.queueInsert.apply(this.media, arguments);
};
CiderReceiver.prototype.queueRemove = function(itemIds, options, callback) {
CiderReceiver.prototype.queueRemove = function (itemIds, options, callback) {
this.media.queueRemove.apply(this.media, arguments);
};
CiderReceiver.prototype.queueReorder = function(itemIds, options, callback) {
CiderReceiver.prototype.queueReorder = function (itemIds, options, callback) {
this.media.queueReorder.apply(this.media, arguments);
};
CiderReceiver.prototype.queueUpdate = function(items, callback) {
CiderReceiver.prototype.queueUpdate = function (items, callback) {
this.media.queueUpdate.apply(this.media, arguments);
};
CiderReceiver.prototype.sendIp = function(opts){
CiderReceiver.prototype.sendIp = function (opts) {
this.mediaReceiver.sendIp.apply(this.mediaReceiver, arguments);
};
CiderReceiver.prototype.kill = function(opts){
CiderReceiver.prototype.kill = function (opts) {
this.mediaReceiver.kill.apply(this.mediaReceiver, arguments);
};

View file

@ -1,7 +1,7 @@
import * as fs from 'fs';
import * as path from 'path';
import * as electron from 'electron'
import {utils} from './utils';
import * as fs from "fs";
import * as path from "path";
import * as electron from "electron";
import { utils } from "./utils";
//
// Hello, this is our loader for the various plugins that the Cider Development Team built for our
@ -16,108 +16,105 @@ import {utils} from './utils';
* @see {@link https://github.com/ciderapp/Cider/wiki/Plugins|Documentation}
*/
export class Plugins {
private static PluginMap: any = {};
private basePluginsPath = path.join(__dirname, '../plugins');
private userPluginsPath = path.join(electron.app.getPath('userData'), 'Plugins');
private readonly pluginsList: any = {};
private static PluginMap: any = {};
private basePluginsPath = path.join(__dirname, "../plugins");
private userPluginsPath = path.join(electron.app.getPath("userData"), "Plugins");
private readonly pluginsList: any = {};
constructor() {
this.pluginsList = this.getPlugins();
constructor() {
this.pluginsList = this.getPlugins();
}
public static getPluginFromMap(plugin: string): any {
if (Plugins.PluginMap[plugin]) {
return Plugins.PluginMap[plugin];
} else {
return plugin;
}
}
public getPlugins(): any {
let plugins: any = {};
if (fs.existsSync(this.basePluginsPath)) {
fs.readdirSync(this.basePluginsPath).forEach((file) => {
if (file.endsWith(".ts") || file.endsWith(".js")) {
const plugin = require(path.join(this.basePluginsPath, file)).default;
if (plugins[file] || plugin.name in plugins) {
console.log(`[${plugin.name}] Plugin already loaded / Duplicate Class Name`);
} else {
plugins[file] = new plugin(utils);
}
}
});
}
public static getPluginFromMap(plugin: string): any {
if (Plugins.PluginMap[plugin]) {
return Plugins.PluginMap[plugin];
} else {
return plugin;
}
}
public getPlugins(): any {
let plugins: any = {};
if (fs.existsSync(this.basePluginsPath)) {
fs.readdirSync(this.basePluginsPath).forEach(file => {
if (file.endsWith('.ts') || file.endsWith('.js')) {
const plugin = require(path.join(this.basePluginsPath, file)).default;
if (plugins[file] || plugin.name in plugins) {
console.log(`[${plugin.name}] Plugin already loaded / Duplicate Class Name`);
} else {
plugins[file] = new plugin(utils);
}
}
});
}
if (fs.existsSync(this.userPluginsPath)) {
fs.readdirSync(this.userPluginsPath).forEach(file => {
// Plugins V1
if (file.endsWith('.ts') || file.endsWith('.js')) {
if (!electron.app.isPackaged) {
const plugin = require(path.join(this.userPluginsPath, file)).default;
file = file.replace('.ts', '').replace('.js', '');
if (plugins[file] || plugin in plugins) {
console.log(`[${plugin.name}] Plugin already loaded / Duplicate Class Name`);
} else {
plugins[file] = new plugin(utils);
}
} else {
const plugin = require(path.join(this.userPluginsPath, file));
file = file.replace('.ts', '').replace('.js', '');
if (plugins[file] || plugin in plugins) {
console.log(`[${plugin.name}] Plugin already loaded / Duplicate Class Name`);
} else {
plugins[file] = new plugin(utils);
}
}
}
// Plugins V2
else if (fs.lstatSync(path.join(this.userPluginsPath, file)).isDirectory()) {
const pluginPath = path.join(this.userPluginsPath, file);
if (fs.existsSync(path.join(pluginPath, 'package.json'))) {
const pluginPackage = require(path.join(pluginPath, "package.json"));
const plugin = require(path.join(pluginPath, pluginPackage.main));
if (plugins[plugin.name] || plugin.name in plugins) {
console.log(`[${plugin.name}] Plugin already loaded / Duplicate Class Name`);
} else {
Plugins.PluginMap[pluginPackage.name] = file;
const pluginEnv = {
app: electron.app,
store: utils.getStore(),
utils: utils,
win: utils.getWindow(),
dir: pluginPath,
dirName: file
}
plugins[plugin.name] = new plugin(pluginEnv);
}
}
}
});
}
console.log('[PluginHandler] Loaded plugins:', Object.keys(plugins));
return plugins;
}
public callPlugins(event: string, ...args: any[]) {
for (const plugin in this.pluginsList) {
if (this.pluginsList[plugin][event]) {
try {
this.pluginsList[plugin][event](...args);
} catch (e) {
console.error(`[${plugin}] An error was encountered: ${e}`);
console.error(e)
}
if (fs.existsSync(this.userPluginsPath)) {
fs.readdirSync(this.userPluginsPath).forEach((file) => {
// Plugins V1
if (file.endsWith(".ts") || file.endsWith(".js")) {
if (!electron.app.isPackaged) {
const plugin = require(path.join(this.userPluginsPath, file)).default;
file = file.replace(".ts", "").replace(".js", "");
if (plugins[file] || plugin in plugins) {
console.log(`[${plugin.name}] Plugin already loaded / Duplicate Class Name`);
} else {
plugins[file] = new plugin(utils);
}
} else {
const plugin = require(path.join(this.userPluginsPath, file));
file = file.replace(".ts", "").replace(".js", "");
if (plugins[file] || plugin in plugins) {
console.log(`[${plugin.name}] Plugin already loaded / Duplicate Class Name`);
} else {
plugins[file] = new plugin(utils);
}
}
}
}
public callPlugin(plugin: string, event: string, ...args: any[]) {
if (this.pluginsList[plugin][event]) {
this.pluginsList[plugin][event](...args);
// Plugins V2
else if (fs.lstatSync(path.join(this.userPluginsPath, file)).isDirectory()) {
const pluginPath = path.join(this.userPluginsPath, file);
if (fs.existsSync(path.join(pluginPath, "package.json"))) {
const pluginPackage = require(path.join(pluginPath, "package.json"));
const plugin = require(path.join(pluginPath, pluginPackage.main));
if (plugins[plugin.name] || plugin.name in plugins) {
console.log(`[${plugin.name}] Plugin already loaded / Duplicate Class Name`);
} else {
Plugins.PluginMap[pluginPackage.name] = file;
const pluginEnv = {
app: electron.app,
store: utils.getStore(),
utils: utils,
win: utils.getWindow(),
dir: pluginPath,
dirName: file,
};
plugins[plugin.name] = new plugin(pluginEnv);
}
}
}
});
}
console.log("[PluginHandler] Loaded plugins:", Object.keys(plugins));
return plugins;
}
public callPlugins(event: string, ...args: any[]) {
for (const plugin in this.pluginsList) {
if (this.pluginsList[plugin][event]) {
try {
this.pluginsList[plugin][event](...args);
} catch (e) {
console.error(`[${plugin}] An error was encountered: ${e}`);
console.error(e);
}
}
}
}
public callPlugin(plugin: string, event: string, ...args: any[]) {
if (this.pluginsList[plugin][event]) {
this.pluginsList[plugin][event](...args);
}
}
}

View file

@ -1,374 +1,320 @@
import * as ElectronStore from 'electron-store';
import * as ElectronStore from "electron-store";
import * as electron from "electron";
import {app} from "electron";
import { app } from "electron";
import fetch from "electron-fetch";
export class Store {
static cfg: ElectronStore;
static cfg: ElectronStore;
private defaults: any = {
"main": {
"PLATFORM": process.platform,
"UPDATABLE": app.isPackaged && (!process.mas || !process.windowsStore || !process.env.FLATPAK_ID)
private defaults: any = {
main: {
PLATFORM: process.platform,
UPDATABLE: app.isPackaged && (!process.mas || !process.windowsStore || !process.env.FLATPAK_ID),
},
general: {
close_button_hide: false,
language: "en_US", // electron.app.getLocale().replace('-', '_') this can be used in future
playbackNotifications: true,
resumeOnStartupBehavior: "local",
privateEnabled: false,
themeUpdateNotification: true,
sidebarItems: {
recentlyAdded: true,
songs: true,
albums: true,
artists: true,
videos: true,
podcasts: true,
},
sidebarCollapsed: {
cider: false,
applemusic: false,
library: false,
amplaylists: false,
playlists: false,
localLibrary: false,
},
onStartup: {
enabled: false,
hidden: false,
},
resumeTabs: {
tab: "home",
dynamicData: "",
},
keybindings: {
search: ["CommandOrControl", "F"],
listnow: ["CommandOrControl", "L"],
browse: ["CommandOrControl", "B"],
recentAdd: ["CommandOrControl", "G"],
songs: ["CommandOrControl", "J"],
albums: ["CommandOrControl", process.platform == "darwin" ? "Option" : process.platform == "linux" ? "Shift" : "Alt", "A"],
artists: ["CommandOrControl", "D"],
togglePrivateSession: ["CommandOrControl", "P"],
webRemote: ["CommandOrControl", process.platform == "darwin" ? "Option" : process.platform == "linux" ? "Shift" : "Alt", "W"],
audioSettings: ["CommandOrControl", process.platform == "darwin" ? "Option" : process.platform == "linux" ? "Shift" : "Alt", "A"],
pluginMenu: ["CommandOrControl", process.platform == "darwin" ? "Option" : process.platform == "linux" ? "Shift" : "Alt", "P"],
castToDevices: ["CommandOrControl", process.platform == "darwin" ? "Option" : process.platform == "linux" ? "Shift" : "Alt", "C"],
settings: [
"CommandOrControl", // Who the hell uses a different key for this? Fucking Option?
",",
],
zoomn: ["Control", "numadd"],
zoomt: ["Control", "numsub"],
zoomrst: ["Control", "num0"],
openDeveloperTools: ["CommandOrControl", "Shift", "I"],
},
showLovedTracksInline: true,
},
connectivity: {
discord_rpc: {
enabled: true,
client: "Cider",
clear_on_pause: true,
hide_buttons: false,
hide_timestamp: false,
state_format: "by {artist}",
details_format: "{title}",
},
lastfm: {
enabled: false,
scrobble_after: 50,
filter_loop: false,
filter_types: {},
remove_featured: false,
secrets: {
username: "",
key: "",
},
"general": {
"close_button_hide": false,
"language": "en_US", // electron.app.getLocale().replace('-', '_') this can be used in future
"playbackNotifications": true,
"resumeOnStartupBehavior": "local",
"privateEnabled": false,
"themeUpdateNotification": true,
"sidebarItems": {
"recentlyAdded": true,
"songs": true,
"albums": true,
"artists": true,
"videos": true,
"podcasts": true
},
"sidebarCollapsed": {
"cider": false,
"applemusic": false,
"library": false,
"amplaylists": false,
"playlists": false,
"localLibrary": false
},
"onStartup": {
"enabled": false,
"hidden": false,
},
"resumeTabs": {
"tab": "home",
"dynamicData": ""
},
"keybindings": {
"search": [
"CommandOrControl",
"F"
],
"listnow": [
"CommandOrControl",
"L"
],
"browse": [
"CommandOrControl",
"B"
],
"recentAdd": [
"CommandOrControl",
"G"
],
"songs": [
"CommandOrControl",
"J"
],
"albums": [
"CommandOrControl",
process.platform == "darwin" ? "Option" : (process.platform == "linux" ? "Shift" : "Alt"),
"A"
],
"artists": [
"CommandOrControl",
"D"
],
"togglePrivateSession": [
"CommandOrControl",
"P"
],
"webRemote": [
"CommandOrControl",
process.platform == "darwin" ? "Option" : (process.platform == "linux" ? "Shift" : "Alt"),
"W"
],
"audioSettings": [
"CommandOrControl",
process.platform == "darwin" ? "Option" : (process.platform == "linux" ? "Shift" : "Alt"),
"A"
],
"pluginMenu": [
"CommandOrControl",
process.platform == "darwin" ? "Option" : (process.platform == "linux" ? "Shift" : "Alt"),
"P"
],
"castToDevices": [
"CommandOrControl",
process.platform == "darwin" ? "Option" : (process.platform == "linux" ? "Shift" : "Alt"),
"C"
],
"settings": [
"CommandOrControl", // Who the hell uses a different key for this? Fucking Option?
","
],
"zoomn": [
"Control",
"numadd",
],
"zoomt": [
"Control",
"numsub",
],
"zoomrst": [
"Control",
"num0",
],
"openDeveloperTools": [
"CommandOrControl",
"Shift",
"I"
]
},
"showLovedTracksInline": true
},
},
home: {
followedArtists: [],
favoriteItems: [],
},
libraryPrefs: {
songs: {
scroll: "paged",
sort: "name",
sortOrder: "asc",
size: "normal",
},
albums: {
scroll: "paged",
sort: "name",
sortOrder: "asc",
viewAs: "covers",
},
playlists: {
scroll: "infinite",
},
localPaths: [],
pageSize: 250,
},
audio: {
volume: 1,
volumeStep: 0.05,
maxVolume: 1,
lastVolume: 1,
muted: false,
playbackRate: 1,
quality: "HIGH",
seamless_audio: true,
normalization: true,
dBSPL: false,
dBSPLcalibration: 90,
maikiwiAudio: {
ciderPPE: true,
ciderPPE_value: "MAIKIWI",
opportunisticCorrection_state: "OFF",
atmosphereRealizer1: false,
atmosphereRealizer1_value: "NATURAL_STANDARD",
atmosphereRealizer2: false,
atmosphereRealizer2_value: "NATURAL_STANDARD",
spatial: false,
spatialProfile: "BPLK",
vibrantBass: {
// Hard coded into the app. Don't include any of this config into exporting presets in store.ts
frequencies: [17.182, 42.169, 53.763, 112.69, 119.65, 264.59, 336.57, 400.65, 505.48, 612.7, 838.7, 1155.3, 1175.6, 3406.8, 5158.6, 5968.1, 6999.9, 7468.6, 8862.9, 9666, 10109],
Q: [2.5, 0.388, 5, 5, 2.5, 7.071, 14.14, 10, 7.071, 14.14, 8.409, 0.372, 7.071, 10, 16.82, 7.071, 28.28, 20, 8.409, 40, 40],
gain: [-0.34, 2.49, 0.23, -0.49, 0.23, -0.12, 0.32, -0.29, 0.33, 0.19, -0.18, -1.27, -0.11, 0.25, -0.18, -0.53, 0.34, 1.32, 1.78, 0.41, -0.28],
},
"connectivity": {
"discord_rpc": {
"enabled": true,
"client": "Cider",
"clear_on_pause": true,
"hide_buttons": false,
"hide_timestamp": false,
"state_format": "by {artist}",
"details_format": "{title}",
},
"lastfm": {
"enabled": false,
"scrobble_after": 50,
"filter_loop": false,
"filter_types": {},
"remove_featured": false,
"secrets": {
"username": "",
"key": ""
}
},
spatial: false,
spatial_properties: {
presets: [],
gain: 0.8,
listener_position: [0, 0, 0],
audio_position: [0, 0, 0],
room_dimensions: {
width: 32,
height: 12,
depth: 32,
},
room_materials: {
left: "metal",
right: "metal",
front: "brick-bare",
back: "brick-bare",
down: "acoustic-ceiling-tiles",
up: "acoustic-ceiling-tiles",
},
},
equalizer: {
preset: "default",
frequencies: [32, 63, 125, 250, 500, 1000, 2000, 4000, 8000, 16000],
gain: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
Q: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
mix: 1,
vibrantBass: 0,
presets: [],
userGenerated: false,
},
},
visual: {
theme: "",
styles: [],
scrollbars: 0, // 0 = show on hover, 2 = always hide, 3 = always show
refresh_rate: 0,
window_background_style: "none", // "none", "artwork", "color"
animated_artwork: "limited", // 0 = always, 1 = limited, 2 = never
animated_artwork_qualityLevel: 1,
bg_artwork_rotation: false,
hw_acceleration: "default", // default, webgpu, disabled
showuserinfo: true,
transparent: false,
miniplayer_top_toggle: true,
directives: {
windowLayout: "default",
},
windowControlPosition: 0, // 0 default right
nativeTitleBar: false,
windowColor: "#000000",
customAccentColor: false,
accentColor: "#fc3c44",
purplePodcastPlaybackBar: false,
maxElementScale: -1, // -1 default, anything else is a custom scale
},
lyrics: {
enable_mxm: true,
mxm_karaoke: false,
mxm_language: "disabled",
enable_qq: false,
enable_yt: false,
},
advanced: {
AudioContext: true,
experiments: [],
playlistTrackMapping: true,
ffmpegLocation: "",
disableLogging: true,
},
connectUser: {
auth: null,
sync: {
themes: false,
plugins: false,
settings: false,
},
},
};
private migrations: any = {};
private schema: ElectronStore.Schema<any> = {
"connectivity.discord_rpc": {
type: "object",
},
};
},
},
"home": {
"followedArtists": [],
"favoriteItems": []
},
"libraryPrefs": {
"songs": {
"scroll": "paged",
"sort": "name",
"sortOrder": "asc",
"size": "normal"
},
"albums": {
"scroll": "paged",
"sort": "name",
"sortOrder": "asc",
"viewAs": "covers"
},
"playlists": {
"scroll": "infinite"
},
"localPaths": [],
"pageSize": 250
},
"audio": {
"volume": 1,
"volumeStep": 0.05,
"maxVolume": 1,
"lastVolume": 1,
"muted": false,
"playbackRate": 1,
"quality": "HIGH",
"seamless_audio": true,
"normalization": true,
"dBSPL": false,
"dBSPLcalibration": 90,
"maikiwiAudio": {
"ciderPPE": true,
"ciderPPE_value": "MAIKIWI",
"opportunisticCorrection_state": "OFF",
"atmosphereRealizer1": false,
"atmosphereRealizer1_value": "NATURAL_STANDARD",
"atmosphereRealizer2": false,
"atmosphereRealizer2_value": "NATURAL_STANDARD",
"spatial": false,
"spatialProfile": "BPLK",
"vibrantBass": { // Hard coded into the app. Don't include any of this config into exporting presets in store.ts
'frequencies': [17.182, 42.169, 53.763, 112.69, 119.65, 264.59, 336.57, 400.65, 505.48, 612.7, 838.7, 1155.3, 1175.6, 3406.8, 5158.6, 5968.1, 6999.9, 7468.6, 8862.9, 9666, 10109],
'Q': [2.5, 0.388, 5, 5, 2.5, 7.071, 14.14, 10, 7.071, 14.14, 8.409, 0.372, 7.071, 10, 16.82, 7.071, 28.28, 20, 8.409, 40, 40],
'gain': [-0.34, 2.49, 0.23, -0.49, 0.23, -0.12, 0.32, -0.29, 0.33, 0.19, -0.18, -1.27, -0.11, 0.25, -0.18, -0.53, 0.34, 1.32, 1.78, 0.41, -0.28]
}
},
"spatial": false,
"spatial_properties": {
"presets": [],
"gain": 0.8,
"listener_position": [0, 0, 0],
"audio_position": [0, 0, 0],
"room_dimensions": {
"width": 32,
"height": 12,
"depth": 32
},
"room_materials": {
"left": 'metal',
"right": 'metal',
"front": 'brick-bare',
"back": 'brick-bare',
"down": 'acoustic-ceiling-tiles',
"up": 'acoustic-ceiling-tiles',
}
},
"equalizer": {
'preset': "default",
'frequencies': [32, 63, 125, 250, 500, 1000, 2000, 4000, 8000, 16000],
'gain': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
'Q': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
'mix': 1,
'vibrantBass': 0,
'presets': [],
'userGenerated': false
},
},
"visual": {
"theme": "",
"styles": [],
"scrollbars": 0, // 0 = show on hover, 2 = always hide, 3 = always show
"refresh_rate": 0,
"window_background_style": "none", // "none", "artwork", "color"
"animated_artwork": "limited", // 0 = always, 1 = limited, 2 = never
"animated_artwork_qualityLevel": 1,
"bg_artwork_rotation": false,
"hw_acceleration": "default", // default, webgpu, disabled
"showuserinfo": true,
"transparent": false,
"miniplayer_top_toggle": true,
"directives": {
"windowLayout": "default"
},
"windowControlPosition": 0, // 0 default right
"nativeTitleBar": false,
"windowColor": "#000000",
"customAccentColor": false,
"accentColor": "#fc3c44",
"purplePodcastPlaybackBar": false,
"maxElementScale": -1 // -1 default, anything else is a custom scale
},
"lyrics": {
"enable_mxm": true,
"mxm_karaoke": false,
"mxm_language": "disabled",
"enable_qq": false,
"enable_yt": false,
},
"advanced": {
"AudioContext": true,
"experiments": [],
"playlistTrackMapping": true,
"ffmpegLocation": "",
"disableLogging": true
},
"connectUser": {
"auth": null,
"sync": {
themes: false,
plugins: false,
settings: false,
}
},
constructor() {
Store.cfg = new ElectronStore({
name: "cider-config",
defaults: this.defaults,
schema: this.schema,
migrations: this.migrations,
clearInvalidConfig: false, //disabled for now
});
Store.cfg.set(this.mergeStore(this.defaults, Store.cfg.store));
this.ipcHandler();
}
static pushToCloud(): void {
if (Store.cfg.get("connectUser.auth") === null) return;
var syncData = Object();
if (Store.cfg.get("connectUser.sync.themes")) {
syncData.push({
themes: Store.cfg.store.themes,
});
}
private migrations: any = {}
private schema: ElectronStore.Schema<any> = {
"connectivity.discord_rpc": {
type: 'object'
},
if (Store.cfg.get("connectUser.sync.plugins")) {
syncData.push({
plugins: Store.cfg.store.plugins,
});
}
constructor() {
Store.cfg = new ElectronStore({
name: 'cider-config',
defaults: this.defaults,
schema: this.schema,
migrations: this.migrations,
clearInvalidConfig: false //disabled for now
});
Store.cfg.set(this.mergeStore(this.defaults, Store.cfg.store))
this.ipcHandler();
if (Store.cfg.get("connectUser.sync.settings")) {
syncData.push({
general: Store.cfg.get("general"),
home: Store.cfg.get("home"),
libraryPrefs: Store.cfg.get("libraryPrefs"),
advanced: Store.cfg.get("advanced"),
});
}
let postBody = {
id: Store.cfg.get("connectUser.id"),
app: electron.app.getName(),
version: electron.app.isPackaged ? electron.app.getVersion() : "dev",
syncData: syncData,
};
static pushToCloud(): void {
if (Store.cfg.get('connectUser.auth') === null) return;
var syncData = Object();
if (Store.cfg.get('connectUser.sync.themes')) {
syncData.push({
themes: Store.cfg.store.themes
})
}
if (Store.cfg.get('connectUser.sync.plugins')) {
syncData.push({
plugins: Store.cfg.store.plugins
})
}
fetch("https://connect.cidercollective.dev/api/v1/setttings/set", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(postBody),
});
}
if (Store.cfg.get('connectUser.sync.settings')) {
syncData.push({
general: Store.cfg.get('general'),
home: Store.cfg.get('home'),
libraryPrefs: Store.cfg.get('libraryPrefs'),
advanced: Store.cfg.get('advanced'),
})
}
let postBody = {
id: Store.cfg.get('connectUser.id'),
app: electron.app.getName(),
version: electron.app.isPackaged ? electron.app.getVersion() : 'dev',
syncData: syncData
}
fetch('https://connect.cidercollective.dev/api/v1/setttings/set', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(postBody)
})
/**
* Merge Configurations
* @param target The target configuration
* @param source The source configuration
*/
private mergeStore = (target: { [x: string]: any }, source: { [x: string]: any }) => {
// Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties
for (const key of Object.keys(source)) {
if (key.includes("migrations")) {
continue;
}
if (source[key] instanceof Array) {
continue;
}
if (source[key] instanceof Object) Object.assign(source[key], this.mergeStore(target[key], source[key]));
}
// Join `target` and modified `source`
Object.assign(target || {}, source);
return target;
};
/**
* Merge Configurations
* @param target The target configuration
* @param source The source configuration
*/
private mergeStore = (target: { [x: string]: any; }, source: { [x: string]: any; }) => {
// Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties
for (const key of Object.keys(source)) {
if (key.includes('migrations')) {
continue;
}
if (source[key] instanceof Array) {
continue
}
if (source[key] instanceof Object) Object.assign(source[key], this.mergeStore(target[key], source[key]))
}
// Join `target` and modified `source`
Object.assign(target || {}, source)
return target
}
/**
* IPC Handler
*/
private ipcHandler(): void {
electron.ipcMain.handle("getStoreValue", (_event, key, defaultValue) => {
return defaultValue ? Store.cfg.get(key, true) : Store.cfg.get(key);
});
/**
* IPC Handler
*/
private ipcHandler(): void {
electron.ipcMain.handle('getStoreValue', (_event, key, defaultValue) => {
return (defaultValue ? Store.cfg.get(key, true) : Store.cfg.get(key));
});
electron.ipcMain.handle("setStoreValue", (_event, key, value) => {
Store.cfg.set(key, value);
});
electron.ipcMain.handle('setStoreValue', (_event, key, value) => {
Store.cfg.set(key, value);
});
electron.ipcMain.on("getStore", (event) => {
event.returnValue = Store.cfg.store;
});
electron.ipcMain.on('getStore', (event) => {
event.returnValue = Store.cfg.store
})
electron.ipcMain.on('setStore', (_event, store) => {
Store.cfg.store = store
})
}
electron.ipcMain.on("setStore", (_event, store) => {
Store.cfg.store = store;
});
}
}

View file

@ -1,97 +1,96 @@
import * as fs from "fs";
import * as path from "path";
import {Store} from "./store";
import {BrowserWindow as bw} from "./browserwindow";
import {app, BrowserWindow, ipcMain} from "electron";
import { Store } from "./store";
import { BrowserWindow as bw } from "./browserwindow";
import { app, BrowserWindow, ipcMain } from "electron";
import fetch from "electron-fetch";
import ElectronStore from "electron-store";
export class utils {
/**
* Playback Functions
*/
static playback = {
pause: () => {
bw.win.webContents.executeJavaScript("MusicKitInterop.pause()");
},
play: () => {
bw.win.webContents.executeJavaScript("MusicKitInterop.play()");
},
playPause: () => {
bw.win.webContents.executeJavaScript("MusicKitInterop.playPause()");
},
next: () => {
bw.win.webContents.executeJavaScript("MusicKitInterop.next()");
},
previous: () => {
bw.win.webContents.executeJavaScript("MusicKitInterop.previous()");
},
seek: (seconds: number) => {
bw.win.webContents.executeJavaScript(`MusicKit.getInstance().seekToTime(${seconds})`);
},
};
/**
* Paths for the application to use
*/
static paths: any = {
srcPath: path.join(__dirname, "../../src"),
rendererPath: path.join(__dirname, "../../src/renderer"),
mainPath: path.join(__dirname, "../../src/main"),
resourcePath: path.join(__dirname, "../../resources"),
i18nPath: path.join(__dirname, "../../src/i18n"),
i18nPathSrc: path.join(__dirname, "../../src/il8n/source"),
ciderCache: path.resolve(app.getPath("userData"), "CiderCache"),
themes: path.resolve(app.getPath("userData"), "Themes"),
plugins: path.resolve(app.getPath("userData"), "Plugins"),
externals: path.resolve(app.getPath("userData"), "externals"),
};
/**
* Playback Functions
*/
static playback = {
pause: () => {
bw.win.webContents.executeJavaScript("MusicKitInterop.pause()")
},
play: () => {
bw.win.webContents.executeJavaScript("MusicKitInterop.play()")
},
playPause: () => {
bw.win.webContents.executeJavaScript("MusicKitInterop.playPause()")
},
next: () => {
bw.win.webContents.executeJavaScript("MusicKitInterop.next()")
},
previous: () => {
bw.win.webContents.executeJavaScript("MusicKitInterop.previous()")
},
seek: (seconds: number) => {
bw.win.webContents.executeJavaScript(`MusicKit.getInstance().seekToTime(${seconds})`)
}
/**
* Get the path
* @returns {string}
* @param name
*/
static getPath(name: string): string {
return this.paths[name];
}
/**
* Get the app
* @returns {Electron.App}
*/
static getApp(): Electron.App {
return app;
}
/**
* Get the IPCMain
*/
static getIPCMain(): Electron.IpcMain {
return ipcMain;
}
/*
* Get the Express instance
* @returns {any}
*/
static getExpress(): any {
return bw.express;
}
/**
* Fetches the i18n locale for the given language.
* @param language {string} The language to fetch the locale for.
* @param key {string} The key to search for.
* @returns {string | Object} The locale value.
*/
static getLocale(language: string, key?: string): string | object {
let i18n: { [index: string]: Object } = JSON.parse(fs.readFileSync(path.join(this.paths.i18nPath, "en_US.json"), "utf8"));
if (language !== "en_US" && fs.existsSync(path.join(this.paths.i18nPath, `${language}.json`))) {
i18n = Object.assign(i18n, JSON.parse(fs.readFileSync(path.join(this.paths.i18nPath, `${language}.json`), "utf8")));
}
/**
* Paths for the application to use
*/
static paths: any = {
srcPath: path.join(__dirname, "../../src"),
rendererPath: path.join(__dirname, "../../src/renderer"),
mainPath: path.join(__dirname, "../../src/main"),
resourcePath: path.join(__dirname, "../../resources"),
i18nPath: path.join(__dirname, "../../src/i18n"),
i18nPathSrc: path.join(__dirname, "../../src/il8n/source"),
ciderCache: path.resolve(app.getPath("userData"), "CiderCache"),
themes: path.resolve(app.getPath("userData"), "Themes"),
plugins: path.resolve(app.getPath("userData"), "Plugins"),
externals: path.resolve(app.getPath("userData"), "externals"),
};
/**
* Get the path
* @returns {string}
* @param name
*/
static getPath(name: string): string {
return this.paths[name];
}
/**
* Get the app
* @returns {Electron.App}
*/
static getApp(): Electron.App {
return app;
}
/**
* Get the IPCMain
*/
static getIPCMain(): Electron.IpcMain {
return ipcMain
}
/*
* Get the Express instance
* @returns {any}
*/
static getExpress(): any {
return bw.express
}
/**
* Fetches the i18n locale for the given language.
* @param language {string} The language to fetch the locale for.
* @param key {string} The key to search for.
* @returns {string | Object} The locale value.
*/
static getLocale(language: string, key?: string): string | object {
let i18n: { [index: string]: Object } = JSON.parse(fs.readFileSync(path.join(this.paths.i18nPath, "en_US.json"), "utf8"));
if (language !== "en_US" && fs.existsSync(path.join(this.paths.i18nPath, `${language}.json`))) {
i18n = Object.assign(i18n, JSON.parse(fs.readFileSync(path.join(this.paths.i18nPath, `${language}.json`), "utf8")));
}
/* else if (!fs.existsSync(path.join(this.paths.i18nPath, `${language}.json`))) {
/* else if (!fs.existsSync(path.join(this.paths.i18nPath, `${language}.json`))) {
fetch(`https://raw.githubusercontent.com/ciderapp/Cider/main/src/i18n/${language}.json`)
.then(res => res.json())
.then(res => {
@ -103,72 +102,70 @@ export class utils {
}
})
} */
if (key) {
return i18n[key]
} else {
return i18n
}
}
/**
* Gets a store value
* @param key
* @returns store value
*/
static getStoreValue(key: string): any {
return Store.cfg.get(key)
if (key) {
return i18n[key];
} else {
return i18n;
}
}
/**
* Sets a store
* @returns store
*/
static getStore(): Object {
return Store.cfg.store
/**
* Gets a store value
* @param key
* @returns store value
*/
static getStoreValue(key: string): any {
return Store.cfg.get(key);
}
/**
* Sets a store
* @returns store
*/
static getStore(): Object {
return Store.cfg.store;
}
/**
* Get the store instance
* @returns {Store}
*/
static getStoreInstance(): ElectronStore {
return Store.cfg;
}
/**
* Sets a store value
* @param key
* @param value
*/
static setStoreValue(key: string, value: any): void {
Store.cfg.set(key, value);
}
/**
* Pushes Store to Connect
* @return Function
*/
static pushStoreToConnect(): Function {
return Store.pushToCloud;
}
/**
* Gets the browser window
*/
static getWindow(): Electron.BrowserWindow {
if (bw.win) {
return bw.win;
} else {
return BrowserWindow.getAllWindows()[0];
}
}
/**
* Get the store instance
* @returns {Store}
*/
static getStoreInstance(): ElectronStore {
return Store.cfg
}
static loadPluginFrontend(path: string): void {}
/**
* Sets a store value
* @param key
* @param value
*/
static setStoreValue(key: string, value: any): void {
Store.cfg.set(key, value)
}
/**
* Pushes Store to Connect
* @return Function
*/
static pushStoreToConnect(): Function {
return Store.pushToCloud
}
/**
* Gets the browser window
*/
static getWindow(): Electron.BrowserWindow {
if (bw.win) {
return bw.win
} else {
return BrowserWindow.getAllWindows()[0]
}
}
static loadPluginFrontend(path: string): void {
}
static loadJSFrontend(path: string): void {
bw.win.webContents.executeJavaScript(fs.readFileSync(path, "utf8"));
}
static loadJSFrontend(path: string): void {
bw.win.webContents.executeJavaScript(fs.readFileSync(path, "utf8"));
}
}

View file

@ -64,4 +64,4 @@
"components/artist-chip",
"components/hello-world",
"components/inline-collection-list"
]
]

View file

@ -100,7 +100,8 @@
"component": "<cider-groupings :data=\"browsepage\"></cider-groupings>",
"condition": "page == 'groupings'",
"onEnter": ""
},{
},
{
"page": "charts",
"component": "<cider-charts :data=\"browsepage\"></cider-charts>",
"condition": "page == 'charts'",
@ -181,4 +182,4 @@
"component": "<replay-page></replay-page>",
"condition": "page == 'replay'"
}
]
]

View file

@ -4,366 +4,421 @@ import * as electron from "electron";
const WebSocketServer = ws.Server;
interface standardResponse {
status?: Number,
message?: String,
data?: any,
type?: string,
status?: Number;
message?: String;
data?: any;
type?: string;
}
export class wsapi {
static clients: any;
port: any = 26369
wss: any = null
clients: any = []
private _win: any;
static clients: any;
port: any = 26369;
wss: any = null;
clients: any = [];
private _win: any;
constructor(win: any) {
this._win = win;
}
constructor(win: any) {
this._win = win;
}
createId() {
// create random guid
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
var r = (Math.random() * 16) | 0,
v = c == "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
createId() {
// create random guid
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
public async InitWebSockets() {
electron.ipcMain.on("wsapi-updatePlaybackState", (_event: any, arg: any) => {
this.updatePlaybackState(arg);
});
public async InitWebSockets() {
electron.ipcMain.on('wsapi-updatePlaybackState', (_event: any, arg: any) => {
this.updatePlaybackState(arg);
})
electron.ipcMain.on("wsapi-returnQueue", (_event: any, arg: any) => {
this.returnQueue(JSON.parse(arg));
});
electron.ipcMain.on('wsapi-returnQueue', (_event: any, arg: any) => {
this.returnQueue(JSON.parse(arg));
});
electron.ipcMain.on("wsapi-returnSearch", (_event: any, arg: any) => {
console.log("SEARCH");
this.returnSearch(JSON.parse(arg));
});
electron.ipcMain.on('wsapi-returnSearch', (_event: any, arg: any) => {
console.log("SEARCH")
this.returnSearch(JSON.parse(arg));
});
electron.ipcMain.on("wsapi-returnSearchLibrary", (_event: any, arg: any) => {
this.returnSearchLibrary(JSON.parse(arg));
});
electron.ipcMain.on('wsapi-returnSearchLibrary', (_event: any, arg: any) => {
this.returnSearchLibrary(JSON.parse(arg));
});
electron.ipcMain.on("wsapi-returnDynamic", (_event: any, arg: any, type: any) => {
this.returnDynamic(JSON.parse(arg), type);
});
electron.ipcMain.on('wsapi-returnDynamic', (_event: any, arg: any, type: any) => {
this.returnDynamic(JSON.parse(arg), type);
});
electron.ipcMain.on("wsapi-returnMusicKitApi", (_event: any, arg: any, method: any) => {
this.returnMusicKitApi(JSON.parse(arg), method);
});
electron.ipcMain.on('wsapi-returnMusicKitApi', (_event: any, arg: any, method: any) => {
this.returnMusicKitApi(JSON.parse(arg), method);
});
electron.ipcMain.on("wsapi-returnLyrics", (_event: any, arg: any) => {
this.returnLyrics(JSON.parse(arg));
});
electron.ipcMain.on("wsapi-returnvolumeMax", (_event: any, arg: any) => {
this.returnmaxVolume(JSON.parse(arg));
});
electron.ipcMain.on("wsapi-libraryStatus", (_event: any, inLibrary: boolean, rating: number) => {
this.returnLibraryStatus(inLibrary, rating);
});
electron.ipcMain.on("wsapi-rate", (_event: any, kind: string, id: string, rating: number) => {
this.returnRatingStatus(kind, id, rating);
});
electron.ipcMain.on("wsapi-change-library", (_event: any, kind: string, id: string, shouldAdd: boolean) => {
this.returnLibraryChange(kind, id, shouldAdd);
});
this.wss = new WebSocketServer({
port: this.port,
perMessageDeflate: {
zlibDeflateOptions: {
// See zlib defaults.
chunkSize: 1024,
memLevel: 7,
level: 3,
},
zlibInflateOptions: {
chunkSize: 10 * 1024,
},
// Other options settable:
clientNoContextTakeover: true, // Defaults to negotiated value.
serverNoContextTakeover: true, // Defaults to negotiated value.
serverMaxWindowBits: 10, // Defaults to negotiated value.
// Below options specified as default values.
concurrencyLimit: 10, // Limits zlib concurrency for perf.
threshold: 1024, // Size (in bytes) below which messages
// should not be compressed if context takeover is disabled.
},
});
console.log(`WebSocketServer started on port: ${this.port}`);
electron.ipcMain.on('wsapi-returnLyrics', (_event: any, arg: any) => {
this.returnLyrics(JSON.parse(arg));
});
electron.ipcMain.on('wsapi-returnvolumeMax', (_event: any, arg: any) => {
this.returnmaxVolume(JSON.parse(arg));
});
electron.ipcMain.on('wsapi-libraryStatus', (_event: any, inLibrary: boolean, rating: number) => {
this.returnLibraryStatus(inLibrary, rating);
});
electron.ipcMain.on('wsapi-rate', (_event: any, kind: string, id: string, rating: number) => {
this.returnRatingStatus(kind, id, rating);
});
electron.ipcMain.on('wsapi-change-library', (_event: any, kind: string, id: string, shouldAdd: boolean) => {
this.returnLibraryChange(kind, id, shouldAdd);
});
this.wss = new WebSocketServer({
port: this.port,
perMessageDeflate: {
zlibDeflateOptions: {
// See zlib defaults.
chunkSize: 1024,
memLevel: 7,
level: 3
},
zlibInflateOptions: {
chunkSize: 10 * 1024
},
// Other options settable:
clientNoContextTakeover: true, // Defaults to negotiated value.
serverNoContextTakeover: true, // Defaults to negotiated value.
serverMaxWindowBits: 10, // Defaults to negotiated value.
// Below options specified as default values.
concurrencyLimit: 10, // Limits zlib concurrency for perf.
threshold: 1024 // Size (in bytes) below which messages
// should not be compressed if context takeover is disabled.
const defaultResponse: standardResponse = {
status: 0,
data: {},
message: "OK",
type: "generic",
};
this.wss.on("connection", (ws: any) => {
ws.id = this.createId();
console.log(`Client ${ws.id} connected`);
this.clients.push(ws);
ws.on("message", function incoming(_message: any) {});
// ws on message
ws.on("message", (message: any) => {
let data = JSON.parse(message);
let response: standardResponse = {
status: 0,
data: {},
message: "OK",
type: "generic",
};
if (data.action) {
data.action.toLowerCase();
}
switch (data.action) {
default:
response.message = "Action not found";
break;
case "identify":
response.message = "Thanks for identifying!";
response.data = {
id: ws.id,
};
ws.identity = {
name: data.name,
author: data.author,
description: data.description,
version: data.version,
};
break;
case "play-next":
this._win.webContents.executeJavaScript(`wsapi.playNext(\`${data.type}\`,\`${data.id}\`)`);
response.message = "Play Next";
break;
case "play-later":
this._win.webContents.executeJavaScript(`wsapi.playLater(\`${data.type}\`,\`${data.id}\`)`);
response.message = "Play Later";
break;
case "quick-play":
this._win.webContents.executeJavaScript(`wsapi.quickPlay(\`${data.term}\`)`);
response.message = "Quick Play";
break;
case "get-lyrics":
this._win.webContents.executeJavaScript(`wsapi.getLyrics()`);
break;
case "shuffle":
this._win.webContents.executeJavaScript(`wsapi.toggleShuffle()`);
break;
case "set-shuffle":
if (data.shuffle == true) {
this._win.webContents.executeJavaScript(`MusicKit.getInstance().shuffleMode = 1`);
} else {
this._win.webContents.executeJavaScript(`MusicKit.getInstance().shuffleMode = 0`);
}
})
console.log(`WebSocketServer started on port: ${this.port}`);
const defaultResponse: standardResponse = {status: 0, data: {}, message: "OK", type: "generic"};
this.wss.on('connection', (ws: any) => {
ws.id = this.createId();
console.log(`Client ${ws.id} connected`)
this.clients.push(ws);
ws.on('message', function incoming(_message: any) {
});
// ws on message
ws.on('message', (message: any) => {
let data = JSON.parse(message);
let response: standardResponse = {status: 0, data: {}, message: "OK", type: "generic"};
if (data.action) {
data.action.toLowerCase();
}
switch (data.action) {
default:
response.message = "Action not found";
break;
case "identify":
response.message = "Thanks for identifying!"
response.data = {
id: ws.id
}
ws.identity = {
name: data.name,
author: data.author,
description: data.description,
version: data.version
}
break;
case "play-next":
this._win.webContents.executeJavaScript(`wsapi.playNext(\`${data.type}\`,\`${data.id}\`)`);
response.message = "Play Next";
break;
case "play-later":
this._win.webContents.executeJavaScript(`wsapi.playLater(\`${data.type}\`,\`${data.id}\`)`);
response.message = "Play Later";
break;
case "quick-play":
this._win.webContents.executeJavaScript(`wsapi.quickPlay(\`${data.term}\`)`);
response.message = "Quick Play";
break;
case "get-lyrics":
this._win.webContents.executeJavaScript(`wsapi.getLyrics()`);
break;
case "shuffle":
this._win.webContents.executeJavaScript(`wsapi.toggleShuffle()`);
break;
case "set-shuffle":
if (data.shuffle == true) {
this._win.webContents.executeJavaScript(`MusicKit.getInstance().shuffleMode = 1`);
} else {
this._win.webContents.executeJavaScript(`MusicKit.getInstance().shuffleMode = 0`);
}
break;
case "repeat":
this._win.webContents.executeJavaScript(`wsapi.toggleRepeat()`);
break;
case "seek":
this._win.webContents.executeJavaScript(`MusicKit.getInstance().seekToTime(${parseFloat(data.time)})`);
response.message = "Seek";
break;
case "pause":
this._win.webContents.executeJavaScript(`MusicKit.getInstance().pause()`);
response.message = "Paused";
break;
case "playpause":
this._win.webContents.executeJavaScript(`MusicKitInterop.playPause()`);
response.message = "Play/Pause";
break
case "play":
this._win.webContents.executeJavaScript(`MusicKit.getInstance().play()`);
response.message = "Playing";
break;
case "stop":
this._win.webContents.executeJavaScript(`MusicKit.getInstance().stop()`);
response.message = "Stopped";
break;
case "volumeMax":
this._win.webContents.executeJavaScript(`wsapi.getmaxVolume()`);
response.message = "maxVolume";
break;
case "volume":
this._win.webContents.executeJavaScript(`MusicKit.getInstance().volume = ${parseFloat(data.volume)}`);
response.message = "Volume";
break;
case "mute":
this._win.webContents.executeJavaScript(`MusicKit.getInstance().mute()`);
response.message = "Muted";
break;
case "unmute":
this._win.webContents.executeJavaScript(`MusicKit.getInstance().unmute()`);
response.message = "Unmuted";
break;
case "next":
this._win.webContents.executeJavaScript(`if (MusicKit.getInstance().queue.nextPlayableItemIndex != -1 && MusicKit.getInstance().queue.nextPlayableItemIndex != null) {
break;
case "repeat":
this._win.webContents.executeJavaScript(`wsapi.toggleRepeat()`);
break;
case "seek":
this._win.webContents.executeJavaScript(`MusicKit.getInstance().seekToTime(${parseFloat(data.time)})`);
response.message = "Seek";
break;
case "pause":
this._win.webContents.executeJavaScript(`MusicKit.getInstance().pause()`);
response.message = "Paused";
break;
case "playpause":
this._win.webContents.executeJavaScript(`MusicKitInterop.playPause()`);
response.message = "Play/Pause";
break;
case "play":
this._win.webContents.executeJavaScript(`MusicKit.getInstance().play()`);
response.message = "Playing";
break;
case "stop":
this._win.webContents.executeJavaScript(`MusicKit.getInstance().stop()`);
response.message = "Stopped";
break;
case "volumeMax":
this._win.webContents.executeJavaScript(`wsapi.getmaxVolume()`);
response.message = "maxVolume";
break;
case "volume":
this._win.webContents.executeJavaScript(`MusicKit.getInstance().volume = ${parseFloat(data.volume)}`);
response.message = "Volume";
break;
case "mute":
this._win.webContents.executeJavaScript(`MusicKit.getInstance().mute()`);
response.message = "Muted";
break;
case "unmute":
this._win.webContents.executeJavaScript(`MusicKit.getInstance().unmute()`);
response.message = "Unmuted";
break;
case "next":
this._win.webContents.executeJavaScript(`if (MusicKit.getInstance().queue.nextPlayableItemIndex != -1 && MusicKit.getInstance().queue.nextPlayableItemIndex != null) {
try {
app.prevButtonBackIndicator = false;
} catch (e) { }
MusicKit.getInstance().changeToMediaAtIndex(MusicKit.getInstance().queue.nextPlayableItemIndex);}`);
response.message = "Next";
break;
case "previous":
this._win.webContents.executeJavaScript(`if (MusicKit.getInstance().queue.previousPlayableItemIndex != -1 && MusicKit.getInstance().queue.previousPlayableItemIndex != null) {MusicKit.getInstance().changeToMediaAtIndex(MusicKit.getInstance().queue.previousPlayableItemIndex)}`);
response.message = "Previous";
break;
case "musickit-api":
this._win.webContents.executeJavaScript(`wsapi.musickitApi(\`${data.method}\`, \`${data.id}\`, ${JSON.stringify(data.params)} , ${data.library})`);
break;
case "musickit-library-api":
break;
case "set-autoplay":
this._win.webContents.executeJavaScript(`wsapi.setAutoplay(${data.autoplay})`);
break;
case "queue-move":
this._win.webContents.executeJavaScript(`wsapi.moveQueueItem(${data.from},${data.to})`);
break;
case "get-queue":
this._win.webContents.executeJavaScript(`wsapi.getQueue()`);
break;
case "search":
if (!data.limit) {
data.limit = 10;
}
this._win.webContents.executeJavaScript(`wsapi.search(\`${data.term}\`, \`${data.limit}\`)`);
break;
case "library-search":
if (!data.limit) {
data.limit = 10;
}
this._win.webContents.executeJavaScript(`wsapi.searchLibrary(\`${data.term}\`, \`${data.limit}\`)`);
break;
case "show-window":
this._win.show()
break;
case "hide-window":
this._win.hide()
break;
case "play-mediaitem":
this._win.webContents.executeJavaScript(`wsapi.playTrackById("${data.id}", \`${data.kind}\`)`);
response.message = "Playing track";
break;
case "get-status":
response.data = {
isAuthorized: true
};
response.message = "Status";
break;
case "get-currentmediaitem":
this._win.webContents.executeJavaScript(`wsapi.getPlaybackState()`);
break;
case "library-status":
this._win.webContents.executeJavaScript(`wsapi.getLibraryStatus("${data.type}", "${data.id}")`);
break;
case "rating":
this._win.webContents.executeJavaScript(`wsapi.rate("${data.type}", "${data.id}", ${data.rating})`);
break;
case "change-library":
this._win.webContents.executeJavaScript(`wsapi.changeLibrary("${data.type}", "${data.id}", ${data.add})`);
break;
case "quit":
electron.app.quit();
break;
}
ws.send(JSON.stringify(response));
});
ws.on('close', () => {
// remove client from list
this.clients.splice(wsapi.clients.indexOf(ws), 1);
console.log(`Client ${ws.id} disconnected`);
});
ws.send(JSON.stringify(defaultResponse));
});
}
sendToClient(_id: any) {
// replace the clients.forEach with a filter to find the client that requested
}
updatePlaybackState(attr: any) {
const response: standardResponse = {status: 0, data: attr, message: "OK", type: "playbackStateUpdate"};
this.clients.forEach(function each(client: any) {
client.send(JSON.stringify(response));
});
}
returnMusicKitApi(results: any, method: any) {
const response: standardResponse = {status: 0, data: results, message: "OK", type: `musickitapi.${method}`};
this.clients.forEach(function each(client: any) {
client.send(JSON.stringify(response));
});
}
returnDynamic(results: any, type: any) {
const response: standardResponse = {status: 0, data: results, message: "OK", type: type};
this.clients.forEach(function each(client: any) {
client.send(JSON.stringify(response));
});
}
returnLyrics(results: any) {
const response: standardResponse = {status: 0, data: results, message: "OK", type: "lyrics"};
this.clients.forEach(function each(client: any) {
client.send(JSON.stringify(response));
});
}
returnSearch(results: any) {
const response: standardResponse = {status: 0, data: results, message: "OK", type: "searchResults"};
this.clients.forEach(function each(client: any) {
client.send(JSON.stringify(response));
});
}
returnSearchLibrary(results: any) {
const response: standardResponse = {status: 0, data: results, message: "OK", type: "searchResultsLibrary"};
this.clients.forEach(function each(client: any) {
client.send(JSON.stringify(response));
});
}
returnQueue(queue: any) {
const response: standardResponse = {status: 0, data: queue, message: "OK", type: "queue"};
this.clients.forEach(function each(client: any) {
client.send(JSON.stringify(response));
});
}
returnmaxVolume(vol: any) {
const response: standardResponse = {status: 0, data: vol, message: "OK", type: "maxVolume"};
this.clients.forEach(function each(client: any) {
client.send(JSON.stringify(response));
});
}
returnLibraryStatus(inLibrary: boolean, rating: number) {
const response: standardResponse = {
status: 0, data: {
inLibrary, rating
}, message: "OK", type: "libraryStatus"
response.message = "Next";
break;
case "previous":
this._win.webContents.executeJavaScript(
`if (MusicKit.getInstance().queue.previousPlayableItemIndex != -1 && MusicKit.getInstance().queue.previousPlayableItemIndex != null) {MusicKit.getInstance().changeToMediaAtIndex(MusicKit.getInstance().queue.previousPlayableItemIndex)}`
);
response.message = "Previous";
break;
case "musickit-api":
this._win.webContents.executeJavaScript(`wsapi.musickitApi(\`${data.method}\`, \`${data.id}\`, ${JSON.stringify(data.params)} , ${data.library})`);
break;
case "musickit-library-api":
break;
case "set-autoplay":
this._win.webContents.executeJavaScript(`wsapi.setAutoplay(${data.autoplay})`);
break;
case "queue-move":
this._win.webContents.executeJavaScript(`wsapi.moveQueueItem(${data.from},${data.to})`);
break;
case "get-queue":
this._win.webContents.executeJavaScript(`wsapi.getQueue()`);
break;
case "search":
if (!data.limit) {
data.limit = 10;
}
this._win.webContents.executeJavaScript(`wsapi.search(\`${data.term}\`, \`${data.limit}\`)`);
break;
case "library-search":
if (!data.limit) {
data.limit = 10;
}
this._win.webContents.executeJavaScript(`wsapi.searchLibrary(\`${data.term}\`, \`${data.limit}\`)`);
break;
case "show-window":
this._win.show();
break;
case "hide-window":
this._win.hide();
break;
case "play-mediaitem":
this._win.webContents.executeJavaScript(`wsapi.playTrackById("${data.id}", \`${data.kind}\`)`);
response.message = "Playing track";
break;
case "get-status":
response.data = {
isAuthorized: true,
};
response.message = "Status";
break;
case "get-currentmediaitem":
this._win.webContents.executeJavaScript(`wsapi.getPlaybackState()`);
break;
case "library-status":
this._win.webContents.executeJavaScript(`wsapi.getLibraryStatus("${data.type}", "${data.id}")`);
break;
case "rating":
this._win.webContents.executeJavaScript(`wsapi.rate("${data.type}", "${data.id}", ${data.rating})`);
break;
case "change-library":
this._win.webContents.executeJavaScript(`wsapi.changeLibrary("${data.type}", "${data.id}", ${data.add})`);
break;
case "quit":
electron.app.quit();
break;
}
this.clients.forEach(function each(client: any) {
client.send(JSON.stringify(response));
});
}
ws.send(JSON.stringify(response));
});
returnRatingStatus(kind: string, id: string, rating: number) {
const response: standardResponse = {
status: 0, data: { kind, id, rating },
message: "OK", type: "rate"
};
this.clients.forEach(function each(client: any) {
client.send(JSON.stringify(response));
});
}
ws.on("close", () => {
// remove client from list
this.clients.splice(wsapi.clients.indexOf(ws), 1);
console.log(`Client ${ws.id} disconnected`);
});
ws.send(JSON.stringify(defaultResponse));
});
}
returnLibraryChange(kind: string, id: string, shouldAdd: boolean) {
const response: standardResponse = {
status: 0, data: { kind, id, add: shouldAdd },
message: "OK", type: "change-library"
};
this.clients.forEach(function each(client: any) {
client.send(JSON.stringify(response));
});
}
}
sendToClient(_id: any) {
// replace the clients.forEach with a filter to find the client that requested
}
updatePlaybackState(attr: any) {
const response: standardResponse = {
status: 0,
data: attr,
message: "OK",
type: "playbackStateUpdate",
};
this.clients.forEach(function each(client: any) {
client.send(JSON.stringify(response));
});
}
returnMusicKitApi(results: any, method: any) {
const response: standardResponse = {
status: 0,
data: results,
message: "OK",
type: `musickitapi.${method}`,
};
this.clients.forEach(function each(client: any) {
client.send(JSON.stringify(response));
});
}
returnDynamic(results: any, type: any) {
const response: standardResponse = {
status: 0,
data: results,
message: "OK",
type: type,
};
this.clients.forEach(function each(client: any) {
client.send(JSON.stringify(response));
});
}
returnLyrics(results: any) {
const response: standardResponse = {
status: 0,
data: results,
message: "OK",
type: "lyrics",
};
this.clients.forEach(function each(client: any) {
client.send(JSON.stringify(response));
});
}
returnSearch(results: any) {
const response: standardResponse = {
status: 0,
data: results,
message: "OK",
type: "searchResults",
};
this.clients.forEach(function each(client: any) {
client.send(JSON.stringify(response));
});
}
returnSearchLibrary(results: any) {
const response: standardResponse = {
status: 0,
data: results,
message: "OK",
type: "searchResultsLibrary",
};
this.clients.forEach(function each(client: any) {
client.send(JSON.stringify(response));
});
}
returnQueue(queue: any) {
const response: standardResponse = {
status: 0,
data: queue,
message: "OK",
type: "queue",
};
this.clients.forEach(function each(client: any) {
client.send(JSON.stringify(response));
});
}
returnmaxVolume(vol: any) {
const response: standardResponse = {
status: 0,
data: vol,
message: "OK",
type: "maxVolume",
};
this.clients.forEach(function each(client: any) {
client.send(JSON.stringify(response));
});
}
returnLibraryStatus(inLibrary: boolean, rating: number) {
const response: standardResponse = {
status: 0,
data: {
inLibrary,
rating,
},
message: "OK",
type: "libraryStatus",
};
this.clients.forEach(function each(client: any) {
client.send(JSON.stringify(response));
});
}
returnRatingStatus(kind: string, id: string, rating: number) {
const response: standardResponse = {
status: 0,
data: { kind, id, rating },
message: "OK",
type: "rate",
};
this.clients.forEach(function each(client: any) {
client.send(JSON.stringify(response));
});
}
returnLibraryChange(kind: string, id: string, shouldAdd: boolean) {
const response: standardResponse = {
status: 0,
data: { kind, id, add: shouldAdd },
message: "OK",
type: "change-library",
};
this.clients.forEach(function each(client: any) {
client.send(JSON.stringify(response));
});
}
}

View file

@ -1,27 +1,27 @@
require("v8-compile-cache");
import {join} from "path";
import {app} from "electron"
import { join } from "path";
import { app } from "electron";
if (!app.isPackaged) {
app.setPath("userData", join(app.getPath("appData"), "Cider"));
app.setPath("userData", join(app.getPath("appData"), "Cider"));
}
import {Store} from "./base/store";
import {AppEvents} from "./base/app";
import {Plugins} from "./base/plugins";
import {BrowserWindow} from "./base/browserwindow";
import {init as Sentry} from "@sentry/electron";
import {RewriteFrames} from "@sentry/integrations";
import {components, ipcMain} from "electron"
import { Store } from "./base/store";
import { AppEvents } from "./base/app";
import { Plugins } from "./base/plugins";
import { BrowserWindow } from "./base/browserwindow";
import { init as Sentry } from "@sentry/electron";
import { RewriteFrames } from "@sentry/integrations";
import { components, ipcMain } from "electron";
// Analytics for debugging fun yeah.
Sentry({
dsn: "https://68c422bfaaf44dea880b86aad5a820d2@o954055.ingest.sentry.io/6112214",
integrations: [
new RewriteFrames({
root: process.cwd(),
}),
],
dsn: "https://68c422bfaaf44dea880b86aad5a820d2@o954055.ingest.sentry.io/6112214",
integrations: [
new RewriteFrames({
root: process.cwd(),
}),
],
});
new Store();
@ -33,31 +33,30 @@ const CiderPlug = new Plugins();
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
app.on("ready", () => {
Cider.ready(CiderPlug);
Cider.ready(CiderPlug);
console.log("[Cider] Application is Ready. Creating Window.")
if (!app.isPackaged) {
console.info("[Cider] Running in development mode.")
require("vue-devtools").install()
}
console.log("[Cider] Application is Ready. Creating Window.");
if (!app.isPackaged) {
console.info("[Cider] Running in development mode.");
require("vue-devtools").install();
}
components.whenReady().then(async () => {
const bw = new BrowserWindow()
const win = await bw.createWindow()
components.whenReady().then(async () => {
const bw = new BrowserWindow();
const win = await bw.createWindow();
app.getGPUInfo("complete").then(gpuInfo => {
console.log(gpuInfo)
})
console.log("[Cider][Widevine] Status:", components.status());
Cider.bwCreated();
win.on("ready-to-show", () => {
console.debug("[Cider] Window is Ready.")
CiderPlug.callPlugins("onReady", win);
win.show();
});
app.getGPUInfo("complete").then((gpuInfo) => {
console.log(gpuInfo);
});
console.log("[Cider][Widevine] Status:", components.status());
Cider.bwCreated();
win.on("ready-to-show", () => {
console.debug("[Cider] Window is Ready.");
CiderPlug.callPlugins("onReady", win);
win.show();
});
});
});
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -65,20 +64,20 @@ app.on("ready", () => {
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
ipcMain.handle("renderer-ready", (event) => {
CiderPlug.callPlugins("onRendererReady", event);
})
CiderPlug.callPlugins("onRendererReady", event);
});
ipcMain.on("playbackStateDidChange", (_event, attributes) => {
CiderPlug.callPlugins("onPlaybackStateDidChange", attributes);
CiderPlug.callPlugins("onPlaybackStateDidChange", attributes);
});
ipcMain.on("nowPlayingItemDidChange", (_event, attributes) => {
CiderPlug.callPlugins("onNowPlayingItemDidChange", attributes);
CiderPlug.callPlugins("onNowPlayingItemDidChange", attributes);
});
app.on("before-quit", () => {
CiderPlug.callPlugins("onBeforeQuit");
console.warn(`${app.getName()} exited.`);
CiderPlug.callPlugins("onBeforeQuit");
console.warn(`${app.getName()} exited.`);
});
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -87,20 +86,20 @@ app.on("before-quit", () => {
// @ts-ignore
app.on("widevine-ready", (version, lastVersion) => {
if (null !== lastVersion) {
console.log("[Cider][Widevine] Widevine " + version + ", upgraded from " + lastVersion + ", is ready to be used!")
} else {
console.log("[Cider][Widevine] Widevine " + version + " is ready to be used!")
}
})
if (null !== lastVersion) {
console.log("[Cider][Widevine] Widevine " + version + ", upgraded from " + lastVersion + ", is ready to be used!");
} else {
console.log("[Cider][Widevine] Widevine " + version + " is ready to be used!");
}
});
// @ts-ignore
app.on("widevine-update-pending", (currentVersion, pendingVersion) => {
console.log("[Cider][Widevine] Widevine " + currentVersion + " is ready to be upgraded to " + pendingVersion + "!")
})
console.log("[Cider][Widevine] Widevine " + currentVersion + " is ready to be upgraded to " + pendingVersion + "!");
});
// @ts-ignore
app.on("widevine-error", (error) => {
console.log("[Cider][Widevine] Widevine installation encountered an error: " + error)
app.exit()
})
console.log("[Cider][Widevine] Widevine installation encountered an error: " + error);
app.exit();
});

View file

@ -1,372 +1,352 @@
import * as electron from 'electron';
import * as os from 'os';
import {resolve} from 'path';
import * as CiderReceiver from '../base/castreceiver';
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 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 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 connectedPlayer: any;
private ciderPort: any = 9000;
// private server = false;
// private bufcount = 0;
// private bufcount2 = 0;
// private headerSent = false;
// 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 connectedPlayer: any;
private ciderPort :any = 9000;
// 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")) {
let a = service.txt.filter((u: any) => String(u).startsWith("fn="));
let name = (a[0] ?? "").substring(3) != "" ? (a[0] ?? "").substring(3) : service.fullname.substring(0, service.fullname.indexOf("._googlecast"));
this.ondeviceup(service.addresses[0], name + " (" + (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);
}
});
private searchForGCDevices() {
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() + ":" + this.ciderPort + "/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");
electron.ipcMain.on("stopGCast", (_event) => {
player.kill();
});
electron.app.on("before-quit", (_event) => {
player.kill();
});
});
}
private getIp() {
let ip: string = "";
let ip2: any = [];
let alias = 0;
const ifaces: any = os.networkInterfaces();
for (let dev in ifaces) {
ifaces[dev].forEach((details: any) => {
if (details.family === "IPv4" && !details.internal) {
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.") {
if (
!ip.startsWith("192.168.") ||
(ip2.startsWith("192.168.") && !ip.startsWith("192.168.") && ip2.startsWith("172.16.") && !ip.startsWith("192.168.") && !ip.startsWith("172.16.")) ||
(ip2.startsWith("10.") && !ip.startsWith("192.168.") && !ip.startsWith("172.16.") && !ip.startsWith("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(utils: { getApp: () => any; getStore: () => any }) {
this._app = utils.getApp();
this._store = utils.getStore();
}
/**
* 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.forEach((client: any) => {
try {
client.stop();
} catch (e) {}
});
this.activeConnections = [];
this.connectedHosts = {};
});
}
let browser = this.mdns.createBrowser(this.mdns.tcp('googlecast'));
browser.on('ready', browser.discover);
/**
* Runs on app stop
*/
onBeforeQuit(): void {}
browser.on('update', (service: any) => {
if (service.addresses && service.fullname && service.fullname.includes('_googlecast._tcp')) {
let a = service.txt.filter((u: any) => String(u).startsWith('fn='))
let name = (((a[0] ?? "").substring(3)) != "") ? ((a[0] ?? "").substring(3)) : (service.fullname.substring(0, service.fullname.indexOf("._googlecast")) )
this.ondeviceup(service.addresses[0], name+ " (" + (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);
}
/**
* Runs on song change
* @param attributes Music Attributes
*/
onNowPlayingItemDidChange(attributes: any): void {}
});
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() + ':'+ this.ciderPort +'/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");
electron.ipcMain.on('stopGCast', (_event) => {
player.kill();
})
electron.app.on('before-quit', (_event) => {
player.kill();
})
});
}
private getIp(){
let ip: string = '';
let ip2: any = [];
let alias = 0;
const ifaces: any = os.networkInterfaces();
for (let dev in ifaces) {
ifaces[dev].forEach((details: any) => {
if (details.family === 'IPv4' && !details.internal) {
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.'
) {
if (!ip.startsWith('192.168.') ||
(ip2.startsWith('192.168.') && !ip.startsWith('192.168.')) &&
(ip2.startsWith('172.16.') && !ip.startsWith('192.168.') && !ip.startsWith('172.16.')) ||
(ip2.startsWith('10.') && !ip.startsWith('192.168.') && !ip.startsWith('172.16.') && !ip.startsWith('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(utils: { getApp: () => any; getStore: () => any; }) {
this._app = utils.getApp();
this._store = utils.getStore()
}
/**
* 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.forEach((client: any) => {
try{
client.stop();
} catch(e){}
})
this.activeConnections = [];
this.connectedHosts = {};
})
}
/**
* Runs on app stop
*/
onBeforeQuit(): void {
}
/**
* Runs on song change
* @param attributes Music Attributes
*/
onNowPlayingItemDidChange(attributes: any): void {
}
onRendererReady(): void {
this._win.webContents.executeJavaScript(
`ipcRenderer.sendSync('get-port')`
).then((result: any) => {
this.ciderPort = result;
});
}
}
onRendererReady(): void {
this._win.webContents.executeJavaScript(`ipcRenderer.sendSync('get-port')`).then((result: any) => {
this.ciderPort = result;
});
}
}

View file

@ -1,304 +1,299 @@
import {AutoClient} from 'discord-auto-rpc'
import {ipcMain} from "electron";
import fetch from 'electron-fetch'
import { AutoClient } from "discord-auto-rpc";
import { ipcMain } from "electron";
import fetch from "electron-fetch";
export default class DiscordRPC {
/**
* Base Plugin Details (Eventually implemented into a GUI in settings)
*/
public name: string = "Discord Rich Presence";
public description: string = "Discord RPC plugin for Cider";
public version: string = "1.1.0";
public author: string = "vapormusic/Core (Cider Collective)";
/**
* Base Plugin Details (Eventually implemented into a GUI in settings)
*/
public name: string = 'Discord Rich Presence';
public description: string = 'Discord RPC plugin for Cider';
public version: string = '1.1.0';
public author: string = 'vapormusic/Core (Cider Collective)';
/**
* Private variables for interaction in plugins
*/
private _utils: any;
private _attributes: any;
private ready: boolean = false;
/**
* Private variables for interaction in plugins
*/
private _utils: any;
private _attributes: any;
private ready: boolean = false;
/**
* Plugin Initialization
*/
private _client: any = null;
private _activityCache: any = {
details: "",
state: "",
largeImageKey: "",
largeImageText: "",
smallImageKey: "",
smallImageText: "",
instance: false,
};
/**
* Plugin Initialization
*/
private _client: any = null;
private _activityCache: any = {
details: '',
state: '',
largeImageKey: '',
largeImageText: '',
smallImageKey: '',
smallImageText: '',
instance: false
/*******************************************************************************************
* Public Methods
* ****************************************************************************************/
/**
* Runs on plugin load (Currently run on application start)
*/
constructor(utils: any) {
this._utils = utils;
console.debug(`[Plugin][${this.name}] Loading Complete.`);
}
/**
* Runs on app ready
*/
onReady(_win: any): void {
const self = this;
this.connect();
console.debug(`[Plugin][${this.name}] Ready.`);
ipcMain.on("updateRPCImage", async (_event, imageurl) => {
if (!this._utils.getStoreValue("general.privateEnabled")) {
let b64data = "";
let postbody = "";
if (imageurl.startsWith("/ciderlocalart")) {
let port = await _win.webContents.executeJavaScript(`app.clientPort`);
console.log("http://localhost:" + port + imageurl);
const response = await fetch("http://localhost:" + port + imageurl);
b64data = (await response.buffer()).toString("base64");
postbody = JSON.stringify({ data: b64data });
fetch("https://api.cider.sh/v1/images", {
method: "POST",
body: postbody,
headers: {
"Content-Type": "application/json",
"User-Agent": _win.webContents.getUserAgent(),
},
})
.then((res) => res.json())
.then(function (json) {
self._attributes["artwork"]["url"] = json.url;
self.setActivity(self._attributes);
});
} else {
postbody = JSON.stringify({ url: imageurl });
fetch("https://api.cider.sh/v1/images", {
method: "POST",
body: postbody,
headers: {
"Content-Type": "application/json",
"User-Agent": _win.webContents.getUserAgent(),
},
})
.then((res) => res.json())
.then(function (json) {
self._attributes["artwork"]["url"] = json.url;
self.setActivity(self._attributes);
});
}
}
});
ipcMain.on("reloadRPC", () => {
console.log(`[DiscordRPC][reload] Reloading DiscordRPC.`);
this._client.destroy();
this._client
.endlessLogin({
clientId: this._utils.getStoreValue("connectivity.discord_rpc.client") === "Cider" ? "911790844204437504" : "886578863147192350",
})
.then(() => {
this.ready = true;
this._utils.getWindow().webContents.send("rpcReloaded", this._client.user);
if (this._activityCache && this._activityCache.details && this._activityCache.state) {
console.info(`[DiscordRPC][reload] Restoring activity cache.`);
this._client.setActivity(this._activityCache);
}
})
.catch((e: any) => console.error(`[DiscordRPC][reload] ${e}`));
// this.connect(true)
});
}
/**
* 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._attributes = attributes;
this.setActivity(attributes);
}
/**
* Runs on song change
* @param attributes Music Attributes
*/
onNowPlayingItemDidChange(attributes: object): void {
this._attributes = attributes;
this.setActivity(attributes);
}
/*******************************************************************************************
* Private Methods
* ****************************************************************************************/
/**
* Connect to Discord RPC
* @private
*/
private connect() {
if (!this._utils.getStoreValue("connectivity.discord_rpc.enabled")) {
return;
}
// Create the client
this._client = new AutoClient({ transport: "ipc" });
// Runs on Ready
this._client.once("ready", () => {
console.info(`[DiscordRPC][connect] Successfully Connected to Discord. Authed for user: ${this._client.user.id}.`);
if (this._activityCache && this._activityCache.details && this._activityCache.state) {
console.info(`[DiscordRPC][connect] Restoring activity cache.`);
this._client.setActivity(this._activityCache);
}
});
// Login to Discord
this._client
.endlessLogin({
clientId: this._utils.getStoreValue("connectivity.discord_rpc.client") === "Cider" ? "911790844204437504" : "886578863147192350",
})
.then(() => {
this.ready = true;
})
.catch((e: any) => console.error(`[DiscordRPC][connect] ${e}`));
}
/**
* Sets the activity
* @param attributes Music Attributes
*/
private setActivity(attributes: any) {
if (!this._client) {
return;
}
// Check if show buttons is (true) or (false)
let activity: Object = {
details: this._utils.getStoreValue("connectivity.discord_rpc.details_format"),
state: this._utils.getStoreValue("connectivity.discord_rpc.state_format"),
largeImageKey: attributes?.artwork?.url?.replace("{w}", "1024").replace("{h}", "1024"),
largeImageText: attributes.albumName,
instance: false, // Whether the activity is in a game session
};
/*******************************************************************************************
* Public Methods
* ****************************************************************************************/
// Filter the activity
activity = this.filterActivity(activity, attributes);
/**
* Runs on plugin load (Currently run on application start)
*/
constructor(utils: any) {
this._utils = utils;
console.debug(`[Plugin][${this.name}] Loading Complete.`);
if (!this.ready) {
this._activityCache = activity;
return;
}
// Set the activity
if (!attributes.status && this._utils.getStoreValue("connectivity.discord_rpc.clear_on_pause")) {
this._client.clearActivity();
} else if (activity && this._activityCache !== activity) {
this._client.setActivity(activity);
}
this._activityCache = activity;
}
/**
* Filter the Discord activity object
*/
private filterActivity(activity: any, attributes: any): Object {
// Add the buttons if people want them
if (!this._utils.getStoreValue("connectivity.discord_rpc.hide_buttons")) {
activity.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
}
// Add the timestamp if its playing and people want them
if (!this._utils.getStoreValue("connectivity.discord_rpc.hide_timestamp") && attributes.status) {
activity.startTimestamp = Date.now() - (attributes?.durationInMillis - attributes?.remainingTime);
activity.endTimestamp = attributes.endTime;
}
// If the user wants to keep the activity when paused
if (!this._utils.getStoreValue("connectivity.discord_rpc.clear_on_pause")) {
activity.smallImageKey = attributes.status ? "play" : "pause";
activity.smallImageText = attributes.status ? "Playing" : "Paused";
}
/**
* Runs on app ready
* Works with:
* {artist}
* {composer}
* {title}
* {album}
* {trackNumber}
*/
onReady(_win: any): void {
const self = this
this.connect();
console.debug(`[Plugin][${this.name}] Ready.`);
ipcMain.on('updateRPCImage', async (_event, imageurl) => {
if (!this._utils.getStoreValue("general.privateEnabled")) {
let b64data = ""
let postbody = ""
if (imageurl.startsWith("/ciderlocalart")){
let port = await _win.webContents.executeJavaScript(
`app.clientPort`
);
console.log("http://localhost:"+port+imageurl)
const response = await fetch("http://localhost:"+port+imageurl)
b64data = (await response.buffer()).toString('base64');
postbody = JSON.stringify({data: b64data})
fetch('https://api.cider.sh/v1/images', {
const rpcVars: any = {
artist: attributes.artistName,
composer: attributes.composerName,
title: attributes.name,
album: attributes.albumName,
trackNumber: attributes.trackNumber,
};
method: 'POST',
body: postbody,
headers: {
'Content-Type': 'application/json',
'User-Agent': _win.webContents.getUserAgent()
},
})
.then(res => res.json())
.then(function (json) {
self._attributes["artwork"]["url"] = json.url
self.setActivity(self._attributes)
})
} else {
postbody = JSON.stringify({url: imageurl})
fetch('https://api.cider.sh/v1/images', {
// Replace the variables
Object.keys(rpcVars).forEach((key) => {
if (activity.details.includes(`{${key}}`)) {
activity.details = activity.details.replace(`{${key}}`, rpcVars[key]);
}
if (activity.state.includes(`{${key}}`)) {
activity.state = activity.state.replace(`{${key}}`, rpcVars[key]);
}
});
method: 'POST',
body: postbody,
headers: {
'Content-Type': 'application/json',
'User-Agent': _win.webContents.getUserAgent()
},
})
.then(res => res.json())
.then(function (json) {
self._attributes["artwork"]["url"] = json.url
self.setActivity(self._attributes)
})
}
}
})
ipcMain.on("reloadRPC", () => {
console.log(`[DiscordRPC][reload] Reloading DiscordRPC.`);
this._client.destroy()
this._client.endlessLogin({clientId: this._utils.getStoreValue("connectivity.discord_rpc.client") === "Cider" ? '911790844204437504' : '886578863147192350'})
.then(() => {
this.ready = true
this._utils.getWindow().webContents.send("rpcReloaded", this._client.user)
if (this._activityCache && this._activityCache.details && this._activityCache.state) {
console.info(`[DiscordRPC][reload] Restoring activity cache.`);
this._client.setActivity(this._activityCache)
}
})
.catch((e: any) => console.error(`[DiscordRPC][reload] ${e}`));
// this.connect(true)
})
// Checks if the details 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) + "...";
}
/**
* Runs on app stop
*/
onBeforeQuit(): void {
console.debug(`[Plugin][${this.name}] Stopped.`);
// Checks if the state is greater than 128 because some songs can be that long
if (activity.state && activity.state.length >= 128) {
activity.state = activity.state.substring(0, 125) + "...";
}
/**
* Runs on playback State Change
* @param attributes Music Attributes (attributes.status = current state)
*/
onPlaybackStateDidChange(attributes: object): void {
this._attributes = attributes
this.setActivity(attributes)
// Checks if the state is greater than 128 because some songs can be that long
if (activity.largeImageText && activity.largeImageText.length >= 128) {
activity.largeImageText = activity.largeImageText.substring(0, 125) + "...";
}
/**
* Runs on song change
* @param attributes Music Attributes
*/
onNowPlayingItemDidChange(attributes: object): void {
this._attributes = attributes
this.setActivity(attributes)
// Check large image
if (activity.largeImageKey == null || activity.largeImageKey === "" || activity.largeImageKey.length > 256) {
activity.largeImageKey = "cider";
}
/*******************************************************************************************
* Private Methods
* ****************************************************************************************/
/**
* Connect to Discord RPC
* @private
*/
private connect() {
if (!this._utils.getStoreValue("connectivity.discord_rpc.enabled")) {
return;
}
// Create the client
this._client = new AutoClient({transport: "ipc"});
// Runs on Ready
this._client.once('ready', () => {
console.info(`[DiscordRPC][connect] Successfully Connected to Discord. Authed for user: ${this._client.user.id}.`);
if (this._activityCache && this._activityCache.details && this._activityCache.state) {
console.info(`[DiscordRPC][connect] Restoring activity cache.`);
this._client.setActivity(this._activityCache)
}
})
// Login to Discord
this._client.endlessLogin({clientId: this._utils.getStoreValue("connectivity.discord_rpc.client") === "Cider" ? '911790844204437504' : '886578863147192350'})
.then(() => {
this.ready = true
})
.catch((e: any) => console.error(`[DiscordRPC][connect] ${e}`));
// Timestamp
if (new Date(attributes.endTime).getTime() < 0) {
delete activity.startTime;
delete activity.endTime;
}
/**
* Sets the activity
* @param attributes Music Attributes
*/
private setActivity(attributes: any) {
if (!this._client) {
return
}
// Check if show buttons is (true) or (false)
let activity: Object = {
details: this._utils.getStoreValue("connectivity.discord_rpc.details_format"),
state: this._utils.getStoreValue("connectivity.discord_rpc.state_format"),
largeImageKey: attributes?.artwork?.url?.replace('{w}', '1024').replace('{h}', '1024'),
largeImageText: attributes.albumName,
instance: false // Whether the activity is in a game session
}
// Filter the activity
activity = this.filterActivity(activity, attributes)
if (!this.ready) {
this._activityCache = activity
return
}
// Set the activity
if (!attributes.status && this._utils.getStoreValue("connectivity.discord_rpc.clear_on_pause")) {
this._client.clearActivity()
} else if (activity && this._activityCache !== activity) {
this._client.setActivity(activity)
}
this._activityCache = activity;
// not sure
if (!attributes.artistName) {
delete activity.state;
}
/**
* Filter the Discord activity object
*/
private filterActivity(activity: any, attributes: any): Object {
// Add the buttons if people want them
if (!this._utils.getStoreValue("connectivity.discord_rpc.hide_buttons")) {
activity.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
}
// Add the timestamp if its playing and people want them
if (!this._utils.getStoreValue("connectivity.discord_rpc.hide_timestamp") && attributes.status) {
activity.startTimestamp = Date.now() - (attributes?.durationInMillis - attributes?.remainingTime)
activity.endTimestamp = attributes.endTime
}
// If the user wants to keep the activity when paused
if (!this._utils.getStoreValue("connectivity.discord_rpc.clear_on_pause")) {
activity.smallImageKey = attributes.status ? 'play' : 'pause';
activity.smallImageText = attributes.status ? 'Playing' : 'Paused';
}
/**
* Works with:
* {artist}
* {composer}
* {title}
* {album}
* {trackNumber}
*/
const rpcVars: any = {
"artist": attributes.artistName,
"composer": attributes.composerName,
"title": attributes.name,
"album": attributes.albumName,
"trackNumber": attributes.trackNumber
}
// Replace the variables
Object.keys(rpcVars).forEach((key) => {
if (activity.details.includes(`{${key}}`)) {
activity.details = activity.details.replace(`{${key}}`, rpcVars[key])
}
if (activity.state.includes(`{${key}}`)) {
activity.state = activity.state.replace(`{${key}}`, rpcVars[key])
}
})
// Checks if the details 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) + '...'
}
// Checks if the state is greater than 128 because some songs can be that long
if (activity.state && activity.state.length >= 128) {
activity.state = activity.state.substring(0, 125) + '...'
}
// Checks if the state is greater than 128 because some songs can be that long
if (activity.largeImageText && activity.largeImageText.length >= 128) {
activity.largeImageText = activity.largeImageText.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
}
return activity
if (!activity.largeImageText || activity.largeImageText.length < 2) {
delete activity.largeImageText;
}
return activity;
}
}

View file

@ -1,236 +1,245 @@
export default class lastfm {
/**
* Base Plugin Information
*/
public name: string = "LastFM Plugin";
public version: string = "2.0.0";
public author: string = "Core (Cider Collective)";
/**
* Base Plugin Information
*/
public name: string = 'LastFM Plugin';
public version: string = '2.0.0';
public author: string = 'Core (Cider Collective)';
private _apiCredentials = {
key: "f9986d12aab5a0fe66193c559435ede3",
secret: "acba3c29bd5973efa38cc2f0b63cc625",
};
/**
* Plugin Initialization
*/
private _lfm: any = null;
private _authenticated: boolean = false;
private _scrobbleDelay: any = null;
private _utils: any = null;
private _scrobbleCache: any = {};
private _nowPlayingCache: any = {};
/**
* Public Methods
*/
private _apiCredentials = {
key: "f9986d12aab5a0fe66193c559435ede3",
secret: "acba3c29bd5973efa38cc2f0b63cc625"
constructor(utils: any) {
this._utils = utils;
}
onReady(_win: Electron.BrowserWindow): void {
this.initializeLastFM("", this._apiCredentials);
// Register the ipcMain handlers
this._utils.getIPCMain().handle("lastfm:url", (event: any) => {
console.debug(`[${lastfm.name}:url] Called.`);
return this._lfm.getAuthenticationUrl({ cb: "cider://auth/lastfm" });
});
this._utils.getIPCMain().on("lastfm:auth", (event: any, token: string) => {
console.debug(`[${lastfm.name}:auth] Token: `, token);
this.authenticateLastFM(token);
});
this._utils.getIPCMain().on("lastfm:disconnect", (_event: any) => {
this._lfm.setSessionCredentials(null, null);
this._authenticated = false;
console.debug(`[${lastfm.name}:disconnect] Disconnected`);
});
this._utils.getIPCMain().on("lastfm:nowPlayingChange", (event: any, attributes: any) => {
if (this._utils.getStoreValue("connectivity.lastfm.filter_loop") || this._utils.getStoreValue("general.privateEnabled")) return;
this.updateNowPlayingTrack(attributes);
});
this._utils.getIPCMain().on("lastfm:scrobbleTrack", (event: any, attributes: any) => {
if (this._utils.getStoreValue("general.privateEnabled")) return;
this.scrobbleTrack(attributes);
});
}
/**
* 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
* @param scrobble
*/
onNowPlayingItemDidChange(attributes: any, scrobble = false): void {
if (this._utils.getStoreValue("general.privateEnabled")) return;
this.updateNowPlayingTrack(attributes);
}
/**
* Initialize LastFM
* @param token
* @param api
* @private
*/
private initializeLastFM(token: string, api: { key: string; secret: string }): void {
console.debug(`[${lastfm.name}:initialize] Initializing LastFM`);
const LastfmAPI = require("lastfmapi");
this._lfm = new LastfmAPI({
api_key: api.key,
secret: api.secret,
});
if (this._utils.getStoreValue("connectivity.lastfm.secrets.username") && this._utils.getStoreValue("connectivity.lastfm.secrets.key")) {
this._lfm.setSessionCredentials(this._utils.getStoreValue("connectivity.lastfm.secrets.username"), this._utils.getStoreValue("connectivity.lastfm.secrets.key"));
this._authenticated = true;
} else {
this.authenticateLastFM(token);
}
/**
* Plugin Initialization
*/
private _lfm: any = null;
private _authenticated: boolean = false;
private _scrobbleDelay: any = null;
private _utils: any = null;
private _scrobbleCache: any = {};
private _nowPlayingCache: any = {};
}
/**
* Public Methods
*/
/**
* Authenticate the user with the given token
* @param token
* @private
*/
private authenticateLastFM(token: string): void {
if (!token) return;
this._lfm.authenticate(token, (err: any, session: any) => {
if (err) {
console.error(`[${lastfm.name}:authenticate] Error: ${typeof err === "string" ? err : err.message}`);
constructor(utils: any) {
this._utils = utils;
}
this._utils.getWindow().webContents.executeJavaScript(`app.notyf.error("${err.message}");`);
return;
}
this._utils.getWindow().webContents.send("lastfm:authenticated", session);
this._authenticated = true;
console.debug(`[${lastfm.name}:authenticate] Authenticated as ${session.username}`);
});
}
onReady(_win: Electron.BrowserWindow): void {
this.initializeLastFM("", this._apiCredentials)
/**
* Verifies the track information with lastfm
* @param attributes
* @param callback
* @private
*/
private verifyTrack(attributes: any, callback: Function): void {
if (!attributes) return attributes;
// Register the ipcMain handlers
this._utils.getIPCMain().handle('lastfm:url', (event: any) => {
console.debug(`[${lastfm.name}:url] Called.`)
return this._lfm.getAuthenticationUrl({"cb": "cider://auth/lastfm"})
})
this._utils.getIPCMain().on('lastfm:auth', (event: any, token: string) => {
console.debug(`[${lastfm.name}:auth] Token: `, token)
this.authenticateLastFM(token)
})
this._utils.getIPCMain().on('lastfm:disconnect', (_event: any) => {
this._lfm.setSessionCredentials(null, null);
this._authenticated = false;
console.debug(`[${lastfm.name}:disconnect] Disconnected`)
})
this._utils.getIPCMain().on('lastfm:nowPlayingChange', (event: any, attributes: any) => {
if (this._utils.getStoreValue("connectivity.lastfm.filter_loop") || this._utils.getStoreValue("general.privateEnabled")) return;
this.updateNowPlayingTrack(attributes)
})
this._utils.getIPCMain().on('lastfm:scrobbleTrack', (event: any, attributes: any) => {
if (this._utils.getStoreValue("general.privateEnabled")) return;
this.scrobbleTrack(attributes)
})
}
/**
* 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
* @param scrobble
*/
onNowPlayingItemDidChange(attributes: any, scrobble = false): void {
if (this._utils.getStoreValue("general.privateEnabled")) return;
this.updateNowPlayingTrack(attributes)
}
/**
* Initialize LastFM
* @param token
* @param api
* @private
*/
private initializeLastFM(token: string, api: { key: string, secret: string }): void {
console.debug(`[${lastfm.name}:initialize] Initializing LastFM`)
const LastfmAPI = require("lastfmapi")
this._lfm = new LastfmAPI({
'api_key': api.key,
'secret': api.secret,
});
if (this._utils.getStoreValue("connectivity.lastfm.secrets.username") && this._utils.getStoreValue("connectivity.lastfm.secrets.key")) {
this._lfm.setSessionCredentials(this._utils.getStoreValue("connectivity.lastfm.secrets.username"), this._utils.getStoreValue("connectivity.lastfm.secrets.key"));
this._authenticated = true;
} else {
this.authenticateLastFM(token)
if (!attributes.lfmAlbum) {
this._lfm.album.getInfo(
{
artist: attributes.primaryArtist,
album: attributes.albumName,
},
(err: any, data: any) => {
if (err) {
console.error(`[${lastfm.name}] [album.getInfo] Error: ${typeof err === "string" ? err : err.message}`);
return {};
}
if (data) {
attributes.lfmAlbum = data;
callback(attributes);
}
}
);
} else {
this._lfm.track.getCorrection(attributes.primaryArtist, attributes.name, (err: any, data: any) => {
if (err) {
console.error(`[${lastfm.name}] [track.getCorrection] Error: ${typeof err === "string" ? err : err.message}`);
return {};
}
if (data) {
attributes.lfmTrack = data.correction.track;
callback(attributes);
}
});
}
}
/**
* Scrobbles the track to lastfm
* @param attributes
* @private
*/
private scrobbleTrack(attributes: any): void {
if (!attributes?.lfmTrack || !attributes?.lfmAlbum) {
this.verifyTrack(attributes, (a: any) => {
this.scrobbleTrack(a);
});
return;
}
/**
* Authenticate the user with the given token
* @param token
* @private
*/
private authenticateLastFM(token: string): void {
if (!token) return;
this._lfm.authenticate(token, (err: any, session: any) => {
if (err) {
console.error(`[${lastfm.name}:authenticate] Error: ${typeof err === "string" ? err : err.message}`);
if (
!this._authenticated ||
!attributes ||
this._utils.getStoreValue("connectivity.lastfm.filter_types")[attributes.playParams.kind] ||
(this._utils.getStoreValue("connectivity.lastfm.filter_loop") && this._scrobbleCache.track === attributes.lfmTrack.name)
)
return;
this._utils.getWindow().webContents.executeJavaScript(`app.notyf.error("${err.message}");`)
return;
}
this._utils.getWindow().webContents.send('lastfm:authenticated', session)
this._authenticated = true;
console.debug(`[${lastfm.name}:authenticate] Authenticated as ${session.username}`)
});
// Scrobble
const scrobble = {
artist: attributes.lfmTrack.artist.name,
track: attributes.lfmTrack.name,
album: attributes.lfmAlbum.name,
albumArtist: attributes.lfmAlbum.artist,
timestamp: new Date().getTime() / 1000,
trackNumber: attributes.trackNumber,
duration: attributes.durationInMillis / 1000,
};
// Easy Debugging
console.debug(`[${lastfm.name}:scrobble] Scrobbling ${scrobble.artist} - ${scrobble.track}`);
// Scrobble the track
this._lfm.track.scrobble(scrobble, (err: any, _res: any) => {
if (err) {
console.error(`[${lastfm.name}:scrobble] Scrobble failed: ${err.message}`);
} else {
console.debug(`[${lastfm.name}:scrobble] Track scrobbled: ${scrobble.artist} - ${scrobble.track}`);
this._scrobbleCache = scrobble;
}
});
}
/**
* Updates the now playing track
* @param attributes
* @private
*/
private updateNowPlayingTrack(attributes: any): void {
if (!attributes?.lfmTrack || !attributes?.lfmAlbum) {
this.verifyTrack(attributes, (a: any) => {
this.updateNowPlayingTrack(a);
});
return;
}
/**
* Verifies the track information with lastfm
* @param attributes
* @param callback
* @private
*/
private verifyTrack(attributes: any, callback: Function): void {
if (!attributes) return attributes;
if (
!this._authenticated ||
!attributes ||
this._utils.getStoreValue("connectivity.lastfm.filter_types")[attributes.playParams.kind] ||
(this._utils.getStoreValue("connectivity.lastfm.filter_loop") && this._nowPlayingCache.track === attributes.lfmTrack.name)
)
return;
if (!attributes.lfmAlbum) {
this._lfm.album.getInfo({
"artist": attributes.primaryArtist,
"album": attributes.albumName
}, (err: any, data: any) => {
if (err) {
console.error(`[${lastfm.name}] [album.getInfo] Error: ${typeof err === "string" ? err : err.message}`)
return {};
}
if (data) {
attributes.lfmAlbum = data
callback(attributes)
}
})
} else {
this._lfm.track.getCorrection(attributes.primaryArtist, attributes.name, (err: any, data: any) => {
if (err) {
console.error(`[${lastfm.name}] [track.getCorrection] Error: ${typeof err === "string" ? err : err.message}`)
return {};
}
if (data) {
attributes.lfmTrack = data.correction.track
callback(attributes)
}
})
}
const nowPlaying = {
artist: attributes.lfmTrack.artist.name,
track: attributes.lfmTrack.name,
album: attributes.lfmAlbum.name,
trackNumber: attributes.trackNumber,
duration: attributes.durationInMillis / 1000,
albumArtist: attributes.lfmAlbum.artist,
};
}
/**
* Scrobbles the track to lastfm
* @param attributes
* @private
*/
private scrobbleTrack(attributes: any): void {
if (!attributes?.lfmTrack || !attributes?.lfmAlbum) {
this.verifyTrack(attributes, (a: any) => {
this.scrobbleTrack(a)
})
return
}
if (!this._authenticated || !attributes || this._utils.getStoreValue("connectivity.lastfm.filter_types")[attributes.playParams.kind] || (this._utils.getStoreValue("connectivity.lastfm.filter_loop") && this._scrobbleCache.track === attributes.lfmTrack.name)) return;
// Scrobble
const scrobble = {
'artist': attributes.lfmTrack.artist.name,
'track': attributes.lfmTrack.name,
'album': attributes.lfmAlbum.name,
'albumArtist': attributes.lfmAlbum.artist,
'timestamp': new Date().getTime() / 1000,
'trackNumber': attributes.trackNumber,
'duration': attributes.durationInMillis / 1000,
}
// Easy Debugging
console.debug(`[${lastfm.name}:scrobble] Scrobbling ${scrobble.artist} - ${scrobble.track}`)
// Scrobble the track
this._lfm.track.scrobble(scrobble, (err: any, _res: any) => {
if (err) {
console.error(`[${lastfm.name}:scrobble] Scrobble failed: ${err.message}`);
} else {
console.debug(`[${lastfm.name}:scrobble] Track scrobbled: ${scrobble.artist} - ${scrobble.track}`);
this._scrobbleCache = scrobble
}
});
}
/**
* Updates the now playing track
* @param attributes
* @private
*/
private updateNowPlayingTrack(attributes: any): void {
if (!attributes?.lfmTrack || !attributes?.lfmAlbum) {
this.verifyTrack(attributes, (a: any) => {
this.updateNowPlayingTrack(a)
})
return
}
if (!this._authenticated || !attributes || this._utils.getStoreValue("connectivity.lastfm.filter_types")[attributes.playParams.kind] || (this._utils.getStoreValue("connectivity.lastfm.filter_loop") && this._nowPlayingCache.track === attributes.lfmTrack.name)) return;
const nowPlaying = {
'artist': attributes.lfmTrack.artist.name,
'track': attributes.lfmTrack.name,
'album': attributes.lfmAlbum.name,
'trackNumber': attributes.trackNumber,
'duration': attributes.durationInMillis / 1000,
'albumArtist': attributes.lfmAlbum.artist,
}
this._lfm.track.updateNowPlaying(nowPlaying, (err: any, res: any) => {
if (err) {
console.error(`[${lastfm.name}:updateNowPlaying] Now Playing Update failed: ${err.message}`);
} else {
console.debug(`[${lastfm.name}:updateNowPlaying] Now Playing Updated: ${nowPlaying.artist} - ${nowPlaying.track}`);
this._nowPlayingCache = nowPlaying
}
});
}
}
this._lfm.track.updateNowPlaying(nowPlaying, (err: any, res: any) => {
if (err) {
console.error(`[${lastfm.name}:updateNowPlaying] Now Playing Update failed: ${err.message}`);
} else {
console.debug(`[${lastfm.name}:updateNowPlaying] Now Playing Updated: ${nowPlaying.artist} - ${nowPlaying.track}`);
this._nowPlayingCache = nowPlaying;
}
});
}
}

View file

@ -1,355 +1,338 @@
import {app, Menu, shell} from "electron";
import {utils} from "../base/utils";
import { app, Menu, shell } from "electron";
import { utils } from "../base/utils";
export default class Thumbar {
/**
* 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";
public contributors: string[] = ["Core", "Qwack", "Monochromish"];
/**
* Menubar Assets
* @private
*/
/**
* 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';
public contributors: string[] = ['Core', 'Qwack', 'Monochromish'];
/**
* Menubar Assets
* @private
*/
private isNotMac: boolean = process.platform !== 'darwin';
private isMac: boolean = process.platform === 'darwin';
private _menuTemplate: any = [
private isNotMac: boolean = process.platform !== "darwin";
private isMac: boolean = process.platform === "darwin";
private _menuTemplate: any = [
{
label: app.getName(),
submenu: [
{
label: app.getName(),
submenu: [
{
label: `${utils.getLocale(utils.getStoreValue('general.language'), 'term.about')} ${app.getName()}`,
click: () => utils.getWindow().webContents.executeJavaScript(`app.appRoute('about')`)
},
{type: 'separator'},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.toggleprivate'),
accelerator: utils.getStoreValue("general.keybindings.togglePrivateSession").join('+'),
click: () => utils.getWindow().webContents.executeJavaScript(`app.cfg.general.privateEnabled = !app.cfg.general.privateEnabled`)
},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.settings'),
accelerator: utils.getStoreValue("general.keybindings.settings").join('+'),
click: () => utils.getWindow().webContents.executeJavaScript(`app.openSettingsPage()`)
},
...(this.isMac ? [
{type: 'separator'},
{role: 'services'},
{type: 'separator'},
{role: 'hide'},
{role: 'hideOthers'},
{role: 'unhide'},
{type: 'separator'},
{role: 'quit'}
] : []),
...(this.isNotMac ? [
{type: 'separator'},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.quit'),
accelerator: 'Control+Q',
click: () => app.quit()
}
] : [])
]
label: `${utils.getLocale(utils.getStoreValue("general.language"), "term.about")} ${app.getName()}`,
click: () => utils.getWindow().webContents.executeJavaScript(`app.appRoute('about')`),
},
{ type: "separator" },
{
label: utils.getLocale(utils.getStoreValue("general.language"), "term.toggleprivate"),
accelerator: utils.getStoreValue("general.keybindings.togglePrivateSession").join("+"),
click: () => utils.getWindow().webContents.executeJavaScript(`app.cfg.general.privateEnabled = !app.cfg.general.privateEnabled`),
},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'menubar.options.view'),
submenu: [
...(this.isMac ? [
{role: 'reload'},
{role: 'forceReload'},
{role: 'toggleDevTools'},
{type: 'separator'},
{role: 'resetZoom'},
{role: 'zoomIn'},
{role: 'zoomOut'},
{type: 'separator'},
{role: 'togglefullscreen'},
{type: 'separator'},
] : []),
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.search'),
accelerator: utils.getStoreValue("general.keybindings.search").join('+'),
click: () => utils.getWindow().webContents.executeJavaScript('app.focusSearch()')
},
{type:'separator'},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.listenNow'),
accelerator: utils.getStoreValue('general.keybindings.listnow').join('+'),
click: () => utils.getWindow().webContents.executeJavaScript(`app.appRoute('listen_now')`)
},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.browse'),
accelerator: utils.getStoreValue("general.keybindings.browse").join('+'),
click: () => utils.getWindow().webContents.executeJavaScript(`app.appRoute('browse')`)
},
{type: 'separator'},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.recentlyAdded')
,accelerator: utils.getStoreValue("general.keybindings.recentAdd").join('+'),
click: () => utils.getWindow().webContents.executeJavaScript(`app.appRoute('library-recentlyadded')`)
},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.songs'),
accelerator: utils.getStoreValue("general.keybindings.songs").join('+'),
click: () => utils.getWindow().webContents.executeJavaScript(`app.appRoute('library-songs')`)
},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.albums'),
accelerator: utils.getStoreValue("general.keybindings.albums").join('+'),
click: () => utils.getWindow().webContents.executeJavaScript(`app.appRoute('library-albums')`)
},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.artists'),
accelerator: utils.getStoreValue("general.keybindings.artists").join('+'),
click: () => utils.getWindow().webContents.executeJavaScript(`app.appRoute('library-artists')`)
},
label: utils.getLocale(utils.getStoreValue("general.language"), "term.settings"),
accelerator: utils.getStoreValue("general.keybindings.settings").join("+"),
click: () => utils.getWindow().webContents.executeJavaScript(`app.openSettingsPage()`),
},
...(this.isMac ? [{ type: "separator" }, { role: "services" }, { type: "separator" }, { role: "hide" }, { role: "hideOthers" }, { role: "unhide" }, { type: "separator" }, { role: "quit" }] : []),
...(this.isNotMac
? [
{ type: "separator" },
{
label: utils.getLocale(utils.getStoreValue("general.language"), "term.quit"),
accelerator: "Control+Q",
click: () => app.quit(),
},
]
: []),
],
},
{
label: utils.getLocale(utils.getStoreValue("general.language"), "menubar.options.view"),
submenu: [
...(this.isMac
? [
{ role: "reload" },
{ role: "forceReload" },
{ role: "toggleDevTools" },
{ type: "separator" },
{ role: "resetZoom" },
{ role: "zoomIn" },
{ role: "zoomOut" },
{ type: "separator" },
{ role: "togglefullscreen" },
{ type: "separator" },
]
: []),
{
label: utils.getLocale(utils.getStoreValue("general.language"), "term.search"),
accelerator: utils.getStoreValue("general.keybindings.search").join("+"),
click: () => utils.getWindow().webContents.executeJavaScript("app.focusSearch()"),
},
{ type: "separator" },
{
label: utils.getLocale(utils.getStoreValue("general.language"), "term.listenNow"),
accelerator: utils.getStoreValue("general.keybindings.listnow").join("+"),
click: () => utils.getWindow().webContents.executeJavaScript(`app.appRoute('listen_now')`),
},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'menubar.options.window'),
submenu: [
{role: 'minimize', label: utils.getLocale(utils.getStoreValue('general.language'), 'menubar.options.minimize')},
{type: 'separator'},
...(this.isMac ? [
{
label: 'Show',
click: () => utils.getWindow().show()
},
{role: 'zoom'},
{type: 'separator'},
{role: 'front'},
{role: 'close'},
{
label: 'Edit',
submenu: [
{role: 'undo'},
{role: 'redo'},
{type: 'separator'},
{role: 'cut'},
{role: 'copy'},
{role: 'paste'},
]
},
{type: 'separator'},
] : [ ]),
...(this.isNotMac ? [
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'menubar.options.zoom'),
submenu: [
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.zoomin'),
role: 'zoomIn',
accelerator: utils.getStoreValue("general.keybindings.zoomn").join('+')
},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.zoomout'),
role: 'zoomOut',
accelerator: utils.getStoreValue("general.keybindings.zoomt").join('+')
},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.zoomreset'),
role: 'resetZoom',
accelerator: utils.getStoreValue("general.keybindings.zoomrst").join('+')
}
]
},
{type: 'separator'},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.fullscreen'),
accelerator: 'Control+Enter',
role: 'togglefullscreen'
},
{type: 'separator'},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'action.close'),
accelerator: 'Control+W',
role: 'close'
},
{type:'separator'},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'menubar.options.reload'),
accelerator: 'Control+R',
role: 'reload'
},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'menubar.options.forcereload'),
accelerator: 'Control+Shift+R',
role: 'forceReload'
},
] : []),
],
label: utils.getLocale(utils.getStoreValue("general.language"), "term.browse"),
accelerator: utils.getStoreValue("general.keybindings.browse").join("+"),
click: () => utils.getWindow().webContents.executeJavaScript(`app.appRoute('browse')`),
},
{ type: "separator" },
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'menubar.options.controls'),
submenu: [
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.playpause'),
accelerator: 'Space',
click: () => utils.getWindow().webContents.executeJavaScript(`app.SpacePause()`)
},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.next'),
accelerator: 'CommandOrControl+Right',
click: () => utils.getWindow().webContents.executeJavaScript(`MusicKitInterop.next()`)
},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.previous'),
accelerator: 'CommandOrControl+Left',
click: () => utils.getWindow().webContents.executeJavaScript(`MusicKitInterop.previous()`)
},
{type: 'separator'},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'menubar.options.volumeup'),
accelerator: 'CommandOrControl+Up',
click: () => utils.getWindow().webContents.executeJavaScript(`app.volumeUp()`)
},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'menubar.options.volumedown'),
accelerator: 'CommandOrControl+Down',
click: () => utils.getWindow().webContents.executeJavaScript(`app.volumeDown()`)
},
{type: 'separator'},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.cast2'),
accelerator: utils.getStoreValue("general.keybindings.castToDevices").join('+'),
click: () => utils.getWindow().webContents.executeJavaScript(`app.modals.castMenu = true`)
},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.webremote'),
accelerator: utils.getStoreValue("general.keybindings.webRemote").join('+'),
sublabel: 'Opens in external window',
click: () => utils.getWindow().webContents.executeJavaScript(`app.appRoute('remote-pair')`)
},
{type: 'separator'},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.audioSettings'),
accelerator: utils.getStoreValue("general.keybindings.audioSettings").join('+'),
click: () => utils.getWindow().webContents.executeJavaScript(`app.modals.audioSettings = true`)
},
{type: 'separator'},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'menubar.options.plugins'),
accelerator: utils.getStoreValue("general.keybindings.pluginMenu").join('+'),
click: () => utils.getWindow().webContents.executeJavaScript(`app.modals.pluginMenu = true`)
}
]
label: utils.getLocale(utils.getStoreValue("general.language"), "term.recentlyAdded"),
accelerator: utils.getStoreValue("general.keybindings.recentAdd").join("+"),
click: () => utils.getWindow().webContents.executeJavaScript(`app.appRoute('library-recentlyadded')`),
},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'menubar.options.account'),
submenu: [
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.accountSettings'),
click: () => utils.getWindow().webContents.executeJavaScript(`app.appRoute('apple-account-settings')`)
},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'menubar.options.signout'),
click: () => utils.getWindow().webContents.executeJavaScript(`app.unauthorize()`)
}
]
label: utils.getLocale(utils.getStoreValue("general.language"), "term.songs"),
accelerator: utils.getStoreValue("general.keybindings.songs").join("+"),
click: () => utils.getWindow().webContents.executeJavaScript(`app.appRoute('library-songs')`),
},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'menubar.options.support'),
role: 'help',
submenu: [
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.discord'),
click: () => shell.openExternal("https://discord.gg/AppleMusic").catch(console.error)
},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'term.github'),
click: () => shell.openExternal("https://github.com/ciderapp/Cider/wiki/Troubleshooting").catch(console.error)
},
{type: 'separator'},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'menubar.options.report'),
submenu: [
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'menubar.options.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: utils.getLocale(utils.getStoreValue('general.language'), 'menubar.options.feature'),
click: () => shell.openExternal("https://github.com/ciderapp/Cider/discussions/new?category=feature-request").catch(console.error)
},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'menubar.options.trans'),
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: utils.getLocale(utils.getStoreValue('general.language'), 'menubar.options.license'),
click: () => shell.openExternal("https://github.com/ciderapp/Cider/blob/main/LICENSE").catch(console.error)
},
{type: 'separator'},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'menubar.options.toggledevtools'),
accelerator: utils.getStoreValue("general.keybindings.openDeveloperTools").join('+'),
click: () => utils.getWindow().webContents.openDevTools()
},
{
label: utils.getLocale(utils.getStoreValue('general.language'), 'menubar.options.conf'),
click: () => utils.getStoreInstance().openInEditor()
}
label: utils.getLocale(utils.getStoreValue("general.language"), "term.albums"),
accelerator: utils.getStoreValue("general.keybindings.albums").join("+"),
click: () => utils.getWindow().webContents.executeJavaScript(`app.appRoute('library-albums')`),
},
{
label: utils.getLocale(utils.getStoreValue("general.language"), "term.artists"),
accelerator: utils.getStoreValue("general.keybindings.artists").join("+"),
click: () => utils.getWindow().webContents.executeJavaScript(`app.appRoute('library-artists')`),
},
],
},
{
label: utils.getLocale(utils.getStoreValue("general.language"), "menubar.options.window"),
submenu: [
{
role: "minimize",
label: utils.getLocale(utils.getStoreValue("general.language"), "menubar.options.minimize"),
},
{ type: "separator" },
...(this.isMac
? [
{
label: "Show",
click: () => utils.getWindow().show(),
},
{ role: "zoom" },
{ type: "separator" },
{ role: "front" },
{ role: "close" },
{
label: "Edit",
submenu: [{ role: "undo" }, { role: "redo" }, { type: "separator" }, { role: "cut" }, { role: "copy" }, { role: "paste" }],
},
{ type: "separator" },
]
}
];
: []),
...(this.isNotMac
? [
{
label: utils.getLocale(utils.getStoreValue("general.language"), "menubar.options.zoom"),
submenu: [
{
label: utils.getLocale(utils.getStoreValue("general.language"), "term.zoomin"),
role: "zoomIn",
accelerator: utils.getStoreValue("general.keybindings.zoomn").join("+"),
},
{
label: utils.getLocale(utils.getStoreValue("general.language"), "term.zoomout"),
role: "zoomOut",
accelerator: utils.getStoreValue("general.keybindings.zoomt").join("+"),
},
{
label: utils.getLocale(utils.getStoreValue("general.language"), "term.zoomreset"),
role: "resetZoom",
accelerator: utils.getStoreValue("general.keybindings.zoomrst").join("+"),
},
],
},
{ type: "separator" },
{
label: utils.getLocale(utils.getStoreValue("general.language"), "term.fullscreen"),
accelerator: "Control+Enter",
role: "togglefullscreen",
},
{ type: "separator" },
{
label: utils.getLocale(utils.getStoreValue("general.language"), "action.close"),
accelerator: "Control+W",
role: "close",
},
{ type: "separator" },
{
label: utils.getLocale(utils.getStoreValue("general.language"), "menubar.options.reload"),
accelerator: "Control+R",
role: "reload",
},
{
label: utils.getLocale(utils.getStoreValue("general.language"), "menubar.options.forcereload"),
accelerator: "Control+Shift+R",
role: "forceReload",
},
]
: []),
],
},
/*******************************************************************************************
* Public Methods
* ****************************************************************************************/
{
label: utils.getLocale(utils.getStoreValue("general.language"), "menubar.options.controls"),
submenu: [
{
label: utils.getLocale(utils.getStoreValue("general.language"), "term.playpause"),
accelerator: "Space",
click: () => utils.getWindow().webContents.executeJavaScript(`app.SpacePause()`),
},
{
label: utils.getLocale(utils.getStoreValue("general.language"), "term.next"),
accelerator: "CommandOrControl+Right",
click: () => utils.getWindow().webContents.executeJavaScript(`MusicKitInterop.next()`),
},
{
label: utils.getLocale(utils.getStoreValue("general.language"), "term.previous"),
accelerator: "CommandOrControl+Left",
click: () => utils.getWindow().webContents.executeJavaScript(`MusicKitInterop.previous()`),
},
{ type: "separator" },
{
label: utils.getLocale(utils.getStoreValue("general.language"), "menubar.options.volumeup"),
accelerator: "CommandOrControl+Up",
click: () => utils.getWindow().webContents.executeJavaScript(`app.volumeUp()`),
},
{
label: utils.getLocale(utils.getStoreValue("general.language"), "menubar.options.volumedown"),
accelerator: "CommandOrControl+Down",
click: () => utils.getWindow().webContents.executeJavaScript(`app.volumeDown()`),
},
{ type: "separator" },
{
label: utils.getLocale(utils.getStoreValue("general.language"), "term.cast2"),
accelerator: utils.getStoreValue("general.keybindings.castToDevices").join("+"),
click: () => utils.getWindow().webContents.executeJavaScript(`app.modals.castMenu = true`),
},
{
label: utils.getLocale(utils.getStoreValue("general.language"), "term.webremote"),
accelerator: utils.getStoreValue("general.keybindings.webRemote").join("+"),
sublabel: "Opens in external window",
click: () => utils.getWindow().webContents.executeJavaScript(`app.appRoute('remote-pair')`),
},
{ type: "separator" },
{
label: utils.getLocale(utils.getStoreValue("general.language"), "term.audioSettings"),
accelerator: utils.getStoreValue("general.keybindings.audioSettings").join("+"),
click: () => utils.getWindow().webContents.executeJavaScript(`app.modals.audioSettings = true`),
},
{ type: "separator" },
{
label: utils.getLocale(utils.getStoreValue("general.language"), "menubar.options.plugins"),
accelerator: utils.getStoreValue("general.keybindings.pluginMenu").join("+"),
click: () => utils.getWindow().webContents.executeJavaScript(`app.modals.pluginMenu = true`),
},
],
},
{
label: utils.getLocale(utils.getStoreValue("general.language"), "menubar.options.account"),
submenu: [
{
label: utils.getLocale(utils.getStoreValue("general.language"), "term.accountSettings"),
click: () => utils.getWindow().webContents.executeJavaScript(`app.appRoute('apple-account-settings')`),
},
{
label: utils.getLocale(utils.getStoreValue("general.language"), "menubar.options.signout"),
click: () => utils.getWindow().webContents.executeJavaScript(`app.unauthorize()`),
},
],
},
{
label: utils.getLocale(utils.getStoreValue("general.language"), "menubar.options.support"),
role: "help",
submenu: [
{
label: utils.getLocale(utils.getStoreValue("general.language"), "term.discord"),
click: () => shell.openExternal("https://discord.gg/AppleMusic").catch(console.error),
},
{
label: utils.getLocale(utils.getStoreValue("general.language"), "term.github"),
click: () => shell.openExternal("https://github.com/ciderapp/Cider/wiki/Troubleshooting").catch(console.error),
},
{ type: "separator" },
{
label: utils.getLocale(utils.getStoreValue("general.language"), "menubar.options.report"),
submenu: [
{
label: utils.getLocale(utils.getStoreValue("general.language"), "menubar.options.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: utils.getLocale(utils.getStoreValue("general.language"), "menubar.options.feature"),
click: () => shell.openExternal("https://github.com/ciderapp/Cider/discussions/new?category=feature-request").catch(console.error),
},
{
label: utils.getLocale(utils.getStoreValue("general.language"), "menubar.options.trans"),
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: utils.getLocale(utils.getStoreValue("general.language"), "menubar.options.license"),
click: () => shell.openExternal("https://github.com/ciderapp/Cider/blob/main/LICENSE").catch(console.error),
},
{ type: "separator" },
{
label: utils.getLocale(utils.getStoreValue("general.language"), "menubar.options.toggledevtools"),
accelerator: utils.getStoreValue("general.keybindings.openDeveloperTools").join("+"),
click: () => utils.getWindow().webContents.openDevTools(),
},
{
label: utils.getLocale(utils.getStoreValue("general.language"), "menubar.options.conf"),
click: () => utils.getStoreInstance().openInEditor(),
},
],
},
];
/**
* Runs on plugin load (Currently run on application start)
*/
constructor(_utils: utils) {
console.debug(`[Plugin][${this.name}] Loading Complete.`);
}
/*******************************************************************************************
* Public Methods
* ****************************************************************************************/
/**
* Runs on app ready
*/
onReady(_win: Electron.BrowserWindow): void {
const menu = Menu.buildFromTemplate(this._menuTemplate);
Menu.setApplicationMenu(menu)
}
/**
* Runs on plugin load (Currently run on application start)
*/
constructor(_utils: utils) {
console.debug(`[Plugin][${this.name}] Loading Complete.`);
}
/**
* Runs on app stop
*/
onBeforeQuit(): void {
console.debug(`[Plugin][${this.name}] Stopped.`);
}
/**
* Runs on app ready
*/
onReady(_win: Electron.BrowserWindow): void {
const menu = Menu.buildFromTemplate(this._menuTemplate);
Menu.setApplicationMenu(menu);
}
/**
* Runs on playback State Change
* @param attributes Music Attributes (attributes.status = current state)
*/
onPlaybackStateDidChange(attributes: object): void {
/**
* Runs on app stop
*/
onBeforeQuit(): void {
console.debug(`[Plugin][${this.name}] Stopped.`);
}
}
/**
* Runs on song change
* @param attributes Music Attributes
*/
onNowPlayingItemDidChange(attributes: object): void {
}
/**
* 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 {}
}

View file

@ -1,177 +1,176 @@
// @ts-ignore
import * as Player from 'mpris-service';
import * as Player from "mpris-service";
export default class mpris {
/**
* Private variables for interaction in plugins
*/
private static utils: any;
/**
* MPRIS Service
*/
private static player: Player.Player;
/**
* Base Plugin Details (Eventually implemented into a GUI in settings)
*/
public name: string = 'MPRIS Service';
public description: string = 'Handles MPRIS service calls for Linux systems.';
public version: string = '1.0.0';
public author: string = 'Core';
/**
* Private variables for interaction in plugins
*/
private static utils: any;
/**
* MPRIS Service
*/
private static player: Player.Player;
/**
* Base Plugin Details (Eventually implemented into a GUI in settings)
*/
public name: string = "MPRIS Service";
public description: string = "Handles MPRIS service calls for Linux systems.";
public version: string = "1.0.0";
public author: string = "Core";
/*******************************************************************************************
* Private Methods
* ****************************************************************************************/
/*******************************************************************************************
* Private Methods
* ****************************************************************************************/
/**
* Runs on plugin load (Currently run on application start)
*/
constructor(utils: any) {
mpris.utils = utils
/**
* Runs on plugin load (Currently run on application start)
*/
constructor(utils: any) {
mpris.utils = utils;
console.debug(`[Plugin][${mpris.name}] Loading Complete.`);
console.debug(`[Plugin][${mpris.name}] Loading Complete.`);
}
/**
* Blocks non-linux systems from running this plugin
* @private
* @decorator
*/
private static linuxOnly(_target: any, _propertyKey: string, descriptor: PropertyDescriptor) {
if (process.platform !== "linux") {
descriptor.value = function () {
return;
};
}
}
/**
* Blocks non-linux systems from running this plugin
* @private
* @decorator
*/
private static linuxOnly(_target: any, _propertyKey: string, descriptor: PropertyDescriptor) {
if (process.platform !== 'linux') {
descriptor.value = function () {
return
}
}
/**
* Connects to MPRIS Service
*/
private static connect() {
const player = Player({
name: "cider",
identity: "Cider",
supportedInterfaces: ["player"],
});
console.debug(`[${mpris.name}:connect] Successfully connected.`);
const renderer = mpris.utils.getWindow().webContents;
const loopType: { [key: string]: number } = {
none: 0,
track: 1,
playlist: 2,
};
player.on("next", () => mpris.utils.playback.next());
player.on("previous", () => mpris.utils.playback.previous());
player.on("playpause", () => mpris.utils.playback.playPause());
player.on("play", () => mpris.utils.playback.play());
player.on("pause", () => mpris.utils.playback.pause());
player.on("quit", () => mpris.utils.getApp().exit());
player.on("position", (args: { position: any }) => mpris.utils.playback.seek(args.position / 1000 / 1000));
player.on("loopStatus", (status: string) => renderer.executeJavaScript(`app.mk.repeatMode = ${loopType[status.toLowerCase()]}`));
player.on("shuffle", () => renderer.executeJavaScript("app.mk.shuffleMode = (app.mk.shuffleMode === 0) ? 1 : 0"));
mpris.utils.getIPCMain().on("mpris:playbackTimeDidChange", (event: any, time: number) => {
player.getPosition = () => time;
});
mpris.utils.getIPCMain().on("repeatModeDidChange", (_e: any, mode: number) => {
switch (mode) {
case 0:
player.loopStatus = Player.LOOP_STATUS_NONE;
break;
case 1:
player.loopStatus = Player.LOOP_STATUS_TRACK;
break;
case 2:
player.loopStatus = Player.LOOP_STATUS_PLAYLIST;
break;
}
});
mpris.utils.getIPCMain().on("shuffleModeDidChange", (_e: any, mode: number) => {
player.shuffle = mode === 1;
});
mpris.player = player;
}
/**
* Update M.P.R.I.S Player Attributes
*/
private static updateMetaData(attributes: any) {
mpris.player.metadata = {
"mpris:trackid": mpris.player.objectPath(`track/${attributes.playParams.id.replace(/[.]+/g, "")}`),
"mpris:length": attributes.durationInMillis * 1000, // In microseconds
"mpris:artUrl": attributes.artwork.url.replace("/{w}x{h}bb", "/512x512bb").replace("/2000x2000bb", "/35x35bb"),
"xesam:title": `${attributes.name}`,
"xesam:album": `${attributes.albumName}`,
"xesam:artist": [`${attributes.artistName}`],
"xesam:genre": attributes.genreNames,
};
}
/*******************************************************************************************
* Public Methods
* ****************************************************************************************/
/**
* Clear state
* @private
*/
private static clearState() {
if (!mpris.player) {
return;
}
mpris.player.metadata = {
"mpris:trackid": "/org/mpris/MediaPlayer2/TrackList/NoTrack",
};
mpris.player.playbackStatus = Player.PLAYBACK_STATUS_STOPPED;
}
/**
* Connects to MPRIS Service
*/
private static connect() {
/**
* Runs on app ready
*/
@mpris.linuxOnly
onReady(_: any): void {
console.debug(`[${mpris.name}:onReady] Ready.`);
}
const player = Player({
name: 'cider',
identity: 'Cider',
supportedInterfaces: ['player']
});
/**
* Renderer ready
*/
@mpris.linuxOnly
onRendererReady(): void {
mpris.connect();
}
console.debug(`[${mpris.name}:connect] Successfully connected.`);
/**
* Runs on app stop
*/
@mpris.linuxOnly
onBeforeQuit(): void {
console.debug(`[Plugin][${mpris.name}] Stopped.`);
mpris.clearState();
}
const renderer = mpris.utils.getWindow().webContents
const loopType: { [key: string]: number; } = {
'none': 0,
'track': 1,
'playlist': 2,
}
player.on('next', () => mpris.utils.playback.next())
player.on('previous', () => mpris.utils.playback.previous())
player.on('playpause', () => mpris.utils.playback.playPause())
player.on('play', () => mpris.utils.playback.play())
player.on('pause', () => mpris.utils.playback.pause())
player.on('quit', () => mpris.utils.getApp().exit())
player.on('position', (args: { position: any; }) => mpris.utils.playback.seek(args.position / 1000 / 1000))
player.on('loopStatus', (status: string) => renderer.executeJavaScript(`app.mk.repeatMode = ${loopType[status.toLowerCase()]}`))
player.on('shuffle', () => renderer.executeJavaScript('app.mk.shuffleMode = (app.mk.shuffleMode === 0) ? 1 : 0'))
mpris.utils.getIPCMain().on('mpris:playbackTimeDidChange', (event: any, time: number) => {
player.getPosition = () => time;
})
mpris.utils.getIPCMain().on('repeatModeDidChange', (_e: any, mode: number) => {
switch (mode) {
case 0:
player.loopStatus = Player.LOOP_STATUS_NONE;
break;
case 1:
player.loopStatus = Player.LOOP_STATUS_TRACK;
break;
case 2:
player.loopStatus = Player.LOOP_STATUS_PLAYLIST;
break;
}
})
mpris.utils.getIPCMain().on('shuffleModeDidChange', (_e: any, mode: number) => {
player.shuffle = mode === 1
})
mpris.player = player;
}
/**
* Update M.P.R.I.S Player Attributes
*/
private static updateMetaData(attributes: any) {
mpris.player.metadata = {
'mpris:trackid': mpris.player.objectPath(`track/${attributes.playParams.id.replace(/[.]+/g, "")}`),
'mpris:length': attributes.durationInMillis * 1000, // In microseconds
'mpris:artUrl': (attributes.artwork.url.replace('/{w}x{h}bb', '/512x512bb')).replace('/2000x2000bb', '/35x35bb'),
'xesam:title': `${attributes.name}`,
'xesam:album': `${attributes.albumName}`,
'xesam:artist': [`${attributes.artistName}`],
'xesam:genre': attributes.genreNames
};
}
/*******************************************************************************************
* Public Methods
* ****************************************************************************************/
/**
* Clear state
* @private
*/
private static clearState() {
if (!mpris.player) {
return
}
mpris.player.metadata = {'mpris:trackid': '/org/mpris/MediaPlayer2/TrackList/NoTrack'}
mpris.player.playbackStatus = Player.PLAYBACK_STATUS_STOPPED;
}
/**
* Runs on app ready
*/
@mpris.linuxOnly
onReady(_: any): void {
console.debug(`[${mpris.name}:onReady] Ready.`);
}
/**
* Renderer ready
*/
@mpris.linuxOnly
onRendererReady(): void {
mpris.connect()
}
/**
* Runs on app stop
*/
@mpris.linuxOnly
onBeforeQuit(): void {
console.debug(`[Plugin][${mpris.name}] Stopped.`);
mpris.clearState()
}
/**
* Runs on playback State Change
* @param attributes Music Attributes (attributes.status = current state)
*/
@mpris.linuxOnly
onPlaybackStateDidChange(attributes: any): void {
mpris.player.playbackStatus = attributes?.status ? Player.PLAYBACK_STATUS_PLAYING : Player.PLAYBACK_STATUS_PAUSED
}
/**
* Runs on song change
* @param attributes Music Attributes
*/
@mpris.linuxOnly
onNowPlayingItemDidChange(attributes: object): void {
mpris.updateMetaData(attributes);
}
/**
* Runs on playback State Change
* @param attributes Music Attributes (attributes.status = current state)
*/
@mpris.linuxOnly
onPlaybackStateDidChange(attributes: any): void {
mpris.player.playbackStatus = attributes?.status ? Player.PLAYBACK_STATUS_PLAYING : Player.PLAYBACK_STATUS_PAUSED;
}
/**
* Runs on song change
* @param attributes Music Attributes
*/
@mpris.linuxOnly
onNowPlayingItemDidChange(attributes: object): void {
mpris.updateMetaData(attributes);
}
}

View file

@ -1,124 +1,118 @@
import fetch from "electron-fetch";
import {app, nativeImage, Notification} from "electron";
import { app, nativeImage, Notification } from "electron";
import NativeImage = Electron.NativeImage;
import {createWriteStream} from "fs";
import {join} from "path";
import { createWriteStream } from "fs";
import { join } from "path";
export default class playbackNotifications {
/**
* Base Plugin Details (Eventually implemented into a GUI in settings)
*/
public name: string = "Playback Notifications";
public description: string = "Creates notifications on playback.";
public version: string = "1.0.0";
public author: string = "Core";
public contributors: string[] = ["Core", "Monochromish"];
private _utils: any;
private _notification: Notification | undefined;
private _artworkImage: { [key: string]: NativeImage } = {};
private _artworkNums: Array<string> = [];
/**
* Base Plugin Details (Eventually implemented into a GUI in settings)
*/
public name: string = 'Playback Notifications';
public description: string = 'Creates notifications on playback.';
public version: string = '1.0.0';
public author: string = 'Core';
public contributors: string[] = ['Core', 'Monochromish'];
/**
* Creates playback notification
* @param a: Music Attributes
*/
createNotification(a: any): void {
if (this._notification) {
this._notification.close();
}
private _utils: any;
private _notification: Notification | undefined;
private _artworkImage: { [key: string]: NativeImage } = {};
private _artworkNums: Array<string> = [];
/**
* Creates playback notification
* @param a: Music Attributes
*/
createNotification(a: any): void {
if (this._notification) {
this._notification.close();
}
this._notification = new Notification({
title: a.name,
body: `${a.artistName}${a.albumName}`,
silent: true,
icon: this._artworkImage[a.artwork.url],
urgency: 'low',
actions: [
{
'type': 'button',
'text': `${this._utils.getLocale(this._utils.getStoreValue('general.language'), 'term.skip')}`
}
],
toastXml: `
this._notification = new Notification({
title: a.name,
body: `${a.artistName}${a.albumName}`,
silent: true,
icon: this._artworkImage[a.artwork.url],
urgency: "low",
actions: [
{
type: "button",
text: `${this._utils.getLocale(this._utils.getStoreValue("general.language"), "term.skip")}`,
},
],
toastXml: `
<toast>
<audio silent="true" />
<visual>
<binding template="ToastImageAndText02">
<image id="1" src="${join(app.getPath("temp"), `${a.songId}-${a.artwork.url.split('/').pop()}`)}" name="Image" />
<text id="1">${a?.name.replace(/&/g, '&amp;')}</text>
<text id="2">${a?.artistName.replace(/&/g, '&amp;')} ${a?.albumName.replace(/&/g, '&amp;')}</text>
<image id="1" src="${join(app.getPath("temp"), `${a.songId}-${a.artwork.url.split("/").pop()}`)}" name="Image" />
<text id="1">${a?.name.replace(/&/g, "&amp;")}</text>
<text id="2">${a?.artistName.replace(/&/g, "&amp;")} ${a?.albumName.replace(/&/g, "&amp;")}</text>
</binding>
</visual>
<actions>
<action content="${this._utils.getLocale(this._utils.getStoreValue('general.language'), 'term.playpause')}" activationType="protocol" arguments="cider://playpause/"/>
<action content="${this._utils.getLocale(this._utils.getStoreValue('general.language'), 'term.next')}" activationType="protocol" arguments="cider://nextitem/"/>
<action content="${this._utils.getLocale(this._utils.getStoreValue("general.language"), "term.playpause")}" activationType="protocol" arguments="cider://playpause/"/>
<action content="${this._utils.getLocale(this._utils.getStoreValue("general.language"), "term.next")}" activationType="protocol" arguments="cider://nextitem/"/>
</actions>
</toast>`
});
</toast>`,
});
console.log(this._notification.toastXml);
console.log(this._notification.toastXml);
this._notification.on('click', (_: any) => {
this._utils.getWindow().show()
this._utils.getWindow().focus()
})
this._notification.on("click", (_: any) => {
this._utils.getWindow().show();
this._utils.getWindow().focus();
});
this._notification.on('close', (_: any) => {
this._notification = undefined;
})
this._notification.on("close", (_: any) => {
this._notification = undefined;
});
this._notification.on('action', (event: any, action: any) => {
this._utils.playback.next()
})
this._notification.on("action", (event: any, action: any) => {
this._utils.playback.next();
});
this._notification.show();
this._notification.show();
}
}
/*******************************************************************************************
* Public Methods
* ****************************************************************************************/
/**
* Runs on plugin load (Currently run on application start)
*/
constructor(utils: any) {
this._utils = utils;
console.debug(`[Plugin][${this.name}] Loading Complete.`);
/*******************************************************************************************
* Public Methods
* ****************************************************************************************/
utils.getIPCMain().on("playbackNotifications:create", (event: any, a: any) => {
a.artwork.url = a.artwork.url.replace("/{w}x{h}bb", "/512x512bb").replace("/2000x2000bb", "/35x35bb");
/**
* Runs on plugin load (Currently run on application start)
*/
constructor(utils: any) {
this._utils = utils;
console.debug(`[Plugin][${this.name}] Loading Complete.`);
utils.getIPCMain().on('playbackNotifications:create', (event: any, a: any) => {
a.artwork.url = a.artwork.url.replace('/{w}x{h}bb', '/512x512bb').replace('/2000x2000bb', '/35x35bb');
if (this._artworkNums.length > 20) {
delete this._artworkImage[this._artworkNums[0]];
this._artworkNums.shift();
}
if (this._artworkImage[a.artwork.url]) {
this.createNotification(a);
} else {
if (process.platform === "win32") {
fetch(a.artwork.url)
.then(res => {
console.log(join(app.getPath("temp"), `${a.songId}-${a.artwork.url.split('/').pop()}`));
const dest = createWriteStream(join(app.getPath("temp"), `${a.songId}-${a.artwork.url.split('/').pop()}`));
// @ts-ignore
res.body.pipe(dest)
this.createNotification(a);
})
} else {
fetch(a.artwork.url).then(async blob => {
this._artworkImage[a.artwork.url] = nativeImage.createFromBuffer(Buffer.from(await blob.arrayBuffer()));
this._artworkNums[this._artworkNums.length] = a.artwork.url;
this.createNotification(a);
});
}
}
})
}
if (this._artworkNums.length > 20) {
delete this._artworkImage[this._artworkNums[0]];
this._artworkNums.shift();
}
if (this._artworkImage[a.artwork.url]) {
this.createNotification(a);
} else {
if (process.platform === "win32") {
fetch(a.artwork.url).then((res) => {
console.log(join(app.getPath("temp"), `${a.songId}-${a.artwork.url.split("/").pop()}`));
const dest = createWriteStream(join(app.getPath("temp"), `${a.songId}-${a.artwork.url.split("/").pop()}`));
// @ts-ignore
res.body.pipe(dest);
this.createNotification(a);
});
} else {
fetch(a.artwork.url).then(async (blob) => {
this._artworkImage[a.artwork.url] = nativeImage.createFromBuffer(Buffer.from(await blob.arrayBuffer()));
this._artworkNums[this._artworkNums.length] = a.artwork.url;
this.createNotification(a);
});
}
}
});
}
}

View file

@ -1,42 +1,39 @@
import * as electron from 'electron';
import * as os from 'os';
import * as fs from 'fs';
import { join, resolve } from 'path';
import * as CiderReceiver from '../base/castreceiver';
import fetch from 'electron-fetch';
import {Stream} from "stream";
import {spawn} from 'child_process';
import {Worker} from 'worker_threads';
import { Blob } from 'buffer';
import * as electron from "electron";
import * as os from "os";
import * as fs from "fs";
import { join, resolve } from "path";
import * as CiderReceiver from "../base/castreceiver";
import fetch from "electron-fetch";
import { Stream } from "stream";
import { spawn } from "child_process";
import { Worker } from "worker_threads";
import { Blob } from "buffer";
export default class RAOP {
/**
* Private variables for interaction in plugins
*/
private _utils: any;
private _win: any;
private _app: any;
private _store: any;
private _cacheAttr: any;
private u: any;
private ipairplay: any = "";
private portairplay: any = "";
/**
* Private variables for interaction in plugins
*/
private _utils: any;
private _win: any;
private _app: any;
private _store: any;
private _cacheAttr: any;
private u: any;
private ipairplay: any = "";
private portairplay: any = "";
private airtunes: any;
private device: any;
private mdns = require('mdns-js');
private ok: any = 1;
private devices: any = [];
private castDevices: any = [];
private i: any = false;
private audioStream: any = new Stream.PassThrough();
private ffmpeg: any = null;
private worker: any = null;
private airtunes: any;
private device: any;
private mdns = require("mdns-js");
private ok: any = 1;
private devices: any = [];
private castDevices: any = [];
private i: any = false;
private audioStream: any = new Stream.PassThrough();
private ffmpeg: any = null;
private worker: any = null;
private processNode = `
private processNode = `
import {parentPort, workerData} from "worker_threads";
function getAudioConv (buffers) {
@ -88,308 +85,297 @@ export default class RAOP {
`;
private ondeviceup(name: any, host: any, port: any, addresses: any, text: any, airplay2: any = null) {
// console.log(this.castDevices.findIndex((item: any) => {return (item.name == host.replace(".local","") && item.port == port )}))
if (this.castDevices.findIndex((item: any) => {return (item != null && item.name == (host ?? "Unknown").replace(".local","") && item.port == port )}) == -1) {
this.castDevices.push({
name: (host ?? "Unknown").replace(".local",""),
host: addresses ? addresses[0] : '',
port: port,
addresses: addresses,
txt: text,
airplay2: airplay2
});
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 ondeviceup(name: any, host: any, port: any, addresses: any, text: any, airplay2: any = null) {
// console.log(this.castDevices.findIndex((item: any) => {return (item.name == host.replace(".local","") && item.port == port )}))
if (
this.castDevices.findIndex((item: any) => {
return item != null && item.name == (host ?? "Unknown").replace(".local", "") && item.port == port;
}) == -1
) {
this.castDevices.push({
name: (host ?? "Unknown").replace(".local", ""),
host: addresses ? addresses[0] : "",
port: port,
addresses: addresses,
txt: text,
airplay2: airplay2,
});
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);
}
}
/**
* Base Plugin Details (Eventually implemented into a GUI in settings)
*/
public name: string = "RAOP";
public description: string = "RAOP Plugin";
public version: string = "0.0.1";
public author: string = "vapormusic / Cider Collective";
/**
* Runs on plugin load (Currently run on application start)
*/
constructor(utils: { getStore: () => any; getApp: () => any }) {
this._utils = utils;
console.debug(`[Plugin][${this.name}] Loading Complete.`);
this._app = utils.getApp();
}
/**
* Runs on app ready
*/
onReady(win: any): void {
this.u = require("airtunes2");
this._win = win;
electron.ipcMain.on("getKnownAirplayDevices", (event) => {
event.returnValue = this.castDevices;
});
electron.ipcMain.on("getAirplayDevice", (event, data) => {
this.castDevices = [];
console.log("scan for airplay devices");
const browser = this.mdns.createBrowser(this.mdns.tcp("raop"));
browser.on("ready", browser.discover);
browser.on("update", (service: any) => {
if (service.addresses && service.fullname && service.fullname.includes("_raop._tcp")) {
// console.log(service.txt)
this._win.webContents.executeJavaScript(`console.log(
"${service.name} ${service.host}:${service.port} ${service.addresses}"
)`);
this.ondeviceup(service.name, service.host, service.port, service.addresses, service.txt);
}
}
});
/**
* Base Plugin Details (Eventually implemented into a GUI in settings)
*/
public name: string = 'RAOP';
public description: string = 'RAOP Plugin';
public version: string = '0.0.1';
public author: string = 'vapormusic / Cider Collective';
const browser2 = this.mdns.createBrowser(this.mdns.tcp("airplay"));
browser2.on("ready", browser2.discover);
/**
* Runs on plugin load (Currently run on application start)
*/
constructor(utils: { getStore: () => any; getApp: () => any; }) {
this._utils = utils;
console.debug(`[Plugin][${this.name}] Loading Complete.`);
this._app = utils.getApp();
}
/**
* Runs on app ready
*/
onReady(win: any): void {
this.u = require('airtunes2');
this._win = win;
electron.ipcMain.on('getKnownAirplayDevices', (event) => {
event.returnValue = this.castDevices
});
electron.ipcMain.on("getAirplayDevice", (event, data) => {
this.castDevices = [];
console.log("scan for airplay devices");
const browser = this.mdns.createBrowser(this.mdns.tcp('raop'));
browser.on('ready', browser.discover);
browser.on('update', (service: any) => {
if (service.addresses && service.fullname && (service.fullname.includes('_raop._tcp'))) {
// console.log(service.txt)
this._win.webContents.executeJavaScript(`console.log(
browser2.on("update", (service: any) => {
if (service.addresses && service.fullname && service.fullname.includes("_airplay._tcp")) {
// console.log(service.txt)
this._win.webContents.executeJavaScript(`console.log(
"${service.name} ${service.host}:${service.port} ${service.addresses}"
)`);
this.ondeviceup(service.name, service.host, service.port, service.addresses, service.txt);
}
});
this.ondeviceup(service.name, service.host, service.port, service.addresses, service.txt, true);
}
});
const browser2 = this.mdns.createBrowser(this.mdns.tcp('airplay'));
browser2.on('ready', browser2.discover);
// const browser2 = this.mdns.createBrowser(this.mdns.tcp('airplay'));
// browser2.on('ready', browser2.discover);
browser2.on('update', (service: any) => {
if (service.addresses && service.fullname && (service.fullname.includes('_airplay._tcp'))) {
// console.log(service.txt)
this._win.webContents.executeJavaScript(`console.log(
"${service.name} ${service.host}:${service.port} ${service.addresses}"
)`);
this.ondeviceup(service.name, service.host, service.port, service.addresses, service.txt, true);
}
});
// const browser2 = this.mdns.createBrowser(this.mdns.tcp('airplay'));
// browser2.on('ready', browser2.discover);
// browser2.on('update', (service: any) => {
// if (service.addresses && service.fullname && (service.fullname.includes('_raop._tcp') || service.fullname.includes('_airplay._tcp'))) {
// // console.log(service.txt)
// this._win.webContents.executeJavaScript(`console.log(
// "${service.name} ${service.host}:${service.port} ${service.addresses}"
// )`);
// this.ondeviceup(service.name, service.host, service.port, service.addresses, service.txt);
// }
// });
});
// browser2.on('update', (service: any) => {
// if (service.addresses && service.fullname && (service.fullname.includes('_raop._tcp') || service.fullname.includes('_airplay._tcp'))) {
// // console.log(service.txt)
// this._win.webContents.executeJavaScript(`console.log(
// "${service.name} ${service.host}:${service.port} ${service.addresses}"
// )`);
// this.ondeviceup(service.name, service.host, service.port, service.addresses, service.txt);
// }
// });
electron.ipcMain.on("performAirplayPCM", (event, ipv4, ipport, sepassword, title, artist, album, artworkURL, txt, airplay2dv) => {
if (ipv4 != this.ipairplay || ipport != this.portairplay) {
if (this.airtunes == null) {
this.airtunes = new this.u();
}
this.ipairplay = ipv4;
this.portairplay = ipport;
this.device = this.airtunes.add(ipv4, {
port: ipport,
volume: 50,
password: sepassword,
txt: txt,
airplay2: airplay2dv,
debug: true,
});
electron.ipcMain.on("performAirplayPCM", (event, ipv4, ipport, sepassword, title, artist, album, artworkURL,txt,airplay2dv) => {
if (ipv4 != this.ipairplay || ipport != this.portairplay) {
if (this.airtunes == null) { this.airtunes = new this.u()}
this.ipairplay = ipv4;
this.portairplay = ipport;
this.device = this.airtunes.add(ipv4, {
port: ipport,
volume: 50,
password: sepassword,
txt: txt,
airplay2: airplay2dv,
debug: true
});
// console.log('lol',txt)
this.device.on('status', (status: any) => {
console.log('device status', status);
if (status == "ready"){
this._win.webContents.setAudioMuted(true);
this._win.webContents.executeJavaScript(`CiderAudio.sendAudio()`).catch((err: any) => console.error(err));
}
if (status == "need_password"){
this._win.webContents.executeJavaScript(`app.setAirPlayCodeUI()`)
}
if (status == "pair_success"){
this._win.webContents.executeJavaScript(`app.sendAirPlaySuccess()`)
}
if (status == "pair_failed"){
this._win.webContents.executeJavaScript(`app.sendAirPlayFailed()`)
}
if (status == 'stopped') {
this.airtunes.stopAll(() => {
console.log('end');
});
this.airtunes = null;
this.device = null;
this.ipairplay = '';
this.portairplay = '';
this.ok = 1;
} else {
setTimeout(() => {
if (this.ok == 1) {
console.log(this.device.key, title ?? '', artist ?? '', album ?? '');
this.airtunes.setTrackInfo(this.device.key, title ?? '', artist?? '', album?? '');
this.uploadImageAirplay(artworkURL);
console.log('done');
this.ok == 2
}
}, 1000);
}
});
}
});
electron.ipcMain.on('setAirPlayPasscode', (event, passcode) => {
if (this.device){
this.device.setPasscode(passcode)
}
})
electron.ipcMain.on('writeWAV', (event, leftbuffer, rightbuffer) => {
if (this.airtunes != null) {
if (this.worker == null) {
try{
const toDataUrl = (js: any) => new URL(`data:text/javascript,${encodeURIComponent(js)}`);
// let blob = new Blob([this.processNode], { type: 'application/javascript' });
//Create new worker
this.worker = new Worker(toDataUrl(this.processNode));
//Listen for a message from worker
this.worker.on("message", (result: any) => {
// fs.writeFile(join(electron.app.getPath('userData'), 'buffer.raw'), Buffer.from(Int8Array.from(result.outbuffer)),{flag: 'a+'}, function (err) {
// if (err) throw err;
// console.log('It\'s saved!');
// });
this.airtunes.circularBuffer.write(Buffer.from(Int8Array.from(result.outbuffer)));
});
this.worker.on("error", (error: any) => {
console.log("bruh",error);
});
this.worker.postMessage({buffer: [leftbuffer, rightbuffer]});
} catch (e){console.log(e)}
// this.ffmpeg != null ? this.ffmpeg.kill() : null;
// this.ffmpeg = spawn(this._utils.getStoreValue("advanced.ffmpegLocation"), [
// '-f', 's16le', // PCM 16bits, little-endian
// '-ar', '48000',
// '-ac', "2",
// '-err_detect','ignore_err',
// '-i', "http://localhost:9000/audio.wav",
// '-acodec', 'pcm_s16le',
// '-f', 's16le', // PCM 16bits, little-endian
// '-ar', '44100', // Sampling rate
// '-ac', "2", // Stereo
// 'pipe:1' // Output on stdout
// ]);
// // pipe data to AirTunes
// this.ffmpeg.stdout.pipe(this.airtunes);
// this.i = true;
} else {
this.worker.postMessage({buffer: [leftbuffer, rightbuffer]});
}
}
});
electron.ipcMain.on('disconnectAirplay', (event) => {
this._win.webContents.setAudioMuted(false);
this.airtunes.stopAll(function () {
console.log('end');
// console.log('lol',txt)
this.device.on("status", (status: any) => {
console.log("device status", status);
if (status == "ready") {
this._win.webContents.setAudioMuted(true);
this._win.webContents.executeJavaScript(`CiderAudio.sendAudio()`).catch((err: any) => console.error(err));
}
if (status == "need_password") {
this._win.webContents.executeJavaScript(`app.setAirPlayCodeUI()`);
}
if (status == "pair_success") {
this._win.webContents.executeJavaScript(`app.sendAirPlaySuccess()`);
}
if (status == "pair_failed") {
this._win.webContents.executeJavaScript(`app.sendAirPlayFailed()`);
}
if (status == "stopped") {
this.airtunes.stopAll(() => {
console.log("end");
});
this.airtunes = null;
this.device = null;
this.ipairplay = '';
this.portairplay = '';
this.ipairplay = "";
this.portairplay = "";
this.ok = 1;
this.i = false;
} else {
setTimeout(() => {
if (this.ok == 1) {
console.log(this.device.key, title ?? "", artist ?? "", album ?? "");
this.airtunes.setTrackInfo(this.device.key, title ?? "", artist ?? "", album ?? "");
this.uploadImageAirplay(artworkURL);
console.log("done");
this.ok == 2;
}
}, 1000);
}
});
}
});
electron.ipcMain.on('updateAirplayInfo', (event, title, artist, album, artworkURL) => {
if (this.airtunes && this.device) {
console.log(this.device.key, title, artist, album);
this.airtunes.setTrackInfo(this.device.key, title, artist, album);
this.uploadImageAirplay(artworkURL)
}
});
electron.ipcMain.on("setAirPlayPasscode", (event, passcode) => {
if (this.device) {
this.device.setPasscode(passcode);
}
});
electron.ipcMain.on('updateRPCImage', (_event, imageurl) => {
this.uploadImageAirplay(imageurl)
})
electron.ipcMain.on("writeWAV", (event, leftbuffer, rightbuffer) => {
if (this.airtunes != null) {
if (this.worker == null) {
try {
const toDataUrl = (js: any) => new URL(`data:text/javascript,${encodeURIComponent(js)}`);
// let blob = new Blob([this.processNode], { type: 'application/javascript' });
//Create new worker
this.worker = new Worker(toDataUrl(this.processNode));
//Listen for a message from worker
this.worker.on("message", (result: any) => {
// fs.writeFile(join(electron.app.getPath('userData'), 'buffer.raw'), Buffer.from(Int8Array.from(result.outbuffer)),{flag: 'a+'}, function (err) {
// if (err) throw err;
// console.log('It\'s saved!');
// });
this.airtunes.circularBuffer.write(Buffer.from(Int8Array.from(result.outbuffer)));
});
this.worker.on("error", (error: any) => {
console.log("bruh", error);
});
this.worker.postMessage({ buffer: [leftbuffer, rightbuffer] });
} catch (e) {
console.log(e);
}
}
// this.ffmpeg != null ? this.ffmpeg.kill() : null;
// this.ffmpeg = spawn(this._utils.getStoreValue("advanced.ffmpegLocation"), [
// '-f', 's16le', // PCM 16bits, little-endian
// '-ar', '48000',
// '-ac', "2",
// '-err_detect','ignore_err',
// '-i', "http://localhost:9000/audio.wav",
// '-acodec', 'pcm_s16le',
// '-f', 's16le', // PCM 16bits, little-endian
// '-ar', '44100', // Sampling rate
// '-ac', "2", // Stereo
// 'pipe:1' // Output on stdout
// ]);
private uploadImageAirplay = (url: any) => {
try {
if (url != null && url != '') {
//console.log(join(this._app.getPath('userData'), 'temp.png'), url);
fetch(url)
.then(res => res.buffer())
.then((buffer) => {
this.airtunes.setArtwork(this.device.key, buffer, "image/png");
}).catch(err => {
console.log(err)
});
}
} catch (e) { console.log(e) }
}
/**
* Runs on app stop
*/
onBeforeQuit(): void {
}
// /**
// * Runs on song change
// * @param attributes Music Attributes
// */
// onNowPlayingItemDidChange(attributes: any): void {
// if (this.airtunes && this.device) {
// let title = attributes.name ? attributes.name : '';
// let artist = attributes.artistName ? attributes.artistName : '';
// let album = attributes.albumName ? attributes.albumName : '';
// let artworkURL = attributes?.artwork?.url?.replace('{w}', '1024').replace('{h}', '1024') ?? null;
// console.log(this.device.key, title, artist, album);
// this.airtunes.setTrackInfo(this.device.key, title, artist, album);
// if (artworkURL)
// this.uploadImageAirplay(artworkURL)
// }
// }
/**
* Runs on playback State Change
* @param attributes Music Attributes (attributes.status = current state)
*/
onPlaybackStateDidChange(attributes: any): void {
if (this.airtunes && this.device) {
let title = attributes?.name ?? '';
let artist = attributes?.artistName ?? '';
let album = attributes?.albumName ?? '';
let artworkURL = attributes?.artwork?.url ?? null;
console.log(this.device.key, title, artist, album);
this.airtunes.setTrackInfo(this.device.key, title, artist, album);
if (artworkURL != null){}
this.uploadImageAirplay(artworkURL.replace('{w}', '1024').replace('{h}', '1024'))
// // pipe data to AirTunes
// this.ffmpeg.stdout.pipe(this.airtunes);
// this.i = true;
} else {
this.worker.postMessage({ buffer: [leftbuffer, rightbuffer] });
}
}
}
});
electron.ipcMain.on("disconnectAirplay", (event) => {
this._win.webContents.setAudioMuted(false);
this.airtunes.stopAll(function () {
console.log("end");
});
this.airtunes = null;
this.device = null;
this.ipairplay = "";
this.portairplay = "";
this.ok = 1;
this.i = false;
});
electron.ipcMain.on("updateAirplayInfo", (event, title, artist, album, artworkURL) => {
if (this.airtunes && this.device) {
console.log(this.device.key, title, artist, album);
this.airtunes.setTrackInfo(this.device.key, title, artist, album);
this.uploadImageAirplay(artworkURL);
}
});
electron.ipcMain.on("updateRPCImage", (_event, imageurl) => {
this.uploadImageAirplay(imageurl);
});
}
private uploadImageAirplay = (url: any) => {
try {
if (url != null && url != "") {
//console.log(join(this._app.getPath('userData'), 'temp.png'), url);
fetch(url)
.then((res) => res.buffer())
.then((buffer) => {
this.airtunes.setArtwork(this.device.key, buffer, "image/png");
})
.catch((err) => {
console.log(err);
});
}
} catch (e) {
console.log(e);
}
};
/**
* Runs on app stop
*/
onBeforeQuit(): void {}
// /**
// * Runs on song change
// * @param attributes Music Attributes
// */
// onNowPlayingItemDidChange(attributes: any): void {
// if (this.airtunes && this.device) {
// let title = attributes.name ? attributes.name : '';
// let artist = attributes.artistName ? attributes.artistName : '';
// let album = attributes.albumName ? attributes.albumName : '';
// let artworkURL = attributes?.artwork?.url?.replace('{w}', '1024').replace('{h}', '1024') ?? null;
// console.log(this.device.key, title, artist, album);
// this.airtunes.setTrackInfo(this.device.key, title, artist, album);
// if (artworkURL)
// this.uploadImageAirplay(artworkURL)
// }
// }
/**
* Runs on playback State Change
* @param attributes Music Attributes (attributes.status = current state)
*/
onPlaybackStateDidChange(attributes: any): void {
if (this.airtunes && this.device) {
let title = attributes?.name ?? "";
let artist = attributes?.artistName ?? "";
let album = attributes?.albumName ?? "";
let artworkURL = attributes?.artwork?.url ?? null;
console.log(this.device.key, title, artist, album);
this.airtunes.setTrackInfo(this.device.key, title, artist, album);
if (artworkURL != null) {
}
this.uploadImageAirplay(artworkURL.replace("{w}", "1024").replace("{h}", "1024"));
}
}
}

View file

@ -1,136 +1,134 @@
import {nativeImage, nativeTheme} from "electron";
import {utils} from "../base/utils";
import {join} from "path";
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;
/**
* 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';
/**
* 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`)),
/**
* 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
*/
private updateButtons(attributes: any) {
console.log(attributes);
if (!attributes) {
return;
}
/*******************************************************************************************
* Private Methods
* ****************************************************************************************/
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();
},
},
];
/**
* 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
}
}
if (!attributes.playParams || attributes.playParams.id === "no-id-found") {
this._win.setThumbarButtons([]);
} else {
this._win.setThumbarButtons(buttons);
}
}
/**
* Update the thumbnail toolbar
*/
private updateButtons(attributes: any) {
/*******************************************************************************************
* Public Methods
* ****************************************************************************************/
console.log(attributes)
/**
* Runs on plugin load (Currently run on application start)
*/
constructor(a: { getApp: () => any }) {
this._app = utils.getApp();
console.debug(`[Plugin][${this.name}] Loading Complete.`);
}
if (!attributes) {
return
}
/**
* Runs on app ready
*/
@Thumbar.windowsOnly
onReady(win: Electron.BrowserWindow): void {
this._win = win;
console.debug(`[Plugin][${this.name}] Ready.`);
}
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()
}
}
];
/**
* Runs on app stop
*/
@Thumbar.windowsOnly
onBeforeQuit(): void {
console.debug(`[Plugin][${this.name}] Stopped.`);
}
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(a: { getApp: () => any; }) {
this._app = utils.getApp();
console.debug(`[Plugin][${this.name}] Loading Complete.`);
}
/**
* Runs on app ready
*/
@Thumbar.windowsOnly
onReady(win: Electron.BrowserWindow): void {
this._win = win;
console.debug(`[Plugin][${this.name}] Ready.`);
}
/**
* Runs on app stop
*/
@Thumbar.windowsOnly
onBeforeQuit(): void {
console.debug(`[Plugin][${this.name}] Stopped.`);
}
/**
* Runs on playback State Change
* @param attributes Music Attributes (attributes.status = current state)
*/
@Thumbar.windowsOnly
onPlaybackStateDidChange(attributes: object): void {
this.updateButtons(attributes)
}
/**
* Runs on song change
* @param attributes Music Attributes
*/
@Thumbar.windowsOnly
onNowPlayingItemDidChange(attributes: object): void {
this.updateButtons(attributes)
}
/**
* Runs on playback State Change
* @param attributes Music Attributes (attributes.status = current state)
*/
@Thumbar.windowsOnly
onPlaybackStateDidChange(attributes: object): void {
this.updateButtons(attributes);
}
/**
* Runs on song change
* @param attributes Music Attributes
*/
@Thumbar.windowsOnly
onNowPlayingItemDidChange(attributes: object): void {
this.updateButtons(attributes);
}
}

View file

@ -1,4 +1,4 @@
import * as WebSocket from 'ws';
import * as WebSocket from "ws";
/**
* 0-pad a number.
@ -6,7 +6,7 @@ import * as WebSocket from 'ws';
* @param {Number} length
* @returns String
*/
const pad = (number: number, length: number) => String(number).padStart(length, '0');
const pad = (number: number, length: number) => String(number).padStart(length, "0");
/**
* Convert seconds to a time string acceptable to Rainmeter
@ -15,230 +15,230 @@ const pad = (number: number, length: number) => String(number).padStart(length,
* @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);
}
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>';
/**
* 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';
private _win: any;
private ws?: WebSocket;
private wsapiConn?: WebSocket;
private playerName: string = "Cider";
constructor() {
console.debug(`[Plugin][${this.name}] Loading Complete.`);
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;
}
}
/**
* 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;
}
}
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];
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}`);
}
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}`);
}
}
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();
/**
* Runs on app ready
*/
@WebNowPlaying.windowsOnly
public onReady(win: any) {
this._win = win;
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}`);
}
}
}
// 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}`);
};
/**
* Runs on app ready
*/
@WebNowPlaying.windowsOnly
public onReady(win: any) {
this._win = win;
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);
}
// 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}`);
};
init();
this.ws.onclose = () => {
clearTimeout(retry);
retry = setTimeout(init, 2000);
};
// Connect to wsapi. Only used to update progress.
try {
this.wsapiConn = new WebSocket('ws://127.0.0.1:26369/');
this.ws.onerror = () => {
clearTimeout(retry);
this.ws?.close();
};
this.wsapiConn.onopen = () => {
console.info('[WebNowPlaying] Connected to wsapi');
};
this.ws.onmessage = this.fireEvent?.bind(this);
} catch (error) {
console.error(error);
}
};
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);
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);
}
console.debug(`[Plugin][${this.name}] Ready.`);
};
} catch (error) {
console.error(error);
}
/**
* 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.`);
}
console.debug(`[Plugin][${this.name}] Ready.`);
}
/**
* Runs on playback State Change
* @param attributes Music Attributes (attributes.status = current state)
*/
@WebNowPlaying.windowsOnly
public onPlaybackStateDidChange(attributes: any) {
this.sendSongInfo(attributes);
/**
* 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 song change
* @param attributes Music Attributes
*/
@WebNowPlaying.windowsOnly
public onNowPlayingItemDidChange(attributes: any) {
this.sendSongInfo(attributes);
}
/**
* 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);
}
}

View file

@ -1,12 +1,12 @@
import * as PouchDB from 'pouchdb-node';
import {join} from 'path';
import {app} from "electron";
PouchDB.plugin(require('pouchdb-upsert'));
import * as PouchDB from "pouchdb-node";
import { join } from "path";
import { app } from "electron";
PouchDB.plugin(require("pouchdb-upsert"));
export class ProviderDB {
public static db: any = null
static init() {
if (ProviderDB.db == null){
ProviderDB.db = new PouchDB(join(app.getPath('userData'), 'tracksdb'))
}
public static db: any = null;
static init() {
if (ProviderDB.db == null) {
ProviderDB.db = new PouchDB(join(app.getPath("userData"), "tracksdb"));
}
}
}
}

View file

@ -1,180 +1,197 @@
import { ProviderDB } from "./db";
import * as path from 'path';
const { readdir } = require('fs').promises;
import { utils } from '../../base/utils';
import * as mm from 'music-metadata';
import {Md5} from 'ts-md5/dist/md5';
import * as path from "path";
const { readdir } = require("fs").promises;
import { utils } from "../../base/utils";
import * as mm from "music-metadata";
import { Md5 } from "ts-md5/dist/md5";
import e from "express";
import { EventEmitter } from 'events';
import { EventEmitter } from "events";
export class LocalFiles {
static localSongs: any = [];
static localSongsArts: any = [];
public static DB = ProviderDB.db;
static eventEmitter = new EventEmitter();
static localSongs: any = [];
static localSongsArts: any = [];
public static DB = ProviderDB.db;
static eventEmitter = new EventEmitter();
static getDataType(item_id : String | any){
if ((item_id ?? ('')).startsWith('ciderlocalart'))
return 'artwork'
else if ((item_id ?? ('')).startsWith('ciderlocal'))
return 'track'
static getDataType(item_id: String | any) {
if ((item_id ?? "").startsWith("ciderlocalart")) return "artwork";
else if ((item_id ?? "").startsWith("ciderlocal")) return "track";
}
static async sendOldLibrary() {
ProviderDB.init();
let rows = (await ProviderDB.db.allDocs({ include_docs: true, attachments: true })).rows.map((item: any) => {
return item.doc;
});
let tracks = rows.filter((item: any) => {
return this.getDataType(item._id) == "track";
});
let arts = rows.filter((item: any) => {
return this.getDataType(item._id) == "artwork";
});
this.localSongs = tracks;
this.localSongsArts = arts;
return tracks;
}
static async scanLibrary() {
ProviderDB.init();
let folders = utils.getStoreValue("libraryPrefs.localPaths");
if (folders == null || folders.length == null || folders.length == 0) folders = [];
let files: any[] = [];
for (var folder of folders) {
// get files from the Music folder
files = files.concat(await LocalFiles.getFiles(folder));
}
static async sendOldLibrary() {
ProviderDB.init()
let rows = (await ProviderDB.db.allDocs({include_docs: true,
attachments: true})).rows.map((item: any)=>{return item.doc})
let tracks = rows.filter((item: any) => {return this.getDataType(item._id) == "track"})
let arts = rows.filter((item: any) => {return this.getDataType(item._id) == "artwork"})
this.localSongs = tracks;
this.localSongsArts = arts;
return tracks;
}
static async scanLibrary() {
ProviderDB.init()
let folders = utils.getStoreValue("libraryPrefs.localPaths")
if (folders == null || folders.length == null || folders.length == 0) folders = []
let files: any[] = []
for (var folder of folders) {
// get files from the Music folder
files = files.concat(await LocalFiles.getFiles(folder))
let supporttedformats = ["mp3", "aac", "webm", "flac", "m4a", "ogg", "wav", "opus"];
let audiofiles = files.filter((f) => supporttedformats.includes(f.substring(f.lastIndexOf(".") + 1)));
let metadatalist = [];
let metadatalistart = [];
let numid = 0;
for (var audio of audiofiles) {
try {
const metadata = await mm.parseFile(audio);
let lochash = Md5.hashStr(audio) ?? numid;
if (metadata != null) {
let form = {
id: "ciderlocal" + lochash,
_id: "ciderlocal" + lochash,
type: "podcast-episodes",
href: audio,
attributes: {
artwork: {
width: 3000,
height: 3000,
url: "/ciderlocalart/" + "ciderlocal" + lochash,
},
topics: [],
url: "",
subscribable: true,
mediaKind: "audio",
genreNames: [""],
// "playParams": {
// "id": "ciderlocal" + numid,
// "kind": "podcast",
// "isLibrary": true,
// "reporting": false },
trackNumber: metadata.common.track?.no ?? 0,
discNumber: metadata.common.disk?.no ?? 0,
name: metadata.common.title ?? audio.substring(audio.lastIndexOf("\\") + 1),
albumName: metadata.common.album,
artistName: metadata.common.artist,
copyright: metadata.common.copyright ?? "",
assetUrl: "file:///" + audio,
contentAdvisory: "",
releaseDateTime: `${metadata?.common?.year ?? "2022"}-05-13T00:23:00Z`,
durationInMillis: Math.floor((metadata.format.duration ?? 0) * 1000),
bitrate: Math.floor((metadata.format?.bitrate ?? 0) / 1000),
offers: [
{
kind: "get",
type: "STDQ",
},
],
contentRating: "clean",
},
flavor: Math.floor((metadata.format?.bitrate ?? 0) / 1000),
localFilesMetadata: {
lossless: metadata.format?.lossless,
container: metadata.format?.container,
bitDepth: metadata.format?.bitsPerSample ?? 0,
sampleRate: metadata.format?.sampleRate ?? 0,
},
};
let art = {
id: "ciderlocal" + lochash,
_id: "ciderlocalart" + lochash,
url: metadata.common.picture != undefined ? metadata.common.picture[0].data.toString("base64") : "",
};
metadatalistart.push(art);
numid += 1;
ProviderDB.db.putIfNotExists(form);
ProviderDB.db.putIfNotExists(art);
metadatalist.push(form);
if (this.localSongs.length === 0 && numid % 10 === 0) {
// send updated chunks only if there is no previous database
this.eventEmitter.emit("newtracks", metadatalist);
}
}
let supporttedformats = ["mp3", "aac", "webm", "flac", "m4a", "ogg", "wav", "opus"]
let audiofiles = files.filter(f => supporttedformats.includes(f.substring(f.lastIndexOf('.') + 1)));
let metadatalist = []
let metadatalistart = []
let numid = 0;
for (var audio of audiofiles) {
try {
const metadata = await mm.parseFile(audio);
let lochash = Md5.hashStr(audio) ?? numid;
if (metadata != null) {
let form = {
"id": "ciderlocal" + lochash,
"_id": "ciderlocal" + lochash,
"type": "podcast-episodes",
"href": audio,
"attributes": {
"artwork": {
"width": 3000,
"height": 3000,
"url": "/ciderlocalart/" + "ciderlocal" + lochash,
},
"topics": [],
"url": "",
"subscribable": true,
"mediaKind": "audio",
"genreNames": [
""
],
// "playParams": {
// "id": "ciderlocal" + numid,
// "kind": "podcast",
// "isLibrary": true,
// "reporting": false },
"trackNumber": metadata.common.track?.no ?? 0,
"discNumber": metadata.common.disk?.no ?? 0,
"name": metadata.common.title ?? audio.substring(audio.lastIndexOf('\\') + 1),
"albumName": metadata.common.album,
"artistName": metadata.common.artist,
"copyright": metadata.common.copyright ?? "",
"assetUrl": "file:///" + audio,
"contentAdvisory": "",
"releaseDateTime": `${metadata?.common?.year ?? '2022'}-05-13T00:23:00Z`,
"durationInMillis": Math.floor((metadata.format.duration ?? 0) * 1000),
"bitrate": Math.floor((metadata.format?.bitrate ?? 0) / 1000),
"offers": [
{
"kind": "get",
"type": "STDQ"
}
],
"contentRating": "clean"
},
flavor: Math.floor((metadata.format?.bitrate ?? 0) / 1000),
localFilesMetadata: {
lossless: metadata.format?.lossless,
container: metadata.format?.container,
bitDepth: metadata.format?.bitsPerSample ?? 0,
sampleRate: metadata.format?.sampleRate ?? 0,
},
};
let art = {
id: "ciderlocal" + lochash,
_id: "ciderlocalart" + lochash,
url: metadata.common.picture != undefined ? metadata.common.picture[0].data.toString('base64') : "",
}
metadatalistart.push(art)
numid += 1;
ProviderDB.db.putIfNotExists(form)
ProviderDB.db.putIfNotExists(art)
metadatalist.push(form)
if (this.localSongs.length === 0 && numid % 10 === 0) { // send updated chunks only if there is no previous database
this.eventEmitter.emit('newtracks', metadatalist)}
}
} catch (e) {console.error("localfiles error:", e)}
}
// console.log('metadatalist', metadatalist);
this.localSongs = metadatalist;
this.localSongsArts = metadatalistart;
return metadatalist;
}
static async getFiles(dir: any) {
const dirents = await readdir(dir, { withFileTypes: true });
const files = await Promise.all(dirents.map((dirent: any) => {
const res = path.resolve(dir, dirent.name);
return dirent.isDirectory() ? this.getFiles(res) : res;
}));
return Array.prototype.concat(...files);
} catch (e) {
console.error("localfiles error:", e);
}
}
// console.log('metadatalist', metadatalist);
this.localSongs = metadatalist;
this.localSongsArts = metadatalistart;
return metadatalist;
}
static async getFiles(dir: any) {
const dirents = await readdir(dir, { withFileTypes: true });
const files = await Promise.all(
dirents.map((dirent: any) => {
const res = path.resolve(dir, dirent.name);
return dirent.isDirectory() ? this.getFiles(res) : res;
})
);
return Array.prototype.concat(...files);
}
static async cleanUpDB(){
let folders = utils.getStoreValue("libraryPrefs.localPaths")
let rows = (await ProviderDB.db.allDocs({include_docs: true,
attachments: true})).rows.map((item: any)=>{return item.doc})
let tracks = rows.filter((item: any) => {return this.getDataType(item._id) == "track" && !folders.some((i: String) => {return item["attributes"]["assetUrl"].startsWith("file:///" + i)})})
let hashs = tracks.map((i: any) => {return i._id})
for (let hash of hashs){
try{
ProviderDB.db.get(hash).then(function (doc: any) {
return ProviderDB.db.remove(doc);
});} catch(e){}
try{
ProviderDB.db.get(hash.replace('ciderlocal','ciderlocalart')).then(function (doc: any) {
return ProviderDB.db.remove(doc);
});} catch(e){}
}
}
static setupHandlers () {
const app = utils.getExpress()
console.log("Setting up handlers for local files")
app.get("/ciderlocal/:songs", (req: any, res: any) => {
const audio = atob(req.params.songs.replace(/_/g, '/').replace(/-/g, '+'));
//console.log('auss', audio)
let data = {
data:
LocalFiles.localSongs.filter((f: any) => audio.split(',').includes(f.id))
};
res.send(data);
static async cleanUpDB() {
let folders = utils.getStoreValue("libraryPrefs.localPaths");
let rows = (await ProviderDB.db.allDocs({ include_docs: true, attachments: true })).rows.map((item: any) => {
return item.doc;
});
let tracks = rows.filter((item: any) => {
return (
this.getDataType(item._id) == "track" &&
!folders.some((i: String) => {
return item["attributes"]["assetUrl"].startsWith("file:///" + i);
})
);
});
let hashs = tracks.map((i: any) => {
return i._id;
});
for (let hash of hashs) {
try {
ProviderDB.db.get(hash).then(function (doc: any) {
return ProviderDB.db.remove(doc);
});
app.get("/ciderlocalart/:songs", (req: any, res: any) => {
const audio = req.params.songs;
// metadata.common.picture[0].data.toString('base64')
res.setHeader('Cache-Control', 'public, max-age=31536000');
res.setHeader('Expires', new Date(Date.now() + 31536000000).toUTCString());
res.setHeader('Content-Type', 'image/jpeg');
let data =
LocalFiles.localSongsArts.filter((f: any) => f.id == audio);
res.status(200).send(Buffer.from(data[0]?.url, 'base64'));
} catch (e) {}
try {
ProviderDB.db.get(hash.replace("ciderlocal", "ciderlocalart")).then(function (doc: any) {
return ProviderDB.db.remove(doc);
});
return app
} catch (e) {}
}
}
}
static setupHandlers() {
const app = utils.getExpress();
console.log("Setting up handlers for local files");
app.get("/ciderlocal/:songs", (req: any, res: any) => {
const audio = atob(req.params.songs.replace(/_/g, "/").replace(/-/g, "+"));
//console.log('auss', audio)
let data = {
data: LocalFiles.localSongs.filter((f: any) => audio.split(",").includes(f.id)),
};
res.send(data);
});
app.get("/ciderlocalart/:songs", (req: any, res: any) => {
const audio = req.params.songs;
// metadata.common.picture[0].data.toString('base64')
res.setHeader("Cache-Control", "public, max-age=31536000");
res.setHeader("Expires", new Date(Date.now() + 31536000000).toUTCString());
res.setHeader("Content-Type", "image/jpeg");
let data = LocalFiles.localSongsArts.filter((f: any) => f.id == audio);
res.status(200).send(Buffer.from(data[0]?.url, "base64"));
});
return app;
}
}