Plug the previously-built SoundcloudClient (PR #478, the build-and-verify phase) into every place a download source needs to appear. Follows the same wiring contract as Tidal/Qobuz/HiFi/Deezer/Lidarr — orchestrator routing, hybrid-mode picker, search dispatch, queue/cancel/clear, provenance + library history, sidebar source label, settings UI all work plug-and-play. Backend wiring: - `core/download_orchestrator.py` — import SoundcloudClient, _safe_init it at startup, add to _client() lookup, get_source_status(), check_connection's sources_to_check default, search source_names map, search_and_download_best _streaming_sources tuple, download source_map + source_names, and every iteration loop in reload_settings download-path-update / get_all_downloads / get_download_status / cancel_download (route + iterate) / clear_all_completed_downloads / cancel_all_downloads. - `core/downloads/monitor.py` — added SoundCloud to the per-client loop that fetches active downloads outside the orchestrator (uses getattr fallback for older soulseek_client snapshots). - `core/downloads/task_worker.py` — added SoundCloud (and Lidarr, which was missing too — bonus fix) to source_clients dict for hybrid fallback dispatch. - `core/downloads/validation.py` — added 'soundcloud' to _streaming_sources so SoundCloud results go through the matching engine validation path instead of the Soulseek quality-filter path. - `core/imports/side_effects.py` — three call sites: source_map for download_source label written to library_history, streaming-source guard for the `||`-encoded stream_id parsing, and source_service map for provenance recording. All three now include 'soundcloud'. - `web_server.py` — five streaming-source detection tuples updated. New `/api/soundcloud/status` endpoint returns {available, configured, reachable} mirroring the Deezer/HiFi status-endpoint pattern; reachability runs a real cheap yt-dlp search so the settings Test Connection button gives a meaningful pass/fail signal. - `config/settings.py` — added empty `soundcloud_download` defaults block so future tier-2 OAuth (SoundCloud Go+ session) doesn't have to migrate existing configs. Frontend: - `webui/index.html` — new `<option value="soundcloud">` in the download-source-mode dropdown, SoundCloud added to both hidden legacy hybrid-source selects, new settings container with info text + Test Connection button. - `webui/static/settings.js` — HYBRID_SOURCES entry (with the SoundCloud cloud SVG icon), _hybridSourceEnabled default, updateDownloadSourceUI container display, allSources for legacy hybrid picker, testSoundcloudConnection function (hits the new status endpoint, color-codes the result), saveSettings soundcloud_download empty block. - `webui/static/shared-helpers.js` — sidebar source-name map includes SoundCloud + Lidarr (Lidarr was also missing, bonus fix). - `webui/static/helper.js` — WHATS_NEW entry under '2.4.2' dev cycle describing the user-visible change in the chill terse voice. Tests: - `tests/test_download_orchestrator_soundcloud.py` — 14 integration tests verifying the wiring: client constructed at startup, _client lookup resolves 'soundcloud', get_source_status includes it, download dispatcher routes username='soundcloud' to the SoundCloud client (and unknown usernames still fall back to Soulseek), hybrid search iterates SoundCloud when in order and skips it cleanly when unconfigured, get_all_downloads / get_download_status / cancel / clear walk SoundCloud, soundcloud-only mode dispatches only to SoundCloud, _streaming_sources tuple in validation includes 'soundcloud'. - `tests/downloads/test_download_orchestrator.py` — added `soundcloud` to the test fixture's _build_orchestrator helper so the new orchestrator attribute doesn't AttributeError in pre- existing tests that bypass __init__. Verified: - Full suite green (1728 passed, 2 deselected for soundcloud_live) - Ruff clean - Live SoundCloud-only mode search returns 25 SoundCloud tracks for "kendrick lamar luther" in <2s, returning properly-shaped TrackResult objects with username='soundcloud' and dispatch-key filename ready for the download path. Out of scope (intentional deferrals): - SoundCloud Go+ OAuth tier (256 kbps AAC) — anonymous-only for now. Adding auth later is a settings-page extension, no orchestrator changes needed. - Album/playlist support — SoundCloud has playlists but they don't map to the album model the rest of SoulSync expects. Singles only.
62 lines
1.7 KiB
Python
62 lines
1.7 KiB
Python
from core.download_orchestrator import DownloadOrchestrator
|
|
|
|
|
|
class _FakeClient:
|
|
def __init__(self, configured=True, clear_result=True):
|
|
self.configured = configured
|
|
self.clear_result = clear_result
|
|
self.clear_calls = 0
|
|
|
|
def is_configured(self):
|
|
return self.configured
|
|
|
|
async def clear_all_completed_downloads(self):
|
|
self.clear_calls += 1
|
|
return self.clear_result
|
|
|
|
|
|
def _build_orchestrator(**clients):
|
|
orch = DownloadOrchestrator.__new__(DownloadOrchestrator)
|
|
orch.soulseek = clients.get("soulseek")
|
|
orch.youtube = clients.get("youtube")
|
|
orch.tidal = clients.get("tidal")
|
|
orch.qobuz = clients.get("qobuz")
|
|
orch.hifi = clients.get("hifi")
|
|
orch.deezer_dl = clients.get("deezer_dl")
|
|
orch.lidarr = clients.get("lidarr")
|
|
orch.soundcloud = clients.get("soundcloud")
|
|
return orch
|
|
|
|
|
|
def _run_async(coro):
|
|
import asyncio
|
|
|
|
loop = asyncio.new_event_loop()
|
|
try:
|
|
return loop.run_until_complete(coro)
|
|
finally:
|
|
loop.close()
|
|
|
|
|
|
def test_clear_all_completed_downloads_ignores_unconfigured_clients():
|
|
orch = _build_orchestrator(
|
|
soulseek=_FakeClient(configured=True, clear_result=True),
|
|
youtube=_FakeClient(configured=False, clear_result=False),
|
|
)
|
|
|
|
result = _run_async(orch.clear_all_completed_downloads())
|
|
|
|
assert result is True
|
|
assert orch.soulseek.clear_calls == 1
|
|
assert orch.youtube.clear_calls == 0
|
|
|
|
|
|
def test_clear_all_completed_downloads_propagates_configured_failures():
|
|
orch = _build_orchestrator(
|
|
soulseek=_FakeClient(configured=True, clear_result=False),
|
|
)
|
|
|
|
result = _run_async(orch.clear_all_completed_downloads())
|
|
|
|
assert result is False
|
|
assert orch.soulseek.clear_calls == 1
|