Files
rippy/rippy-windows/src/kern/metadaten/tmdb.ts
T
HitonabiandClaude Fable 5 c5dd90cc18
Ampel / ampel (push) Successful in 1m25s
fix(v5): Die drei Funde aus dem ersten Commander-Test
FUND 1 — 'Key korrekt, aber als ungueltig angezeigt': Stimmt beides.
Live gemessen: Der aktuelle Forum-Key ist in Ordnung, aber MakeMKV
1.18.4 ist ZU ALT fuer ihn — makemkvcon meldet 5020 UND 5021 zusammen,
und die alte Pruefung brach beim ersten Code ab und beschuldigte den
Schluessel. urteilAusAusgabe sammelt jetzt ALLE Codes (5021 schlaegt
5020, MakeMKV-Version aus MSG 1005 wird mitgelesen) und die Kachel
sagt den wahren Satz: 'Der Key ist in Ordnung — MakeMKV 1.18.4 ist zu
alt. Rippy hat ihn entfernt, damit der Beta-Modus weiterlaeuft (Rips
klappen trotzdem). Abhilfe: MakeMKV aktualisieren.' Neues Feld
weiterBetrieb: Beta-Modus ist GELB (Warnung), nicht rot. Test mit der
echten Ausgabe als Fixture.

FUND 2 — 'Komprimieren auf CPU statt GPU': Der Default 'H.265 MKV
1080p30' ist ein CPU-Preset. Neue GEMESSENE Empfehlung
(gemeinsam/preset-empfehlung.ts): aus den echten Backends und der
echten Preset-Liste des eigenen HandBrake — VCN vor NVENC vor QSV vor
CPU, nur Namen die WIRKLICH in der Liste stehen, HandBrake-Presets
deckeln die Aufloesung nur (DVD bleibt SD). Kette: eigene Wahl >
allgemeines Preset > Empfehlung > CPU-Default. Auf dem Commander-PC:
bluray/dvd -> 'H.265 VCN 1080p', uhd -> 'H.265 VCN 2160p 4K'. Die
Preset-Auswahl zeigt 'automatisch: <Empfehlung>' als Leer-Option.

FUND 3 — 'keine Alternative waehlbar': 'Aendern …'-Dialog an der
Laufwerks-Kachel (§ 5 Schritt 4): TMDb-Suche (Film + Serie, mit
Postern), Klick uebernimmt — Nutzer-Wahl gilt als 100 % und steuert
Ablage-Ordner, NFO und Poster. Neue Nachrichten metadaten-suchen /
zuordnung-setzen / metadaten-vorschlaege.

Dazu: Poster in der Laufwerks-Kachel (CSP um image.tmdb.org
erweitert), Restzeit-Schaetzung am Fortschritt.

192 Tests gruen, Smoke gruen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 20:20:10 +02:00

131 lines
4.2 KiB
TypeScript

// TMDb-Client (KONZEPT § 4.2) — Regeln aus clients/tmdb.py: Der Key kann
// ein v3-Key (32 Hex, api_key-Query) ODER ein v4-Token ('eyJ…',
// Bearer-Header) sein — der Befund vom 24.07.: der alte Client konnte nur
// eine Form, und die andere sah aus wie „falscher Key". Sprache de-DE.
export interface FilmTreffer {
id: number
title?: string
name?: string
poster_path?: string
release_date?: string
first_air_date?: string
}
export interface FilmDetails {
id: number
title?: string
name?: string
release_date?: string
first_air_date?: string
overview?: string
poster_path?: string
backdrop_path?: string
runtime?: number
genres?: Array<{ name: string }>
}
export function istV4Token(key: string): boolean {
return key.startsWith('eyJ')
}
const BASIS = 'https://api.themoviedb.org/3'
export class TmdbClient {
constructor(
private readonly apiKey: string,
private readonly holen: typeof fetch = fetch,
) {}
get verfuegbar(): boolean {
return this.apiKey.length > 0
}
private async anfrage(weg: string, params: Record<string, string>): Promise<unknown> {
if (!this.verfuegbar) return null
const url = new URL(`${BASIS}/${weg}`)
for (const [name, wert] of Object.entries(params)) url.searchParams.set(name, wert)
const kopf: Record<string, string> = { Accept: 'application/json' }
if (istV4Token(this.apiKey)) {
kopf['Authorization'] = `Bearer ${this.apiKey}`
} else {
url.searchParams.set('api_key', this.apiKey)
}
try {
const antwort = await this.holen(url.toString(), {
headers: kopf,
signal: AbortSignal.timeout(15_000),
})
if (antwort.status >= 300) return null
return await antwort.json()
} catch {
// R2: Ein verpasster Abruf ist keine Nachricht über die Welt — der
// Aufrufer bekommt null („weiß nicht"), nie eine leere Behauptung.
return null
}
}
async searchMovie(titel: string): Promise<FilmTreffer[] | null> {
const daten = (await this.anfrage('search/movie', {
query: titel,
language: 'de-DE',
include_adult: 'false',
})) as { results?: FilmTreffer[] } | null
return daten?.results ?? null
}
async searchTv(titel: string): Promise<FilmTreffer[] | null> {
const daten = (await this.anfrage('search/tv', {
query: titel,
language: 'de-DE',
include_adult: 'false',
})) as { results?: FilmTreffer[] } | null
return daten?.results ?? null
}
async movieDetails(id: number): Promise<FilmDetails | null> {
return (await this.anfrage(`movie/${id}`, { language: 'de-DE' })) as FilmDetails | null
}
async tvDetails(id: number): Promise<FilmDetails | null> {
return (await this.anfrage(`tv/${id}`, { language: 'de-DE' })) as FilmDetails | null
}
/** Staffel-Details mit Episoden-Laufzeiten — für die Serien-Zuordnung. */
async tvStaffel(id: number, staffel: number): Promise<{ episodes?: Array<{ episode_number: number; runtime?: number }> } | null> {
return (await this.anfrage(`tv/${id}/season/${staffel}`, { language: 'de-DE' })) as {
episodes?: Array<{ episode_number: number; runtime?: number }>
} | null
}
/** IMDb-ID → deutscher Datensatz (für OMDb-Treffer, die nur Englisch
* können — Commander-Wunsch 24.07.). */
async findByImdb(imdbId: string): Promise<Record<string, unknown> | null> {
const daten = (await this.anfrage(`find/${imdbId}`, {
external_source: 'imdb_id',
language: 'de-DE',
})) as { movie_results?: FilmDetails[]; tv_results?: FilmDetails[] } | null
const film = daten?.movie_results?.[0]
if (film !== undefined) {
return {
title: film.title,
overview: film.overview,
poster_path: film.poster_path,
year: film.release_date !== undefined && film.release_date.length >= 4 ? Number(film.release_date.slice(0, 4)) : null,
type: 'movie',
}
}
const serie = daten?.tv_results?.[0]
if (serie !== undefined) {
return {
title: serie.name,
overview: serie.overview,
poster_path: serie.poster_path,
year: serie.first_air_date !== undefined && serie.first_air_date.length >= 4 ? Number(serie.first_air_date.slice(0, 4)) : null,
type: 'tv',
}
}
return null
}
}