Praxis-Feedback-Runde: CIFS-Cap-Fix, TMDB v3-Keys, 4K-UHD-Typ, Vollautomatik, Job-Verwaltung, Worker-Namen
Ampel / ampel (push) Successful in 29s
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:
+27
-1
@@ -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"):
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user