"""``local`` source โ€” indexes audio files from a mounted folder. Walks a configured root directory and yields each audio file as a :class:`SourceFile`. It does **not** parse tags or resolve artist/album โ€” that's enrichment's job (plan ยง6.2); this stays a thin discovery layer. ``source_id`` is the path relative to the root, so re-scans are idempotent. """ import os from collections.abc import Iterator from pathlib import Path from app.domain.sources import SourceFile, SourceInfo from app.infrastructure.db.models.enums import TrackSource # Extensions we treat as audio. Mirrors the formats StreamingService serves. _AUDIO_EXTENSIONS = frozenset( {"mp3", "flac", "m4a", "aac", "ogg", "opus", "wav", "wma", "aiff", "aif", "alac"} ) class LocalFolderSource: """Implements :class:`app.domain.ports.IndexableSource`.""" name = TrackSource.LOCAL.value def __init__(self, root: Path) -> None: self._root = root def info(self) -> SourceInfo: return SourceInfo( name=self.name, label="Local folder", kind="indexable", available=self.is_available(), ) def is_available(self) -> bool: return self._root.is_dir() def scan(self) -> Iterator[SourceFile]: if not self.is_available(): return for dirpath, _dirnames, filenames in os.walk(self._root): for filename in sorted(filenames): ext = Path(filename).suffix.lower().lstrip(".") if ext not in _AUDIO_EXTENSIONS: continue path = Path(dirpath) / filename try: size = path.stat().st_size except OSError: continue # vanished/unreadable between walk and stat โ†’ skip yield SourceFile( source_id=path.relative_to(self._root).as_posix(), path=path, suggested_title=path.stem or "Unknown", file_format=ext, file_size=size, )