soulsync/tests/test_soulseek_album_fallback.py
BoulderBadgeDad 1590330171 Fix #796: Soulseek album bundle left completed files in slskd download folder
The album-bundle path COPIES slskd's completed files into private staging (then
on to the library) but never removed slskd's originals, so they piled up in the
download folder. (copy, not move, is correct for the torrent/usenet bundle paths
— those clients keep seeding — so the shared copier can't just always delete.)

Add an opt-in remove_source to copy_audio_files_atomically that deletes each
source ONLY after it copies successfully (never on a failed stage), and set it
for the Soulseek path only. Torrent/usenet keep their originals.

Tests: keeps source by default / removes when requested / keeps on failed copy.
2026-06-04 21:56:07 -07:00

100 lines
4 KiB
Python

"""Regression: a Soulseek album folder that yields ZERO usable files must fall
back to the per-track flow rather than hard-failing the whole batch.
The Slipknot case — the preflight-selected peer aborts/stalls every transfer (all
tracks reported "Completed, Aborted" at 0 bytes) — makes
``_poll_album_bundle_downloads`` return ``[]``. ``download_album_to_staging`` used
to return that empty result with ``fallback=False``, so the dispatcher
(``core/downloads/album_bundle_dispatch.try_dispatch``) hit its ``mark_failed``
branch and the batch died with nothing tried elsewhere.
The fix flips that branch to ``fallback=True`` so the existing, proven per-track
flow takes over and re-searches every missing track across ALL sources/peers —
reusing that robustness instead of looping candidate folders inside the bundle.
The downstream "fallback=True -> per-track" routing is covered by
``tests/test_album_bundle_dispatch.py``
(``test_dispatch_fallback_failure_returns_false_for_per_track_flow``); these tests
prove the empty-folder branch actually sets the flag, and that a healthy folder
does NOT fall back.
"""
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import patch
from core.soulseek_client import SoulseekClient
class _Stub:
"""Stand-in exposing only what download_album_to_staging touches on the
preflight-reuse path (preferred_source + preferred_tracks skip search/browse)."""
def __init__(self, poll_result):
self._poll_result = poll_result
def is_configured(self):
return True
def filter_results_by_quality_preference(self, tracks):
return tracks
def download(self, username, filename, size):
return f"dl-{filename}" # truthy id; run_async patched to identity
def _poll_album_bundle_downloads(self, transfer_keys, emit):
return self._poll_result
def _track(name):
return SimpleNamespace(username="deadpeer", filename=name, size=100)
def _run(poll_result):
stub = _Stub(poll_result)
tracks = [_track("01.flac"), _track("02.flac")]
with patch("core.soulseek_client.run_async", lambda x: x):
result = SoulseekClient.download_album_to_staging(
stub,
album_name="All Hope Is Gone",
artist_name="Slipknot",
staging_dir="/tmp/staging-does-not-matter",
preferred_source={
"username": "deadpeer",
"folder_path": "music/Slipknot/All Hope Is Gone",
},
preferred_tracks=tracks,
)
return result, tracks
def test_empty_folder_falls_back_to_per_track():
# Poll returns [] — every transfer aborted / stalled (dead peer).
result, _ = _run([])
assert result['success'] is False
assert result['fallback'] is True # <-- the fix: hand off to per-track
assert result['files'] == []
assert 'per-track' in (result['error'] or '')
def test_healthy_folder_does_not_fall_back():
# Positive control: the poll yields staged files and the copy succeeds, so we
# must NOT flip fallback — this proves the empty-branch isn't blanket-returning
# True. Patch the atomic copy to echo the completed paths through.
from pathlib import Path
completed = [Path("/staged/01.flac"), Path("/staged/02.flac")]
with patch("core.soulseek_client.copy_audio_files_atomically", lambda files, dest, **kw: list(files)):
stub = _Stub(completed)
with patch("core.soulseek_client.run_async", lambda x: x):
result = SoulseekClient.download_album_to_staging(
stub,
album_name="All Hope Is Gone",
artist_name="Slipknot",
staging_dir="/tmp/staging-does-not-matter",
preferred_source={"username": "goodpeer", "folder_path": "music/Slipknot/AHIG"},
preferred_tracks=[_track("01.flac"), _track("02.flac")],
)
assert result['success'] is True
assert result['fallback'] is False
assert result['partial'] is False
assert result['files'] == completed