CI / pruefen (pull_request) Canceled after 0s
Der Abgleich ordnete nur gegen Titel zu, die Flimmer schon kannte. Auf einer frisch aufgesetzten Instanz meldet Jellyfin damit tausend Eintraege und Flimmer ordnet keinen einzigen zu — der Bericht konnte nur aufzaehlen, was er nicht einsortieren konnte. Aus dem Bericht wird eine Auswahlliste: anhaken, was uebernommen werden soll, und nur das aus TMDB nachladen. Ein pauschales Haekchen waere bei tausend Eintraegen keine Wahl gewesen — in einer Mediathek liegen Konzertmitschnitte neben dem, was man wirklich verfolgt. Der Bericht ist dafuer umgebaut: - Zusammengefasst je TMDB-Kennung. Jellyfin fuehrt oft mehrere Dateien zum selben Film; in der Liste steht er einmal, mit der Zahl der Dateien. - Die Grenze stand bei 50 und war bei ueber tausend gelesenen Eintraegen ein Blick durchs Schluesselloch. Jetzt 1000, und was darueber hinausgeht, wird gezaehlt statt stillschweigend abgeschnitten. - Der Typ kommt aus dem Item statt aus dem Umkehrschluss "keine Episode, also Film". Eine Probe zeigte sonst eine Serie als Film. Nachgeladen wird nur auf Klick, hoechstens hundert je Lauf, und ein einzelner Fehlschlag beendet den Lauf nicht. Aus der Liste verschwindet nur, was tatsaechlich ankam: ein Fehlschlag laesst den Eintrag stehen, damit man es erneut versuchen kann, ohne den ganzen Abgleich zu wiederholen. Mein erster Entwurf strich auch Gescheiterte — der Test hielt das falsche Verhalten fest und wurde mitkorrigiert. Eintraege ohne TMDB-Kennung bleiben sichtbar, aber nicht auswaehlbar: dort fehlt die Kennung in Jellyfin selbst, da kann Flimmer nichts holen. Fuenf Tests dazu. Der Importschritt selbst laesst sich lokal nur ueber die Domaenenschicht pruefen — der Entwicklungs-Stub speist sich aus dem Katalog und kann per Definition nichts liefern, was dort fehlt. Der Klickweg belegt Liste, Entdopplung, Suche, Auswahl, die gesperrten Zeilen und die ehrliche Fehlermeldung. Refs #16
173 lines
5.3 KiB
TypeScript
173 lines
5.3 KiB
TypeScript
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;
|
|
},
|
|
};
|
|
}
|