diff --git a/apps/backend/src/__tests__/meta-cache.test.ts b/apps/backend/src/__tests__/meta-cache.test.ts index d3ab2be..86992c6 100644 --- a/apps/backend/src/__tests__/meta-cache.test.ts +++ b/apps/backend/src/__tests__/meta-cache.test.ts @@ -1,4 +1,4 @@ -import type { StremioMeta } from "@stremlist/shared"; +import type { StremioManifest, StremioMeta } from "@stremlist/shared"; import { describe, it, expect, beforeEach, vi } from "vitest"; import app from "../index.js"; @@ -68,6 +68,13 @@ const SHAWSHANK: StremioMeta = { description: "", }; +const BREAKING_BAD: StremioMeta = { + ...SHAWSHANK, + id: "tt0903747", + type: "series", + name: "Breaking Bad", +}; + interface MetaResponse { meta: Record | null; } @@ -83,6 +90,57 @@ beforeEach(() => { }); describe("meta route serves from cache only", () => { + it("declines cached series without accessing the cache so clients try an episode provider", async () => { + seedUser(OWNER); + seedWatchlist(UUID_1); + seedCache(UUID_1, [BREAKING_BAD]); + const lookup = vi.spyOn(watchlistSvc, "findMetaInUserCache"); + + const res = await app.request( + `/${OWNER}/meta/series/${BREAKING_BAD.id}.json`, + ); + + expect(res.status).toBe(200); + expect((await res.json()) as MetaResponse).toEqual({ meta: null }); + expect(lookup).not.toHaveBeenCalled(); + }); + + it.each(["/manifest.json", `/${OWNER}/manifest.json`])( + "%s advertises movie metadata and keeps both catalog types", + async (path) => { + seedUser(OWNER); + seedWatchlist(UUID_1); + + const res = await app.request(path); + const manifest = (await res.json()) as StremioManifest; + + expect(res.status).toBe(200); + expect(manifest.resources).toEqual([ + "catalog", + { name: "meta", types: ["movie"], idPrefixes: ["tt"] }, + ]); + expect(manifest.types).toEqual(["movie", "series"]); + expect(manifest.catalogs.map((catalog) => catalog.type)).toEqual([ + "movie", + "series", + ]); + }, + ); + + it("keeps cached series available in the series catalog", async () => { + seedUser(OWNER); + seedWatchlist(UUID_1); + seedCache(UUID_1, [SHAWSHANK, BREAKING_BAD]); + + const res = await app.request( + `/${OWNER}/catalog/series/wl-${UUID_1}-series.json`, + ); + const body = (await res.json()) as { metas: StremioMeta[] }; + + expect(res.status).toBe(200); + expect(body.metas).toEqual([BREAKING_BAD]); + }); + it("returns the cached meta for an item that is in the user's list", async () => { seedUser(OWNER); seedWatchlist(UUID_1); @@ -114,7 +172,7 @@ describe("meta route serves from cache only", () => { seedWatchlist(UUID_1); seedCache(UUID_1, [SHAWSHANK]); - const res = await app.request(`/${OWNER}/meta/series/tt99999999.json`); + const res = await app.request(`/${OWNER}/meta/movie/tt99999999.json`); expect(res.status).toBe(200); expect((await res.json()) as MetaResponse).toEqual({ meta: null }); diff --git a/apps/backend/src/routes/meta.ts b/apps/backend/src/routes/meta.ts index fc01e15..3910fbd 100644 --- a/apps/backend/src/routes/meta.ts +++ b/apps/backend/src/routes/meta.ts @@ -3,16 +3,15 @@ import { findMetaInUserCache } from "../services/watchlist"; const meta = new Hono(); -// Stremlist serves meta ONLY from cache: it never scrapes IMDb, never throws, -// and on any miss returns { meta: null } so Stremio falls back to Cinemeta. The -// `meta` resource stays advertised in BASE_MANIFEST because some Stremlist-only -// movies have no Cinemeta entry and would otherwise lose their detail/stream -// page (dropping it is a regression). Removing the old per-request watchlist -// fan-out — which re-scraped IMDb on stale caches and 500'd on failure — is what -// fixes the prod 500/504 storm on /meta. +// Keep cache-only movie metadata for titles missing from Cinemeta. Catalog +// previews lack episodes, so series must resolve through another meta addon. meta.get("/:userId/meta/:type/:id.json", async (c) => { const userId = c.req.param("userId"); const type = c.req.param("type"); + + // Clients can request the catalog source directly or retain an old manifest. + if (type === "series") return c.json({ meta: null }); + const id = (c.req.param("id") ?? c.req.param("id.json")).replace( /\.json$/u, "", diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index 218b118..5e67f9e 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -99,7 +99,7 @@ export const BASE_MANIFEST: StremioManifest = { "catalog", { name: "meta", - types: ["movie", "series"], + types: ["movie"], idPrefixes: ["tt"], }, ],