diff --git a/plugins/DiscordRPC/src/Settings.tsx b/plugins/DiscordRPC/src/Settings.tsx index 52668772..ed42a4ca 100644 --- a/plugins/DiscordRPC/src/Settings.tsx +++ b/plugins/DiscordRPC/src/Settings.tsx @@ -1,10 +1,11 @@ -import { LunaSettings, LunaSwitchSetting, LunaTextSetting } from "@luna/ui"; +import { LunaSettings, LunaSwitchSetting, LunaTextSetting, LunaSelectSetting, LunaSelectItem } from "@luna/ui"; import { ReactiveStore } from "@luna/core"; import React from "react"; import { errSignal, trace } from "."; import { updateActivity } from "./updateActivity"; +import { getAvailablePipes } from "./getAvailablePipes.native"; const defaultCustomStatusText = "{track} by {artist}"; @@ -13,18 +14,52 @@ export const settings = await ReactiveStore.getPluginStorage("DiscordRPC", { displayArtistIcon: true, displayPlaylistButton: true, customStatusText: defaultCustomStatusText, + pipeId: -1, }); if (!settings.customStatusText || settings.customStatusText === "") settings.customStatusText = defaultCustomStatusText; +if (settings.pipeId === undefined) settings.pipeId = -1; export const Settings = () => { const [displayOnPause, setDisplayOnPause] = React.useState(settings.displayOnPause); const [displayArtistIcon, setDisplayArtistIcon] = React.useState(settings.displayArtistIcon); const [displayPlaylistButton, setDisplayPlaylistButton] = React.useState(settings.displayPlaylistButton); const [customStatusText, setCustomStatusText] = React.useState(settings.customStatusText); + + const [pipeId, setPipeId] = React.useState(settings.pipeId); + const [availablePipes, setAvailablePipes] = React.useState([]); + + React.useEffect(() => { + getAvailablePipes().then((pipes) => { + setAvailablePipes(pipes); + if (pipes.length > 0 && !pipes.includes(settings.pipeId)) { + setPipeId((settings.pipeId = pipes[0])); + updateActivity().catch(() => {}); + } + }).catch(() => {}); + }, []); return ( + {availablePipes.length > 1 && ( + { + setPipeId((settings.pipeId = parseInt(e.target.value, 10))); + updateActivity() + .then(() => (errSignal!._ = undefined)) + .catch(trace.err.withContext("Failed to set activity")); + }} + > + {availablePipes.map((pipe) => ( + + Pipe {pipe} + + ))} + + )} { +let currentPipeId: number = -1; + +export const getClient = asyncDebounce(async (preferredPipeId: number = -1) => { + const availablePipes = await getAvailablePipes(); + + let targetPipe = preferredPipeId; + if (availablePipes.length > 0) { + targetPipe = availablePipes.includes(preferredPipeId) ? preferredPipeId : availablePipes[0]; + } + + if (rpcClient && currentPipeId !== targetPipe) { + await rpcClient.destroy(); + } + const isAvailable = rpcClient && rpcClient.transport.isConnected && rpcClient.user; if (isAvailable) return rpcClient!; if (rpcClient) await rpcClient.destroy(); - rpcClient = new Client({ clientId: "1130698654987067493" }); - await rpcClient.connect(); + const clientOptions: ClientOptions = { + clientId: "1130698654987067493" + }; + + if (targetPipe >= 0) { + clientOptions.pipeId = targetPipe; + } + + rpcClient = new Client(clientOptions); + currentPipeId = targetPipe; + + await rpcClient.connect(); return rpcClient; -}; +}); -export const setActivity = async (activity?: SetActivity) => { - const client = await getClient(); - if (!client.user) return; +export const setActivity = async (activity?: SetActivity, preferredPipeId: number = -1) => { + const client = await getClient(preferredPipeId); + if (!client?.user) return; if (!activity) return client.user.clearActivity(); - return client.user.setActivity(activity); + return await client.user.setActivity(activity); }; -export const cleanupRPC = () => rpcClient?.destroy()!; +export const cleanupRPC = () => rpcClient?.destroy(); + export const StatusDisplayTypeEnum = () => ({ Name: StatusDisplayType.NAME, State: StatusDisplayType.STATE, diff --git a/plugins/DiscordRPC/src/getAvailablePipes.native.ts b/plugins/DiscordRPC/src/getAvailablePipes.native.ts new file mode 100644 index 00000000..8d93ad8b --- /dev/null +++ b/plugins/DiscordRPC/src/getAvailablePipes.native.ts @@ -0,0 +1,29 @@ +import { readdir } from "node:fs/promises"; +import { tmpdir } from "node:os"; + +export const getAvailablePipes = async (): Promise => { + const pipes = new Set(); + try { + if (process.platform === "win32") { + const files = await readdir("\\\\.\\pipe\\"); + for (const file of files) { + if (file.startsWith("discord-ipc-")) { + const id = parseInt(file.replace("discord-ipc-", ""), 10); + if (!isNaN(id)) pipes.add(id); + } + } + } else { + const tempDir = process.env.XDG_RUNTIME_DIR ?? tmpdir(); + const files = await readdir(tempDir); + for (const file of files) { + if (file.startsWith("discord-ipc-")) { + const id = parseInt(file.replace("discord-ipc-", ""), 10); + if (!isNaN(id)) pipes.add(id); + } + } + } + } catch { + // Ignore read errors + } + return Array.from(pipes).sort((a, b) => a - b); +}; diff --git a/plugins/DiscordRPC/src/updateActivity.ts b/plugins/DiscordRPC/src/updateActivity.ts index e688a028..eccf1d22 100644 --- a/plugins/DiscordRPC/src/updateActivity.ts +++ b/plugins/DiscordRPC/src/updateActivity.ts @@ -9,7 +9,7 @@ import { settings } from "./Settings"; const StatusDisplayType = await StatusDisplayTypeEnum(); export const updateActivity = async (mediaItem?: MediaItem) => { - if (!PlayState.playing && !settings.displayOnPause) return await setActivity(); + if (!PlayState.playing && !settings.displayOnPause) return await setActivity(undefined, settings.pipeId); mediaItem ??= await MediaItem.fromPlaybackContext(); if (mediaItem === undefined) return; @@ -83,5 +83,5 @@ export const updateActivity = async (mediaItem?: MediaItem) => { activity.largeImageUrl = `https://tidal.com/album/${album.id}/u`; } - await setActivity(activity); + await setActivity(activity, settings.pipeId); };