diff --git a/package.json b/package.json index 50fa2d19..68a09948 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "youtube-search-without-api-key": "^1.0.7" }, "devDependencies": { + "@types/discord-rpc": "^4.0.0", "@types/express": "^4.17.13", "electron": "https://github.com/castlabs/electron-releases.git", "electron-builder": "^22.14.5", diff --git a/src/main/index.ts b/src/main/index.ts index 16f5a160..30ed265b 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -29,10 +29,10 @@ electron.app.on('ready', () => { require('vue-devtools').install() } - electron.components.whenReady().then(() => { - Cider.createWindow().then((win) => { - plug.callPlugins('onReady', win); - }) + electron.components.whenReady().then(async () => { + await Cider.createWindow() + plug.callPlugins('onReady', Cider); + }) diff --git a/src/main/plugins/Extras/sendSongToTitlebar.ts b/src/main/plugins/Extras/sendSongToTitlebar.ts index 9f384268..540e32f4 100644 --- a/src/main/plugins/Extras/sendSongToTitlebar.ts +++ b/src/main/plugins/Extras/sendSongToTitlebar.ts @@ -1,5 +1,3 @@ -import * as electron from "electron"; - export default class sendSongToTitlebar { /** * Base Plugin Details (Eventually implemented into a GUI in settings) @@ -29,7 +27,7 @@ export default class sendSongToTitlebar { * @param attributes Music Attributes (attributes.state = current state) */ onPlaybackStateDidChange(attributes: any): void { - this._win.setTitle(`${(attributes != null && attributes.name != null && attributes.name.length > 0) ? (attributes.name + " - ") : ''}Cider`) + this._win.win.setTitle(`${(attributes != null && attributes.name != null && attributes.name.length > 0) ? (attributes.name + " - ") : ''}Cider`) } /** * Runs on song change diff --git a/src/main/plugins/discordrpc.ts b/src/main/plugins/discordrpc.ts new file mode 100644 index 00000000..599560df --- /dev/null +++ b/src/main/plugins/discordrpc.ts @@ -0,0 +1,205 @@ +import * as DiscordRPC from 'discord-rpc' +export default class DiscordRPCPlugin { + /** + * Private variables for interaction in plugins + */ + private _win: any; + private _app: 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; + + 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 }); + + // Login to Discord + this._discord.login({ clientId }) + .then(() => { + this._discord.isConnected = true; + }) + .catch((e : any) => console.error(`[DiscordRPC][connect] ${e}`)); + + this._discord.on('ready', () => { + console.log(`[DiscordRPC][connect] Successfully Connected to Discord. Authed for user: ${client.user.username} (${client.user.id})`); + }) + + // Handles Errors + this._discord.on('error', (err: any) => { + console.error(`[DiscordRPC] ${err}`); + this.disconnect() + this._discord.isConnected = false; + }); + } + + /** + * Disconnects from Discord RPC + */ + private disconnect() { + if (this._win.store.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 (err) { + console.error(err) + } + } + + /** + * Sets the activity of the client + * @param {object} attributes + */ + private updateActivity(attributes : any) { + if (this._win.store.store.general.discord_rpc == 0) return; + + if (!this._discord.isConnected) { + 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) + + interface ActObject { + details?: any, + state?: any, + startTimestamp?: any, + endTimestamp?: any, + largeImageKey? : any, + largeImageText?: any, + smallImageKey?: any, + smallImageText?: any, + instance: true, + buttons?: [ + { label: "Listen on Cider", url?: any }, + ] + } + + 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', + largeImageText: attributes.albumName, + smallImageKey: (attributes.status ? 'play' : 'pause'), + smallImageText: (attributes.status ? 'Playing' : 'Paused'), + instance: true, + buttons: [ + { label: "Listen on Cider", url: listenURL }, + ] + }; + if (ActivityObject.largeImageKey == "" || ActivityObject.largeImageKey == null) { + ActivityObject.largeImageKey = (this._win.store.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) { + delete ActivityObject.smallImageKey + delete ActivityObject.smallImageText + } + + // Deletes the timestamp if its not greater than 0 + if (!((new Date(attributes.endTime)).getTime() > 0)) { + delete ActivityObject.startTimestamp + delete ActivityObject.endTimestamp + } + + // Artist check + if (!attributes.artistName) { + delete ActivityObject.state + } + + // Album text check + if (!ActivityObject.largeImageText || ActivityObject.largeImageText.length < 2) { + delete ActivityObject.largeImageText + } + + // Checks if the name is greater than 128 because some songs can be that long + if (ActivityObject.details.length > 128) { + ActivityObject.details = ActivityObject.details.substring(0, 125) + '...' + } + + + + + // 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 + } else { + delete ActivityObject.startTimestamp + delete ActivityObject.endTimestamp + ActivityObject.smallImageKey = 'pause' + ActivityObject.smallImageText = 'Paused' + } + } + + + if (ActivityObject && ActivityObject !== this._discord.activityCache && ActivityObject.details && ActivityObject.state) { + try { + // console.log(`[DiscordRPC][setActivity] Setting activity to ${JSON.stringify(ActivityObject)}`); + this._discord.setActivity(ActivityObject) + this._discord.activityCache = ActivityObject + } catch (err) { + console.error(`[DiscordRPC][setActivity] ${err}`) + } + + } + } + + + /** + * Base Plugin Details (Eventually implemented into a GUI in settings) + */ + public name: string = 'DiscordRPCPlugin'; + public description: string = 'Discord RPC plugin for Cider'; + public version: string = '0.0.1'; + public author: string = 'vapormusic / Cider Collective'; + + /** + * Runs on plugin load (Currently run on application start) + */ + constructor(app: any) { + this._app = app; + } + + /** + * Runs on app ready + */ + onReady(win: any): void { + this._win = win; + this.connect((this._win.store.store.general.discord_rpc == 1) ? '911790844204437504' : '886578863147192350'); + } + + /** + * Runs on app stop + */ + onBeforeQuit(): void { + console.log('Example plugin stopped'); + } + + /** + * Runs on playback State Change + * @param attributes Music Attributes (attributes.state = current state) + */ + onPlaybackStateDidChange(attributes: object): void { + this.updateActivity(attributes) + } + + /** + * Runs on song change + * @param attributes Music Attributes + */ + onNowPlayingItemDidChange(attributes: object): void { + this.updateActivity(attributes) + } + +}