Recursive album discovery + Jellyfin Playlist Generator integration

scanner.py: collect_album_dirs() now recursively finds album dirs
- Dirs with audio files at root → album
- Dirs with disc subdirs (CD1/CD2) and no root audio → multi-CD album
- Container dirs without audio → recurse into subdirs

music_enricher.py:
- After execute_album(), auto-discovers jellyfin_playlist_generator.py
  in ../Jellyfin_Playlist_Generator/ (or via --playlist-generator PATH)
- Calls generate_playlist() directly via importlib — no subprocess,
  no destructive cleanup_all_playlists, targeted to the enriched album
- New --playlist-generator CLI option for custom generator path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-04-29 07:07:55 +02:00
commit 776c977573
2 changed files with 96 additions and 8 deletions

View file

@ -5,10 +5,12 @@ KI-gestützter Musik-Metadaten-Enricher für Jellyfin-Bibliotheken.
Pipeline pro Album:
Scan HintExtractor MetadataResolver CoverHandler Review Executor
(optional) Jellyfin Playlist Generator
"""
from __future__ import annotations
import argparse
import importlib.util
import os
import sys
from pathlib import Path
@ -32,6 +34,46 @@ def maybe_tqdm(iterable, show: bool, **kwargs):
return tqdm(iterable, **kwargs) if show else iterable
# ---------------------------------------------------------------------------
# Jellyfin Playlist Generator integration
# ---------------------------------------------------------------------------
def _find_jellyfin_generator(album_dir: Path, explicit: Optional[Path]) -> Optional[Path]:
"""Sucht jellyfin_playlist_generator.py — explizit oder im Geschwister-Verzeichnis."""
if explicit:
return explicit.expanduser().resolve() if explicit.exists() else None
# Auto-Discover: ../Jellyfin_Playlist_Generator/ relativ zum Album-Root
candidate = album_dir.parent / "Jellyfin_Playlist_Generator" / "jellyfin_playlist_generator.py"
return candidate if candidate.exists() else None
def _run_jellyfin_generator(album_dir: Path, generator_path: Path) -> None:
"""
Importiert den Jellyfin Playlist Generator und erstellt die Playlist für album_dir.
Kein subprocess, kein cleanup_all_playlists nur gezielt dieses eine Album.
"""
try:
spec = importlib.util.spec_from_file_location("jellyfin_pg", generator_path)
mod = importlib.util.module_from_spec(spec) # type: ignore[arg-type]
spec.loader.exec_module(mod) # type: ignore[union-attr]
media_files = mod.collect_media_recursive(album_dir)
if not media_files:
print(f" ⚠️ Jellyfin-Generator: keine Mediendateien in {album_dir.name}", file=sys.stderr)
return
deduped = sorted(set(media_files), key=mod.natural_sort_key)
tracks = mod.enrich_tracks(
[mod.TrackInfo(p, p.stem, p.suffix.lower()) for p in deduped],
album_dir,
)
tracks = mod.sort_tracks_for_playlist(tracks, album_dir)
pl_path = mod.generate_playlist(album_dir, tracks, None, dry_run=False)
print(f" 🎵 Jellyfin-Playlist erstellt: {pl_path.name}")
except Exception as e:
print(f" ⚠️ Jellyfin-Generator-Fehler ({album_dir.name}): {e}", file=sys.stderr)
# ---------------------------------------------------------------------------
# Review / Display
# ---------------------------------------------------------------------------
@ -166,6 +208,11 @@ def process_album(
for k, v in album_stats.items():
stats[k] = stats.get(k, 0) + v
# Jellyfin Playlist Generator aufrufen
generator_path = _find_jellyfin_generator(album_dir, getattr(args, "playlist_generator", None))
if generator_path:
_run_jellyfin_generator(album_dir, generator_path)
except Exception as e:
stats["errors"] += 1
print(f" ❌ Fehler in {album_dir.name}: {e}", file=sys.stderr)
@ -181,7 +228,7 @@ def main() -> None:
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument("paths", nargs="*",
help="Root-Verzeichnisse (direkte Unterordner = Alben)")
help="Root-Verzeichnisse (rekursiv nach Alben durchsucht)")
parser.add_argument("--album", type=Path,
help="Einzelnes Album-Verzeichnis verarbeiten")
parser.add_argument("--dry-run", action="store_true",
@ -191,7 +238,7 @@ def main() -> None:
parser.add_argument("--confidence", type=float, default=0.85,
help="Min-Konfidenz für --auto (default: 0.85)")
parser.add_argument("--rename", action="store_true",
help="Dateien nach Schema umbenennen: TT - Artist - Titel.ext")
help="Dateien nach Schema umbenennen: TT_-_Artist_-_Titel.ext")
parser.add_argument("--embed-cover", action="store_true",
help="Cover-Art in Audiodatei einbetten")
parser.add_argument("--backup", type=Path,
@ -206,6 +253,9 @@ def main() -> None:
help="Kein Cover-Art-Download")
parser.add_argument("--no-tqdm", action="store_true",
help="Fortschrittsanzeige deaktivieren")
parser.add_argument("--playlist-generator", type=Path, dest="playlist_generator",
help="Pfad zu jellyfin_playlist_generator.py\n"
"(Standard: ../Jellyfin_Playlist_Generator/jellyfin_playlist_generator.py)")
args = parser.parse_args()