fix(v5): Laufzeit-Pfade plattformneutral — pipeline/struktur auf node:path
Ampel / ampel (push) Successful in 1m21s
Ampel / ampel (push) Successful in 1m21s
Der NFO-Ablage-Test brach in der Linux-CI: pipeline/struktur bauten mit path.win32 auf POSIX-Testordnern (mkdir legte 'x\Filme' als EINEN Namen an, readdir fand nichts). Regel jetzt dokumentiert: Module ueber LAUFZEIT-Wurzeln (Ablage) nutzen plattform-path — zur echten Laufzeit immer Windows, in der CI physisch korrekt auf POSIX; Module mit FESTER Windows-Semantik (katalog, dateiDaneben) bleiben bei path.win32. Testerwartungen entsprechend ueber join() statt Literale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
3dc296f071
commit
e840b7f327
@@ -0,0 +1,127 @@
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user