refactor(plex): share showEpisodeKeys + honest empty Play-all
Extract the show->ordered-episode-keys flatten into one pure, unit-tested helper (lib/plexQueue.ts) and use it at all three sites — the collection Play-all (S4) and the show/season play-through pages in plexViews — so the season-then-episode queue-order invariant lives in one place instead of three hand-rolled copies. Behaviour-identical (the helper's ?? [] is a safe superset of the collection site's previously non-null read). Also: a show-collection whose shows expand to zero episodes had an enabled Play-all that silently no-op'd — the same 'does nothing' state S4 removed for the empty-queue case. Surface an honest info notice (plex.collection.playEmpty, HU+EN) instead.
This commit is contained in:
@@ -6,6 +6,8 @@ import { qk } from "../lib/queryKeys";
|
||||
import { api, isIntrinsicReadOnly, type PlexCard, type PlexCollectionDetail } from "../lib/api";
|
||||
import { formatRuntime } from "../lib/format";
|
||||
import { notifyActionError, notifyCollectionEditError } from "../lib/actionErrors";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { showEpisodeKeys } from "../lib/plexQueue";
|
||||
import { useConfirm } from "./ConfirmProvider";
|
||||
import { BackBtn, ActionBtn, DetailHero } from "../lib/plexDetailUi";
|
||||
import { PlexPosterCard } from "./plexCards";
|
||||
@@ -87,7 +89,7 @@ export default function PlexCollectionView({
|
||||
queryKey: qk.plexShow(c.id),
|
||||
queryFn: () => api.plexShow(c.id),
|
||||
});
|
||||
return detail.seasons.flatMap((s) => s.episodes.map((e) => e.id));
|
||||
return showEpisodeKeys(detail);
|
||||
}),
|
||||
);
|
||||
return perMember.flat();
|
||||
@@ -98,7 +100,10 @@ export default function PlexCollectionView({
|
||||
try {
|
||||
let queue = await buildQueue();
|
||||
if (shuffle) queue = shuffled(queue);
|
||||
// canPlay gates on "has a show member", but a show can expand to zero episodes (unmatched /
|
||||
// empty). An enabled Play-all that silently no-ops is a dishonest state, so say so plainly.
|
||||
if (queue.length > 0) onPlay(queue[0]!, queue);
|
||||
else notify({ level: "info", message: t("plex.collection.playEmpty") });
|
||||
} catch (e) {
|
||||
notifyActionError(e, t("plex.collection.playFailed"));
|
||||
} finally {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { plexLang } from "../i18n";
|
||||
import { pickRatingBadge } from "../lib/plexRating";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
import { toggleWatchState } from "../lib/plexWatch";
|
||||
import { showEpisodeKeys } from "../lib/plexQueue";
|
||||
import {
|
||||
ActionBtn,
|
||||
BackBtn,
|
||||
@@ -306,7 +307,7 @@ export function PlexShowView({
|
||||
|
||||
const show = d?.show;
|
||||
const showLib = show?.library ?? undefined;
|
||||
const allKeys = (d?.seasons ?? []).flatMap((se) => se.episodes.map((e) => e.id));
|
||||
const allKeys = showEpisodeKeys(d);
|
||||
const watched = show?.status === "watched";
|
||||
const {
|
||||
artBg,
|
||||
@@ -568,7 +569,7 @@ export function PlexSeasonView({
|
||||
// Play-through queue spans the WHOLE show (all seasons, in order) so a season's last episode rolls
|
||||
// straight into the next season's first — the same cross-season order the show page already uses.
|
||||
// seasonKeys stays only for the season-scoped "add to playlist" action.
|
||||
const allKeys = (d?.seasons ?? []).flatMap((s) => s.episodes.map((e) => e.id));
|
||||
const allKeys = showEpisodeKeys(d);
|
||||
const watched = se?.status === "watched";
|
||||
const { artBg, showCast, crossfade, toggleArtBg, toggleCast, toggleCrossfade } = useDetailPrefs();
|
||||
useCrossfadeBackdrop(
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"deleteConfirm": "Delete the collection \"{{title}}\"? This removes it from Plex too.",
|
||||
"editFailed": "Collection edit failed",
|
||||
"playFailed": "Couldn't build the play queue — try again.",
|
||||
"playEmpty": "Nothing to play in this collection yet.",
|
||||
"newCollection": "New collection",
|
||||
"namePlaceholder": "Collection name",
|
||||
"nameTaken": "A collection with this name already exists.",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"deleteConfirm": "Törlöd a(z) „{{title}}” kollekciót? Ez a Plexből is eltávolítja.",
|
||||
"editFailed": "A kollekció szerkesztése sikertelen",
|
||||
"playFailed": "Nem sikerült összeállítani a lejátszási sort — próbáld újra.",
|
||||
"playEmpty": "Ebben a kollekcióban még nincs mit lejátszani.",
|
||||
"newCollection": "Új kollekció",
|
||||
"namePlaceholder": "Kollekció neve",
|
||||
"nameTaken": "Már létezik ilyen nevű kollekció.",
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { showEpisodeKeys } from "./plexQueue";
|
||||
import type { PlexCard, PlexSeasonDetail, PlexShowDetail } from "./api";
|
||||
|
||||
const ep = (id: string): PlexCard => ({ id }) as PlexCard;
|
||||
const season = (episodes: PlexCard[]): PlexSeasonDetail =>
|
||||
({ episodes }) as unknown as PlexSeasonDetail;
|
||||
const show = (seasons: PlexSeasonDetail[]): PlexShowDetail =>
|
||||
({ seasons }) as unknown as PlexShowDetail;
|
||||
|
||||
describe("showEpisodeKeys", () => {
|
||||
it("flattens seasons in order, episodes in order, concatenated across seasons", () => {
|
||||
const detail = show([
|
||||
season([ep("s1e1"), ep("s1e2")]),
|
||||
season([ep("s2e1"), ep("s2e2"), ep("s2e3")]),
|
||||
]);
|
||||
expect(showEpisodeKeys(detail)).toEqual(["s1e1", "s1e2", "s2e1", "s2e2", "s2e3"]);
|
||||
});
|
||||
|
||||
it("returns [] for a show with no seasons", () => {
|
||||
expect(showEpisodeKeys(show([]))).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns [] for a season with no episodes (the enabled-but-empty Play-all case)", () => {
|
||||
expect(showEpisodeKeys(show([season([])]))).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns [] for null/undefined so an in-flight query's data passes straight through", () => {
|
||||
expect(showEpisodeKeys(null)).toEqual([]);
|
||||
expect(showEpisodeKeys(undefined)).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { PlexShowDetail } from "./api";
|
||||
|
||||
// The ordered episode rating-keys of a show: season order, then episode order within each season —
|
||||
// the play-queue order the show/season pages and the collection Play-all all rely on. Kept in ONE
|
||||
// place so that invariant (and any future explicit sort, e.g. pushing season 0 "Specials" last)
|
||||
// lives at a single site rather than the three hand-rolled `seasons.flatMap(...)` copies it grew
|
||||
// into. `detail` is nullable so callers can pass an in-flight query's `data` straight through.
|
||||
export function showEpisodeKeys(detail: PlexShowDetail | null | undefined): string[] {
|
||||
return (detail?.seasons ?? []).flatMap((s) => s.episodes.map((e) => e.id));
|
||||
}
|
||||
Reference in New Issue
Block a user