6.5 KiB
Quality gates — soundcloud provider
Criteria an implementation of soundclouddy
(architecture/soundcloud-provider.md) must satisfy. Each is pass/fail by
reading/reasoning; automated coverage lives in soundclouddy/src/tests.rs
(unit, no network, over a FakeSc) and soundclouddy/tests/live.rs (ignored,
real SoundCloud).
Status (2026-07-24): G1, G3–G6, G10–G13, G15–G18, G20–G23 are
implemented and verified offline (unit tests + fmt/clippy/machete). G7/G8/G9
(scrape + re-auth + persist) are implemented; their live behaviour and
G14/G19 (real resolve/playlist + mp3-HLS play-to-EOS) are confirmed only by
tests/live.rs and a manual play-through on a machine with creds/audio. G6's
per-call timeouts are enforced; a whole-HLS-fetch deadline is deferred (the
reqwest client's per-segment timeout bounds each fetch).
Secrets (hard rule — highest priority)
- G1 —
client_id/oauth_tokennever inDebug.Settings,Client, andScApiDebugoutput redact both. (test:settings_debug_redacts_secrets.) - G2 — The signed media URL never reaches a log or error. The
.m3u8URL fromSc::resolve_stream_url(and the segment URLs inHlsStream) are never passed todebug!/warn!/error!. Segment fetch errors carry status only (usereqwest::Error::without_url()likewindowed_http.rs). - G3 —
ScApi::Debugredactsclient_idandoauth_token(manual impl, not derived).
Errors and robustness (hard rule: no panics on input/network)
- G4 — No panics on bad input or network failures. Backend failures map
to a typed
ProviderError(FetchErrorfor fetches,MalformedPathfor bad paths,InvalidInputfor empty titles/URLs). Nounwrap/expect/panic!on request, path, or response data. (tests:backend_failures_are_typed,foreign_and_malformed_paths_reject.) - G5 — Defensive decoding. Wire DTOs use
#[serde(default)]; missing fields degrade (empty string /None/ dropped entry), never error the whole call. Tracks/playlists without an id are dropped, not panicked on. - G6 — Timeouts on every external call.
ScApisets a per-callreqwesttimeout fromcall_timeout_secs(default 30);HlsStreambounds the whole fetch byhls_deadline_secs(default 300).
Auth: client_id self-heal, optional login (D5)
- G7 — Works with no config. With an empty
soundcloud.toml, init scrapes aclient_idand the provider serves public browse/play; a total scrape failure disables the provider non-fatally (never crashes startup). - G8 — Re-scrape on 401. A
401/403triggers exactly one re-scrape and retry; a second failure surfaces asFetchError::Unauthorized(no infinite loop). (test:reauth_rescrapes_once_then_gives_up.) - G9 — Scraped
client_idis persisted.settings()folds the cachedclient_id/app_versionback into the TOML so the next start does not re-scrape. (test:settings_round_trip_persists_scraped_id.) - G10 — Login is optional and gates only personal nodes. No
oauth_token→get_lib_rootchildren aresearch+resolveonly; a token →likes+playlistsalso appear. Public browse/play is identical either way. (tests:root_without_login_has_no_personal_nodes,root_with_login_shows_personal_nodes.)
Tree and path contract
- G11 — Path parsing is total and tag-safe. Every
/soundcloud/...shape maps to a variant orMalformedPath; the literalssearch/resolve/likes/playlists/track/playlistnever collide with a numeric id. Empty segments (/soundcloud//x) areMalformedPath. (tests: the*_parse/*_rejectcases.) - G12 — Canonical leaves. Every track child points at
/soundcloud/track/<id>and every playlist child at/soundcloud/playlist/<id>, so a track id alone resolves a stream (no browse context needed). (test:search_children_use_canonical_paths.) - G13 — Search lists tracks and playlists.
/soundcloud/search/<term>children include matching tracks (queueable leaves) and matching playlists (containers). (test:search_term_lists_tracks_and_playlists.) - G14 — Resolve handles both kinds.
/soundcloud/resolve/<url>yields a single track leaf or a playlist container per the resolvedkind. (test:resolve_entry_shapes_track_and_playlist.) - G15 — Playlists hydrate within bounds. A playlist node hydrates stub
ids in ≤50-id batches up to
playlist_tracks(default 500); a truncated playlist islog-ged, not silently cut. (test:playlist_node_hydrates_stub_tracks.) - G16 — Download blessing. Every node serving tracks and each queueable
child raises
is_downloadable, matching abs/fyyd. (test:queueable_nodes_are_downloadable.) - G17 — Search terms / resolve URLs are creatable/editable/deletable.
/soundcloud/searchand/soundcloud/resolveareis_creatable; their term children areis_editable+is_deletable, with implicit recreation on stale paths. (tests:create_rename_delete_search_term,create_resolve_url_entry.)
Playback: HLS (D4)
- G18 —
get_urls_for_trackreturns one m3u8. For atrack/<id>path it returns exactly[m3u8_url]viaresolve_stream_url; a non-track path isMalformedPath; a non-streamable track surfaces a typed error (skipped, never a crash). (test:get_urls_for_track_returns_m3u8.) - G19 —
HlsStreamstreams segments in order as continuous mp3. The media playlist is parsed (hand-written; relative URIs resolved against the playlist URL; one level of master-playlist fallback), segments are fetched in order, and the concatenated bytes decode as mp3. (gate: live/integration — a real or fixture m3u8 plays to EOS without panic.) - G20 —
HlsStreamis forward-only and non-seekable.supports_seek()isfalseandcontent_length()isNone, so symphonia never attempts the end-seek that panics rodio 0.22 on a length-less stream. (gate: verified in the play-to-EOS integration check.) - G21 — HLS routing.
audio-player'sopen_sourceroutes a.m3u8URL toHlsStreamand every other http URL to the windowed-HTTP path; opus/other content-sniffing downstream is unchanged.
Docs and hygiene
- G22 — Public items documented. Every
pubitem has a doc comment stating intent and error/edge behavior. - G23 —
cargo fmt/clippyclean forsoundclouddyandaudio-player(workspace lint level), andcargo machetereports no unused deps.