From b5931a7b669518b88ebb4925efb5534f06eac13f Mon Sep 17 00:00:00 2001 From: Jonathan Date: Fri, 10 Jul 2026 22:40:53 +0200 Subject: [PATCH] feat: add SoulseekAdapter with injectable client seam --- worker/lyra_worker/adapters/soulseek.py | 62 ++++++++++++++++++++++ worker/tests/test_soulseek_adapter.py | 68 +++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 worker/lyra_worker/adapters/soulseek.py create mode 100644 worker/tests/test_soulseek_adapter.py diff --git a/worker/lyra_worker/adapters/soulseek.py b/worker/lyra_worker/adapters/soulseek.py new file mode 100644 index 0000000..489f397 --- /dev/null +++ b/worker/lyra_worker/adapters/soulseek.py @@ -0,0 +1,62 @@ +from typing import Callable, Protocol + +from lyra_worker.types import Candidate, DownloadResult, MBTarget, Quality + + +class SoulseekClient(Protocol): + def is_configured(self) -> bool: + ... + + def search_album(self, artist: str, album: str) -> list[dict]: + """Album matches: dicts with source_ref, title, artist, track_count, format, bitrate.""" + ... + + def download(self, source_ref: str, dest: str, on_progress: Callable[[float], None]) -> dict: + """Download into dest; return {track_count, path}. Raise on failure.""" + ... + + +def _quality(fmt: str, bitrate: int | None) -> Quality: + if fmt.upper() == "FLAC": + return Quality(fmt="FLAC", lossless=True, bit_depth=16, sample_rate=44100) + return Quality(fmt="MP3", lossless=False, bitrate_kbps=bitrate or 320) + + +class SoulseekAdapter: + name = "soulseek" + tier = 1 + + def __init__(self, client: SoulseekClient): + self._client = client + + def health(self) -> bool: + return self._client.is_configured() + + def search(self, target: MBTarget) -> list[Candidate]: + candidates: list[Candidate] = [] + for r in self._client.search_album(target.artist, target.album): + candidates.append( + Candidate( + source="soulseek", + source_ref=r["source_ref"], + matched_artist=r.get("artist", ""), + matched_album=r.get("title", ""), + quality=_quality(r.get("format", "MP3"), r.get("bitrate")), + track_count=r.get("track_count", 0) or 0, + source_tier=self.tier, + ) + ) + return candidates + + def download( + self, candidate: Candidate, dest: str, on_progress: Callable[[float], None] + ) -> DownloadResult: + try: + info = self._client.download(candidate.source_ref, dest, on_progress) + except Exception as e: + return DownloadResult(ok=False, error=str(e)) + return DownloadResult( + ok=True, + path=info.get("path", dest), + track_count=info.get("track_count", candidate.track_count), + ) diff --git a/worker/tests/test_soulseek_adapter.py b/worker/tests/test_soulseek_adapter.py new file mode 100644 index 0000000..fa8ae30 --- /dev/null +++ b/worker/tests/test_soulseek_adapter.py @@ -0,0 +1,68 @@ +from lyra_worker.adapters.soulseek import SoulseekAdapter +from lyra_worker.quality import quality_class +from lyra_worker.types import MBTarget + +TARGET = MBTarget(artist="John Mayer", album="Continuum", track_count=12) + + +class FakeSlskdClient: + def __init__(self, configured=True, results=None, fail=False): + self._configured = configured + self._results = results if results is not None else [ + {"source_ref": "ref-flac", "title": "Continuum", "artist": "John Mayer", + "track_count": 12, "format": "FLAC", "bitrate": None}, + ] + self._fail = fail + + def is_configured(self): + return self._configured + + def search_album(self, artist, album): + return self._results + + def download(self, source_ref, dest, on_progress): + if self._fail: + raise RuntimeError("slskd boom") + on_progress(1.0) + return {"track_count": 12, "path": dest} + + +def test_name_and_tier(): + a = SoulseekAdapter(FakeSlskdClient()) + assert a.name == "soulseek" + assert a.tier == 1 + + +def test_health_reflects_configuration(): + assert SoulseekAdapter(FakeSlskdClient(configured=True)).health() is True + assert SoulseekAdapter(FakeSlskdClient(configured=False)).health() is False + + +def test_flac_maps_to_lossless_candidate(): + c = SoulseekAdapter(FakeSlskdClient()).search(TARGET)[0] + assert c.source == "soulseek" + assert c.source_ref == "ref-flac" + assert c.matched_artist == "John Mayer" + assert c.matched_album == "Continuum" + assert c.track_count == 12 + assert c.source_tier == 1 + assert c.quality.fmt == "FLAC" + assert c.quality.lossless is True + assert quality_class(c.quality) == 2 # CD lossless + + +def test_mp3_maps_to_lossy_candidate(): + results = [{"source_ref": "ref-mp3", "title": "Continuum", "artist": "John Mayer", + "track_count": 12, "format": "MP3", "bitrate": 320}] + c = SoulseekAdapter(FakeSlskdClient(results=results)).search(TARGET)[0] + assert c.quality.fmt == "MP3" + assert c.quality.lossless is False + assert c.quality.bitrate_kbps == 320 + assert quality_class(c.quality) == 1 # lossy + + +def test_search_empty_and_download_failure(): + assert SoulseekAdapter(FakeSlskdClient(results=[])).search(TARGET) == [] + bad = SoulseekAdapter(FakeSlskdClient(fail=True)).download( + SoulseekAdapter(FakeSlskdClient()).search(TARGET)[0], "/tmp/x", lambda _p: None) + assert bad.ok is False and bad.error