Praxis-Feedback-Runde: CIFS-Cap-Fix, TMDB v3-Keys, 4K-UHD-Typ, Vollautomatik, Job-Verwaltung, Worker-Namen
Ampel / ampel (push) Successful in 29s

Zwei bewiesene Bug-Fixes:
- SMB-Mount 'Unable to apply new capability set': mount.cifs hebt
  CAP_DAC_READ_SEARCH an, die in Dockers Default-Caps fehlt (auf der VM
  reproduziert: Bounding-Set a82425fb ohne Bit 2; mit der Capability
  verschwindet der Fehler). Fix: cap_add DAC_READ_SEARCH fuer den
  api-Container.
- TMDB fiel still aus: Client konnte nur v4-Bearer-Tokens, der uebliche
  32-Hex-v3-Key bekam 401 und die Suche lieferte nur OMDb. Jetzt beide
  Key-Arten (ist_v4_token + api_key-Query-Param lt.
  developer.themoviedb.org, mit Tests) — damit kommen auch die deutschen
  Texte an (language=de-DE war ueberall schon gesetzt). Neu:
  GET /metadata/status + 'Verbindung pruefen' in Einstellungen -> APIs
  (Live-Check am Cache vorbei).

Features aus dem Commander-Feedback:
- 4K UHD als eigener Disc-Typ: classify >= 55 GiB (BD-66/BD-100; BD-50
  bleibt bluray), beide detection.py + Tests, eigene Badge-Farbe in
  Dashboard/Laufwerken, Prescan-Label '4K UHD'. UHD-Rip-Fehler 'Failed to
  open disc' (Code 11) bekommt Klartext: LibreDrive-Firmware noetig.
- Vollautomatik (Setting autoRipStart): Disc erkannt -> Rip startet ohne
  Popup in den Schnellwahl-Ordner (Serie->Serien, sonst Filme, CD->Musik).
- Job-Verwaltung: 'Neu komprimieren' nur noch mit can_retry (Rohdaten
  liegen wirklich da), DELETE /jobs/{id} + 'Erledigte aufraeumen'
  (Dateien bleiben immer), 'Alle herunterladen' im Job-Detail
  (gestaffelte Einzel-Downloads statt Server-Zip von 40-GB-Dateien).
- Worker zuordenbar: WORKER_NAME-Env als stabiler Anzeigename (fixt auch
  die Offline-Leichen nach Rebuilds), info.hostname/ip gemeldet,
  Online-Abgleich ueber hostname, DELETE /workers/{name} + Papierkorb im
  UI, Quelle-Anzeige an der Disc-Karte ('Quelle: TMDB - 99 % sicher').
- Ripping-Tab nach Medium gegliedert (Video/Audio-CD/Allgemein) +
  Untertitel-Klartext (--all-audio/--all-subtitles bleiben komplett),
  CD -> Musik-Vorauswahl im Ziel-Dialog, Ordner-Verwaltung beschriftet
  und standardmaessig eingeklappt.
- Docs: SAVEPOINT v3.3, ROADMAP Etappe 14 + Ideen (nativer
  Windows-Worker, deutsche OMDb-Texte via TMDB-Find), README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-24 13:29:10 +02:00
parent ab75134931
commit 5778ac4645
23 changed files with 691 additions and 81 deletions
+27 -1
View File
@@ -26,6 +26,26 @@ def erkenne_encoder() -> list:
return gefunden
def erkenne_ip() -> str:
"""Beste erratbare eigene IP (UDP-Route-Trick, KEIN echter Traffic).
Ehrlicher Vorbehalt: im Docker-Bridge-Netz ist das die Container-IP —
für die Maschinen-Zuordnung ist deshalb WORKER_NAME (Anzeigename in
compose/.env) die verlässliche Größe; die IP ist Zusatz-Indiz.
"""
import socket
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(("192.0.2.1", 9)) # TEST-NET — nur Routing, kein Paket nötig
return s.getsockname()[0]
finally:
s.close()
except OSError:
return ""
def werkzeug_versionen() -> dict:
"""Kern-Werkzeuge dieses Workers — fürs UI (Einstellungen → System).
@@ -34,7 +54,13 @@ def werkzeug_versionen() -> dict:
HandBrakeCLI --version ist dokumentiert (handbrake.fr/docs, CLI Options)
und gibt z.B. "HandBrake 1.6.1" aus.
"""
info = {}
import socket
info = {
# Zuordnung im UI: hostname (für den Celery-Online-Abgleich) + IP
"hostname": socket.gethostname(),
"ip": erkenne_ip(),
}
if shutil.which("makemkvcon"):
info["makemkv"] = os.getenv("MAKEMKV_VERSION") or "installiert"
if shutil.which("HandBrakeCLI"):
+6 -1
View File
@@ -39,12 +39,17 @@ def melde_faehigkeiten(**kwargs):
import threading
import time
# Anzeigename: WORKER_NAME (compose/.env) macht die Maschine im UI
# zuordenbar UND stabil — der Container-Hostname wechselt bei jedem
# Rebuild und hinterließ Offline-Leichen in der Worker-Liste.
anzeigename = os.getenv("WORKER_NAME") or socket.gethostname()
def herzschlag():
while True:
try:
db.init_db()
db.save_worker(
socket.gethostname(),
anzeigename,
caps.erkenne_encoder(),
caps.werkzeug_versionen(),
)
+7 -1
View File
@@ -36,6 +36,10 @@ BLKGETSIZE64 = 0x80081272
# Eine DVD9 fasst ~8,5 GB; Blu-ray beginnt bei 25 GB (Single Layer).
# Alles ab 10 GB ist also sicher eine Blu-ray.
BLURAY_MIN_BYTES = 10 * 1024**3
# 4K-UHD-Discs sind BD-66 (66 GB) oder BD-100 — eine normale BD-50 bleibt
# unter ~47 GiB. Ab 55 GiB ist es also sicher eine UHD. (Seltene 50-GB-UHDs
# laufen als "bluray" — der Rip-Weg ist ohnehin identisch.)
UHD_MIN_BYTES = 55 * 1024**3
def _open_nonblock(device_path: str) -> int:
@@ -73,10 +77,12 @@ def disc_size_bytes(device_path: str) -> int:
def classify(disc_status_code: int, size_bytes: int) -> str:
"""Pure Zuordnung (testbar): Disc-Status + Größe → cd | dvd | bluray | unknown."""
"""Pure Zuordnung (testbar): Disc-Status + Größe → cd | dvd | bluray | uhd | unknown."""
if disc_status_code in (CDS_AUDIO, CDS_MIXED):
return "cd"
if disc_status_code in (CDS_DATA_1, CDS_DATA_2, CDS_XA_2_1, CDS_XA_2_2):
if size_bytes >= UHD_MIN_BYTES:
return "uhd"
return "bluray" if size_bytes >= BLURAY_MIN_BYTES else "dvd"
return "unknown"
+20 -7
View File
@@ -142,7 +142,7 @@ def _job_abschliessen(job_id: str, ergebnis: dict) -> None:
return
if ergebnis.get("status") == "success":
ausgabe = ergebnis.get("output_dir")
if ausgabe and (job.get("disc_type") in ("dvd", "bluray")):
if ausgabe and (job.get("disc_type") in ("dvd", "bluray", "uhd")):
# Media-Server-Aufbereitung (Jellyfin/Emby/Kodi: NFO + Poster)
einstellungen = db.get_settings()
try:
@@ -219,12 +219,10 @@ def rip_disc(self, device_path: str, job_id: str, target_dir: str = None):
db.update_job(job_id, progress=progress)
einstellungen = db.get_settings()
transcode_an = (
disc_type in ("dvd", "bluray")
and einstellungen.get("transcodeEnabled", True)
)
ist_video = disc_type in ("dvd", "bluray", "uhd")
transcode_an = ist_video and einstellungen.get("transcodeEnabled", True)
if disc_type in ("dvd", "bluray"):
if ist_video:
# UI-Key schlägt Env-Key — Monats-Key ohne Rebuild aktualisierbar
_makemkv_key_anwenden(einstellungen)
@@ -251,7 +249,7 @@ def rip_disc(self, device_path: str, job_id: str, target_dir: str = None):
except OSError:
disc_bytes = 0
marge = 1024**3 # 1 GB Sicherheitsabstand
if disc_type in ("dvd", "bluray"):
if ist_video:
rip_ziel = raw_dir if transcode_an else final_dir
fehler = _platz_pruefen(rip_ziel, disc_bytes + marge, "den Roh-Rip")
if not fehler and transcode_an and einstellungen.get("keepOriginal", False):
@@ -276,6 +274,21 @@ def rip_disc(self, device_path: str, job_id: str, target_dir: str = None):
progress_cb=fortschritt, output_dir=final_dir,
)
# 4K-UHD scheitert mit Nicht-LibreDrive-Laufwerken IMMER an der
# Verschlüsselung — "Failed to open disc" heißt dann nicht "Disc kaputt",
# sondern "Laufwerk kann kein UHD" (Firmware-Faden, siehe SAVEPOINT).
if (
ergebnis.get("status") == "error"
and disc_type == "uhd"
and "Failed to open disc" in (ergebnis.get("error") or "")
):
ergebnis["error"] += (
" — 4K-UHD erkannt: Das Laufwerk kann UHD-Discs vermutlich nicht "
"entschlüsseln. Dafür ist eine LibreDrive-Firmware nötig (BU40N: "
"Crossflash auf 1.03-MK, MakeMKV-Forum 'Ultimate UHD Drives "
"Flashing Guide'). Normale BD/DVD gehen weiterhin."
)
if ergebnis.get("status") == "success" and transcode_an:
# Kompression als eigener Task auf der transcode-Queue — kann vom
# lokalen Worker ODER einem Remote-GPU-Worker übernommen werden.
+13
View File
@@ -11,6 +11,7 @@ from detection import (
CDS_AUDIO,
CDS_DATA_1,
CDS_MIXED,
UHD_MIN_BYTES,
classify,
)
@@ -34,6 +35,18 @@ def test_bluray_ab_schwelle():
assert classify(CDS_DATA_1, BLURAY_MIN_BYTES) == "bluray"
def test_bd50_bleibt_bluray():
# BD-50 = 50 * 10^9 Bytes ≈ 46,6 GiB — knapp unter der UHD-Schwelle
assert classify(CDS_DATA_1, 50 * 1000**3) == "bluray"
def test_uhd_ab_schwelle():
# BD-66 (66 GB) und BD-100 sind 4K-UHD-Medien
assert classify(CDS_DATA_1, 66 * 1000**3) == "uhd"
assert classify(CDS_DATA_1, 100 * 1000**3) == "uhd"
assert classify(CDS_DATA_1, UHD_MIN_BYTES) == "uhd"
def test_unbekannter_status():
assert classify(999, 5 * 1024**3) == "unknown"
assert classify(0, 0) == "unknown"