- Extract 11-char YouTube video IDs from audio filenames - Fetch title, uploader, chapters via yt-dlp (--dump-json) - Use chapters as tracklist when no .txt tracklist is available - Store yt_title / yt_uploader in AlbumHints for LLM prompt context - Fall back to YouTube video title as track title for single-file albums Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
84 lines
2.6 KiB
Python
Executable file
84 lines
2.6 KiB
Python
Executable file
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Optional, List, Dict
|
|
|
|
|
|
AUDIO_EXTENSIONS = {
|
|
".mp3", ".flac", ".m4a", ".aac", ".ogg", ".opus",
|
|
".wav", ".wma", ".aiff", ".ape",
|
|
}
|
|
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif"}
|
|
TRACKLIST_EXTENSIONS = {".txt", ".htm", ".html", ".nfo"}
|
|
PLAYLIST_EXTENSIONS = {".m3u", ".m3u8", ".pls"}
|
|
|
|
|
|
@dataclass
|
|
class ScannedFile:
|
|
path: Path
|
|
kind: str # "audio" | "image" | "tracklist" | "playlist" | "other"
|
|
|
|
|
|
@dataclass
|
|
class AlbumScan:
|
|
album_dir: Path
|
|
audio_files: List[Path] = field(default_factory=list)
|
|
image_files: List[Path] = field(default_factory=list)
|
|
tracklist_files: List[Path] = field(default_factory=list)
|
|
playlist_files: List[Path] = field(default_factory=list) # .m3u / .m3u8 / .pls
|
|
other_files: List[Path] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class TrackHints:
|
|
path: Path
|
|
track_number: Optional[int] = None
|
|
disc_number: Optional[int] = None
|
|
title: Optional[str] = None
|
|
artist: Optional[str] = None
|
|
duration: Optional[float] = None
|
|
existing_tags: Dict[str, str] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass
|
|
class AlbumHints:
|
|
album_dir: Path
|
|
dir_artist: Optional[str] = None
|
|
dir_album: Optional[str] = None
|
|
dir_year: Optional[str] = None
|
|
tracklist_text: Optional[str] = None # merged text from all tracklist files
|
|
cover_images: List[Path] = field(default_factory=list)
|
|
tracks: List[TrackHints] = field(default_factory=list)
|
|
yt_title: Optional[str] = None # YouTube video title (if found)
|
|
yt_uploader: Optional[str] = None # YouTube channel/uploader name
|
|
|
|
|
|
@dataclass
|
|
class TrackProposal:
|
|
path: Path
|
|
title: str
|
|
artist: str
|
|
track_number: Optional[int]
|
|
disc_number: Optional[int]
|
|
new_filename: Optional[str] = None # only set when --rename is active
|
|
mbid: Optional[str] = None
|
|
conductor: Optional[str] = None # classical: Dirigent
|
|
orchestra: Optional[str] = None # classical: Orchester / Ensemble
|
|
|
|
|
|
@dataclass
|
|
class AlbumProposal:
|
|
album_dir: Path
|
|
album: str
|
|
albumartist: str
|
|
date: Optional[str]
|
|
genre: Optional[str]
|
|
label: Optional[str]
|
|
mbid: Optional[str] # MusicBrainz release ID
|
|
cover_path: Optional[Path] # resolved local or downloaded cover
|
|
cover_source: Optional[str] # "local" | "musicbrainz" | "discogs"
|
|
tracks: List[TrackProposal]
|
|
confidence: float
|
|
sources: List[str] = field(default_factory=list)
|
|
notes: List[str] = field(default_factory=list)
|