feat(v5): Kern 5.4.0 — Vorgaenge, Kompressions-Warteschlange, Roh-Aufraeumen, Sprachen je Titel, Erkennung v2
WAS: Der Rip endet, sobald die Roh-Dateien liegen; die Kompression laeuft danach in einer Warteschlange (eine zur Zeit), das Laufwerk ist frei fuer die naechste Disc. Jeder Rip ist jetzt ein Datensatz (Tabelle vorgaenge, Schema 4): Roh-Ordner, Zielordner, Phase, Auftrag als JSON. Darauf bauen: Roh-Aufraeumen nach Regel (Vorgabe 7 Tage, nur bei gelungener Kompression und nur wenn jede fertige Datei da und nicht leer ist), Wiederholen ab Kompression, Aufraeumen halber Dateien nach Abbruch/Fehler, Wiederaufnahme nach Neustart. Unkomprimierte Rips (4K, kein HandBrake) werden in den Zielordner VERSCHOBEN statt unter roh\ liegen zu bleiben. Sprachen je Titel reisen vom Info-Lauf (SINFO) bis in HandBrake; fremder Ton schaltet den deutschen Untertitel vor (--subtitle-default=1). Vor der Kompression prueft ein HandBrake-Scan die Roh-Datei. MakeMKVs Auswahlregel wird gesetzt, wenn keine steht, damit alle Sprachen in die Roh-Datei kommen. Staffel und erste Folge von Hand schlagen die Laufzeit-Raterei. Durchgehender Fortschritt, nach Groesse gewichtet. Erkennung v2: leere TMDb-Eintraege (Stubs) zaehlen nicht, Laufzeit wird nach dem Info-Lauf gegen TMDb gehalten, Titel-Struktur als Fingerabdruck, bdmt_deu als zweite Titel-Variante. TMDb liefert Logo, Tagline, Bewertung, FSK und Besetzung in derselben Anfrage. Laufwerks-Gesundheit aus dem Windows-Ereignisprotokoll (wevtutil): Disc oder Hardware. WARUM: Uebergabe-Punkte 1-5, 8-12 des SAVEPOINT vom 01.09.2026 — vom Commander gutgeheissen (7 Tage / Untertitel vorgewaehlt / entkoppelte Kompression waren seine Wahl). GEMESSEN (Regel D): HandBrake 1.11 Sprach-Schalter an einer ffmpeg- Testdatei (jpn/deu/deu/eng + deu/eng), HandBrake --scan --json Format, TMDb movie/149 + tv/46296 mit append_to_response, wevtutil /f:xml an diesem PC (338x cdrom 7, 8x cdrom 11 in 30 Tagen), makemkvcon --minlength, MakeMKV-Auswahlregel laut forum.makemkv.com t=4386. 348 Tests gruen (vorher 261), Typpruefung und Bau gruen. Oberflaeche folgt im naechsten Schritt. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
d70a81d194
commit
3f7f962dff
@@ -2,14 +2,50 @@
|
||||
// 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.
|
||||
//
|
||||
// 5.4.0 — Kino-Banner v2: Details holen in DERSELBEN Anfrage per
|
||||
// `append_to_response` Logo, Besetzung und Freigabe mit. Die Feldnamen
|
||||
// sind GEMESSEN (01.09.2026 mit dem hinterlegten v4-Token, Regel D):
|
||||
// movie/149?append_to_response=images,credits,release_dates
|
||||
// &include_image_language=de,en,null
|
||||
// → tagline „Neo-Tokyo ist am EXPLODIEREN", runtime 124,
|
||||
// vote_average 7.94, images.logos[{iso_639_1:'de'|'en'|null,
|
||||
// file_path:'/….png'|'.svg', width, height, vote_average}],
|
||||
// credits.cast[{name, character, profile_path, order}],
|
||||
// release_dates.results[{iso_3166_1:'DE', release_dates:[{certification:'16', type:3|5|6}]}]
|
||||
// tv/46296?append_to_response=images,credits,content_ratings
|
||||
// → episode_run_time [53], content_ratings.results[{iso_3166_1:'DE', rating:'18'}],
|
||||
// seasons[{season_number, name, episode_count}], number_of_episodes
|
||||
// configuration.images: logo_sizes w45…w500, profile_sizes w45,w185,h632
|
||||
// Und der Stub-Fund: „Spartacus: Gods of the Arena" gibt es als drei
|
||||
// leere Serien-Einträge (popularity 1, kein Backdrop, 1 Folge) — der
|
||||
// echte Inhalt ist die Extras-Staffel von „Spartacus" (46296).
|
||||
|
||||
export interface FilmTreffer {
|
||||
id: number
|
||||
title?: string
|
||||
name?: string
|
||||
poster_path?: string
|
||||
poster_path?: string | null
|
||||
backdrop_path?: string | null
|
||||
release_date?: string
|
||||
first_air_date?: string
|
||||
overview?: string
|
||||
popularity?: number
|
||||
}
|
||||
|
||||
export interface TmdbLogo {
|
||||
iso_639_1: string | null
|
||||
file_path: string
|
||||
width?: number
|
||||
height?: number
|
||||
vote_average?: number
|
||||
}
|
||||
|
||||
export interface TmdbCast {
|
||||
name: string
|
||||
character?: string
|
||||
profile_path?: string | null
|
||||
order?: number
|
||||
}
|
||||
|
||||
export interface FilmDetails {
|
||||
@@ -19,10 +55,22 @@ export interface FilmDetails {
|
||||
release_date?: string
|
||||
first_air_date?: string
|
||||
overview?: string
|
||||
poster_path?: string
|
||||
backdrop_path?: string
|
||||
poster_path?: string | null
|
||||
backdrop_path?: string | null
|
||||
runtime?: number
|
||||
genres?: Array<{ name: string }>
|
||||
tagline?: string
|
||||
vote_average?: number
|
||||
vote_count?: number
|
||||
popularity?: number
|
||||
episode_run_time?: number[]
|
||||
number_of_episodes?: number
|
||||
number_of_seasons?: number
|
||||
seasons?: Array<{ season_number: number; name?: string; episode_count?: number }>
|
||||
images?: { logos?: TmdbLogo[] }
|
||||
credits?: { cast?: TmdbCast[] }
|
||||
release_dates?: { results?: Array<{ iso_3166_1: string; release_dates?: Array<{ certification?: string; type?: number }> }> }
|
||||
content_ratings?: { results?: Array<{ iso_3166_1: string; rating?: string }> }
|
||||
}
|
||||
|
||||
export function istV4Token(key: string): boolean {
|
||||
@@ -31,6 +79,44 @@ export function istV4Token(key: string): boolean {
|
||||
|
||||
const BASIS = 'https://api.themoviedb.org/3'
|
||||
|
||||
/** Das beste Logo (pur): deutsch vor englisch vor sprachlos, darin das
|
||||
* bestbewertete. '' wenn keins. */
|
||||
export function logoAus(logos: readonly TmdbLogo[] | undefined): string {
|
||||
if (logos === undefined || logos.length === 0) return ''
|
||||
const rang = (l: TmdbLogo): number => (l.iso_639_1 === 'de' ? 0 : l.iso_639_1 === 'en' ? 1 : l.iso_639_1 === null ? 2 : 3)
|
||||
const sortiert = [...logos]
|
||||
.filter((l) => typeof l.file_path === 'string' && l.file_path.length > 0)
|
||||
.sort((a, b) => rang(a) - rang(b) || (b.vote_average ?? 0) - (a.vote_average ?? 0))
|
||||
return sortiert[0]?.file_path ?? ''
|
||||
}
|
||||
|
||||
/** Die deutsche Freigabe (pur): Film aus release_dates (Kino vor
|
||||
* Heimvideo vor TV), Serie aus content_ratings. '' wenn unbekannt. */
|
||||
export function fskAus(details: FilmDetails, typ: 'movie' | 'tv'): string {
|
||||
if (typ === 'tv') {
|
||||
const de = details.content_ratings?.results?.find((r) => r.iso_3166_1 === 'DE')
|
||||
return (de?.rating ?? '').trim()
|
||||
}
|
||||
const de = details.release_dates?.results?.find((r) => r.iso_3166_1 === 'DE')
|
||||
const eintraege = (de?.release_dates ?? []).filter((e) => (e.certification ?? '').trim().length > 0)
|
||||
if (eintraege.length === 0) return ''
|
||||
const rang = (typ2: number | undefined): number => (typ2 === 3 ? 0 : typ2 === 5 ? 1 : typ2 === 4 ? 2 : typ2 === 6 ? 3 : 4)
|
||||
return [...eintraege].sort((a, b) => rang(a.type) - rang(b.type))[0].certification!.trim()
|
||||
}
|
||||
|
||||
/** Die ersten Darsteller mit Bild (pur). */
|
||||
export function besetzungAus(
|
||||
cast: readonly TmdbCast[] | undefined,
|
||||
hoechstens = 6,
|
||||
): Array<{ name: string; rolle: string; bildPfad: string }> {
|
||||
if (cast === undefined) return []
|
||||
return [...cast]
|
||||
.filter((c) => typeof c.name === 'string' && c.name.length > 0)
|
||||
.sort((a, b) => (a.order ?? 999) - (b.order ?? 999))
|
||||
.slice(0, hoechstens)
|
||||
.map((c) => ({ name: c.name, rolle: (c.character ?? '').trim(), bildPfad: c.profile_path ?? '' }))
|
||||
}
|
||||
|
||||
export class TmdbClient {
|
||||
constructor(
|
||||
private readonly apiKey: string,
|
||||
@@ -84,11 +170,19 @@ export class TmdbClient {
|
||||
}
|
||||
|
||||
async movieDetails(id: number): Promise<FilmDetails | null> {
|
||||
return (await this.anfrage(`movie/${id}`, { language: 'de-DE' })) as FilmDetails | null
|
||||
return (await this.anfrage(`movie/${id}`, {
|
||||
language: 'de-DE',
|
||||
append_to_response: 'images,credits,release_dates',
|
||||
include_image_language: 'de,en,null',
|
||||
})) as FilmDetails | null
|
||||
}
|
||||
|
||||
async tvDetails(id: number): Promise<FilmDetails | null> {
|
||||
return (await this.anfrage(`tv/${id}`, { language: 'de-DE' })) as FilmDetails | null
|
||||
return (await this.anfrage(`tv/${id}`, {
|
||||
language: 'de-DE',
|
||||
append_to_response: 'images,credits,content_ratings',
|
||||
include_image_language: 'de,en,null',
|
||||
})) as FilmDetails | null
|
||||
}
|
||||
|
||||
/** Staffel-Details mit Episoden-Laufzeiten — für die Serien-Zuordnung. */
|
||||
|
||||
Reference in New Issue
Block a user