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
|
||||
- [ ] Jellyfin-Post-Processing an den Worker anbinden (NFO/Poster nach dem Rip)
|
||||
- [ ] 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.
|
||||
|
||||
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:
|
||||
with open(device_path, "rb") as f:
|
||||
@@ -50,6 +51,59 @@ def read_iso_volume_label(device_path: str) -> Optional[str]:
|
||||
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:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -129,11 +183,12 @@ class PreScan:
|
||||
"duration": 0
|
||||
})
|
||||
else:
|
||||
# Titel-Quelle 1: ISO-Volume-Label direkt vom Medium — läuft
|
||||
# überall. (makemkvcon gibt es NUR im Worker-Container; der
|
||||
# alte Aufruf hier scheiterte in der API still und der Titel
|
||||
# blieb ewig "Unknown Title".)
|
||||
label = read_iso_volume_label(device_path)
|
||||
# Titel-Quelle 1: Volume-Label direkt vom Medium — läuft
|
||||
# überall. ISO-9660 für DVDs, UDF für Blu-rays (die haben
|
||||
# meist keine ISO-Bridge). (makemkvcon gibt es NUR im
|
||||
# Worker-Container; der alte Aufruf hier scheiterte in der
|
||||
# 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:
|
||||
toc["title"] = normalize_disc_label(label)
|
||||
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
"""Tests für die puren Pre-Scan-/OMDb-Helfer (Label-Normalisierung, Parsing)."""
|
||||
|
||||
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():
|
||||
|
||||
Reference in New Issue
Block a user