import { SERIES_ID, MOVIE_ID, MOVIE_2_ID } from "./tmdb"; /** * Nachgebildete Jellyfin-Antworten. * * Damit lässt sich der ganze Abgleich prüfen, ohne einen Jellyfin-Server zu * brauchen. Der Aufbau entspricht dem, was Jellyfin tatsächlich liefert — * gekürzt auf die Felder, die Flimmer liest. * * Absichtlich enthalten: ein Item **ohne** TMDB-Kennung und eines mit einer * Kennung, die nicht im Katalog steht. Beides muss im Bericht landen, statt * den Lauf zu stören. */ export const JELLYFIN_URL = "http://jellyfin.test:8096"; export const JELLYFIN_USER_ID = "jf-user-0001"; export const JELLYFIN_TOKEN = "jf-token-abcdef"; export const SERIES_ITEM_ID = "jf-series-1"; export const items = { /** Film, gesehen — mit Termin. */ filmGesehen: { Id: "jf-movie-1", Name: "Die Stille zwischen den Bildern", Type: "Movie", ProviderIds: { Tmdb: String(MOVIE_ID) }, ProductionYear: 2024, UserData: { Played: true, PlayCount: 3, LastPlayedDate: "2026-07-14T20:15:00.0000000Z" }, }, /** Film, vorhanden aber nicht gesehen. */ filmUngesehen: { Id: "jf-movie-2", Name: "Die Stille zwischen den Bildern II", Type: "Movie", ProviderIds: { Tmdb: String(MOVIE_2_ID) }, ProductionYear: 2026, UserData: { Played: false, PlayCount: 0, LastPlayedDate: null }, }, /** Episode, gesehen — die TMDB-Kennung hängt an der Serie darüber. */ episodeGesehen: { Id: "jf-episode-1", Name: "Erste Nacht", Type: "Episode", SeriesId: SERIES_ITEM_ID, SeriesName: "Nordlicht", ParentIndexNumber: 1, IndexNumber: 1, ProviderIds: {}, UserData: { Played: true, PlayCount: 1, LastPlayedDate: "2026-08-01T21:00:00.0000000Z" }, }, /** Episode, ungesehen. */ episodeUngesehen: { Id: "jf-episode-2", Name: "Zweite Nacht", Type: "Episode", SeriesId: SERIES_ITEM_ID, SeriesName: "Nordlicht", ParentIndexNumber: 1, IndexNumber: 2, ProviderIds: {}, UserData: { Played: false, PlayCount: 0, LastPlayedDate: null }, }, /** Ohne TMDB-Kennung — landet im Bericht. */ ohneKennung: { Id: "jf-movie-3", Name: "Urlaubsvideo 2019", Type: "Movie", ProviderIds: {}, UserData: { Played: true, PlayCount: 1, LastPlayedDate: "2026-01-01T12:00:00.0000000Z" }, }, /** Kennung vorhanden, aber nicht im Katalog — landet ebenfalls im Bericht. */ fremd: { Id: "jf-movie-4", Name: "Ein fremder Film", Type: "Movie", ProviderIds: { Tmdb: "424242" }, UserData: { Played: false, PlayCount: 0, LastPlayedDate: null }, }, /** * Zweite Datei zu demselben fremden Film — so führt Jellyfin Extended * Editions. In der Auswahlliste muss der Titel trotzdem einmal stehen. */ fremdZweiteFassung: { Id: "jf-movie-5", Name: "Ein fremder Film (Extended)", Type: "Movie", ProviderIds: { Tmdb: "424242" }, UserData: { Played: false, PlayCount: 0, LastPlayedDate: null }, }, } as const; export const seriesItem = { Id: SERIES_ITEM_ID, Name: "Nordlicht", Type: "Series", ProviderIds: { Tmdb: String(SERIES_ID) }, }; export const ALLE_ITEMS = Object.values(items); type StubOptions = { /** Ohne UserData — so verhält sich Jellyfin bei Anmeldung per API-Key. */ ohneUserData?: boolean; /** Anmeldung ablehnen. */ abweisen?: boolean; }; /** * Hängt sich in `fetch` und beantwortet die Jellyfin-Endpunkte. * * Gibt die aufgerufenen Pfade zurück — damit lässt sich prüfen, dass ein * zweiter Lauf nicht mehr Arbeit macht als nötig. */ export function installJellyfinStub(options: StubOptions = {}) { const original = globalThis.fetch; const calls: string[] = []; globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => { const url = new URL(typeof input === "string" ? input : input.toString()); calls.push(url.pathname); const json = (body: unknown) => new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" }, }); if (url.pathname === "/Users/AuthenticateByName") { if (options.abweisen) return new Response("", { status: 401 }); return json({ AccessToken: JELLYFIN_TOKEN, User: { Id: JELLYFIN_USER_ID, Name: "florian" }, }); } if (url.pathname === "/System/Info") { return json({ ServerName: "Heimserver", Version: "10.10.3" }); } if (url.pathname === "/Items") { const ids = url.searchParams.get("Ids"); if (ids) { // Abfrage der Serien-Kopfdaten. const gesucht = new Set(ids.split(",")); return json({ Items: gesucht.has(SERIES_ITEM_ID) ? [seriesItem] : [] }); } const startIndex = Number(url.searchParams.get("StartIndex") ?? "0"); const limit = Number(url.searchParams.get("Limit") ?? "500"); const alle = ALLE_ITEMS.map((item) => options.ohneUserData ? { ...item, UserData: undefined } : item, ); return json({ Items: alle.slice(startIndex, startIndex + limit), TotalRecordCount: alle.length, }); } return new Response(JSON.stringify({ error: `Kein Fixture für ${url.pathname}` }), { status: 404, headers: { "content-type": "application/json" }, }); }) as typeof fetch; return { calls, restore() { globalThis.fetch = original; }, }; }