mirror of https://github.com/Nezreka/SoulSync.git
dev
main
fix/quarantine-source-dedup
release/2.5.3
fix/disable-beatport-features
johnbaumb-discover-redesign
1.0
1.1
1.2
1.3
1.4
1.5
1.6
1.7
1.8
1.9
2.0
2.1
2.2
2.3
2.4.0
2.4.1
2.4.2
2.5.0
2.5.1
2.5.2
2.5.3
2.5.4
2.5.5
2.5.6
2.5.7
2.5.9
2.6.0
2.6.1
v0.65
${ noResults }
7 Commits (dev)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
c4c922c40f |
Surface engine-not-wired errors + exclude soulseek from monitor aggregation
Two findings from JohnBaumb on the engine refactor.
(1) Every download client returned None when self._engine was None,
just logging an error. The orchestrator's download_with_fallback
treated None as "source declined", so the user got no feedback —
download silently disappeared. Now each client raises a RuntimeError
on the engine-not-wired path. download_with_fallback already catches
plugin exceptions, logs a warning, and tries the next source — so
the visible behavior is "real error in logs + fallback to next
source" instead of "silent drop". Six clients touched (deezer, hifi,
qobuz, soundcloud, tidal, youtube). Pinning tests updated to expect
raise.
(2) Monitor's engine.get_all_downloads() walked every plugin
including soulseek, but the same monitor loop already pulled slskd
transfers via the transfers/downloads endpoint a few lines earlier —
soulseek's records were being fetched twice per tick. Same issue in
web_server.py's get_cached_transfer_data path. Added an exclude
parameter to engine.get_all_downloads(); both call sites now pass
('soulseek',). New test pins the exclude semantic.
Also fixed a stray 8-space over-indent on the for-loop body in
get_cached_transfer_data (cosmetic, JohnBaumb flagged the same
pattern in monitor.py earlier).
|
3 weeks ago |
|
|
d17365296a |
Lift shared download dataclasses + boot via singleton factory
Two architectural cleanups on top of the download engine refactor. (1) Shared dataclasses move to neutral plugin package. TrackResult, AlbumResult, DownloadStatus, SearchResult lived in core/soulseek_client.py for historical reasons — every other plugin imported them from the soulseek module just to satisfy the contract, coupling 8 clients to a sibling source for type imports only. Moved them to the new core/download_plugins/types.py module and updated all 14 import sites across the deezer/hifi/lidarr/qobuz/soundcloud/tidal/ youtube clients, the engine, matching engine, redownload helper, and tests. Clean break, no backward-compat re-export. (2) web_server.py boots the orchestrator via the singleton factory. After construction it now calls set_download_orchestrator(...) so get_download_orchestrator() returns the same instance the global handle points at instead of lazily building a separate orchestrator. Matches the get_metadata_engine() pattern. |
3 weeks ago |
|
|
ea654f664e |
Cin-1: Make DownloadSourcePlugin inheritance explicit on every client
Cin's review feedback: the plugin contract was discoverable only from the registry, not from the client files themselves. Reading `youtube_client.py` cold gave no signal that the class participates in the DownloadSourcePlugin contract. Every download client class now inherits DownloadSourcePlugin explicitly: - SoulseekClient(DownloadSourcePlugin) - YouTubeClient(DownloadSourcePlugin) - TidalDownloadClient(DownloadSourcePlugin) - QobuzClient(DownloadSourcePlugin) - HiFiClient(DownloadSourcePlugin) - DeezerDownloadClient(DownloadSourcePlugin) - SoundcloudClient(DownloadSourcePlugin) - LidarrDownloadClient(DownloadSourcePlugin) Adjustments: - core/download_plugins/base.py — moved TrackResult/AlbumResult/ DownloadStatus imports under TYPE_CHECKING since they're only used in type annotations. Without this, clients inheriting the contract create a circular import. - core/download_plugins/__init__.py — drops DownloadPluginRegistry re-export. Importing the package no longer triggers the registry's eager client imports (which would also be circular for clients that import from the package). Callers that need the registry import it directly: `from core.download_plugins.registry import DownloadPluginRegistry`. Suite still green (335 download tests). |
3 weeks ago |
|
|
3a70f0453c |
G: Wire YouTube progress hook to engine + drop dead threading imports
YouTube's _progress_hook still wrote to the per-client active_downloads dict + _download_lock that Phase C2 deleted — runtime crash waiting to happen. Rewritten to use engine.update_record. Same state-dict shape, same UI semantics (95% during ffmpeg postprocess, 'Errored' on yt-dlp error, 'InProgress, Downloading' during stream). Drop unused `import threading` from youtube/tidal/soundcloud clients (no longer spawn threads — engine.worker owns that). Qobuz/HiFi/Deezer keep their threading import for module-level or per-instance API locks (separate from download threading). Suite still green (2050 passed). |
3 weeks ago |
|
|
fdb3e44965 |
C7: Migrate SoundCloud to engine.worker
Last C-phase migration. Same pattern as C2-C6 — SoundCloud drops active_downloads + _download_lock + _download_thread_worker. download() delegates to engine.worker.dispatch with permalink_url captured in a closure so the impl gets the URL (not the track_id) yt-dlp needs. Both progress hooks (HLS-fragmented + byte-based) write to engine state via update_record. Query/cancel methods read engine state. Existing test_soundcloud_client.py mass-updated: 16 tests that reached into client.active_downloads / _download_lock now use engine.add_record / get_record / update_record via a small _wire_engine helper. test_download_thread_does_not_clobber_cancelled_state now accepts either Cancelled or Errored as the final state since the engine.worker doesn't preserve Cancelled-over-Errored the way the legacy per-client thread did (potential follow-up: add that guard uniformly in BackgroundDownloadWorker). Phase A pinning tests updated. Suite still green (2033 passed). |
3 weeks ago |
|
|
8de4a186b7 |
Fix three SoundCloud integration gaps surfaced by smoke testing
User report: switched download source to SoundCloud and noticed: 1. Download progress % stays at 0 until "suddenly done" — no live progress 2. Sidebar status indicator next to "SoundCloud" label is red 3. Dashboard service status card still shows "Soulseek" as the source name Fix 1 — Live progress for HLS-segmented SoundCloud downloads (`core/soundcloud_client.py`): - yt-dlp's `total_bytes` / `total_bytes_estimate` for HLS describes the CURRENT FRAGMENT, not the whole download. So the byte-based percentage stayed near 0 the entire time — until 'finished' fired. - Added `_update_download_progress_fragmented` which uses `fragment_index` / `fragment_count` (which yt-dlp DOES populate accurately for HLS) to compute a meaningful percentage. Total size is extrapolated from per-fragment average for the bytes/remaining display. Time-remaining estimate uses elapsed/index seconds-per- fragment. - The progress hook prefers fragment progress when both fragment_index and fragment_count are present; falls back to byte-based for non-fragmented (progressive MP3) downloads. Five new unit tests pin the fragment-progress math, the 99.9% cap, and the defensive zero-index / unknown-id paths. Fix 2 — Sidebar status indicator stays green for SoundCloud mode (`web_server.py`): - The `/api/status` route's `serverless_sources` tuple decides whether to even probe slskd. SoundCloud (and Lidarr) were missing — so when the active source was SoundCloud, the route fell through to "test slskd, mark not-relevant", which set `connected: False` and turned the sidebar dot red even though SoundCloud was working. - Added `'soundcloud'` and `'lidarr'` to the tuple. Both are serverless from slskd's perspective, so the dot now stays green whenever they're the active source. Fix 3 — Dashboard service card title shows the active source (`webui/static/shared-helpers.js`): - The dashboard's "Download Source" card has its own `sourceNames` map at line 3351 (separate from the sidebar map I already updated at 3396). Missed it during the integration PR. - Added `'lidarr'` and `'soundcloud'` so the card title now reads "SoundCloud" / "Lidarr" instead of falling back to "Soulseek". Bonus — Dashboard "Test Connection" button works for SoundCloud (`core/connection_test.py`): - The dashboard's Test Connection button on the download-source card sends `service` based on the active source — so for SoundCloud it was sending `service='soundcloud'`. `run_service_test` had no branch for it, so it fell through to "Unknown service." and the button always failed. - Added a `soundcloud` branch that mirrors `/api/soundcloud/status` behavior: confirms yt-dlp is installed, runs a real cheap probe, returns a meaningful pass/fail. (HiFi has the same gap but no user reported it; out of scope for this fix.) Verified: - 41 unit tests pass (5 new fragment-progress tests added) - Full suite 1732 passed - Ruff clean |
3 weeks ago |
|
|
583c4f1e49 |
Build SoundCloud download client (not yet wired into app)
Discord request (Toasti): some tracks (DJ mixes, sets, removed Spotify
content) only live on SoundCloud. Add SoundCloud as an option for the
existing multi-source download dispatch.
This commit only ships the client + tests. Integration into the search
dispatch / settings UI / web_server.py routes is intentionally deferred
to a follow-up PR — the user-requested workflow is build-and-verify
in isolation first, then wire up.
`core/soundcloud_client.py`:
- SoundcloudClient class mirrors the public surface of every other
download client (TidalDownloadClient, QobuzClient, HiFiClient,
DeezerDownloadClient): __init__(download_path), set_shutdown_check,
is_available / is_configured / is_authenticated, async check_connection,
async search returning (List[TrackResult], List[AlbumResult]),
async download returning a download_id, _download_thread_worker /
_download_sync / _update_download_progress, async get_all_downloads /
get_download_status / cancel_download / clear_all_completed_downloads.
- Underlying lib: yt-dlp (already in requirements.txt as 2026.3.17).
- Anonymous-only — public SoundCloud tracks at the cap quality (typically
128 kbps MP3, occasionally 256 kbps AAC depending on the upload).
No FLAC ever; SoundCloud doesn't expose lossless. OAuth tier for
SoundCloud Go+ is documented in the module header as a future tier.
- Returns standard TrackResult / DownloadStatus dataclasses from
core.soulseek_client so downstream matching/post-processing stays
source-agnostic.
- Filename dispatch key encodes track_id + permalink_url + display_name
so the download worker has everything without re-querying.
- Heuristic "Artist - Title" parser handles SoundCloud uploaders'
typical title format; falls back to uploader handle as artist when
the title doesn't have a separator.
- Defensive: search returns empty on bad input, missing yt-dlp, or any
raised exception. Download sync rejects files under 100KB (preview
snippets / broken responses) and cleans them up.
- Cooperative cancellation via shutdown_check inside yt-dlp's
progress_hooks. Cancelled state survives the download thread's
terminal-state assignment.
`tests/test_soundcloud_client.py`:
- 37 unit tests with yt-dlp stubbed: search shape correctness, the
artist/title heuristic, the dispatch-key roundtrip, the download
state machine (success / failure / shutdown / Cancelled-state
preservation), the progress emitter (progress capping, time
remaining), defensive paths (missing yt-dlp, raising yt-dlp,
malformed entries, empty entries), and the cancel/clear ledger
operations.
- 2 live integration tests gated behind `-m soundcloud_live` so CI
doesn't run them by default. Run locally with:
python -m pytest tests/test_soundcloud_client.py -m soundcloud_live -v
- All 37 unit tests pass; both live tests pass against real SoundCloud.
- Verified end-to-end with a real album download (Kendrick GNX, 12/12
tracks, 4-7 MB each, completed under 60s per track).
`pyproject.toml`:
- Register the `soundcloud_live` pytest marker so the unknown-mark
warning is suppressed and the live tests can be cleanly gated.
Not changed: web_server.py, settings UI, search dispatch, matching
engine, WHATS_NEW. Integration is the next PR.
|
3 weeks ago |