Files
Lyra/worker/tests/test_slskd_retrieve.py
Jonathan f74fd5ab88 fix(worker): retrieve Soulseek downloads from slskd's dir into staging
SlskdClient enqueued the transfer and polled to Completed but never moved any
files into `dest` — slskd saves them to its OWN downloads dir, so staging stayed
empty and every Soulseek job failed "incomplete download". Qobuz/YouTube write
straight into staging and were unaffected.

- SlskdClient._retrieve() moves the finished files out of slskd's downloads dir
  into staging after completion, matching tolerantly by basename and preferring
  a parent-folder + size match (slskd's on-disk layout varies by version).
- download() returns the moved count as track_count so the pipeline's
  completeness check reflects what actually landed; raises loudly if nothing was
  found (misconfigured/unmounted downloads dir).
- Downloads dir resolves slskd.downloadsDir Config > SLSKD_DOWNLOADS_ROOT env >
  /slskd-downloads default; docker-compose mounts ${SLSKD_DOWNLOADS_DIR} there.
- .env.example documents SLSKD_DOWNLOADS_DIR; retrieval unit-tested offline.

slskd runs as an external daemon, so the shared dir must be a host path both
slskd and the worker mount — set once Lyra runs on the slskd host. Not yet
verified live end-to-end (no slskd access from this VM).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 09:59:16 +02:00

74 lines
2.8 KiB
Python

"""Offline tests for SlskdClient._retrieve — the step that moves slskd's finished
downloads out of its own dir into the pipeline's staging dir. The search/download REST
flow stays live-only (test_soulseek_live.py); retrieval is pure filesystem, so it is
unit-testable here."""
import os
from lyra_worker.adapters._slskd import SlskdClient
def _client(downloads_dir: str) -> SlskdClient:
return SlskdClient({"slskd.url": "http://x", "slskd.api_key": "k",
"slskd.downloadsDir": downloads_dir})
def _write(path: str, content: bytes = b"audio") -> None:
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as fh:
fh.write(content)
def test_retrieve_moves_files_from_slskd_dir(tmp_path):
downloads = tmp_path / "slskd"
dest = tmp_path / "staging"
# slskd's typical layout: <downloads>/<remote parent folder>/<file>
_write(str(downloads / "Artist - Album" / "01 One.flac"))
_write(str(downloads / "Artist - Album" / "02 Two.flac"))
files = [
{"filename": r"@@abc\music\Artist - Album\01 One.flac", "size": 5},
{"filename": r"@@abc\music\Artist - Album\02 Two.flac", "size": 5},
]
moved = _client(str(downloads))._retrieve(files, str(dest))
assert moved == 2
assert sorted(os.listdir(dest)) == ["01 One.flac", "02 Two.flac"]
# the source files were moved, not copied
assert os.listdir(downloads / "Artist - Album") == []
def test_retrieve_disambiguates_by_parent_and_size(tmp_path):
downloads = tmp_path / "slskd"
dest = tmp_path / "staging"
# a decoy with the same basename in a different folder + wrong size
_write(str(downloads / "Other Album" / "01 One.flac"), b"wrongsize!!")
_write(str(downloads / "Artist - Album" / "01 One.flac"), b"right")
files = [{"filename": r"x\Artist - Album\01 One.flac", "size": len(b"right")}]
moved = _client(str(downloads))._retrieve(files, str(dest))
assert moved == 1
with open(dest / "01 One.flac", "rb") as fh:
assert fh.read() == b"right" # picked the parent+size match, not the decoy
def test_retrieve_returns_zero_when_nothing_matches(tmp_path):
downloads = tmp_path / "slskd"
downloads.mkdir()
dest = tmp_path / "staging"
files = [{"filename": r"x\Album\missing.flac", "size": 1}]
assert _client(str(downloads))._retrieve(files, str(dest)) == 0
def test_retrieve_raises_when_downloads_dir_absent(tmp_path):
dest = tmp_path / "staging"
client = _client(str(tmp_path / "does-not-exist"))
files = [{"filename": r"x\Album\a.flac", "size": 1}]
try:
client._retrieve(files, str(dest))
except RuntimeError as e:
assert "downloads dir not found" in str(e)
else:
raise AssertionError("expected RuntimeError for a missing downloads dir")