diff --git a/src/recc/omdb.test.ts b/src/recc/omdb.test.ts index 5f555dcc..e5cd6d7a 100644 --- a/src/recc/omdb.test.ts +++ b/src/recc/omdb.test.ts @@ -66,13 +66,35 @@ describe("fetchTitleMeta (by id)", () => { }); describe("fetchTitleMetaByName", () => { - it("builds a title lookup with year and type", async () => { + it("builds a title lookup with year and type, for a film", async () => { + // A film's OMDb year IS its identity — Ashfall 1999 and Ashfall 2024 are + // different films — so unlike a series (below), it is sent unconditionally. const { impl, urls } = jsonImpl(200, { Response: "True", imdbID: "tt2", Plot: "P", Poster: "https://p.jpg" }); - const res = await fetchTitleMetaByName("Harrowgate", "KEY", { year: 2022, type: "series", fetchImpl: impl }); + const res = await fetchTitleMetaByName("Ashfall", "KEY", { year: 1999, type: "movie", fetchImpl: impl }); expect(res).toEqual({ ok: true, type: null, imdbId: "tt2", plot: "P", posterUrl: "https://p.jpg" }); const u = urls[0]!; - expect(u).toContain("t=Harrowgate"); - expect(u).toContain("y=2022"); + expect(u).toContain("t=Ashfall"); + expect(u).toContain("y=1999"); + expect(u).toContain("type=movie"); + }); + + it("drops the year for a series — it names the release's own air date, not the show's debut", async () => { + // "The Boys S05 Season 5 2026 1080p..." parses year 2026, the season's own + // air year. OMDb keys a series by when it DEBUTED (2019 here), so sending + // y=2026 finds no match and OMDb falls back to an unrelated title that + // happens to answer to that year — a real bug this protects against. + const { impl, urls } = jsonImpl(200, { Response: "True", imdbID: "tt1190634", Plot: "P", Poster: "https://p.jpg" }); + const res = await fetchTitleMetaByName("The Boys", "KEY", { year: 2026, type: "series", fetchImpl: impl }); + expect(res).toEqual({ + ok: true, + type: null, + imdbId: "tt1190634", + plot: "P", + posterUrl: "https://p.jpg", + }); + const u = urls[0]!; + expect(u).toContain("t=The+Boys"); + expect(u).not.toContain("y=2026"); expect(u).toContain("type=series"); }); diff --git a/src/recc/omdb.ts b/src/recc/omdb.ts index 723a7d5f..bb5408a2 100644 --- a/src/recc/omdb.ts +++ b/src/recc/omdb.ts @@ -96,8 +96,19 @@ export async function fetchTitleMetaByName( } = {}, ): Promise { if (!title.trim()) return { ok: false, error: "no title" }; + // A film's OMDb year IS its identity — "Ashfall" 1999 and "Ashfall" 2024 are + // different films, and `y=` is what tells OMDb which one. A SERIES release's + // year is not the same kind of fact: it is usually when the release's own + // season aired, not when the show debuted, and OMDb keys a series by the + // latter. "The Boys S05 Season 5 2026 1080p..." parses year 2026 — the + // season's air year — and OMDb has no "The Boys" answering to that year, so + // an exact-match search comes back empty and falls back to an unrelated + // title that does. Every season of a show would carry a different embedded + // year and searching wrongly narrowed on each one, so the correct fix is not + // sending y= for a series at all — the title alone is the disambiguator here. + const seriesSafeYear = opts.type !== "series" ? opts.year : undefined; const params = new URLSearchParams({ t: title.trim() }); - if (opts.year) params.set("y", String(opts.year)); + if (seriesSafeYear) params.set("y", String(seriesSafeYear)); if (opts.type) params.set("type", opts.type); const wantsEpisode = opts.season !== undefined && opts.episode !== undefined; if (opts.season !== undefined) params.set("Season", String(opts.season)); @@ -114,7 +125,7 @@ export async function fetchTitleMetaByName( // function — the terminal directly, the browser via `/api/title` — and a // fallback written twice is the copy-then-drift this codebase keeps recording. const seriesParams = new URLSearchParams({ t: title.trim() }); - if (opts.year) seriesParams.set("y", String(opts.year)); + if (seriesSafeYear) seriesParams.set("y", String(seriesSafeYear)); if (opts.type) seriesParams.set("type", opts.type); const series = await request(seriesParams, apiKey, opts, `by name ${title} (poster)`); // A failed fallback leaves the episode answer exactly as it was: no poster is