Fix Jellyfin playlist integration and tracklist matching for single-CD albums
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
776c977573
commit
701b05a75d
2 changed files with 55 additions and 4 deletions
|
|
@ -138,7 +138,56 @@ def _read_tags(path: Path) -> Tuple[Dict[str, str], Optional[float]]:
|
||||||
return {}, None
|
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]]:
|
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]] = []
|
tracks: List[Dict[str, str]] = []
|
||||||
current_disc = 1
|
current_disc = 1
|
||||||
|
|
||||||
|
|
@ -522,13 +571,14 @@ def extract_hints(scan: AlbumScan, use_ocr: bool = True) -> AlbumHints:
|
||||||
if parsed_tracklist:
|
if parsed_tracklist:
|
||||||
matched_tl: Optional[Dict[str, str]] = None
|
matched_tl: Optional[Dict[str, str]] = None
|
||||||
|
|
||||||
# 1. Exakt per Tracknummer + Disc (nur wenn beides aus Tag/Dateiname bekannt)
|
# 1. Exakt per Tracknummer + Disc (disc_num=None → Single-CD, assume 1)
|
||||||
if track_num and disc_num:
|
if track_num:
|
||||||
|
assumed_disc = disc_num if disc_num else 1
|
||||||
for tl_entry in parsed_tracklist:
|
for tl_entry in parsed_tracklist:
|
||||||
tl_track = tl_entry.get("track")
|
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
|
if (tl_track and int(tl_track) == track_num
|
||||||
and int(tl_disc) == disc_num):
|
and tl_disc == assumed_disc):
|
||||||
matched_tl = tl_entry
|
matched_tl = tl_entry
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ def _run_jellyfin_generator(album_dir: Path, generator_path: Path) -> None:
|
||||||
try:
|
try:
|
||||||
spec = importlib.util.spec_from_file_location("jellyfin_pg", generator_path)
|
spec = importlib.util.spec_from_file_location("jellyfin_pg", generator_path)
|
||||||
mod = importlib.util.module_from_spec(spec) # type: ignore[arg-type]
|
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]
|
spec.loader.exec_module(mod) # type: ignore[union-attr]
|
||||||
|
|
||||||
media_files = mod.collect_media_recursive(album_dir)
|
media_files = mod.collect_media_recursive(album_dir)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue