Files
Lyra/worker/lyra_worker/adapters/fakes.py
T
Jonathan 4907d0d5a4 feat: library scan — probe seam + scan_library (have + monitored + followed)
Adds the AudioProbe seam (probe.py, _probe.py MutagenProbe with a lazy
mutagen import, FakeAudioProbe) and scan_library, which walks /music,
resolves each album against MusicBrainz (falling back to embedded tags
when the folder name doesn't resolve), and idempotently records matched
albums as have (LibraryItem), monitored (MonitoredRelease, fulfilled),
and followed (WatchedArtist, autoMonitorFuture=false).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 17:00:33 +02:00

93 lines
2.8 KiB
Python

from typing import Callable
from lyra_worker.browser import ArtistHit, ReleaseGroupInfo
from lyra_worker.probe import ProbeResult
from lyra_worker.types import Candidate, DownloadResult, MBTarget, Quality
_QUALITIES = {
"qobuz": Quality(fmt="FLAC", lossless=True, bit_depth=24, sample_rate=96000),
"soulseek": Quality(fmt="FLAC", lossless=True, bit_depth=16, sample_rate=44100),
"youtube": Quality(fmt="OPUS", lossless=False, bitrate_kbps=160),
}
class _BaseFake:
name = "fake"
tier = 99
def __init__(self, matches: bool = True):
self._matches = matches
def health(self) -> bool:
return True
def search(self, target: MBTarget) -> list[Candidate]:
if not self._matches:
return []
tracks = target.track_count or 10
return [
Candidate(
source=self.name,
source_ref=f"{self.name}:{target.artist}:{target.album}",
matched_artist=target.artist,
matched_album=target.album,
quality=_QUALITIES.get(self.name, _QUALITIES["youtube"]),
track_count=tracks,
source_tier=self.tier,
)
]
def download(
self, candidate: Candidate, dest: str, on_progress: Callable[[float], None]
) -> DownloadResult:
for pct in (0.25, 0.5, 0.75, 1.0):
on_progress(pct)
return DownloadResult(ok=True, path=dest, track_count=candidate.track_count)
class FakeQobuz(_BaseFake):
name = "qobuz"
tier = 0
class FakeSoulseek(_BaseFake):
name = "soulseek"
tier = 1
class FakeYouTube(_BaseFake):
name = "youtube"
tier = 2
class FailingAdapter(_BaseFake):
name = "qobuz" # pretends to be a real source that then fails to download
tier = 0
def download(
self, candidate: Candidate, dest: str, on_progress: Callable[[float], None]
) -> DownloadResult:
return DownloadResult(ok=False, error="simulated download failure")
class FakeMbBrowser:
"""In-memory MbBrowser for tests. `releases` maps artist mbid -> [ReleaseGroupInfo]."""
def __init__(self, artists=None, releases=None):
self._artists = list(artists or [])
self._releases = dict(releases or {})
def search_artist(self, name: str) -> list[ArtistHit]:
return list(self._artists)
def browse_release_groups(self, artist_mbid: str) -> list[ReleaseGroupInfo]:
return list(self._releases.get(artist_mbid, []))
class FakeAudioProbe:
"""In-memory AudioProbe for tests."""
def __init__(self, artist="", album="", fmt="FLAC", quality_class=3):
self._r = ProbeResult(artist=artist, album=album, fmt=fmt, quality_class=quality_class)
def probe(self, path: str) -> ProbeResult:
return self._r