- Versionsanzeige für den Windows-Worker im UI inkl. Prüfung - Absicherung der install.sh gegen fehlendes systemd - Serien-Episoden-Erkennung und Laufzeitabgleich anhand TMDB-Daten (Heuristik) - Umstellung der Windows-Worker-Installation auf SchTasks (Dienst-Ersatz) - Kodi-Bibliotheks-Refresh über JSON-RPC integriert
This commit is contained in:
@@ -401,4 +401,16 @@ def werkzeug_versionen() -> dict:
|
||||
info["schluessel"] = str(makemkv_daten.schluesselspeicher_status().get("schluessel", 0))
|
||||
except Exception:
|
||||
info["schluessel"] = "unbekannt"
|
||||
|
||||
# Version für den Windows-Worker (aus version.txt) oder Linux-Worker (aus Env)
|
||||
try:
|
||||
ver_pfad = os.path.join(os.path.dirname(__file__), "version.txt")
|
||||
if os.path.isfile(ver_pfad):
|
||||
with open(ver_pfad, "r", encoding="utf-8") as f:
|
||||
info["rippy_version"] = f.read().strip()
|
||||
else:
|
||||
info["rippy_version"] = os.getenv("RIPPY_VERSION", "dev")
|
||||
except Exception:
|
||||
info["rippy_version"] = "unbekannt"
|
||||
|
||||
return info
|
||||
|
||||
+30
-11
@@ -140,24 +140,43 @@ def episoden_umbenennen(ordner: str, serie: str, staffel: int, zuordnung: list)
|
||||
|
||||
|
||||
def bibliothek_refresh(media_server: str, url: str, api_key: str) -> str:
|
||||
"""Stößt den Bibliotheks-Scan des Media-Servers an (Jellyfin/Emby).
|
||||
"""Stößt den Bibliotheks-Scan des Media-Servers an (Jellyfin/Emby/Kodi).
|
||||
|
||||
API laut jellyfin.org/docs (Emby-kompatibel): POST /Library/Refresh,
|
||||
Auth über den Header X-Emby-Token. Rückgabe: Klartext fürs Log.
|
||||
Jellyfin/Emby: POST /Library/Refresh, Auth über X-Emby-Token.
|
||||
Kodi: POST /jsonrpc, VideoLibrary.Scan, Auth über Basic Auth (api_key = user:pass).
|
||||
Wirft nie — der Refresh ist Komfort, nicht Teil des Rips.
|
||||
"""
|
||||
if media_server not in ("jellyfin", "emby") or not url or not api_key:
|
||||
if not url or media_server not in ("jellyfin", "emby", "kodi"):
|
||||
return ""
|
||||
try:
|
||||
antwort = requests.post(
|
||||
url.rstrip("/") + "/Library/Refresh",
|
||||
headers={"X-Emby-Token": api_key}, timeout=15,
|
||||
)
|
||||
if antwort.status_code < 300:
|
||||
return f"{media_server}-Bibliothek aktualisiert ({url})"
|
||||
return f"Bibliotheks-Refresh: HTTP {antwort.status_code} von {url}"
|
||||
if media_server in ("jellyfin", "emby") and api_key:
|
||||
antwort = requests.post(
|
||||
url.rstrip("/") + "/Library/Refresh",
|
||||
headers={"X-Emby-Token": api_key}, timeout=15,
|
||||
)
|
||||
if antwort.status_code < 300:
|
||||
return f"{media_server.title()}-Bibliothek aktualisiert ({url})"
|
||||
return f"Bibliotheks-Refresh: HTTP {antwort.status_code} von {url}"
|
||||
|
||||
elif media_server == "kodi":
|
||||
auth = None
|
||||
if api_key and ":" in api_key:
|
||||
user, pw = api_key.split(":", 1)
|
||||
auth = (user, pw)
|
||||
|
||||
payload = {"jsonrpc": "2.0", "method": "VideoLibrary.Scan", "id": 1}
|
||||
antwort = requests.post(
|
||||
url.rstrip("/") + "/jsonrpc",
|
||||
json=payload, auth=auth, timeout=15,
|
||||
)
|
||||
if antwort.status_code < 300:
|
||||
return f"Kodi-Bibliothek aktualisiert ({url})"
|
||||
return f"Kodi-Refresh: HTTP {antwort.status_code} von {url}"
|
||||
|
||||
except requests.RequestException as e:
|
||||
return f"Bibliotheks-Refresh fehlgeschlagen: {e}"
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
def baue_nfo(meta: dict, titel: str, jahr=None) -> str:
|
||||
|
||||
@@ -348,13 +348,45 @@ def rip_titel_auswahl(device_path: str, output_dir: str, titel_liste: list,
|
||||
return {"status": "success", "output_dir": output_dir, "files": alle_dateien}
|
||||
|
||||
|
||||
def laengster_titel(dauern: dict):
|
||||
"""Titel-Nummer mit der längsten Laufzeit — der Hauptfilm (pure Funktion)."""
|
||||
def laengster_titel(dauern: dict, meta: dict = None):
|
||||
"""Titel-Nummer für den Hauptfilm (pure Funktion).
|
||||
|
||||
Mit meta['runtime'] wird heuristisch der Titel gesucht, dessen Laufzeit am nächsten
|
||||
an der TMDB-Laufzeit liegt (vermeidet 'Play All'-Tracks, die länger als der Film sind).
|
||||
Ohne runtime wird klassisch der längste Titel gewählt.
|
||||
"""
|
||||
if not dauern:
|
||||
return None
|
||||
|
||||
ziel_s = (meta.get("runtime") or 0) * 60 if meta else 0
|
||||
if ziel_s > 0:
|
||||
bester = min(dauern, key=lambda t: abs(dauern[t] - ziel_s))
|
||||
if abs(dauern[bester] - ziel_s) <= 20 * 60:
|
||||
return bester
|
||||
|
||||
return max(dauern, key=dauern.get)
|
||||
|
||||
|
||||
def episoden_titel(dauern: dict, meta: dict) -> list:
|
||||
"""Findet Episoden-Titel anhand der TMDB-Episoden-Laufzeiten.
|
||||
|
||||
Toleranz: +/- 10 Minuten (600 Sekunden) zum Durchschnitt.
|
||||
Gibt eine sortierte Liste der gefundenen Titelnummern zurück.
|
||||
"""
|
||||
if not dauern or not meta or not meta.get("episode_runtimes"):
|
||||
return []
|
||||
|
||||
runtimes_s = [r * 60 for r in meta["episode_runtimes"]]
|
||||
avg_runtime = sum(runtimes_s) / len(runtimes_s)
|
||||
|
||||
gefundene_titel = []
|
||||
for titel, dauer in dauern.items():
|
||||
if abs(dauer - avg_runtime) <= 10 * 60:
|
||||
gefundene_titel.append(int(titel))
|
||||
|
||||
return sorted(gefundene_titel)
|
||||
|
||||
|
||||
def lies_titel_dauern(device_path: str, timeout: int = 300) -> dict:
|
||||
"""Fragt die Titel-Laufzeiten der Disc ab (makemkvcon info, Robot-Mode)."""
|
||||
try:
|
||||
@@ -832,15 +864,14 @@ def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str
|
||||
|
||||
def rip_video(device_path: str, disc_id: str, disc_type: str = "dvd", progress_cb=None,
|
||||
output_dir: str = None, nur_hauptfilm: bool = False,
|
||||
titel_liste: list = None, log_cb=None) -> dict:
|
||||
titel_liste: list = None, log_cb=None, meta: dict = None) -> dict:
|
||||
"""Rippt eine DVD oder Blu-ray verlustfrei mit MakeMKV.
|
||||
|
||||
Bewusst KEIN eigener Celery-Task: der einzige Task ist worker.tasks.rip_disc,
|
||||
der hier mit seinem eigenen Fortschritts-Callback durchgreift. output_dir
|
||||
überschreibt das Ziel (Transcode-Fall: Roh-Rip nach /app/temp).
|
||||
titel_liste: exakte Titel-Auswahl aus dem UI (schlägt nur_hauptfilm).
|
||||
nur_hauptfilm: Vorab-Info-Lauf, dann nur den LÄNGSTEN Titel rippen —
|
||||
das löst das eingestellte (vorher wirkungslose) mainFeatureOnly ein.
|
||||
nur_hauptfilm: Vorab-Info-Lauf, dann heuristisch den besten Titel rippen.
|
||||
"""
|
||||
if output_dir is None:
|
||||
output_dir = os.path.join(RIP_OUTPUT_DIR, disc_type, disc_id)
|
||||
@@ -849,10 +880,18 @@ def rip_video(device_path: str, disc_id: str, disc_type: str = "dvd", progress_c
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
return rip_titel_auswahl(device_path, output_dir, titel_liste, progress_cb, log_cb)
|
||||
|
||||
# Serien-Erkennung: Wenn keine Titel explizit gewählt wurden und es eine Serie ist
|
||||
if meta and meta.get("series") and meta.get("episode_runtimes"):
|
||||
dauern = lies_titel_dauern(device_path)
|
||||
episoden = episoden_titel(dauern, meta)
|
||||
if episoden:
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
return rip_titel_auswahl(device_path, output_dir, episoden, progress_cb, log_cb)
|
||||
|
||||
titel = "all"
|
||||
if nur_hauptfilm:
|
||||
dauern = lies_titel_dauern(device_path)
|
||||
haupt = laengster_titel(dauern)
|
||||
haupt = laengster_titel(dauern, meta)
|
||||
if haupt is not None:
|
||||
titel = str(haupt)
|
||||
# Kein Titel ermittelbar → ehrlich auf 'all' zurückfallen statt raten
|
||||
|
||||
Reference in New Issue
Block a user