b584cc29ad
Ampel / ampel (push) Successful in 29s
Commander-Ziel: All-in-one, universell, weitergebbar.
- First-Run-Wizard: startet automatisch bei neuer Installation (Keys,
Verarbeitung, erkannte Hardware); /setup + /setup/complete
- Data-Mounts via UI: Einstellungen -> Speicherziele haengt NFS/SMB direkt
ein (mounts.py, CAP_SYS_ADMIN + rshared-Propagation, Auto-Remount beim
Start, CIFS-Creds via Datei statt Kommandozeile); nfs-common/cifs-utils
im api-Image
- Encoder-Erkennung: jeder Worker meldet beim Start ehrlich seine
Faehigkeiten (caps.py -> workers-Tabelle), GET /capabilities, Anzeige
in Wizard + Verarbeitung-Tab
- Task-Split: transcode_files als eigener Task auf Queue "transcode"
(Basis fuer optionale Remote-GPU-Worker, deploy/remote-transcode-worker.yml
EXPERIMENTELL) + POST /jobs/{id}/retry-transcode + UI-Knopf
"Neu komprimieren" bei fehlgeschlagenen Jobs
- API-Keys aus der DB: Settings-UI/Wizard ueberstimmen Env — vorher waren
die Key-Felder im UI reine Dekoration (Clients lasen nur Env)
- README komplett neu: generischer Schnellstart, Laufwerk-Override via
docker-compose.override.yml, Architektur, Env-Tabelle
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
79 lines
2.6 KiB
Python
79 lines
2.6 KiB
Python
"""OMDb API Client — Fallback-Quelle für die Metadaten-Erkennung.
|
||
|
||
Kommt zum Zug, wenn TMDB nichts (Sicheres) findet. Kostenloser Key von
|
||
https://www.omdbapi.com/apikey.aspx (1000 Anfragen/Tag reichen fürs Heimlab).
|
||
"""
|
||
|
||
from typing import Dict, Optional
|
||
|
||
import requests
|
||
|
||
from cache import get, set as cache_set
|
||
from config import settings
|
||
|
||
OMDB_BASE_URL = "https://www.omdbapi.com/"
|
||
|
||
|
||
def parse_runtime_minutes(runtime: str) -> int:
|
||
"""OMDb liefert '142 min' als Text → Minuten als Zahl (pure Funktion, testbar)."""
|
||
if not runtime:
|
||
return 0
|
||
teil = runtime.strip().split(" ")[0]
|
||
return int(teil) if teil.isdigit() else 0
|
||
|
||
|
||
def parse_year(year: str) -> Optional[int]:
|
||
"""OMDb-Jahr kann '1999' oder '2005–2012' (Serie) sein → erstes Jahr."""
|
||
if not year:
|
||
return None
|
||
erste = year.strip()[:4]
|
||
return int(erste) if erste.isdigit() else None
|
||
|
||
|
||
class OMDbClient:
|
||
def __init__(self):
|
||
# DB-Einstellung (Settings-UI/Wizard) gewinnt gegen die Env-Variable
|
||
from db import get_settings
|
||
self.api_key = get_settings().get("omdbApiKey") or settings.omdb_api_key
|
||
self.session = requests.Session()
|
||
|
||
def lookup(self, title: str, year: Optional[int] = None) -> Optional[Dict]:
|
||
"""Sucht per Titel (t=), liefert normalisierte Metadaten oder None."""
|
||
if not self.api_key:
|
||
return None
|
||
|
||
cache_key = f"omdb:{title.lower()}:{year or ''}"
|
||
cached = get(cache_key)
|
||
if cached:
|
||
return cached
|
||
|
||
params = {"apikey": self.api_key, "t": title, "r": "json"}
|
||
if year:
|
||
params["y"] = str(year)
|
||
|
||
try:
|
||
response = self.session.get(OMDB_BASE_URL, params=params, timeout=10)
|
||
response.raise_for_status()
|
||
data = response.json()
|
||
except Exception as e:
|
||
print(f"OMDb API Error: {e}")
|
||
return None
|
||
|
||
if data.get("Response") != "True":
|
||
return None
|
||
|
||
ergebnis = {
|
||
"type": "tv" if data.get("Type") == "series" else "movie",
|
||
"id": data.get("imdbID", ""),
|
||
"title": data.get("Title", title),
|
||
"year": parse_year(data.get("Year", "")),
|
||
"overview": data.get("Plot", ""),
|
||
"poster_path": data.get("Poster", "") if data.get("Poster") != "N/A" else "",
|
||
"backdrop_path": "",
|
||
"runtime": parse_runtime_minutes(data.get("Runtime", "")),
|
||
"genres": [g.strip() for g in data.get("Genre", "").split(",") if g.strip()],
|
||
"source": "omdb",
|
||
}
|
||
cache_set(cache_key, ergebnis)
|
||
return ergebnis
|