# 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_token` never in `Debug`.** `Settings`, `Client`, and `ScApi` `Debug` output redact both. *(test: `settings_debug_redacts_secrets`.)* - [ ] **G2 — The signed media URL never reaches a log or error.** The `.m3u8` URL from `Sc::resolve_stream_url` (and the segment URLs in `HlsStream`) are never passed to `debug!`/`warn!`/`error!`. Segment fetch errors carry status only (use `reqwest::Error::without_url()` like `windowed_http.rs`). - [ ] **G3 — `ScApi::Debug` redacts `client_id` and `oauth_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` (`FetchError` for fetches, `MalformedPath` for bad paths, `InvalidInput` for empty titles/URLs). No `unwrap`/`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.** `ScApi` sets a per-call `reqwest` timeout from `call_timeout_secs` (default 30); `HlsStream` bounds the whole fetch by `hls_deadline_secs` (default 300). ## Auth: client_id self-heal, optional login (D5) - [ ] **G7 — Works with no config.** With an empty `soundcloud.toml`, init scrapes a `client_id` and 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/403` triggers exactly **one** re-scrape and retry; a second failure surfaces as `FetchError::Unauthorized` (no infinite loop). *(test: `reauth_rescrapes_once_then_gives_up`.)* - [ ] **G9 — Scraped `client_id` is persisted.** `settings()` folds the cached `client_id`/`app_version` back 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_root` children are `search` + `resolve` only; a token → `likes` + `playlists` also 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 or `MalformedPath`; the literals `search`/`resolve`/`likes`/`playlists`/`track`/`playlist` never collide with a numeric id. Empty segments (`/soundcloud//x`) are `MalformedPath`. *(tests: the `*_parse` / `*_reject` cases.)* - [ ] **G12 — Canonical leaves.** Every track child points at `/soundcloud/track/` and every playlist child at `/soundcloud/playlist/`, 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/` children include matching tracks (queueable leaves) and matching playlists (containers). *(test: `search_term_lists_tracks_and_playlists`.)* - [ ] **G14 — Resolve handles both kinds.** `/soundcloud/resolve/` yields a single track leaf or a playlist container per the resolved `kind`. *(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 is `log`-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/search` and `/soundcloud/resolve` are `is_creatable`; their term children are `is_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_track` returns one m3u8.** For a `track/` path it returns exactly `[m3u8_url]` via `resolve_stream_url`; a non-track path is `MalformedPath`; a non-streamable track surfaces a typed error (skipped, never a crash). *(test: `get_urls_for_track_returns_m3u8`.)* - [ ] **G19 — `HlsStream` streams 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 — `HlsStream` is forward-only and non-seekable.** `supports_seek()` is `false` and `content_length()` is `None`, 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`'s `open_source` routes a `.m3u8` URL to `HlsStream` and every other http URL to the windowed-HTTP path; opus/other content-sniffing downstream is unchanged. ## Docs and hygiene - [ ] **G22 — Public items documented.** Every `pub` item has a doc comment stating intent and error/edge behavior. - [ ] **G23 — `cargo fmt`/`clippy` clean** for `soundclouddy` and `audio-player` (workspace lint level), and `cargo machete` reports no unused deps.