The Episode Focus View's episode strip collapsed to just the current episode on some series. Two causes: - Series that expose episodes directly as children rather than under season folders yielded an empty season fetch, leaving allEpisodes empty. The library page now groups those flat episode children by their season number and synthesizes minimal season headers. - isCurrentEpisode over-matched: episodes with no season/episode number compared equal (undefined === undefined) and every one of them looked like the focused episode. Extracts the strip's pure logic into episodeStrip.ts so both behaviours are unit-tested, per the failing-test-first rule. TRACES: UR-058 | DR-087
62 lines
2.4 KiB
TypeScript
62 lines
2.4 KiB
TypeScript
// Pure logic for the "More Episodes" strip in EpisodeFocusView.
|
|
//
|
|
// Extracted from the component so it can be unit-tested: the strip must never
|
|
// collapse to just the current episode while real siblings exist, and it must
|
|
// not mistake number-less episodes for the current one.
|
|
//
|
|
// TRACES: UR-048 | DR-062
|
|
import type { MediaItem } from "$lib/api/types";
|
|
|
|
/**
|
|
* Does `ep` refer to the same episode as `current`?
|
|
*
|
|
* Matches by id first. Falls back to season+episode number, but ONLY when both
|
|
* numbers are known on both sides — otherwise `undefined === undefined` would
|
|
* mark every number-less episode as the current one (the bug that made the
|
|
* whole strip look like the current episode).
|
|
*/
|
|
export function isCurrentEpisode(ep: MediaItem, current: MediaItem): boolean {
|
|
if (ep.id === current.id) return true;
|
|
if (
|
|
ep.indexNumber == null || current.indexNumber == null ||
|
|
ep.parentIndexNumber == null || current.parentIndexNumber == null
|
|
) {
|
|
return false;
|
|
}
|
|
return (
|
|
ep.parentIndexNumber === current.parentIndexNumber &&
|
|
ep.indexNumber === current.indexNumber
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The window of episodes shown under the hero: up to 3 before and 6 after the
|
|
* current episode. Degrades gracefully:
|
|
* - prefers the current season, falling back to the full list when the season
|
|
* is unknown (e.g. the episode was fetched directly on an API-ID mismatch);
|
|
* - splices the current episode into the pool at its numeric position when it
|
|
* isn't present, so it still anchors the window;
|
|
* - returns just `[current]` only when there genuinely are no other episodes.
|
|
*/
|
|
export function adjacentEpisodes(current: MediaItem, allEpisodes: MediaItem[]): MediaItem[] {
|
|
const seasonMatches = allEpisodes.filter(
|
|
(e) => current.parentIndexNumber != null && e.parentIndexNumber === current.parentIndexNumber
|
|
);
|
|
const pool = (seasonMatches.length > 0 ? seasonMatches : allEpisodes)
|
|
.slice()
|
|
.sort((a, b) => (a.indexNumber ?? 0) - (b.indexNumber ?? 0));
|
|
|
|
let idx = pool.findIndex((e) => isCurrentEpisode(e, current));
|
|
|
|
if (idx === -1) {
|
|
const epNum = current.indexNumber ?? 0;
|
|
const insertAt = pool.findIndex((e) => (e.indexNumber ?? 0) > epNum);
|
|
idx = insertAt === -1 ? pool.length : insertAt;
|
|
pool.splice(idx, 0, current);
|
|
}
|
|
|
|
const start = Math.max(0, idx - 3);
|
|
const end = Math.min(pool.length, idx + 7);
|
|
return pool.slice(start, end);
|
|
}
|