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>
This commit is contained in:
Z ❤'s Life 2025-10-22 12:33:19 -04:00 • committed by GitHub
parent 17aa9a1df0
commit 5619852a62
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 111 additions and 15 deletions

3
.gitignore vendored
View file

@ -29,3 +29,6 @@ ExtensionCache/
# vencord companion module cache
.modules
# wakatime related stuff
.wakatime-project

View file

@ -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;

View file

@ -1166,6 +1166,10 @@ export const EquicordDevs = Object.freeze({
name: "mmeta",
id: 297075664530440192n
},
ZcraftElite: {
name: "ZcraftElite",
id: 926788037785047050n
},
} satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly