fix: korrekte Track-Nummerierung, Scanner-Rekursion, M3U-Reihenfolge
scanner: nicht in Unterordner wenn Root Audio-Dateien enthält (verhindert Doppel-Scan bei versehentlichen Unterordner-Kopien); nur Disc-Ordner (CD1, Disc 2…) werden bei Multi-CD-Alben rekursiert. hint_extractor: M3U/Playlist-Dateien als Track-Reihenfolge-Quelle; BOM- Bereinigung; Tracklist-Matching auch per Titel (nicht nur per Nummer); tracknumber=0 wird als 'keine Nummer' gewertet. metadata_resolver: sequenzielle Fallback-Nummerierung (1,2,3…) für Tracks ohne Tracknummer — verhindert '00'-Präfix beim --rename; dir_artist hat Vorrang vor 'Various Artists'-Heuristik; LLM darf bei Konfidenz <0.3 auch bestehende Werte korrigieren (Tippfehler im Verzeichnisnamen). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c205fa8943
commit
d91eb36007
4 changed files with 189 additions and 48 deletions
90
scanner.py
90
scanner.py
|
|
@ -1,51 +1,91 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from models import AlbumScan, AUDIO_EXTENSIONS, IMAGE_EXTENSIONS, TRACKLIST_EXTENSIONS
|
||||
from models import AlbumScan, AUDIO_EXTENSIONS, IMAGE_EXTENSIONS, TRACKLIST_EXTENSIONS, PLAYLIST_EXTENSIONS
|
||||
|
||||
_DISC_DIR_RE = re.compile(r"(?i)^(?:cd|disc|disk|side)[_ \-]*\d{1,2}$")
|
||||
|
||||
|
||||
def _is_hidden(name: str) -> bool:
|
||||
return name.startswith(".") or name.startswith("_")
|
||||
|
||||
|
||||
def _is_disc_dir(name: str) -> bool:
|
||||
"""True für Ordner wie 'CD1', 'Disc 2', 'Side A', 'Disk_1'."""
|
||||
return bool(_DISC_DIR_RE.match(name))
|
||||
|
||||
|
||||
def scan_album(album_dir: Path) -> AlbumScan:
|
||||
"""
|
||||
Scannt ein Album-Verzeichnis.
|
||||
|
||||
Rekursions-Regel:
|
||||
- Hat das Album-Verzeichnis selbst Audio-Dateien → kein Abstieg in Unterordner
|
||||
(Einzelscheibe; Sub-Ordner wie Artworks, Scans, irrtümliche Kopien werden ignoriert).
|
||||
- Hat der Root KEINE Audio-Dateien → Abstieg nur in Disc-Unterordner (CD1, Disc 2 …).
|
||||
"""
|
||||
result = AlbumScan(album_dir=album_dir)
|
||||
|
||||
for dirpath, dirnames, filenames in album_dir.walk() if hasattr(album_dir, "walk") else _os_walk(album_dir):
|
||||
dirnames[:] = [d for d in dirnames if not _is_hidden(d)]
|
||||
current = Path(dirpath) if isinstance(dirpath, str) else dirpath
|
||||
# Erst nur die Wurzel-Ebene scannen, um zu entscheiden ob rekursiert wird
|
||||
root_has_audio = any(
|
||||
(album_dir / name).suffix.lower() in AUDIO_EXTENSIONS
|
||||
for name in _listdir(album_dir)
|
||||
if not _is_hidden(name)
|
||||
)
|
||||
|
||||
for name in filenames:
|
||||
if _is_hidden(name):
|
||||
continue
|
||||
p = current / name
|
||||
ext = p.suffix.lower()
|
||||
|
||||
if ext in AUDIO_EXTENSIONS:
|
||||
result.audio_files.append(p)
|
||||
elif ext in IMAGE_EXTENSIONS:
|
||||
result.image_files.append(p)
|
||||
elif ext in TRACKLIST_EXTENSIONS:
|
||||
result.tracklist_files.append(p)
|
||||
else:
|
||||
result.other_files.append(p)
|
||||
if root_has_audio:
|
||||
# Nur Root-Ebene — keine Unterordner
|
||||
_scan_dir(album_dir, album_dir, result, recurse=False)
|
||||
else:
|
||||
# Kein Audio an der Wurzel → Multi-CD: nur Disc-Unterordner
|
||||
_scan_dir(album_dir, album_dir, result, recurse=True)
|
||||
|
||||
result.audio_files.sort()
|
||||
result.image_files.sort()
|
||||
result.tracklist_files.sort()
|
||||
result.playlist_files.sort()
|
||||
return result
|
||||
|
||||
|
||||
def _os_walk(album_dir: Path):
|
||||
import os
|
||||
return os.walk(
|
||||
album_dir,
|
||||
followlinks=False,
|
||||
onerror=lambda e: print(f"⚠️ Scan-Fehler: {e}", file=sys.stderr),
|
||||
)
|
||||
def _listdir(path: Path) -> List[str]:
|
||||
try:
|
||||
return [e.name for e in path.iterdir()]
|
||||
except (PermissionError, OSError) as e:
|
||||
print(f"⚠️ Scan-Fehler: {e}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
|
||||
def _scan_dir(current: Path, album_dir: Path, result: AlbumScan, recurse: bool) -> None:
|
||||
try:
|
||||
entries = sorted(current.iterdir())
|
||||
except (PermissionError, OSError) as e:
|
||||
print(f"⚠️ Scan-Fehler {current}: {e}", file=sys.stderr)
|
||||
return
|
||||
|
||||
for entry in entries:
|
||||
name = entry.name
|
||||
if _is_hidden(name):
|
||||
continue
|
||||
if entry.is_dir():
|
||||
if recurse and _is_disc_dir(name):
|
||||
_scan_dir(entry, album_dir, result, recurse=True)
|
||||
# Andere Unterordner (Artworks, irrtümliche Kopien…) werden übersprungen
|
||||
elif entry.is_file():
|
||||
ext = entry.suffix.lower()
|
||||
if ext in AUDIO_EXTENSIONS:
|
||||
result.audio_files.append(entry)
|
||||
elif ext in IMAGE_EXTENSIONS:
|
||||
result.image_files.append(entry)
|
||||
elif ext in TRACKLIST_EXTENSIONS:
|
||||
result.tracklist_files.append(entry)
|
||||
elif ext in PLAYLIST_EXTENSIONS:
|
||||
result.playlist_files.append(entry)
|
||||
else:
|
||||
result.other_files.append(entry)
|
||||
|
||||
|
||||
def collect_album_dirs(root: Path) -> List[Path]:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue