From 3a1ed79efd7608cf680cfc29bc1feb32fa9cafe9 Mon Sep 17 00:00:00 2001 From: Inrixia Date: Tue, 25 Jun 2024 06:46:37 +1200 Subject: [PATCH] Add DiscordRPC --- plugins/DiscordRPC/package-lock.json | 154 +++++++++++++++++++++++++++ plugins/DiscordRPC/package.json | 9 ++ plugins/DiscordRPC/plugin.json | 6 ++ plugins/DiscordRPC/src/DiscordRPC.ts | 19 ++++ plugins/DiscordRPC/src/index.ts | 106 ++++++++++++++++++ 5 files changed, 294 insertions(+) create mode 100644 plugins/DiscordRPC/package-lock.json create mode 100644 plugins/DiscordRPC/package.json create mode 100644 plugins/DiscordRPC/plugin.json create mode 100644 plugins/DiscordRPC/src/DiscordRPC.ts create mode 100644 plugins/DiscordRPC/src/index.ts diff --git a/plugins/DiscordRPC/package-lock.json b/plugins/DiscordRPC/package-lock.json new file mode 100644 index 0000000..eb22152 --- /dev/null +++ b/plugins/DiscordRPC/package-lock.json @@ -0,0 +1,154 @@ +{ + "name": "DiscordRPC", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@inrixia/lib": "../_lib", + "discord-rpc": "^4.0.1" + }, + "devDependencies": { + "@types/discord-rpc": "^4.0.8" + } + }, + "../_lib": { + "dependencies": { + "idb": "^8.0.0", + "music-metadata": "^7.14.0" + } + }, + "node_modules/@inrixia/lib": { + "resolved": "../_lib", + "link": true + }, + "node_modules/@types/discord-rpc": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/discord-rpc/-/discord-rpc-4.0.8.tgz", + "integrity": "sha512-1tZf217Natkj+TziNXRRLwNmdm5GNa1bnrQr8VWowquo/Su5hMjdhobj8URxW1COMk2da28XCU1ahsYCAlxirA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/events": "*" + } + }, + "node_modules/@types/events": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz", + "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==", + "dev": true, + "license": "MIT" + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/discord-rpc": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/discord-rpc/-/discord-rpc-4.0.1.tgz", + "integrity": "sha512-HOvHpbq5STRZJjQIBzwoKnQ0jHplbEWFWlPDwXXKm/bILh4nzjcg7mNqll0UY7RsjFoaXA7e/oYb/4lvpda2zA==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "ws": "^7.3.1" + }, + "optionalDependencies": { + "register-scheme": "github:devsnek/node-register-scheme" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT", + "optional": true + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "license": "MIT", + "optional": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/register-scheme": { + "version": "0.0.2", + "resolved": "git+ssh://git@github.com/devsnek/node-register-scheme.git#e7cc9a63a1f512565da44cb57316d9fb10750e17", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bindings": "^1.3.0", + "node-addon-api": "^1.3.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/plugins/DiscordRPC/package.json b/plugins/DiscordRPC/package.json new file mode 100644 index 0000000..91e4ea3 --- /dev/null +++ b/plugins/DiscordRPC/package.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "@inrixia/lib": "../_lib", + "discord-rpc": "^4.0.1" + }, + "devDependencies": { + "@types/discord-rpc": "^4.0.8" + } +} diff --git a/plugins/DiscordRPC/plugin.json b/plugins/DiscordRPC/plugin.json new file mode 100644 index 0000000..7a362ec --- /dev/null +++ b/plugins/DiscordRPC/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "Discord RPC", + "description": "Shows what song you're currently listening to on Discord.", + "author": "toonlink", + "main": "./src/index.js" +} \ No newline at end of file diff --git a/plugins/DiscordRPC/src/DiscordRPC.ts b/plugins/DiscordRPC/src/DiscordRPC.ts new file mode 100644 index 0000000..afae009 --- /dev/null +++ b/plugins/DiscordRPC/src/DiscordRPC.ts @@ -0,0 +1,19 @@ +import { Client } from "discord-rpc"; +const onCleanupErr = (err: Error) => console.warn("Encountered error while cleaning up DiscordRPC", err); +export class DiscordRPC { + public rpcClient?: Client; + constructor(private readonly clientId: string) {} + + public isConnected() { + // @ts-expect-error Types dont include internals like transport + return !!this.rpcClient?.transport?.socket; + } + async ensureRPC() { + if (this.isConnected()) return this.rpcClient!; + return (this.rpcClient = await new Client({ transport: "ipc" }).login({ clientId: this.clientId })); + } + async cleanp(clearActivity?: false) { + if (this.isConnected() && clearActivity) await this.rpcClient!.clearActivity().catch(onCleanupErr); + await this.rpcClient?.destroy().catch(onCleanupErr); + } +} diff --git a/plugins/DiscordRPC/src/index.ts b/plugins/DiscordRPC/src/index.ts new file mode 100644 index 0000000..85e4f13 --- /dev/null +++ b/plugins/DiscordRPC/src/index.ts @@ -0,0 +1,106 @@ +import { store, intercept } from "@neptune"; +import { getMediaURLFromID } from "@neptune/utils"; +import { Presence } from "discord-rpc"; +import { html } from "@neptune/voby"; + +import { DiscordRPC } from "./DiscordRPC"; + +// @ts-expect-error Types dont include @plugin +import { storage } from "@plugin"; + +import { MediaItem } from "neptune-types/tidal"; +import { SwitchSetting } from "@inrixia/lib/components/SwitchSetting"; +import getPlaybackControl from "@inrixia/lib/getPlaybackControl"; +import { TrackItemCache } from "@inrixia/lib/Caches/TrackItemCache"; + +const rpcClient = new DiscordRPC("1130698654987067493"); + +const STR_MAX_LEN = 127; +const formatLongString = (s?: string) => { + if (s === undefined) return ""; + return s.length >= STR_MAX_LEN ? s.slice(0, STR_MAX_LEN - 3) + "..." : s; +}; + +enum AudioQuality { + HiRes = "HI_RES_LOSSLESS", + MQA = "HI_RES", + High = "LOSSLESS", + Low = "HIGH", + Lowest = "LOW", +} +interface PlaybackContext { + actualAssetPresentation: string; + actualAudioMode: string; + actualAudioQuality: AudioQuality; + actualDuration: number; + actualProductId: string; + actualStreamType: unknown; + actualVideoQuality: unknown; + assetPosition: number; + bitDepth: number | null; + codec: string; + playbackSessionId: string; + sampleRate: number | null; +} +const unloadTimeUpdate = intercept("playbackControls/TIME_UPDATE", ([current]) => { + updateRPC(current); +}); + +const updateRPC = async (currentTime?: number) => { + const { playbackContext, playbackState, latestCurrentTime } = getPlaybackControl(); + currentTime ??= latestCurrentTime; + + const mediaItemId = (playbackContext)?.actualProductId; + if (mediaItemId === undefined) return; + + const currentlyPlaying = await TrackItemCache.ensure(mediaItemId); + if (currentlyPlaying === undefined) return; + + const _rpcClient = await rpcClient.ensureRPC().catch((err) => console.error("Failed to connect to DiscordRPC", err)); + if (_rpcClient === undefined) return; + + const activityState: Presence = { + buttons: [], + }; + if (currentlyPlaying.url) activityState.buttons?.push({ url: currentlyPlaying.url, label: "Play on Tidal" }); + + // Pause indicator + if (playbackState === "NOT_PLAYING") { + if (storage.keepRpcOnPause === false) return _rpcClient.clearActivity(); + activityState.smallImageKey = "paused-icon"; + activityState.smallImageText = "Paused"; + } else if (currentlyPlaying.duration !== undefined && currentTime !== undefined) { + // Playback/Time + activityState.startTimestamp = Math.floor(Date.now() / 1000); + activityState.endTimestamp = Math.floor((Date.now() + (currentlyPlaying.duration - currentTime) * 1000) / 1000); + } + + // Album + if (currentlyPlaying.album !== undefined) { + activityState.largeImageKey = getMediaURLFromID(currentlyPlaying.album.cover); + activityState.largeImageText = formatLongString(currentlyPlaying.album.title); + } + + // Title/Artist + const artist = `by ${currentlyPlaying?.artist?.name ?? currentlyPlaying.artists?.[0]?.name ?? "Unknown Artist"}`; + const desc = `${currentlyPlaying.title} ${artist}`; + if (desc.length >= 32) { + activityState.details = formatLongString(currentlyPlaying.title); + activityState.state = formatLongString(artist); + } else { + activityState.details = formatLongString(desc); + } + + return _rpcClient.setActivity(activityState); +}; + +export async function onUnload() { + unloadTimeUpdate(); + await rpcClient.cleanp(); +} +updateRPC(); + +storage.keepRpcOnPause ??= false; +export function Settings() { + return html` <${SwitchSetting} checked=${storage.keepRpcOnPause} onClick=${() => (storage.keepRpcOnPause = !storage.keepRpcOnPause)} title="Keep RPC on pause" /> `; +}