diff --git a/frontend/src/components/PlexInfo.tsx b/frontend/src/components/PlexInfo.tsx
index 9bca97cb..79dcce46 100644
--- a/frontend/src/components/PlexInfo.tsx
+++ b/frontend/src/components/PlexInfo.tsx
@@ -10,6 +10,7 @@ import { DetailCustomizeMenu, Filterable, PrefToggle } from "./plexDetailUi";
import { DetailAside, DetailAsideToggles, DetailKeywords } from "./plexDetailAside";
import {
DETAIL_POSTER,
+ episodeLine,
tmdbBackdropUrl,
useCrossfadeBackdrop,
useDetailAside,
@@ -315,7 +316,7 @@ export default function PlexInfo({
)}
{detail.kind === "episode" && (
- S{detail.season_number}·E{detail.episode_number} — {detail.title}
+ {episodeLine(detail)}
)}
diff --git a/frontend/src/components/PlexPlayer.tsx b/frontend/src/components/PlexPlayer.tsx
index c14ec731..575332ea 100644
--- a/frontend/src/components/PlexPlayer.tsx
+++ b/frontend/src/components/PlexPlayer.tsx
@@ -52,6 +52,7 @@ import { Ctrl, PanelHead } from "./plexPlayerControls";
import { PlexPlayerSettings } from "./PlexPlayerSettings";
import { PlexStatsOverlay } from "./PlexStatsOverlay";
import { createFragMeter } from "../lib/plexStats";
+import { episodeLine } from "../lib/plexDetail";
import { HlsEngine } from "../lib/hlsEngine";
import { createHlsPort } from "../lib/hlsPort";
import { enabled, log } from "../lib/log";
@@ -586,10 +587,7 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
useMediaSession(
videoEl,
detail && {
- title:
- detail.kind === "episode" && detail.season_number != null
- ? `S${detail.season_number}·E${detail.episode_number} — ${detail.title}`
- : detail.title,
+ title: detail.kind === "episode" ? episodeLine(detail) : detail.title,
artist:
detail.kind === "episode" ? detail.show_title : detail.year ? String(detail.year) : null,
artwork: api.plexImageUrl(id),
@@ -1258,9 +1256,7 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
{detail?.kind === "episode" && detail.show_title ? detail.show_title : detail?.title}
{detail?.kind === "episode" && (
-
- S{detail.season_number}·E{detail.episode_number} — {detail.title}
-
+ {episodeLine(detail)}
)}
{/* Live wall clock — handy on a lean-back / HTPC screen (toggle + style in the Clock tab). */}
diff --git a/frontend/src/lib/plexDetail.episodeLine.test.ts b/frontend/src/lib/plexDetail.episodeLine.test.ts
new file mode 100644
index 00000000..a5454062
--- /dev/null
+++ b/frontend/src/lib/plexDetail.episodeLine.test.ts
@@ -0,0 +1,20 @@
+// @vitest-environment jsdom
+import { describe, expect, it } from "vitest";
+import { episodeLine } from "./plexDetail";
+
+describe("episodeLine", () => {
+ it("names the episode by its season and number", () => {
+ expect(episodeLine({ season_number: 2, episode_number: 7, title: "Pilot" })).toBe(
+ "S2·E7 — Pilot",
+ );
+ });
+
+ it("is the title alone for an episode Plex has no number for", () => {
+ expect(episodeLine({ season_number: 1, episode_number: null, title: "Special" })).toBe(
+ "Special",
+ );
+ expect(episodeLine({ season_number: null, episode_number: 3, title: "Special" })).toBe(
+ "Special",
+ );
+ });
+});
diff --git a/frontend/src/lib/plexDetail.ts b/frontend/src/lib/plexDetail.ts
index c2c54de5..002e068a 100644
--- a/frontend/src/lib/plexDetail.ts
+++ b/frontend/src/lib/plexDetail.ts
@@ -210,6 +210,17 @@ export function epLabel(ep?: PlexCard | null): string | undefined {
return ep.title;
}
+/** An episode's line under its show's name: "S1·E4 — Title" — or the title alone when Plex has no
+ * number for it (a special): a template literal printed the missing one as "Enull", JSX as "E". */
+export function episodeLine(ep: {
+ season_number?: number | null;
+ episode_number?: number | null;
+ title: string;
+}): string {
+ if (ep.season_number == null || ep.episode_number == null) return ep.title;
+ return `S${ep.season_number}·E${ep.episode_number} — ${ep.title}`;
+}
+
// Plex names seasons in the library's language ("Season 1", "Specials"), so a Hungarian reader got
// English there. A generic name is rebuilt from the number in the reader's language; a season
// with a name of its own ("Book One: Water") keeps it.