feat: add SoulseekAdapter with injectable client seam
This commit is contained in:
@@ -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),
|
||||||
|
)
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user