diff --git a/music_enricher.py b/music_enricher.py index 3e86d4c..d2ac0b4 100755 --- a/music_enricher.py +++ b/music_enricher.py @@ -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() diff --git a/scanner.py b/scanner.py index de06281..47d1f01 100755 --- a/scanner.py +++ b/scanner.py @@ -89,11 +89,49 @@ def _scan_dir(current: Path, album_dir: Path, result: AlbumScan, recurse: bool) def collect_album_dirs(root: Path) -> List[Path]: - dirs: List[Path] = [] + """ + Findet rekursiv alle Album-Verzeichnisse unterhalb von root. + Ein Verzeichnis gilt als Album wenn: + - es direkt Audio-Dateien enthält, ODER + - es keine direkten Audio-Dateien hat aber Disc-Unterordner mit Audio (Multi-CD). + Container-Verzeichnisse ohne Audio werden rekursiv durchsucht. + """ + result: List[Path] = [] + _find_albums(root, result) + return result + + +def _has_local_audio(path: Path) -> bool: try: - for item in sorted(root.iterdir()): - if item.is_dir() and not _is_hidden(item.name): - dirs.append(item) + return any( + e.suffix.lower() in AUDIO_EXTENSIONS + for e in path.iterdir() + if e.is_file() and not _is_hidden(e.name) + ) + except (PermissionError, OSError): + return False + + +def _find_albums(current: Path, result: List[Path]) -> None: + try: + entries = sorted(current.iterdir()) + subdirs = [e for e in entries if e.is_dir() and not _is_hidden(e.name)] except (PermissionError, OSError) as e: - print(f"⚠️ Lesefehler {root}: {e}", file=sys.stderr) - return dirs + print(f"⚠️ Lesefehler {current}: {e}", file=sys.stderr) + return + + # Verzeichnis enthält direkt Audio → Album + if any(e.is_file() and not _is_hidden(e.name) and e.suffix.lower() in AUDIO_EXTENSIONS + for e in entries): + result.append(current) + return + + # Disc-Unterordner mit Audio → Multi-CD-Album (scan_album übernimmt die Disc-Logik) + disc_dirs = [d for d in subdirs if _is_disc_dir(d.name)] + if disc_dirs and any(_has_local_audio(d) for d in disc_dirs): + result.append(current) + return + + # Container-Verzeichnis → rekursiv weiter suchen + for subdir in subdirs: + _find_albums(subdir, result)