diff --git a/src/equicordplugins/absRPC/index.tsx b/src/equicordplugins/absRPC/index.tsx deleted file mode 100644 index 8f389f20c..000000000 --- a/src/equicordplugins/absRPC/index.tsx +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -// alot of the code is from JellyfinRPC -import { definePluginSettings } from "@api/Settings"; -import { HeadingSecondary } from "@components/Heading"; -import { Paragraph } from "@components/Paragraph"; -import { EquicordDevs } from "@utils/constants"; -import { Logger } from "@utils/Logger"; -import definePlugin, { OptionType } from "@utils/types"; -import { ApplicationAssetUtils, FluxDispatcher, showToast } from "@webpack/common"; - -interface ActivityAssets { - large_image?: string; - large_text?: string; - small_image?: string; - small_text?: string; -} - -interface Activity { - state: string; - details?: string; - timestamps?: { - start?: number; - }; - assets?: ActivityAssets; - name: string; - application_id: string; - metadata?: { - button_urls?: Array; - }; - type: number; - flags: number; -} - -interface MediaData { - name: string; - type: string; - author?: string; - series?: string; - duration?: number; - currentTime?: number; - progress?: number; - url?: string; - imageUrl?: string; - isFinished?: boolean; -} - -const settings = definePluginSettings({ - serverUrl: { - description: "AudioBookShelf server URL (e.g., https://abs.example.com)", - type: OptionType.STRING, - }, - username: { - description: "AudioBookShelf username", - type: OptionType.STRING, - }, - password: { - description: "AudioBookShelf password", - type: OptionType.STRING, - }, -}); - -const applicationId = "1381423044907503636"; - -const logger = new Logger("AudioBookShelfRichPresence"); - -let authToken: string | null = null; -let updateInterval: NodeJS.Timeout | undefined; - -async function getApplicationAsset(key: string): Promise { - return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0]; -} - -function setActivity(activity: Activity | null) { - FluxDispatcher.dispatch({ - type: "LOCAL_ACTIVITY_UPDATE", - activity, - socketId: "ABSRPC", - }); -} - -export default definePlugin({ - name: "AudioBookShelfRichPresence", - description: "Rich presence for AudioBookShelf media server", - authors: [EquicordDevs.vmohammad], - - settingsAboutComponent: () => ( - <> - How to connect to AudioBookShelf - - Enter your AudioBookShelf server URL, username, and password to display your currently playing audiobooks as Discord Rich Presence. -

- The plugin will automatically authenticate and fetch your listening progress. -
- - ), - - settings, - - start() { - this.updatePresence(); - updateInterval = setInterval(() => { this.updatePresence(); }, 10000); - }, - - stop() { - clearInterval(updateInterval); - updateInterval = undefined; - }, - - async authenticate(): Promise { - if (!settings.store.serverUrl || !settings.store.username || !settings.store.password) { - logger.warn("AudioBookShelf server URL, username, or password is not set in settings."); - showToast("AudioBookShelf RPC is not configured.", "failure", { - duration: 15000, - }); - return false; - } - - try { - const baseUrl = settings.store.serverUrl.replace(/\/$/, ""); - const url = `${baseUrl}/login`; - - const res = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - username: settings.store.username, - password: settings.store.password, - }), - }); - - if (!res.ok) throw `${res.status} ${res.statusText}`; - - const data = await res.json(); - authToken = data.user?.token; - return !!authToken; - } catch (e) { - logger.error("Failed to authenticate with AudioBookShelf", e); - authToken = null; - return false; - } - }, - - async fetchMediaData(): Promise { - if (!authToken && !(await this.authenticate())) { - return null; - } - - const isPlayingNow = session => { - const now = Date.now(); - const lastUpdate = session.updatedAt; - const diffSeconds = (now - lastUpdate) / 1000; - return diffSeconds <= 30; - }; - try { - const baseUrl = settings.store.serverUrl!.replace(/\/$/, ""); - const url = `${baseUrl}/api/me/listening-sessions`; - - const res = await fetch(url, { - headers: { - "Authorization": `Bearer ${authToken}`, - }, - }); - - if (!res.ok) { - if (res.status === 401) { - authToken = null; - if (await this.authenticate()) { - return this.fetchMediaData(); - } - } - throw `${res.status} ${res.statusText}`; - } - - const { sessions } = await res.json(); - const activeSession = sessions.find((session: any) => - session.updatedAt && !session.isFinished - ); - - if (!activeSession || !isPlayingNow(activeSession)) return null; - - const { mediaMetadata: media, mediaType, duration, currentTime, libraryItemId } = activeSession; - if (!media) return null; - console.log(media); - return { - name: media.title || "Unknown", - type: mediaType || "book", - author: media.author || media.publisher, - series: media.series[0]?.name, - duration, - currentTime, - imageUrl: libraryItemId ? `${baseUrl}/api/items/${libraryItemId}/cover` : undefined, - isFinished: activeSession.isFinished || false, - }; - } catch (e) { - logger.error("Failed to query AudioBookShelf API", e); - return null; - } - }, - - async updatePresence() { - setActivity(await this.getActivity()); - }, - - async getActivity(): Promise { - const mediaData = await this.fetchMediaData(); - if (!mediaData || mediaData.isFinished) return null; - - const largeImage = mediaData.imageUrl; - console.log("Large Image URL:", largeImage); - const assets: ActivityAssets = { - large_image: largeImage ? await getApplicationAsset(largeImage) : await getApplicationAsset("audiobookshelf"), - large_text: mediaData.series || mediaData.author || undefined, - }; - - const getDetails = () => { - return mediaData.name; - }; - - const getState = () => { - if (mediaData.series && mediaData.author) { - return `${mediaData.series} • ${mediaData.author}`; - } - return mediaData.author || "AudioBook"; - }; - - const timestamps = mediaData.currentTime && mediaData.duration ? { - start: Date.now() - (mediaData.currentTime * 1000), - end: Date.now() + ((mediaData.duration - mediaData.currentTime) * 1000) - } : undefined; - - return { - application_id: applicationId, - name: "AudioBookShelf", - - details: getDetails(), - state: getState(), - assets, - timestamps, - - type: 2, - flags: 1, - }; - } -}); diff --git a/src/equicordplugins/gensokyoRadioRPC/index.tsx b/src/equicordplugins/gensokyoRadioRPC/index.tsx deleted file mode 100644 index bd4892e66..000000000 --- a/src/equicordplugins/gensokyoRadioRPC/index.tsx +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { definePluginSettings } from "@api/Settings"; -import { Paragraph } from "@components/Paragraph"; -import { Devs, EquicordDevs } from "@utils/constants"; -import definePlugin, { OptionType, PluginNative, ReporterTestable } from "@utils/types"; -import { Activity } from "@vencord/discord-types"; -import { ActivityFlags, ActivityType } from "@vencord/discord-types/enums"; -import { ApplicationAssetUtils, FluxDispatcher } from "@webpack/common"; - -const Native = VencordNative.pluginHelpers.GensokyoRadioRPC as PluginNative; - -const applicationId = "1253772057926303804"; - -function setActivity(activity: Activity | null) { - FluxDispatcher.dispatch({ - type: "LOCAL_ACTIVITY_UPDATE", - activity, - socketId: "GensokyoRadio", - }); -} - -function getImageAsset(data: string) { - return ApplicationAssetUtils.fetchAssetIds(applicationId, [data]).then(ids => ids[0]); -} - -const settings = definePluginSettings({ - refreshInterval: { - type: OptionType.SLIDER, - description: "The interval between activity refreshes (seconds)", - markers: [1, 2, 2.5, 3, 5, 10, 15], - default: 15, - restartNeeded: true, - } -}); - -let updateInterval: NodeJS.Timeout | undefined; - -export default definePlugin({ - name: "GensokyoRadioRPC", - description: "Discord rich presence for Gensokyo Radio!", - authors: [Devs.RyanCaoDev, EquicordDevs.Prince527], - reporterTestable: ReporterTestable.None, - - settingsAboutComponent() { - return ( - - Discord rich presence for Gensokyo Radio! - - ); - }, - - settings, - - start() { - this.updatePresence(); - updateInterval = setInterval(() => { this.updatePresence(); }, settings.store.refreshInterval * 1000); - }, - - stop() { - clearInterval(updateInterval); - updateInterval = undefined; - FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null }); - }, - - updatePresence() { - this.getActivity().then(activity => { setActivity(activity); }); - }, - - async getActivity(): Promise { - const trackData = await Native.fetchTrackData(); - if (!trackData) return null; - - return { - application_id: applicationId, - - name: "Gensokyo Radio", - details: trackData.title, - state: trackData.artist, - - timestamps: { - // start: Date.now() - (trackData.position * 1000), - start: trackData.position * 1000, - // end: Date.now() - (trackData.position * 1000) + (trackData.duration * 1000), - end: trackData.duration * 1000, - }, - - assets: { - large_image: await getImageAsset(trackData.artwork), - large_text: trackData.album, - small_image: await getImageAsset("logo"), - small_text: "Gensokyo Radio" - }, - - buttons: undefined, - metadata: { button_urls: undefined }, - - type: ActivityType.LISTENING, - flags: ActivityFlags.INSTANCE, - }; - } -}); diff --git a/src/equicordplugins/gensokyoRadioRPC/types.ts b/src/equicordplugins/gensokyoRadioRPC/types.ts deleted file mode 100644 index 21b44194e..000000000 --- a/src/equicordplugins/gensokyoRadioRPC/types.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -export interface ActivityAssets { - large_image?: string; - large_text?: string; - small_image?: string; - small_text?: string; -} - -export interface Activity { - state: string; - details?: string; - timestamps?: { - start?: number; - end?: number; - }; - assets?: ActivityAssets; - buttons?: Array; - name: string; - application_id: string; - metadata?: { - button_urls?: Array; - }; - type: number; - flags: number; -} - -export interface ActivityAssets { - large_image?: string; - large_text?: string; - small_image?: string; - small_text?: string; -} - -export interface TrackData { - title: string; - album: string; - artist: string; - - artwork: string; - - position: number; - duration: number; -} diff --git a/src/equicordplugins/jellyfinRichPresence/index.tsx b/src/equicordplugins/jellyfinRichPresence/index.tsx deleted file mode 100644 index 2b4ac9ec6..000000000 --- a/src/equicordplugins/jellyfinRichPresence/index.tsx +++ /dev/null @@ -1,419 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -// alot of the code is from LastFMRichPresence -import { definePluginSettings } from "@api/Settings"; -import { HeadingSecondary } from "@components/Heading"; -import { Paragraph } from "@components/Paragraph"; -import { Devs, EquicordDevs } from "@utils/constants"; -import { Logger } from "@utils/Logger"; -import definePlugin, { OptionType } from "@utils/types"; -import { ApplicationAssetUtils, FluxDispatcher, showToast } from "@webpack/common"; - -interface ActivityAssets { - large_image?: string; - large_text?: string; - small_image?: string; - small_text?: string; -} - -interface ActivityButton { - label: string; - url: string; -} - -interface Activity { - state: string; - details?: string; - timestamps?: { - start?: number; - }; - assets?: ActivityAssets; - buttons?: Array; - name: string; - application_id: string; - metadata?: { - button_urls?: Array; - }; - type: number; - flags: number; -} - -interface MediaData { - name: string; - type: string; - artist?: string; - album?: string; - seriesName?: string; - seasonNumber?: number; - episodeNumber?: number; - year?: number; - url?: string; - imageUrl?: string; - duration?: number; - position?: number; - isPaused?: boolean; -} - -const settings = definePluginSettings({ - serverUrl: { - description: "Jellyfin server URL (e.g., https://jellyfin.example.com)", - type: OptionType.STRING, - }, - apiKey: { - description: "Jellyfin API key obtained from your Jellyfin administration dashboard", - type: OptionType.STRING, - }, - userId: { - description: "Jellyfin user ID obtained from your user profile URL", - type: OptionType.STRING, - }, - nameDisplay: { - description: "Choose how the application name should appear in Rich Presence", - type: OptionType.SELECT, - options: [ - { label: "Series/Movie Name", value: "default", default: true }, - { label: "Series - Episode/Track/Movie Name", value: "full" }, - { label: "Custom", value: "custom" }, - ], - }, - customName: { - description: "Custom Rich Presence name (only used if 'Custom' is selected).\nOptions: {name}, {series}, {season}, {episode}, {artist}, {album}, {year}", - type: OptionType.STRING, - }, - coverType: { - description: "Choose which cover to display when watching a TV show", - type: OptionType.SELECT, - options: [ - { label: "Series Cover", value: "series", default: true }, - { label: "Episode Cover", value: "episode" }, - ], - }, - episodeFormat: { - description: "Episode number format", - type: OptionType.SELECT, - options: [ - { label: "S01E01", value: "long", default: true }, - { label: "1x01", value: "short" }, - { label: "Season 1 Episode 1", value: "fulltext" }, - ], - }, - showEpisodeName: { - description: "Show episode name after season/episode info", - type: OptionType.BOOLEAN, - default: false, - }, - overrideRichPresenceType: { - description: "Override the rich presence type", - type: OptionType.SELECT, - options: [ - { - label: "Off", - value: false, - default: true, - }, - { - label: "Listening", - value: 2, - }, - { - label: "Playing", - value: 0, - }, - { - label: "Streaming", - value: 1, - }, - { - label: "Watching", - value: 3 - }, - ], - }, - showPausedState: { - description: "Show Rich Presence when media is paused", - type: OptionType.BOOLEAN, - default: true, - }, - privacyMode: { - description: "Privacy Mode (Hide media details like Episode/Song Name)", - type: OptionType.BOOLEAN, - default: false, - }, -}); - -const applicationId = "1381368130164625469"; - -const logger = new Logger("JellyfinRichPresence"); - -let updateInterval: NodeJS.Timeout | undefined; - -async function getApplicationAsset(key: string): Promise { - return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0]; -} - -function setActivity(activity: Activity | null) { - FluxDispatcher.dispatch({ - type: "LOCAL_ACTIVITY_UPDATE", - activity, - socketId: "Jellyfin", - }); -} - -export default definePlugin({ - name: "JellyfinRichPresence", - description: "Rich presence for Jellyfin media server", - authors: [EquicordDevs.vmohammad, Devs.SerStars, EquicordDevs.ZcraftElite], - - settingsAboutComponent: () => ( - <> - How to get an API key - - Auth token can be found by following these steps: -
    -
  1. 1. Log into your Jellyfin instance
  2. -
  3. 2. Open your browser's Developer Tools (usually F12 or right-click then Inspect)
  4. -
  5. 3. Go to the Network tab in Developer Tools
  6. -
  7. 4. Look for requests to your Jellyfin server
  8. -
  9. - 5. In the request headers, find X-MediaBrowser-Token or Authorization -
    - - Easiest way: press Ctrl+F in the Developer Tools and search for X-MediaBrowser-Token - -
  10. -
-
- You'll also need your User ID, which can be found in the url of your user profile page. -
- - ), - - settings, - - start() { - this.updatePresence(); - updateInterval = setInterval(() => { this.updatePresence(); }, 10000); - }, - - stop() { - clearInterval(updateInterval); - updateInterval = undefined; - }, - - async fetchMediaData(): Promise { - if (!settings.store.serverUrl || !settings.store.apiKey || !settings.store.userId) { - logger.warn("Jellyfin server URL, API key, or user ID is not set in settings."); - showToast("JellyfinRPC is not configured.", "failure", { - duration: 15000, - }); - return null; - } - - try { - const baseUrl = settings.store.serverUrl.replace(/\/$/, ""); - const url = `${baseUrl}/Sessions?api_key=${settings.store.apiKey}`; - - const res = await fetch(url); - if (!res.ok) throw `${res.status} ${res.statusText}`; - - const sessions = await res.json(); - const userSession = sessions.find((session: any) => - session.UserId === settings.store.userId && session.NowPlayingItem - ); - - if (!userSession || !userSession.NowPlayingItem) return null; - - const item = userSession.NowPlayingItem; - const playState = userSession.PlayState; - - if (playState?.IsPaused && !settings.store.showPausedState) return null; - - const imageUrl = item.ImageTags?.Primary - ? `${baseUrl}/Items/${item.Type === "Episode" && - item.SeriesId && - settings.store.coverType === "series" - ? item.SeriesId - : item.Id - }/Images/Primary` - : undefined; - - return { - name: item.Name || "Unknown", - type: item.Type, - artist: item.Artists?.[0] || item.AlbumArtist, - album: item.Album, - seriesName: item.SeriesName, - seasonNumber: item.ParentIndexNumber, - episodeNumber: item.IndexNumber, - year: item.ProductionYear, - url: `${baseUrl}/web/#!/details?id=${item.Id}`, - imageUrl, - duration: item.RunTimeTicks ? Math.floor(item.RunTimeTicks / 10000000) : undefined, - position: playState?.PositionTicks ? Math.floor(playState.PositionTicks / 10000000) : undefined, - isPaused: !!playState?.IsPaused, - }; - } catch (e) { - logger.error("Failed to query Jellyfin API", e); - return null; - } - }, - - async updatePresence() { - setActivity(await this.getActivity()); - }, - - async getActivity(): Promise { - let richPresenceType; - let appName: string; - const nameSetting = settings.store.nameDisplay || "default"; - - const mediaData = await this.fetchMediaData(); - if (!mediaData) return null; - - if (settings.store.overrideRichPresenceType) { - richPresenceType = settings.store.overrideRichPresenceType; - } else { - switch (mediaData.type) { - case "Audio": - richPresenceType = 2; - break; - default: - richPresenceType = 3; - break; - } - } - - const templateReplace = (template: string) => { - return template - .replace(/\{name\}/g, mediaData.name || "") - .replace(/\{series\}/g, mediaData.seriesName || "") - .replace(/\{season\}/g, mediaData.seasonNumber?.toString() || "") - .replace(/\{episode\}/g, mediaData.episodeNumber?.toString() || "") - .replace(/\{artist\}/g, mediaData.artist || "") - .replace(/\{album\}/g, mediaData.album || "") - .replace(/\{year\}/g, mediaData.year?.toString() || ""); - }; - - switch (nameSetting) { - case "full": - if (mediaData.type === "Episode" && mediaData.seriesName) { - appName = settings.store.privacyMode - ? `${mediaData.seriesName} - [Episode Hidden]` - : `${mediaData.seriesName} - ${mediaData.name}`; - } else if (mediaData.type === "Audio") { - appName = settings.store.privacyMode - ? "[Track Hidden]" - : `${mediaData.artist || "Unknown Artist"} - ${mediaData.name}`; - } else { - appName = settings.store.privacyMode - ? "[Movie Hidden]" - : mediaData.name || "Jellyfin"; - } - break; - case "custom": - appName = templateReplace(settings.store.customName || "{name} on Jellyfin"); - if (settings.store.privacyMode) { - appName = appName - .replace(mediaData.name || "", "[Title Hidden]") - .replace(mediaData.seriesName || "", "[Series Hidden]") - .replace(mediaData.artist || "", "[Artist Hidden]") - .replace(mediaData.album || "", "[Album Hidden]"); - } - break; - case "default": - default: - if (mediaData.type === "Episode" && mediaData.seriesName) { - appName = mediaData.seriesName; - } else { - appName = settings.store.privacyMode - ? "[Media Hidden]" - : mediaData.name || "Jellyfin"; - } - break; - } - - const assets: ActivityAssets = { - large_image: (mediaData.imageUrl - ? await getApplicationAsset(mediaData.imageUrl) - : undefined), - large_text: mediaData.seriesName || mediaData.album || undefined, - }; - - if (settings.store.privacyMode) { - assets.large_image = undefined; - } - - const buttons: ActivityButton[] = []; - - const getDetails = () => { - if (mediaData.type === "Episode" && mediaData.seriesName) { - return settings.store.privacyMode ? "Watching a TV Show" : mediaData.seriesName; - } - return settings.store.privacyMode ? "Watching Something" : mediaData.name; - }; - - const getState = () => { - if (mediaData.isPaused) { - return "Paused"; - } - if (mediaData.type === "Episode" && mediaData.seriesName) { - let episodeFormat = ""; - const season = mediaData.seasonNumber; - const episode = mediaData.episodeNumber; - const format = settings.store.episodeFormat || "long"; - - if (season != null && episode != null) { - switch (format) { - case "long": - episodeFormat = `S${season.toString().padStart(2, "0")}E${episode.toString().padStart(2, "0")}`; - break; - case "short": - episodeFormat = `${season}x${episode.toString().padStart(2, "0")}`; - break; - case "fulltext": - episodeFormat = `Season ${season} Episode ${episode}`; - break; - } - } else if (season != null) { - episodeFormat = format === "fulltext" ? `Season ${season}` : `S${season.toString().padStart(2, "0")}`; - } else if (episode != null) { - episodeFormat = format === "fulltext" ? `Episode ${episode}` : `E${episode.toString().padStart(2, "0")}`; - } - - if (settings.store.showEpisodeName && mediaData.name && !settings.store.privacyMode) { - return `${episodeFormat} - ${mediaData.name}`; - } - return episodeFormat; - } - if (settings.store.privacyMode) { - return mediaData.type === "Audio" ? "Listening to music" : (mediaData.year ? "(????)" : undefined); - } - return mediaData.artist || (mediaData.year ? `(${mediaData.year})` : undefined); - }; - - const timestamps = (!mediaData.isPaused && mediaData.position != null && mediaData.duration != null) ? { - start: Date.now() - (mediaData.position * 1000), - end: Date.now() + ((mediaData.duration - mediaData.position) * 1000) - } : undefined; - - return { - application_id: applicationId, - name: appName, - details: getDetails(), - state: getState() || "something", - assets, - timestamps, - - buttons: buttons.length ? buttons.map(v => v.label) : undefined, - metadata: { - button_urls: buttons.map(v => v.url), - }, - type: richPresenceType, - flags: 1 - }; - } -}); diff --git a/src/equicordplugins/listenBrainzRPC/index.tsx b/src/equicordplugins/listenBrainzRPC/index.tsx deleted file mode 100644 index b69230856..000000000 --- a/src/equicordplugins/listenBrainzRPC/index.tsx +++ /dev/null @@ -1,376 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { definePluginSettings } from "@api/Settings"; -import { HeadingSecondary } from "@components/Heading"; -import { Link } from "@components/Link"; -import { Paragraph } from "@components/Paragraph"; -import { EquicordDevs } from "@utils/constants"; -import { Logger } from "@utils/Logger"; -import definePlugin, { OptionType } from "@utils/types"; -import { Activity, ActivityAssets, ActivityButton } from "@vencord/discord-types"; -import { ActivityFlags, ActivityType } from "@vencord/discord-types/enums"; -import { findByPropsLazy } from "@webpack"; -import { ApplicationAssetUtils, FluxDispatcher } from "@webpack/common"; - -interface TrackData { - name: string; - album: string; - artist: string; - durationMs?: number; - recordingMBID?: string; - url: string; - imageUrl?: string; -} - -const enum NameFormat { - StatusName = "status-name", - ArtistFirst = "artist-first", - SongFirst = "song-first", - ArtistOnly = "artist", - SongOnly = "song", - AlbumName = "album", -} - -const applicationId = "1090155131007406132"; -const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f"; - -const logger = new Logger("ListenBrainzRPC"); - -const PresenceStore = findByPropsLazy("getLocalPresence"); - -let updateInterval: NodeJS.Timeout | undefined; - -async function getApplicationAsset(key: string): Promise { - return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0]; -} - -function setActivity(activity: Activity | null) { - FluxDispatcher.dispatch({ - type: "LOCAL_ACTIVITY_UPDATE", - activity, - socketId: "ListenBrainz", - }); -} - -const settings = definePluginSettings({ - username: { - description: "ListenBrainz username", - type: OptionType.STRING, - }, - mbContact: { - description: "ListenBrainz contact", - type: OptionType.STRING, - }, - shareUsername: { - description: - "show link to ListenBrainz profile (may only be visible to other users)", - type: OptionType.BOOLEAN, - default: false, - }, - shareSong: { - description: - "show link to song on ListenBrainz (may only be visible to other users)", - type: OptionType.BOOLEAN, - default: true, - }, - hideWithSpotify: { - description: "hide ListenBrainz presence if Spotify is running", - type: OptionType.BOOLEAN, - default: true, - }, - hideWithActivity: { - description: "hide ListenBrainz presence if you have any other presence", - type: OptionType.BOOLEAN, - default: false, - }, - useTimeBar: { - description: - "use track duration to display a time bar (must be using Listening status)", - type: OptionType.BOOLEAN, - default: true, - }, - statusName: { - description: "custom status text", - type: OptionType.STRING, - default: "some music", - }, - nameFormat: { - description: "Show name of song and artist in status name", - type: OptionType.SELECT, - options: [ - { - label: "Use custom status name", - value: NameFormat.StatusName, - default: true, - }, - { - label: "Use format 'artist - song'", - value: NameFormat.ArtistFirst, - }, - { - label: "Use format 'song - artist'", - value: NameFormat.SongFirst, - }, - { - label: "Use artist name only", - value: NameFormat.ArtistOnly, - }, - { - label: "Use song name only", - value: NameFormat.SongOnly, - }, - { - label: - "Use album name (falls back to custom status text if song has no album)", - value: NameFormat.AlbumName, - }, - ], - }, - useListeningStatus: { - description: 'show "Listening to" status instead of "Playing"', - type: OptionType.BOOLEAN, - default: true, - }, - missingArt: { - description: "When album or album art is missing", - type: OptionType.SELECT, - options: [ - { - label: "Use large ListenBrainz logo", - value: "listenbrainzLogo", - default: true, - }, - { - label: "Use generic placeholder", - value: "placeholder", - }, - ], - }, - useLogo: { - description: "Show ListenBrainz logo on album art", - type: OptionType.BOOLEAN, - default: true, - }, -}); - -var currentRecordingMBID = ""; -var currentStart = 0; - -export default definePlugin({ - name: "ListenBrainzRPC", - description: "Little plugin for ListenBrainz rich presence", - authors: [EquicordDevs.qouesm], - - settingsAboutComponent: () => ( - <> - About MusicBrainz API - - The MusicBrainz API does not require an API key, but it does require a{" "} - - {" "} - meaningful user-agent string - {" "} - . For most, an email address should suffice. - - - ), - - settings, - - start() { - this.updatePresence(); - updateInterval = setInterval(() => { - this.updatePresence(); - }, 16000); - }, - - stop() { - clearInterval(updateInterval); - updateInterval = undefined; - }, - - async fetchTrackData(): Promise { - if (!settings.store.username) return null; - - try { - const lbRes = await fetch( - `https://api.listenbrainz.org/1/user/${settings.store.username}/playing-now`, - ); - if (!lbRes.ok) throw `${lbRes.status} ${lbRes.statusText}`; - - const lbJson = await lbRes.json(); - if (lbJson.error) { - logger.error( - "Error from ListenBrainz API", - `${lbJson.error}: ${lbJson.message}`, - ); - return null; - } - - const listen = lbJson.payload?.listens?.[0]; - if (!listen?.playing_now) return null; - - const trackMetadata = listen.track_metadata; - const albumName = trackMetadata.release_name || "Unknown"; - const artistName = trackMetadata.artist_name || "Unknown"; - - const mbRes = await fetch(`https://musicbrainz.org/ws/2/release/?query=release:${encodeURIComponent(albumName)}%20AND%20artist:${encodeURIComponent(artistName)}&fmt=json`,); - - if (!mbRes.ok) throw `${mbRes.status} ${mbRes.statusText}`; - - const mbJson = await mbRes.json(); - const releases = mbJson.releases || []; - - const releaseGroup = releases[0]["release-group"].id; - - const caaRes = await fetch( - `https://coverartarchive.org/release-group/${releaseGroup}`, - ); - if (!caaRes.ok) throw `${caaRes.status} ${caaRes.statusText}`; - const caaJson = await caaRes.json(); - - const url: string = caaJson.release; - - const { images } = caaJson; - let imageUrl: string = ""; - for (const image of images) { - imageUrl = image.thumbnails.large || ""; - if (!imageUrl) continue; - break; - } - - return { - name: trackMetadata.track_name || "Unknown", - album: albumName, - artist: artistName, - durationMs: trackMetadata.additional_info.duration_ms, - recordingMBID: trackMetadata.additional_info.recording_mbid, - url: url, - imageUrl: imageUrl, - }; - } catch (e) { - logger.error("Failed to query ListenBrainz API", e); - // will clear the rich presence if API fails - return null; - } - }, - - async updatePresence() { - setActivity(await this.getActivity()); - }, - - getLargeImage(track: TrackData): string | undefined { - if (track.imageUrl && !track.imageUrl.includes(placeholderId)) - return track.imageUrl; - - if (settings.store.missingArt === "placeholder") return "placeholder"; - }, - - async getActivity(): Promise { - if (settings.store.hideWithActivity) { - if ( - PresenceStore.getActivities().some( - a => a.application_id !== applicationId, - ) - ) { - return null; - } - } - - if (settings.store.hideWithSpotify) { - if ( - PresenceStore.getActivities().some( - a => - a.type === ActivityType.LISTENING && - a.application_id !== applicationId, - ) - ) { - // there is already music status because of Spotify or richerCider (probably more) - return null; - } - } - - const trackData = await this.fetchTrackData(); - if (!trackData) return null; - - const largeImage = this.getLargeImage(trackData); - const assets: ActivityAssets = largeImage - ? { - large_image: await getApplicationAsset(largeImage), - large_text: trackData.album || undefined, - small_image: settings.store.useLogo - ? await getApplicationAsset("listenbrainz") - : undefined, - small_text: "ListenBrainz", - } - : { - large_image: await getApplicationAsset("listenbrainz"), - large_text: trackData.album || undefined, - }; - - const buttons: ActivityButton[] = []; - - if (settings.store.shareUsername) - buttons.push({ - label: "ListenBrainz Profile", - url: `https://www.listenbrainz.org/user/${settings.store.username}`, - }); - - if (settings.store.shareSong) - buttons.push({ - label: "View Song", - url: trackData.url, - }); - - const statusName = (() => { - switch (settings.store.nameFormat) { - case NameFormat.ArtistFirst: - return trackData.artist + " - " + trackData.name; - case NameFormat.SongFirst: - return trackData.name + " - " + trackData.artist; - case NameFormat.ArtistOnly: - return trackData.artist; - case NameFormat.SongOnly: - return trackData.name; - case NameFormat.AlbumName: - return trackData.album || settings.store.statusName; - default: - return settings.store.statusName; - } - })(); - - if (trackData.recordingMBID && trackData.recordingMBID !== currentRecordingMBID) { - currentRecordingMBID = trackData.recordingMBID; - currentStart = Date.now(); - } - - return { - application_id: applicationId, - name: statusName, - - details: trackData.name, - state: trackData.artist, - assets, - - timestamps: { - start: settings.store.useTimeBar ? currentStart : undefined, - end: settings.store.useTimeBar - ? currentStart + (trackData.durationMs ?? 0) - : undefined, - }, - buttons: buttons.length ? buttons.map(v => v.label) : undefined, - metadata: { - button_urls: buttons.map(v => v.url), - }, - - type: settings.store.useListeningStatus - ? ActivityType.LISTENING - : ActivityType.PLAYING, - flags: ActivityFlags.INSTANCE, - }; - }, -}); diff --git a/src/equicordplugins/richPresence/SettingsPanel.tsx b/src/equicordplugins/richPresence/SettingsPanel.tsx new file mode 100644 index 000000000..86760ba70 --- /dev/null +++ b/src/equicordplugins/richPresence/SettingsPanel.tsx @@ -0,0 +1,233 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { SettingsSection } from "@components/settings/tabs/plugins/components/Common"; +import { Switch } from "@components/Switch"; +import { Margins } from "@utils/margins"; +import { classes } from "@utils/misc"; +import { Select, Slider, TabBar, TextInput, useState } from "@webpack/common"; + +import { onServiceChange, settings, SettingsStore } from "./settings"; +import { NameFormat, ServiceTab } from "./types"; + +type SettingsKey = keyof SettingsStore; + +function SwitchSetting({ name, description, settingsKey }: { name: string; description: string; settingsKey: SettingsKey; }) { + const [value, setValue] = useState(settings.store[settingsKey] ?? false); + return ( + + { setValue(v); (settings.store[settingsKey] as boolean) = v; onServiceChange?.(); }} /> + + ); +} + +function TextSetting({ name, description, settingsKey, placeholder }: { name: string; description: string; settingsKey: SettingsKey; placeholder?: string; }) { + const [value, setValue] = useState(settings.store[settingsKey] ?? ""); + return ( + + { setValue(v); (settings.store[settingsKey] as string) = v; }} + placeholder={placeholder ?? "Enter a value"} + /> + + ); +} + +function SelectSetting({ name, description, settingsKey, options }: { name: string; description: string; settingsKey: SettingsKey; options: { label: string; value: string; }[]; }) { + const [value, setValue] = useState(settings.store[settingsKey] ?? options[0]?.value); + return ( + +