From 5619852a62f4b0ba90fe9ab46e8757c4c2789e7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Z=20=E2=9D=A4=27s=20Life?= Date: Wed, 22 Oct 2025 12:33:19 -0400 Subject: [PATCH] jellyfinRichPresence: Complete Plugin Overhaul (#464) * [Feat] jellyfinRichPresence: Enabled Ability to Use Series Cover * Enabled the ability for the user to use the series cover (which is more similar to the movie cover) instead of the episode cover when watching an episode. * Added myself as a contributor to the plugin. * [QOL] Correct Mistake with Developer Crediting * [Feat] jellyfinRichPresence: Added Ability to Show Episode Name & Change Season-Episode Display Format * Added the ability to change the display formatting between 3 different options: - `S00E00` - `0x00` - `Season 0 Episode 0` * Added the ability to display the episode name after the episode number. * [Feat] jellyfinRichPresence: Added the Ability to Show Presence when Paused * Added a settings option that will allow the user to continue showing the presence while the player is paused. * [Feat] jellyfinRichPresence: Added Privacy Mode * Added the ability for the user to enable privacy mode, in which the name of their media will not be displayed. * [Fix] Hide Image when in Privacy Mode * Made the image hidden when in privacy mode * Fixed a few es-lint violations * [Fix] jellyfinRichPresence: Hide When Paused by Default * Switched the defaults to hide the presence when media is paused * [Fix] jellyfinRichPresence: Used Different Method for Image Hiding * Fixed the image not hiding by using a different method * [Fix] jellyfinRichPresence: Remove Comment that Wasn't Purged, Fix Ancient Autocorrect Mistake * Update .gitignore for Wakatime files Add Wakatime related entries to .gitignore * [Fix] jellyfinRichPresence: Fix Autocorrect Mistake from Commit `a31a3ac` --------- Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com> --- .gitignore | 3 + .../jellyfinRichPresence/index.tsx | 119 +++++++++++++++--- src/utils/constants.ts | 4 + 3 files changed, 111 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 9ed57e067..34faacbfa 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ ExtensionCache/ # vencord companion module cache .modules + +# wakatime related stuff +.wakatime-project diff --git a/src/equicordplugins/jellyfinRichPresence/index.tsx b/src/equicordplugins/jellyfinRichPresence/index.tsx index e0559b63b..f3448c0c7 100644 --- a/src/equicordplugins/jellyfinRichPresence/index.tsx +++ b/src/equicordplugins/jellyfinRichPresence/index.tsx @@ -56,6 +56,7 @@ interface MediaData { imageUrl?: string; duration?: number; position?: number; + isPaused?: boolean; } const settings = definePluginSettings({ @@ -84,6 +85,28 @@ const settings = definePluginSettings({ 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, + }, showTMDBButton: { description: "Show TheMovieDB button in Rich Presence", type: OptionType.BOOLEAN, @@ -116,6 +139,16 @@ const settings = definePluginSettings({ }, ], }, + 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"; @@ -137,7 +170,7 @@ function setActivity(activity: Activity | null) { export default definePlugin({ name: "JellyfinRichPresence", description: "Rich presence for Jellyfin media server", - authors: [EquicordDevs.vmohammad, Devs.SerStars], + authors: [EquicordDevs.vmohammad, Devs.SerStars, EquicordDevs.ZcraftElite], settingsAboutComponent: () => ( <> @@ -200,10 +233,16 @@ export default definePlugin({ const item = userSession.NowPlayingItem; const playState = userSession.PlayState; - if (playState?.IsPaused) return null; + if (playState?.IsPaused && !settings.store.showPausedState) return null; const imageUrl = item.ImageTags?.Primary - ? `${baseUrl}/Items/${item.Id}/Images/Primary` + ? `${baseUrl}/Items/${ + item.Type === "Episode" && + item.SeriesId && + settings.store.coverType === "series" + ? item.SeriesId + : item.Id + }/Images/Primary` : undefined; return { @@ -218,7 +257,8 @@ export default definePlugin({ 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 + position: playState?.PositionTicks ? Math.floor(playState.PositionTicks / 10000000) : undefined, + isPaused: !!playState?.IsPaused, }; } catch (e) { logger.error("Failed to query Jellyfin API", e); @@ -265,22 +305,37 @@ export default definePlugin({ switch (nameSetting) { case "full": if (mediaData.type === "Episode" && mediaData.seriesName) { - appName = `${mediaData.seriesName} - ${mediaData.name}`; + appName = settings.store.privacyMode + ? `${mediaData.seriesName} - [Episode Hidden]` + : `${mediaData.seriesName} - ${mediaData.name}`; } else if (mediaData.type === "Audio") { - appName = `${mediaData.artist || "Unknown Artist"} - ${mediaData.name}`; + appName = settings.store.privacyMode + ? "[Track Hidden]" + : `${mediaData.artist || "Unknown Artist"} - ${mediaData.name}`; } else { - appName = mediaData.name || "Jellyfin"; + appName = settings.store.privacyMode + ? "[Movie Hidden]" + : mediaData.name || "Jellyfin"; } break; case "custom": - appName = templateReplace(settings.store.customName || "{name} on Jellyfish"); + 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 = mediaData.name || "Jellyfin"; + appName = settings.store.privacyMode + ? "[Media Hidden]" + : mediaData.name || "Jellyfin"; } break; } @@ -292,25 +347,59 @@ export default definePlugin({ 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 mediaData.seriesName; + return settings.store.privacyMode ? "Watching a TV Show" : mediaData.seriesName; } - return mediaData.name; + return settings.store.privacyMode ? "Watching Something" : mediaData.name; }; const getState = () => { + if (mediaData.isPaused) { + return "Paused"; + } if (mediaData.type === "Episode" && mediaData.seriesName) { - const season = mediaData.seasonNumber ? `S${mediaData.seasonNumber}` : ""; - const episode = mediaData.episodeNumber ? `E${mediaData.episodeNumber}` : ""; - return `${mediaData.name} (${season} - ${episode})`.trim(); + 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.position && mediaData.duration ? { + 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; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 335498418..7840577cf 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1166,6 +1166,10 @@ export const EquicordDevs = Object.freeze({ name: "mmeta", id: 297075664530440192n }, + ZcraftElite: { + name: "ZcraftElite", + id: 926788037785047050n + }, } satisfies Record); // iife so #__PURE__ works correctly