import { Client, SetActivity } from "@xhayper/discord-rpc"; import { app, ipcMain } from "electron"; import { globalEvents } from "../constants/globalEvents"; import { settings } from "../constants/settings"; import { Logger } from "../features/logger"; import { convertDurationToSeconds } from "../features/time/parse"; import { MediaStatus } from "../models/mediaStatus"; import { mediaInfo } from "./mediaInfo"; import { settingsStore } from "./settings"; const clientId = "833617820704440341"; export let rpc: Client; const ACTIVITY_LISTENING = 2; const MAX_RETRIES = 5; const RETRY_DELAY = 10000; const observer = () => { if (rpc) { updateActivity(); } }; const defaultPresence = { largeImageKey: "tidal-hifi-icon", largeImageText: `TIDAL Hi-Fi ${app.getVersion()}`, instance: false, type: ACTIVITY_LISTENING }; const updateActivity = () => { const showIdle = settingsStore.get(settings.discord.showIdle) ?? true; if (mediaInfo.status === MediaStatus.paused && !showIdle) { rpc.user?.clearActivity(); } else { rpc.user?.setActivity(getActivity()); } }; const getActivity = (): SetActivity => { const presence: SetActivity = { ...defaultPresence }; if (mediaInfo.status === MediaStatus.paused) { presence.details = settingsStore.get(settings.discord.idleText) ?? "Browsing Tidal"; } else { const showSong = settingsStore.get(settings.discord.showSong) ?? false; if (showSong) { const { includeTimestamps, detailsPrefix, buttonText } = getFromStore(); includeTimeStamps(includeTimestamps); setPresenceFromMediaInfo(detailsPrefix, buttonText); } else { presence.details = settingsStore.get(settings.discord.usingText) ?? "Playing media on TIDAL"; } } return presence; function getFromStore() { const includeTimestamps = settingsStore.get(settings.discord.includeTimestamps) ?? true; const detailsPrefix = settingsStore.get(settings.discord.detailsPrefix) ?? "Listening to "; const buttonText = settingsStore.get(settings.discord.buttonText) ?? "Play on TIDAL"; return { includeTimestamps, detailsPrefix, buttonText }; } /** * Pad a string using spaces to at least 2 characters * @param input string to pad with 2 characters * @returns */ function pad(input: string): string { return input.padEnd(2, " "); } function setPresenceFromMediaInfo(detailsPrefix: string, buttonText: string) { // discord requires a minimum of 2 characters const title = pad(mediaInfo.title); const album = pad(mediaInfo.album); const artists = pad(mediaInfo.artists); if (mediaInfo.url) { presence.details = `${detailsPrefix}${title}`; presence.state = artists ? artists : "unknown artist(s)"; presence.largeImageKey = mediaInfo.image; if (album) { presence.largeImageText = album; } presence.buttons = [{ label: buttonText, url: mediaInfo.url }]; } else { presence.details = `Watching ${title}`; presence.state = artists; } } function includeTimeStamps(includeTimestamps: boolean) { if (includeTimestamps) { const currentSeconds = convertDurationToSeconds(mediaInfo.current); const durationSeconds = convertDurationToSeconds(mediaInfo.duration); const now = Math.trunc((Date.now() + 500) / 1000); presence.startTimestamp = now - currentSeconds; presence.endTimestamp = presence.startTimestamp + durationSeconds; } } }; /** * Try to login to RPC and retry if it errors * @param retryCount Max retry count */ const connectWithRetry = async (retryCount = 0) => { try { await rpc.login(); Logger.log('Connected to Discord'); rpc.on("ready", updateActivity); Object.values(globalEvents).forEach(event => ipcMain.on(event, observer)); } catch (error) { if (retryCount < MAX_RETRIES) { Logger.log(`Failed to connect to Discord, retrying in ${RETRY_DELAY/1000} seconds... (Attempt ${retryCount + 1}/${MAX_RETRIES})`); setTimeout(() => connectWithRetry(retryCount + 1), RETRY_DELAY); } else { Logger.log('Failed to connect to Discord after maximum retry attempts'); } } }; /** * Set up the discord rpc and listen on globalEvents.updateInfo */ export const initRPC = () => { rpc = new Client({ transport: {type: "ipc"}, clientId }); connectWithRetry(); }; /** * Remove any RPC connection with discord and remove the event listener on globalEvents.updateInfo */ export const unRPC = () => { if (rpc) { rpc.user?.clearActivity(); rpc.destroy(); rpc = null; ipcMain.removeListener(globalEvents.updateInfo, observer); } };