diff --git a/src/main/base/app.ts b/src/main/base/app.ts index b79f9963..beb61a23 100644 --- a/src/main/base/app.ts +++ b/src/main/base/app.ts @@ -50,7 +50,7 @@ export class AppEvents { /*********************************************************************************************************************** * Commandline arguments **********************************************************************************************************************/ - switch (store.get("visual.hw_acceleration")) { + switch (store.visual.hw_acceleration) { default: case "default": electron.app.commandLine.appendSwitch('enable-accelerated-mjpeg-decode') @@ -75,6 +75,10 @@ export class AppEvents { break; } + if (process.platform === "linux") { + electron.app.commandLine.appendSwitch('disable-features', 'MediaSessionService'); + } + /*********************************************************************************************************************** * Protocols **********************************************************************************************************************/ diff --git a/src/main/base/plugins.ts b/src/main/base/plugins.ts index 637f6bd2..92663ab9 100644 --- a/src/main/base/plugins.ts +++ b/src/main/base/plugins.ts @@ -5,10 +5,11 @@ import * as electron from 'electron' export default class PluginHandler { private basePluginsPath = path.join(__dirname, '../plugins'); private userPluginsPath = path.join(electron.app.getPath('userData'), 'plugins'); - private pluginsList: any = {}; - - constructor() { + private readonly pluginsList: any = {}; + private readonly _store: any; + constructor(config: any) { + this._store = config; this.pluginsList = this.getPlugins(); } @@ -23,7 +24,7 @@ export default class PluginHandler { if (plugins[file] || plugin.name in plugins) { console.log(`[${plugin.name}] Plugin already loaded / Duplicate Class Name`); } else { - plugins[file] = new plugin(electron.app); + plugins[file] = new plugin(electron.app, this._store); } } }); @@ -38,7 +39,7 @@ export default class PluginHandler { if (plugins[file] || plugin in plugins) { console.log(`[${plugin.name}] Plugin already loaded / Duplicate Class Name`); } else { - plugins[file] = new plugin(electron.app); + plugins[file] = new plugin(electron.app, this._store); } } }); diff --git a/src/main/base/store.ts b/src/main/base/store.ts index cd7fe219..143c1384 100644 --- a/src/main/base/store.ts +++ b/src/main/base/store.ts @@ -2,7 +2,7 @@ import * as Store from 'electron-store'; import * as electron from "electron"; export class ConfigStore { - public store: Store | undefined; + private _store: Store; private defaults: any = { "general": { @@ -96,14 +96,26 @@ export class ConfigStore { private migrations: any = {} constructor() { - this.store = new Store({ + this._store = new Store({ name: 'cider-config', defaults: this.defaults, migrations: this.migrations, }); - this.store.set(this.mergeStore(this.defaults, this.store.store)) - this.ipcHandler(this.store); + this._store.set(this.mergeStore(this.defaults, this._store.store)) + this.ipcHandler(this._store); + } + + get store() { + return this._store.store; + } + + get(key: string) { + return this._store.get(key); + } + + set(key: string, value: any) { + this._store.set(key, value); } /** diff --git a/src/main/base/win.ts b/src/main/base/win.ts index 5ce72d6d..da304567 100644 --- a/src/main/base/win.ts +++ b/src/main/base/win.ts @@ -15,10 +15,10 @@ import {wsapi} from "./wsapi"; import * as jsonc from "jsonc"; export class Win { - win: any | undefined = null; - app: any | undefined = null; - store: any | undefined = null; - devMode: boolean = !electron.app.isPackaged; + private win: any | undefined = null; + private app: any | undefined = null; + private store: any | undefined = null; + private devMode: boolean = !electron.app.isPackaged; constructor(app: electron.App, store: any) { this.app = app; diff --git a/src/main/index.ts b/src/main/index.ts index 72391f5b..6862aa18 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -13,7 +13,7 @@ import PluginHandler from "./base/plugins"; const config = new ConfigStore(); const App = new AppEvents(config.store); const Cider = new Win(electron.app, config.store) -const plug = new PluginHandler(); +const plug = new PluginHandler(config.store); let win: Electron.BrowserWindow; @@ -34,7 +34,7 @@ electron.app.on('ready', () => { win = await Cider.createWindow() App.bwCreated(win); /// please dont change this for plugins to get proper and fully initialized Win objects - plug.callPlugins('onReady', Cider); + plug.callPlugins('onReady', win); win.on("ready-to-show", () => { win.show(); }); diff --git a/src/main/plugins/Extras/examplePlugin.ts b/src/main/plugins/Extras/examplePlugin.ts index 78a194a9..e96045fb 100644 --- a/src/main/plugins/Extras/examplePlugin.ts +++ b/src/main/plugins/Extras/examplePlugin.ts @@ -1,10 +1,11 @@ let i = 1, k = 1; export default class ExamplePlugin { - /** - * Private variables for interaction in plugins - */ - private _win: any; - private _app: any; + /** + * Private variables for interaction in plugins + */ + private _win: any; + private _app: any; + private _store: any; /** * Base Plugin Details (Eventually implemented into a GUI in settings) @@ -17,16 +18,17 @@ export default class ExamplePlugin { /** * Runs on plugin load (Currently run on application start) */ - constructor(app: any) { - this._app = app; - console.log('Example plugin loaded'); - } + constructor(app: any, store: any) { + this._app = app; + this._store = store; + console.log('Example plugin loaded'); + } /** * Runs on app ready */ onReady(win: any): void { - this._win = win; + this._win = win; console.log('Example plugin ready'); } @@ -42,7 +44,7 @@ export default class ExamplePlugin { * @param attributes Music Attributes (attributes.state = current state) */ onPlaybackStateDidChange(attributes: object): void { - console.log('onPlaybackStateDidChange has been called ' + i +' times'); + console.log('onPlaybackStateDidChange has been called ' + i + ' times'); i++ } @@ -51,7 +53,7 @@ export default class ExamplePlugin { * @param attributes Music Attributes */ onNowPlayingItemDidChange(attributes: object): void { - console.log('onNowPlayingDidChange has been called ' + k +' times'); + console.log('onNowPlayingDidChange has been called ' + k + ' times'); k++ } diff --git a/src/main/plugins/discordrpc.ts b/src/main/plugins/discordrpc.ts index 77ce5482..43d4b849 100644 --- a/src/main/plugins/discordrpc.ts +++ b/src/main/plugins/discordrpc.ts @@ -1,28 +1,30 @@ -import * as electron from 'electron'; import * as DiscordRPC from 'discord-rpc' + export default class DiscordRPCPlugin { - /** - * Private variables for interaction in plugins - */ - private _win: any; - private _app: any; + /** + * Private variables for interaction in plugins + */ + private _win: Electron.BrowserWindow | undefined; + private _app: any; + private _store: any; private _discord: any; + private connect(clientId: any) { - this._discord = { isConnected: false }; - if (this._win.store.store.general.discord_rpc == 0 || this._discord.isConnected) return; + this._discord = {isConnected: false}; + if (this._store.general.discord_rpc == 0 || this._discord.isConnected) return; DiscordRPC.register(clientId) // Apparently needed for ask to join, join, spectate etc. - const client = new DiscordRPC.Client({ transport: "ipc" }); - this._discord = Object.assign(client, { error: false, activityCache: null, isConnected: false }); + const client = new DiscordRPC.Client({transport: "ipc"}); + this._discord = Object.assign(client, {error: false, activityCache: null, isConnected: false}); // Login to Discord - this._discord.login({ clientId }) + this._discord.login({clientId}) .then(() => { this._discord.isConnected = true; }) - .catch((e : any) => console.error(`[DiscordRPC][connect] ${e}`)); + .catch((e: any) => console.error(`[DiscordRPC][connect] ${e}`)); - this._discord.on('ready', () => { + this._discord.on('ready', () => { console.log(`[DiscordRPC][connect] Successfully Connected to Discord. Authed for user: ${client.user.username} (${client.user.id})`); }) @@ -34,19 +36,19 @@ export default class DiscordRPCPlugin { }); } - /** + /** * Disconnects from Discord RPC */ private disconnect() { - if (this._win.store.store.general.discord_rpc == 0 || !this._discord.isConnected) return; - + if (this._store.general.discord_rpc == 0 || !this._discord.isConnected) return; + try { this._discord.destroy().then(() => { this._discord.isConnected = false; console.log('[DiscordRPC][disconnect] Disconnected from discord.') - }).catch((e : any) => console.error(`[DiscordRPC][disconnect] ${e}`)); + }).catch((e: any) => console.error(`[DiscordRPC][disconnect] ${e}`)); } catch (err) { - console.error(err) + console.error(err) } } @@ -54,54 +56,57 @@ export default class DiscordRPCPlugin { * Sets the activity of the client * @param {object} attributes */ - private updateActivity(attributes : any) { - if (this._win.store.store.general.discord_rpc == 0) return; + private updateActivity(attributes: any) { + if (this._store.general.discord_rpc == 0) return; if (!this._discord.isConnected) { - this._discord.clearActivity().catch((e : any) => console.error(`[DiscordRPC][updateActivity] ${e}`)); + this._discord.clearActivity().catch((e: any) => console.error(`[DiscordRPC][updateActivity] ${e}`)); return; } // console.log('[DiscordRPC][updateActivity] Updating Discord Activity.') const listenURL = `https://cider.sh/p?s&id=${attributes.playParams.id}` // cider://play/s/[id] (for song) - //console.log(attributes) + //console.log(attributes) - interface ActObject { + interface ActObject extends DiscordRPC.Presence { details?: any, state?: any, startTimestamp?: any, endTimestamp?: any, - largeImageKey? : any, + largeImageKey?: any, largeImageText?: any, smallImageKey?: any, smallImageText?: any, instance: true, buttons?: [ - { label: "Listen on Cider", url?: any }, + { + label: string, + url: string + } ] - } + } - let ActivityObject : ActObject | null = { + let ActivityObject: ActObject | null = { details: attributes.name, state: `by ${attributes.artistName}`, startTimestamp: attributes.startTime, endTimestamp: attributes.endTime, - largeImageKey : (attributes.artwork.url.replace('{w}', '1024').replace('{h}', '1024')) ?? 'cider', + largeImageKey: (attributes.artwork.url.replace('{w}', '1024').replace('{h}', '1024')) ?? 'cider', largeImageText: attributes.albumName, smallImageKey: (attributes.status ? 'play' : 'pause'), smallImageText: (attributes.status ? 'Playing' : 'Paused'), instance: true, buttons: [ - { label: "Listen on Cider", url: listenURL }, + {label: "Listen on Cider", url: listenURL}, ] }; if (ActivityObject.largeImageKey == "" || ActivityObject.largeImageKey == null) { - ActivityObject.largeImageKey = (this._win.store.store.general.discord_rpc == 1) ? "cider" : "logo" + ActivityObject.largeImageKey = (this._store.general.discord_rpc == 1) ? "cider" : "logo" } // Remove the pause/play icon and test for clear activity on pause - if (this._win.store.store.general.discordClearActivityOnPause == 1) { + if (this._store.general.discordClearActivityOnPause == 1) { delete ActivityObject.smallImageKey delete ActivityObject.smallImageText } @@ -128,13 +133,11 @@ export default class DiscordRPCPlugin { } - - // Check if its pausing (false) or playing (true) if (!attributes.status) { - if (this._win.store.store.general.discordClearActivityOnPause == 1) { - this._discord.clearActivity().catch((e : any) => console.error(`[DiscordRPC][clearActivity] ${e}`)); - ActivityObject = null + if (this._store.general.discordClearActivityOnPause == 1) { + this._discord.clearActivity().catch((e: any) => console.error(`[DiscordRPC][clearActivity] ${e}`)); + ActivityObject = null } else { delete ActivityObject.startTimestamp delete ActivityObject.endTimestamp @@ -168,16 +171,17 @@ export default class DiscordRPCPlugin { /** * Runs on plugin load (Currently run on application start) */ - constructor(app: any) { - this._app = app; - } + constructor(app: any, store: any) { + this._app = app; + this._store = store + } /** * Runs on app ready */ onReady(win: any): void { - this._win = win; - this.connect((this._win.store.store.general.discord_rpc == 1) ? '911790844204437504' : '886578863147192350'); + this._win = win; + this.connect((this._store.general.discord_rpc == 1) ? '911790844204437504' : '886578863147192350'); // electron.ipcMain.on("forceUpdateRPC", (event, attributes : object) => { // this.updateActivity(attributes) // }); diff --git a/src/main/plugins/lastfm.ts b/src/main/plugins/lastfm.ts index 78fdfa90..d586b857 100644 --- a/src/main/plugins/lastfm.ts +++ b/src/main/plugins/lastfm.ts @@ -1,7 +1,6 @@ import * as electron from 'electron'; import * as fs from 'fs'; import {resolve} from 'path'; -//@ts-ignore export default class LastFMPlugin { private sessionPath = resolve(electron.app.getPath('userData'), 'session.json'); @@ -15,6 +14,7 @@ export default class LastFMPlugin { private _win: any; private _app: any; private _lastfm: any; + private _store: any; private authenticateFromFile() { let sessionData = require(this.sessionPath) @@ -26,12 +26,12 @@ export default class LastFMPlugin { authenticate() { try { - if (this._win.store.store.lastfm.auth_token) { - this._win.store.store.lastfm.enabled = true; + if (this._store.lastfm.auth_token) { + this._store.lastfm.enabled = true; } - if (!this._win.store.store.lastfm.enabled || !this._win.store.store.lastfm.auth_token) { - this._win.store.store.lastfm.enabled = false; + if (!this._store.lastfm.enabled || !this._store.lastfm.auth_token) { + this._store.lastfm.enabled = false; return } /// dont move this require to top , app wont load @@ -47,8 +47,8 @@ export default class LastFMPlugin { if (err) { console.error("[LastFM][Session] Session file couldn't be opened or doesn't exist,", err) console.log("[LastFM][Auth] Beginning authentication from configuration") - console.log("[LastFM][tk]", this._win.store.store.lastfm.auth_token) - this._lastfm.authenticate(this._win.store.store.lastfm.auth_token, (err: any, session: any) => { + console.log("[LastFM][tk]", this._store.lastfm.auth_token) + this._lastfm.authenticate(this._store.lastfm.auth_token, (err: any, session: any) => { if (err) { throw err; } @@ -78,7 +78,7 @@ export default class LastFMPlugin { } private async scrobbleSong(attributes: any) { - await new Promise(resolve => setTimeout(resolve, Math.round(attributes.durationInMillis * (this._win.store.store.lastfm.scrobble_after / 100)))); + await new Promise(resolve => setTimeout(resolve, Math.round(attributes.durationInMillis * (this._store.lastfm.scrobble_after / 100)))); const currentAttributes = attributes; if (!this._lastfm || this._lastfm.cachedAttributes === attributes) { @@ -117,7 +117,7 @@ export default class LastFMPlugin { } private filterArtistName(artist: any) { - if (!this._win.store.store.lastfm.enabledRemoveFeaturingArtists) return artist; + if (!this._store.lastfm.enabledRemoveFeaturingArtists) return artist; artist = artist.split(' '); if (artist.includes('&')) { @@ -135,7 +135,7 @@ export default class LastFMPlugin { } private updateNowPlayingSong(attributes: any) { - if (!this._lastfm || this._lastfm.cachedNowPlayingAttributes === attributes || !this._win.store.store.lastfm.NowPlaying) { + if (!this._lastfm || this._lastfm.cachedNowPlayingAttributes === attributes || !this._store.lastfm.NowPlaying) { return } @@ -177,8 +177,9 @@ export default class LastFMPlugin { /** * Runs on plugin load (Currently run on application start) */ - constructor(app: any) { + constructor(app: any, store: any) { this._app = app; + this._store = store electron.app.on('second-instance', (_e: any, argv: any) => { // Checks if first instance is authorized and if second instance has protocol args argv.forEach((value: any) => { @@ -187,8 +188,8 @@ export default class LastFMPlugin { let authURI = String(argv).split('/auth/')[1]; if (authURI.startsWith('lastfm')) { // If we wanted more auth options const authKey = authURI.split('lastfm?token=')[1]; - this._win.store.store.lastfm.enabled = true; - this._win.store.store.lastfm.auth_token = authKey; + this._store.lastfm.enabled = true; + this._store.lastfm.auth_token = authKey; console.log(authKey); this._win.win.webContents.send('LastfmAuthenticated', authKey); this.authenticate(); @@ -203,8 +204,8 @@ export default class LastFMPlugin { let authURI = String(arg).split('/auth/')[1]; if (authURI.startsWith('lastfm')) { // If we wanted more auth options const authKey = authURI.split('lastfm?token=')[1]; - this._win.store.store.lastfm.enabled = true; - this._win.store.store.lastfm.auth_token = authKey; + this._store.lastfm.enabled = true; + this._store.lastfm.auth_token = authKey; this._win.win.webContents.send('LastfmAuthenticated', authKey); console.log(authKey); this.authenticate(); diff --git a/src/main/plugins/mpris.ts b/src/main/plugins/mpris.ts new file mode 100644 index 00000000..03cdb544 --- /dev/null +++ b/src/main/plugins/mpris.ts @@ -0,0 +1,196 @@ +// @ts-ignore +import * as Player from 'mpris-service'; + +export default class MPRIS { + /** + * 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 = 'MPRIS Service'; + public description: string = 'Handles MPRIS service calls for Linux systems.'; + public version: string = '1.0.0'; + public author: string = 'Core'; + + /** + * MPRIS Service + */ + private mpris: any; + private mprisEvents: Object = { + "playpause": "pausePlay", + "play": "pausePlay", + "pause": "pausePlay", + "next": "nextTrack", + "previous": "previousTrack", + } + + /******************************************************************************************* + * Private Methods + * ****************************************************************************************/ + + /** + * Runs a media event + * @param type - pausePlay, nextTrack, PreviousTrack + * @private + */ + private runMediaEvent(type: string) { + if (this._win) { + this._win.webContents.executeJavaScript(`MusicKitInterop.${type}()`).catch(console.error) + } + } + + /** + * Blocks non-linux systems from running this plugin + * @private + */ + private static linuxOnly(_target: any, _propertyKey: string, descriptor: PropertyDescriptor) { + if (process.platform !== 'linux') { + descriptor.value = function () { + return + } + } + + } + + + /** + * Connects to MPRIS Service + */ + @MPRIS.linuxOnly + private connect() { + this.mpris = Player({ + name: 'Cider', + identity: 'Cider', + supportedUriSchemes: [], + supportedMimeTypes: [], + supportedInterfaces: ['player'] + }); + this.mpris = Object.assign(this.mpris, { + canQuit: true, + canControl: true, + canPause: true, + canPlay: true, + canGoNext: true, + active: true + }) + + + const pos_atr = {durationInMillis: 0}; + this.mpris.getPosition = function () { + const durationInMicro = pos_atr.durationInMillis * 1000; + const percentage = parseFloat("0") || 0; + return durationInMicro * percentage; + } + + for (const [key, value] of Object.entries(this.mprisEvents)) { + this.mpris.on(key, () => { + this.runMediaEvent(value) + }); + } + } + + /** + * Update MPRIS Player Attributes + */ + @MPRIS.linuxOnly + private updatePlayer(attributes: any) { + + const MetaData = { + 'mpris:trackid': this.mpris.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 + } + + if (this.mpris.metadata["mpris:trackid"] === MetaData["mpris:trackid"]) { + return + } + + this.mpris.metadata = MetaData + + } + + /** + * Update MPRIS Player State + * @private + * @param attributes + */ + @MPRIS.linuxOnly + private updatePlayerState(attributes: any) { + + let status = 'Stopped'; + if (attributes.status) { + status = 'Playing'; + } else if (attributes.status === false) { + status = 'Paused'; + } + + if (this.mpris.playbackStatus === status) { + return + } + this.mpris.playbackStatus = status; + } + + /** + * Clear state + * @private + */ + private clearState() { + this.mpris.metadata = {'mpris:trackid': '/org/mpris/MediaPlayer2/TrackList/NoTrack'} + this.mpris.playbackStatus = 'Stopped'; + } + + + /******************************************************************************************* + * Public Methods + * ****************************************************************************************/ + + /** + * Runs on plugin load (Currently run on application start) + */ + constructor(app: any, _store: any) { + this._app = app; + console.log(`[${this.name}] plugin loaded`); + } + + /** + * Runs on app ready + */ + onReady(win: any): void { + this._win = win; + console.log(`[${this.name}] plugin ready`); + this.connect() + } + + /** + * Runs on app stop + */ + onBeforeQuit(): void { + console.log(`[${this.name}] plugin stopped`); + this.clearState() + } + + /** + * Runs on playback State Change + * @param attributes Music Attributes (attributes.state = current state) + */ + onPlaybackStateDidChange(attributes: object): void { + this.updatePlayerState(attributes) + } + + /** + * Runs on song change + * @param attributes Music Attributes + */ + onNowPlayingItemDidChange(attributes: object): void { + this.updatePlayer(attributes); + } + +} diff --git a/tsconfig.json b/tsconfig.json index a3ea67e3..80e5dcda 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "experimentalDecorators": true, "target": "esnext", "module": "commonjs", "noImplicitAny": true,