Ampel / ampel (push) Successful in 1m25s
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>
131 lines
4.2 KiB
TypeScript
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
|
|
}
|
|
}
|