Merge branch 'develop'

This commit is contained in:
child_duckling 2022-03-08 18:07:59 -08:00
commit df2f7b7216
195 changed files with 62285 additions and 12895 deletions

View file

@ -1,7 +1,15 @@
import {app, Menu, nativeImage, Tray} from 'electron';
import {app, Menu, nativeImage, Tray, ipcMain, clipboard, shell} from 'electron';
import {readFileSync} from "fs";
import * as path from 'path';
import {utils} from './utils'
import * as log from 'electron-log';
import {utils} from './utils';
/**
* @file Creates App instance
* @author CiderCollective
*/
/** @namespace */
export class AppEvents {
private protocols: string[] = [
"ame",
@ -15,6 +23,7 @@ export class AppEvents {
private tray: any = undefined;
private i18n: any = undefined;
/** @constructor */
constructor() {
this.start();
}
@ -24,6 +33,7 @@ export class AppEvents {
* @returns {void}
*/
private start(): void {
AppEvents.initLogging()
console.info('[AppEvents] App started');
/**********************************************************************************************************************
@ -87,6 +97,7 @@ export class AppEvents {
/***********************************************************************************************************************
* Protocols
**********************************************************************************************************************/
/** */
if (process.defaultApp) {
if (process.argv.length >= 2) {
this.protocols.forEach((protocol: string) => {
@ -120,6 +131,13 @@ export class AppEvents {
}
})
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()
this.InitTray()
}
@ -169,6 +187,18 @@ export class AppEvents {
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()
}
}
@ -197,6 +227,7 @@ export class AppEvents {
app.quit()
} else if (utils.getWindow()) {
if (utils.getWindow().isMinimized()) utils.getWindow().restore()
utils.getWindow().show()
utils.getWindow().focus()
}
})
@ -264,7 +295,7 @@ export class AppEvents {
const menu = Menu.buildFromTemplate([
{
label: (visible ? this.i18n['action.tray.minimize'] : this.i18n['action.tray.show'].includes("{appName}") ? `${this.i18n['action.tray.show'].replace("{appName}", app.getName())}` : `${this.i18n['action.tray.show']} ${app.getName()}`),
label: (visible ? this.i18n['action.tray.minimize'] : `${this.i18n['action.tray.show']} ${app.getName()}`),
click: () => {
if (utils.getWindow()) {
if (visible) {
@ -284,4 +315,18 @@ export class AppEvents {
])
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);
ipcMain.on('fetch-log', (_event) => {
const data = readFileSync(log.transports.file.getFile().path, {encoding: 'utf8', flag: 'r'});
clipboard.writeText(data)
})
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,24 @@
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);
var self = this;
function onclose() {
self.stop();
}
}
util.inherits(CiderCastController, RequestResponseController);
CiderCastController.prototype.sendIp = function(ip) {
// TODO: Implement Callback
let data = {
ip : ip
}
this.request(data);
};
module.exports = CiderCastController;

View file

@ -0,0 +1,77 @@
//@ts-nocheck
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');
function CiderReceiver(client, session) {
Application.apply(this, arguments);
this.media = this.createController(MediaController);
this.mediaReceiver = this.createController(CiderCastController);
this.media.on('status', onstatus);
var self = this;
function onstatus(status) {
self.emit('status', status);
}
}
// FE96A351
// 27E1334F
CiderReceiver.APP_ID = 'FE96A351';
util.inherits(CiderReceiver, Application);
CiderReceiver.prototype.getStatus = function(callback) {
this.media.getStatus.apply(this.media, arguments);
};
CiderReceiver.prototype.load = function(media, options, callback) {
this.media.load.apply(this.media, arguments);
};
CiderReceiver.prototype.play = function(callback) {
this.media.play.apply(this.media, arguments);
};
CiderReceiver.prototype.pause = function(callback) {
this.media.pause.apply(this.media, arguments);
};
CiderReceiver.prototype.stop = function(callback) {
this.media.stop.apply(this.media, arguments);
};
CiderReceiver.prototype.seek = function(currentTime, callback) {
this.media.seek.apply(this.media, arguments);
};
CiderReceiver.prototype.queueLoad = function(items, options, callback) {
this.media.queueLoad.apply(this.media, arguments);
};
CiderReceiver.prototype.queueInsert = function(items, options, callback) {
this.media.queueInsert.apply(this.media, arguments);
};
CiderReceiver.prototype.queueRemove = function(itemIds, options, callback) {
this.media.queueRemove.apply(this.media, arguments);
};
CiderReceiver.prototype.queueReorder = function(itemIds, options, callback) {
this.media.queueReorder.apply(this.media, arguments);
};
CiderReceiver.prototype.queueUpdate = function(items, callback) {
this.media.queueUpdate.apply(this.media, arguments);
};
CiderReceiver.prototype.sendIp = function(opts){
this.mediaReceiver.sendIp.apply(this.mediaReceiver, arguments);
};
module.exports = CiderReceiver;

View file

@ -3,15 +3,36 @@ import * as path from 'path';
import * as electron from 'electron'
import {utils} from './utils';
//
// Hello, this our loader for the various plugins that the Cider Development Team built for our
// numerous plugins internally and ones made by the community
//
// To learn how to make your own, visit https://github.com/ciderapp/Cider/wiki/Plugins
//
/**
* @class
* Plugin Loading
* @author booploops#7139
* @see {@link https://github.com/ciderapp/Cider/wiki/Plugins|Documentation}
*/
export class Plugins {
private basePluginsPath = path.join(__dirname, '../plugins');
private userPluginsPath = path.join(electron.app.getPath('userData'), 'plugins');
private userPluginsPath = path.join(electron.app.getPath('userData'), 'Plugins');
private readonly pluginsList: any = {};
private static PluginMap: any = {};
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 = {};
@ -32,13 +53,46 @@ export class Plugins {
if (fs.existsSync(this.userPluginsPath)) {
fs.readdirSync(this.userPluginsPath).forEach(file => {
// Plugins V1
if (file.endsWith('.ts') || file.endsWith('.js')) {
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`);
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(electron.app, utils.getStore());
}
} else {
plugins[file] = new plugin(electron.app, utils.getStore());
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(electron.app, utils.getStore());
}
}
}
// 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);
}
}
}
});

View file

@ -6,12 +6,24 @@ export class Store {
private defaults: any = {
"general": {
"close_button_hide": true,
"close_button_hide": false,
"open_on_startup": false,
"discord_rpc": 1, // 0 = disabled, 1 = enabled as Cider, 2 = enabled as Apple Music
"discord_rpc_clear_on_pause": true,
"language": "en_US", // electron.app.getLocale().replace('-', '_') this can be used in future
"playbackNotifications": true
"playbackNotifications": true,
"update_branch": "main",
"resumeOnStartupBehavior": "local",
"privateEnabled": false,
"themeUpdateNotification": true,
"sidebarItems": {
"recentlyAdded": true,
"songs": true,
"albums": true,
"artists": true,
"videos": true,
"podcasts": true
}
},
"home": {
"followedArtists": [],
@ -22,20 +34,37 @@ export class Store {
"sort": "name",
"sortOrder": "asc",
"size": "normal"
}
},
"albums": {
"sort": "name",
"sortOrder": "asc",
"viewAs": "covers"
},
},
"audio": {
"volume": 1,
"volumeStep": 0.1,
"maxVolume": 1,
"lastVolume": 1,
"muted": false,
"quality": "256",
"quality": "HIGH",
"seamless_audio": true,
"normalization": false,
"maikiwiAudio": {
"ciderPPE": false,
"ciderPPE_value": 0.5,
"analogWarmth": false,
"analogWarmth_value": 1.25,
"spatial": false,
"spatialType": 0,
"vibrantBass": { // Hard coded into the app. Don't include any of this config into exporting presets in store.ts
'multiplier': 0,
'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,
"maxVolume": 1,
"volumePrecision": 0.1,
"volumeRoundMax": 0.9,
"volumeRoundMin": 0.1,
"spatial_properties": {
"presets": [],
"gain": 0.8,
@ -59,19 +88,12 @@ export class Store {
'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],
'preamp' : 0,
'mix' : 1,
'vibrantBass' : 0,
'Q': [1,1,1,1,1,1,1,1,1,1],
'mix': 1,
'vibrantBass': 0,
'presets': [],
'userGenerated': false
},
"vibrantBass": { // Hard coded into the app. Don't include any of this config into exporting presets in store.ts
'multiplier': 0,
'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]
}
},
"visual": {
"theme": "",
@ -83,7 +105,11 @@ export class Store {
"bg_artwork_rotation": false,
"hw_acceleration": "default", // default, webgpu, disabled
"showuserinfo": true,
"miniplayer_top_toggle": true
"transparent": false,
"miniplayer_top_toggle": true,
"directives": {
"windowLayout": "default"
}
},
"lyrics": {
"enable_mxm": false,
@ -101,7 +127,8 @@ export class Store {
},
"advanced": {
"AudioContext": false,
"experiments": []
"experiments": [],
"playlistTrackMapping": true
}
}
private migrations: any = {}
@ -142,11 +169,11 @@ export class Store {
* IPC Handler
*/
private ipcHandler(): void {
electron.ipcMain.handle('getStoreValue', (event, key, defaultValue) => {
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) => {
electron.ipcMain.handle('setStoreValue', (_event, key, value) => {
Store.cfg.set(key, value);
});
@ -154,7 +181,7 @@ export class Store {
event.returnValue = Store.cfg.store
})
electron.ipcMain.on('setStore', (event, store) => {
electron.ipcMain.on('setStore', (_event, store) => {
Store.cfg.store = store
})
}

View file

@ -1,11 +1,38 @@
import * as fs from "fs";
import * as path from "path";
import {jsonc} from "jsonc";
import {Store} from "./store";
import {BrowserWindow as bw} from "./browserwindow";
import {app, dialog, ipcMain, Notification, shell } from "electron";
import fetch from "electron-fetch";
import {AppImageUpdater, NsisUpdater} from "electron-updater";
import * as log from "electron-log";
export class utils {
/**
* Paths for the application to use
*/
private 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"),
};
/**
* Get the path
* @returns {string}
* @param name
*/
static getPath(name: string): string {
return this.paths[name];
}
/**
* Fetches the i18n locale for the given language.
* @param language {string} The language to fetch the locale for.
@ -13,10 +40,10 @@ export class utils {
* @returns {string | Object} The locale value.
*/
static getLocale(language: string, key?: string): string | object {
let i18n: { [index: string]: Object } = jsonc.parse(fs.readFileSync(path.join(__dirname, "../../src/i18n/en_US.jsonc"), "utf8"));
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(__dirname, `../../src/i18n/${language}.jsonc`))) {
i18n = Object.assign(i18n, jsonc.parse(fs.readFileSync(path.join(__dirname, `../../src/i18n/${language}.jsonc`), "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")));
}
if (key) {
@ -58,4 +85,110 @@ export class utils {
static getWindow(): Electron.BrowserWindow {
return bw.win
}
}
static loadPluginFrontend(path: string): void {
}
static loadJSFrontend(path: string): void {
bw.win.webContents.executeJavaScript(fs.readFileSync(path, "utf8"));
}
/**
* 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()")
}
}
/**
* Checks the application for updates
*/
static async checkForUpdate(): Promise<void> {
if (!app.isPackaged) {
new Notification({ title: "Application Update", body: "Can't update as app is in DEV mode. Please build or grab a copy by clicking me"})
.on('click', () => {shell.openExternal('https://download.cider.sh/?utm_source=app&utm_medium=dev-mode-warning')})
.show()
bw.win.webContents.send('update-response', "update-error")
return;
}
// Get the artifacts
const response = await fetch(`https://circleci.com/api/v1.1/project/gh/ciderapp/Cider/latest/artifacts?branch=${utils.getStoreValue('general.update_branch')}&filter=successful`)
if (response.status != 200) {
bw.win.webContents.send('update-response', 'update-timeout')
return;
}
// Get the urls
const jsonResponse = await response.json()
let base_url = jsonResponse[0].url
base_url = base_url.substring(0, base_url.lastIndexOf('/'))
const options: any = {
provider: 'generic',
url: base_url,
allowDowngrade: true,
}
let autoUpdater: any = null
if (process.platform === 'win32') { //Windows
autoUpdater = await new NsisUpdater(options)
} else {
autoUpdater = await new AppImageUpdater(options) //Linux and Mac (AppImages work on macOS btw)
}
autoUpdater.on('checking-for-update', () => {
new Notification({ title: "Cider Update", body: "Cider is currently checking for updates."}).show()
})
autoUpdater.on('error', (error: any) => {
console.error(`[AutoUpdater] Error: ${error}`)
bw.win.webContents.send('update-response', "update-error")
})
autoUpdater.on('update-not-available', () => {
console.log('[AutoUpdater] Update not available.')
bw.win.webContents.send('update-response', "update-not-available");
})
autoUpdater.on('download-progress', (event: any, progress: any) => {
bw.win.setProgressBar(progress.percent / 100)
})
autoUpdater.on('update-downloaded', (info: any) => {
console.log('[AutoUpdater] Update downloaded.')
bw.win.webContents.send('update-response', "update-downloaded");
const dialogOpts = {
type: 'info',
buttons: ['Restart', 'Later'],
title: 'Application Update',
message: info,
detail: 'A new version has been downloaded. Restart the application to apply the updates.'
}
dialog.showMessageBox(dialogOpts).then((returnValue) => {
if (returnValue.response === 0) autoUpdater.quitAndInstall()
})
new Notification({ title: "Application Update", body: info}).on('click', () => {
bw.win.show()
}).show()
})
log.transports.file.level = "debug"
autoUpdater.logger = log
await autoUpdater.checkForUpdatesAndNotify()
}
}

View file

@ -64,6 +64,9 @@ export class wsapi {
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));
});
this.wss = new WebSocketServer({
port: this.port,
perMessageDeflate: {
@ -165,6 +168,10 @@ export class wsapi {
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";
@ -178,11 +185,15 @@ export class wsapi {
response.message = "Unmuted";
break;
case "next":
this._win.webContents.executeJavaScript(`MusicKit.getInstance().skipToNextItem()`);
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(`MusicKit.getInstance().skipToPreviousItem()`);
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":
@ -290,4 +301,11 @@ export class wsapi {
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));
});
}
}