From 701b05a75d0bed4406a88f1814950c5f02a3ff6f Mon Sep 17 00:00:00 2001 From: dschlueter Date: Wed, 29 Apr 2026 07:58:41 +0200 Subject: [PATCH] Fix Jellyfin playlist integration and tracklist matching for single-CD albums MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - hint_extractor: add _normalize_vertical_tracklist() to handle bare-number/ title/duration format (Tufaranka-style tracklists) - hint_extractor: fix level-1 tracklist match — allow disc_num=None (single-CD) by assuming disc=1; previously no tracklist title was ever applied to single- CD tracks because the guard required disc_num to be set - music_enricher: register module in sys.modules before exec_module() so @dataclass definitions in jellyfin_playlist_generator work correctly Co-Authored-By: Claude Sonnet 4.6 --- hint_extractor.py | 58 +++++++++++++++++++++++++++++++++++++++++++---- music_enricher.py | 1 + 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/hint_extractor.py b/hint_extractor.py index a5dcced..cfcb0ff 100755 --- a/hint_extractor.py +++ b/hint_extractor.py @@ -138,7 +138,56 @@ def _read_tags(path: Path) -> Tuple[Dict[str, str], Optional[float]]: return {}, None +_STANDALONE_NUM_RE = re.compile(r"^\d{1,3}$") +_DURATION_ONLY_RE = re.compile(r"^\d{1,2}:\d{2}$") + + +def _normalize_vertical_tracklist(text: str) -> Optional[str]: + """ + Erkennt 'vertikales' Format: + 1 + Katka dovádí + 3:22 + 2 + Záludná + → konvertiert zu '1. Katka dovádí 3:22\\n2. Záludná ...' + """ + non_empty = [l.strip() for l in text.splitlines() if l.strip()] + # Mindestens 3 Standalone-Zahlen als Heuristik + num_lines = sum(1 for l in non_empty if _STANDALONE_NUM_RE.match(l)) + if num_lines < 3: + return None + + result = [] + i = 0 + while i < len(non_empty): + line = non_empty[i] + if _STANDALONE_NUM_RE.match(line) and i + 1 < len(non_empty): + title_candidate = non_empty[i + 1] + # Nächste Zeile darf selbst keine Zahl und keine Dauer sein + if not _STANDALONE_NUM_RE.match(title_candidate) and not _DURATION_ONLY_RE.match(title_candidate): + duration = "" + skip = 2 + if i + 2 < len(non_empty) and _DURATION_ONLY_RE.match(non_empty[i + 2]): + duration = non_empty[i + 2] + skip = 3 + entry = f"{line}. {title_candidate}" + if duration: + entry += f" {duration}" + result.append(entry) + i += skip + continue + i += 1 + + return "\n".join(result) if len(result) >= 3 else None + + def _parse_tracklist(text: str) -> List[Dict[str, str]]: + # Vertikales Format normalisieren bevor das reguläre Parsing läuft + normalized = _normalize_vertical_tracklist(text) + if normalized: + text = normalized + tracks: List[Dict[str, str]] = [] current_disc = 1 @@ -522,13 +571,14 @@ def extract_hints(scan: AlbumScan, use_ocr: bool = True) -> AlbumHints: if parsed_tracklist: matched_tl: Optional[Dict[str, str]] = None - # 1. Exakt per Tracknummer + Disc (nur wenn beides aus Tag/Dateiname bekannt) - if track_num and disc_num: + # 1. Exakt per Tracknummer + Disc (disc_num=None → Single-CD, assume 1) + if track_num: + assumed_disc = disc_num if disc_num else 1 for tl_entry in parsed_tracklist: tl_track = tl_entry.get("track") - tl_disc = tl_entry.get("disc", "1") + tl_disc = int(tl_entry.get("disc", "1")) if (tl_track and int(tl_track) == track_num - and int(tl_disc) == disc_num): + and tl_disc == assumed_disc): matched_tl = tl_entry break diff --git a/music_enricher.py b/music_enricher.py index d2ac0b4..a8d6355 100755 --- a/music_enricher.py +++ b/music_enricher.py @@ -55,6 +55,7 @@ def _run_jellyfin_generator(album_dir: Path, generator_path: Path) -> None: try: spec = importlib.util.spec_from_file_location("jellyfin_pg", generator_path) mod = importlib.util.module_from_spec(spec) # type: ignore[arg-type] + sys.modules["jellyfin_pg"] = mod # muss vor exec_module stehen (für @dataclass) spec.loader.exec_module(mod) # type: ignore[union-attr] media_files = mod.collect_media_recursive(album_dir)