UDF-Volume-Label-Leser (Blu-rays haben keine ISO-Bridge) + Etappe 12 (ARM)
Ampel / ampel (push) Successful in 35s
Ampel / ampel (push) Successful in 35s
Praxis-Befund mit echter BD-50: reines UDF, der ISO-PVD-Leser griff nicht. Jetzt ECMA-167-Weg (AVDP Sektor 256 -> Main VDS -> PVD Tag-ID 1 -> Volume Identifier als d-string), mit Tests fuer das d-string-Parsing. ROADMAP Etappe 12 aus der ARM-Vollanalyse: was wir uebernehmen (Fingerprint-DB, bdmt_eng.xml, Suchdegradation, Manual Mode, Apprise, Multi-Drive, Backup-Modus) und wo wir ARM schlagen (Serien-Episoden- Matching, Main-Feature per Laufzeitabgleich). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+43
@@ -251,3 +251,46 @@ Bind-Mounts statt devices: (EPERM), UI rief hartkodiert localhost:8000 auf.
|
|||||||
- [x] compose: devices: statt Bind-Mounts; Ausgabe aufs media-Volume
|
- [x] compose: devices: statt Bind-Mounts; Ausgabe aufs media-Volume
|
||||||
- [ ] Jellyfin-Post-Processing an den Worker anbinden (NFO/Poster nach dem Rip)
|
- [ ] Jellyfin-Post-Processing an den Worker anbinden (NFO/Poster nach dem Rip)
|
||||||
- [ ] Auth vor die schreibenden Endpoints (JWT existiert, schützt aber nichts)
|
- [ ] Auth vor die schreibenden Endpoints (JWT existiert, schützt aber nichts)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Etappe 12: Besser als ARM (aus ARM-Analyse 23.07.2026)
|
||||||
|
|
||||||
|
**Quelle:** Vollanalyse von Automatic Ripping Machine v2.24 (Code, arm.yaml,
|
||||||
|
Wiki, Issues). Was Rippy strukturell schon besser macht: Lossless-first statt
|
||||||
|
Transcode-first (ARMs BD-Default ist ein 1080p30-Downscale!), Celery+Postgres
|
||||||
|
statt Prozess-pro-udev-Event+SQLite (ARM-UI friert beim Rippen ein, #1046),
|
||||||
|
ioctl-Watcher statt fragiler udev-Ketten (ARMs größte Support-Quelle), NFO +
|
||||||
|
Jellyfin-Konvention nativ (ARM kann nur Emby-Refresh, keine NFOs).
|
||||||
|
|
||||||
|
**Übernehmen (priorisiert):**
|
||||||
|
- [ ] **Disc-Fingerprint-DB (selbstlernend)** — ARMs beste Idee, besser gemacht:
|
||||||
|
Tabelle disc_fingerprints (DVD-CRC64/MusicBrainz-DiscID/BD-Label-Hash →
|
||||||
|
bestätigte Metadaten). Jede UI-Bestätigung schreibt zurück; zweite Disc
|
||||||
|
derselben Serie wird sofort erkannt. Lokal statt ARMs Crowd-Server
|
||||||
|
(Single Point of Failure auf Free-Hosting).
|
||||||
|
- [ ] **BD-Titel aus BDMV/META/DL/bdmt_eng.xml** als Identifikations-Stufe vor
|
||||||
|
den APIs (echter Disc-Titel statt Label-Raterei).
|
||||||
|
- [ ] **Progressive Suchdegradation** bei TMDB/OMDb: mit Jahr → ohne Jahr →
|
||||||
|
tokenweises Titel-Stripping; jede Stufe senkt die Confidence.
|
||||||
|
- [ ] **Duplikat-Schutz**: Fingerprint-Treffer → „schon gerippt am X — trotzdem?"
|
||||||
|
- [ ] **Track-Auswahl (Manual Mode), besser als ARM**: Job-Status
|
||||||
|
awaiting_selection nach makemkvcon-info; Track-Tabelle im UI mit
|
||||||
|
Heuristik-Vorauswahl; Timeout → Weiter mit Default (ARM bricht hart ab,
|
||||||
|
erlaubt nur EINE Auswahl).
|
||||||
|
- [ ] **Min/Max-Titellänge + Extras**: Bonusmaterial nach extras/ (versteht
|
||||||
|
Jellyfin nativ) statt alles flach.
|
||||||
|
- [ ] **Apprise-Benachrichtigungen**: eine Lib für Telegram/Discord/…,
|
||||||
|
Hooks auf job_completed/failed/awaiting_selection, Deep-Link ins UI.
|
||||||
|
- [ ] **Multi-Drive-Verwaltung**: drives-Tabelle (Seriennummer, Custom-Name,
|
||||||
|
UHD-fähig-Flag), parallele Rips, Job↔Drive-Zuordnung.
|
||||||
|
- [ ] **Backup-Modus** (makemkvcon backup --decrypt) als Plan B für Problem-Discs.
|
||||||
|
- [ ] **Jellyfin-Refresh** nach Ablage (POST /Library/Refresh).
|
||||||
|
|
||||||
|
**ARM schlagen (Alleinstellung):**
|
||||||
|
- [ ] **Serien-Episoden-Erkennung** — ARMs offene Wunde seit 2019 (#395):
|
||||||
|
TVDB-Episodenlaufzeiten gegen Track-Laufzeiten matchen (±5 %,
|
||||||
|
Reihenfolge-Constraint, Disc-Nr aus Label), Vorschlag
|
||||||
|
„Show/Season 02/Show S02E05.mkv" im Preview bestätigen.
|
||||||
|
- [ ] **Main-Feature per Laufzeitabgleich** mit TMDB-Runtime (±3 min) statt
|
||||||
|
ARMs „größte Datei gewinnt" (#1297-Fehlklasse).
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ def read_iso_volume_label(device_path: str) -> Optional[str]:
|
|||||||
"""Liest das Volume-Label aus dem ISO-9660 Primary Volume Descriptor.
|
"""Liest das Volume-Label aus dem ISO-9660 Primary Volume Descriptor.
|
||||||
|
|
||||||
Sektor 16 (Offset 32768), Bytes 40-71 = Volume Identifier. Funktioniert für
|
Sektor 16 (Offset 32768), Bytes 40-71 = Volume Identifier. Funktioniert für
|
||||||
DVDs und die meisten Blu-rays (UDF-Bridge) — ganz ohne Zusatzwerkzeuge.
|
DVDs — Blu-rays sind oft reines UDF ohne ISO-Bridge (dann greift
|
||||||
|
read_udf_volume_label).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with open(device_path, "rb") as f:
|
with open(device_path, "rb") as f:
|
||||||
@@ -50,6 +51,59 @@ def read_iso_volume_label(device_path: str) -> Optional[str]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_udf_dstring(data: bytes) -> str:
|
||||||
|
"""Dekodiert einen UDF d-string (ECMA-167 1/7.2.12; pure Funktion, testbar).
|
||||||
|
|
||||||
|
Letztes Byte = genutzte Länge, erstes Byte = Kompressions-ID
|
||||||
|
(8 = Latin-1, 16 = UTF-16BE), dazwischen der Text.
|
||||||
|
"""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
laenge = data[-1]
|
||||||
|
if laenge < 2 or laenge > len(data) - 1:
|
||||||
|
return ""
|
||||||
|
comp_id, text = data[0], data[1:laenge]
|
||||||
|
try:
|
||||||
|
if comp_id == 8:
|
||||||
|
return text.decode("latin-1").strip()
|
||||||
|
if comp_id == 16:
|
||||||
|
return text.decode("utf-16-be").strip()
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
pass
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def read_udf_volume_label(device_path: str) -> Optional[str]:
|
||||||
|
"""Liest das Volume-Label aus dem UDF Primary Volume Descriptor.
|
||||||
|
|
||||||
|
Weg laut ECMA-167: Anchor Volume Descriptor Pointer bei Sektor 256 →
|
||||||
|
zeigt auf die Main Volume Descriptor Sequence → darin der PVD (Tag-ID 1)
|
||||||
|
mit dem Volume Identifier (d-string, 32 Bytes ab Offset 24).
|
||||||
|
"""
|
||||||
|
sektor = 2048
|
||||||
|
try:
|
||||||
|
with open(device_path, "rb") as f:
|
||||||
|
f.seek(256 * sektor)
|
||||||
|
avdp = f.read(sektor)
|
||||||
|
if len(avdp) < 24 or int.from_bytes(avdp[0:2], "little") != 2:
|
||||||
|
return None
|
||||||
|
vds_ort = int.from_bytes(avdp[20:24], "little")
|
||||||
|
f.seek(vds_ort * sektor)
|
||||||
|
# VDS-Deskriptoren durchgehen, bis der PVD (Tag-ID 1) kommt
|
||||||
|
for _ in range(32):
|
||||||
|
block = f.read(sektor)
|
||||||
|
if len(block) < 56:
|
||||||
|
return None
|
||||||
|
tag_id = int.from_bytes(block[0:2], "little")
|
||||||
|
if tag_id == 1:
|
||||||
|
return parse_udf_dstring(block[24:56]) or None
|
||||||
|
if tag_id == 8: # Terminating Descriptor — Sequenz zu Ende
|
||||||
|
return None
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class PreScanResult:
|
class PreScanResult:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -129,11 +183,12 @@ class PreScan:
|
|||||||
"duration": 0
|
"duration": 0
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
# Titel-Quelle 1: ISO-Volume-Label direkt vom Medium — läuft
|
# Titel-Quelle 1: Volume-Label direkt vom Medium — läuft
|
||||||
# überall. (makemkvcon gibt es NUR im Worker-Container; der
|
# überall. ISO-9660 für DVDs, UDF für Blu-rays (die haben
|
||||||
# alte Aufruf hier scheiterte in der API still und der Titel
|
# meist keine ISO-Bridge). (makemkvcon gibt es NUR im
|
||||||
# blieb ewig "Unknown Title".)
|
# Worker-Container; der alte Aufruf hier scheiterte in der
|
||||||
label = read_iso_volume_label(device_path)
|
# API still und der Titel blieb ewig "Unknown Title".)
|
||||||
|
label = read_iso_volume_label(device_path) or read_udf_volume_label(device_path)
|
||||||
if label:
|
if label:
|
||||||
toc["title"] = normalize_disc_label(label)
|
toc["title"] = normalize_disc_label(label)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,26 @@
|
|||||||
"""Tests für die puren Pre-Scan-/OMDb-Helfer (Label-Normalisierung, Parsing)."""
|
"""Tests für die puren Pre-Scan-/OMDb-Helfer (Label-Normalisierung, Parsing)."""
|
||||||
|
|
||||||
from clients.omdb import parse_runtime_minutes, parse_year
|
from clients.omdb import parse_runtime_minutes, parse_year
|
||||||
from prescan.prescan import normalize_disc_label
|
from prescan.prescan import normalize_disc_label, parse_udf_dstring
|
||||||
|
|
||||||
|
|
||||||
|
def test_udf_dstring_latin1():
|
||||||
|
# d-string: [comp_id=8]["MEIN_FILM"][padding][len=10]
|
||||||
|
daten = bytes([8]) + b"MEIN_FILM" + bytes(21) + bytes([10])
|
||||||
|
assert parse_udf_dstring(daten) == "MEIN_FILM"
|
||||||
|
|
||||||
|
|
||||||
|
def test_udf_dstring_utf16():
|
||||||
|
text = "FILM".encode("utf-16-be")
|
||||||
|
daten = bytes([16]) + text + bytes(32 - 2 - len(text)) + bytes([1 + len(text)])
|
||||||
|
assert parse_udf_dstring(daten) == "FILM"
|
||||||
|
|
||||||
|
|
||||||
|
def test_udf_dstring_kaputt_gibt_leer():
|
||||||
|
assert parse_udf_dstring(b"") == ""
|
||||||
|
assert parse_udf_dstring(bytes(32)) == "" # Länge 0
|
||||||
|
daten = bytes([8]) + b"X" + bytes(29) + bytes([99]) # Länge > Puffer
|
||||||
|
assert parse_udf_dstring(daten) == ""
|
||||||
|
|
||||||
|
|
||||||
def test_label_normalisierung():
|
def test_label_normalisierung():
|
||||||
|
|||||||
Reference in New Issue
Block a user