crabidy/plan/soundcloud.md

6.5 KiB
Raw Blame History

Task plan — soundcloud provider

Status (2026-07-24): all phases AE implemented. Everything offline is verified (unit tests, parsers, server build, fmt/clippy/machete). The live-only gates — the real client_id scrape (B2/B3), live JSON shapes (B4/B5), and mp3-HLS play-to-EOS (C4), plus browsing /soundcloud (D4) and tests/live.rs (E1) — need real SoundCloud access and are left to run on a machine with creds and audio. See plan/summary.md.

Executes architecture/soundcloud-provider.md against the stubs in soundclouddy/ and audio-player/src/hls.rs, satisfying the gates in quality/soundcloud-provider.md. Ordered by dependency. Verification column names the test(s) / gate(s) each task must satisfy.

Phase A — Provider logic (no network; unit-testable now)

  • A1 — Settings + accessors. Bounds accessors (search_results, playlist_tracks, call_timeout, hls_deadline) with the DEFAULT_* consts; redacting Debug already present. (G1; settings_debug_redacts_secrets.)
  • A2 — get_lib_root builds root children directly (sync, no network): search + resolve, plus likes + playlists iff logged_in. Serve the Root node from here (decision #1) and have get_lib_node(Root) delegate to it. (G10; root_without_login_has_no_personal_nodes, root_with_login_shows_personal_nodes.)
  • A3 — sc_track mapper ScTrack→proto Track at /soundcloud/track/<id>, provider_item_id = "track:<id>", ms→Option<u32>. (G12.)
  • A4 — search_node + term store (create/rename/delete, dedup, implicit recreation). (G17; create_rename_delete_search_term.)
  • A5 — search_term_node lists tracks (into node.tracks, canonical paths, queueable node) and playlists (into node.children, canonical container paths, queueable). (G13, G12, G16; search_term_lists_tracks_and_playlists_with_canonical_paths.)
  • A6 — resolve_node + URL store (create/rename/delete), and resolve_entry_node: track→node.tracks=[t]; playlist→child container. (G14, G17; resolve_entry_shapes_track_and_playlist, create_resolve_url_entry.)
  • A7 — playlist_node fetch detail, hydrate stub ids in ≤50 batches up to playlist_tracks, log truncation. (G15; playlist_node_hydrates_stub_tracks.)
  • A8 — likes_node / playlists_node (login only). (G10.)
  • A9 — get_urls_for_track / get_metadata_for_track via resolve_stream_url / track_detail; non-track path → MalformedPath. (G18; get_urls_for_track_returns_the_m3u8.)
  • A10 — Download blessing in get_lib_node (node + children), and typed fetch_err mapping (no panics). (G4, G16; backend_failures_are_typed.)
  • A11 — Gate: cargo test -p soundclouddy — all 15 tests pass. (G11, G22.)

Phase B — HTTP seam ScApi (needs live SoundCloud to fully verify)

  • B1 — ScApi::new builds the reqwest client with per-call timeout + user-agent; holds client_id/app_version behind RwLock, optional OAuth. (G3, G6.)
  • B2 — scrape_client_id free fn — fetch soundcloud.com, find script bundles, regex client_id + app_version. Unit-test the parser against a captured HTML/JS fixture (no network). (G7; new scrape_parses_fixture test.)
  • B3 — Signed GET helper: append client_id/app_version, OAuth header when present; map status→FetchError (404/401/403); re-scrape once on 401/403 then retry. (G8; reauth_rescrapes_once_then_gives_up over a mock/loopback.)
  • B4 — Defensive wire DTOs (#[serde(default)], drop id-less entries) and into_* converters for track/playlist/resolve/search/transcoding. (G5.)
  • B5 — resolve_stream_url: pick hls+audio/mpeg transcoding, GET its url, return the .m3u8; NotStreamable when absent. (G18.)
  • B6 — hydrate_tracks batched /tracks?ids= (≤50). (G15.)
  • B7 — settings() folds cached client_id/app_version back in. (G9; settings_round_trip_persists_scraped_id.)
  • B8 — init parses TOML, builds ScApi, ensures a client_id (scrape if none), non-fatal on total failure. (G7.)

Phase C — HLS playback in audio-player

  • C1 — parse_media_playlist hand-written line parser (#EXTINF + URI, resolve relative against base, one-level master fallback). Unit test with a fixture playlist string. (G19; new hls::tests::parses_media_playlist.)
  • C2 — HlsStream state machine (Stream + SourceStream): fetch segments in order, advance at boundaries, finish after last; reconnect re-opens the current segment; forward-only, content_length None, supports_seek false; total-deadline bound. (G6, G19, G20.)
  • C3 — Route .m3u8 in open_source to HlsStream (via StreamDownload) before the windowed-HTTP path; opus/other sniffing downstream unchanged. Wire mod hls; (already declared). (G21.)
  • C4 — Gate (integration): an ffmpeg-built mp3-HLS fixture (or a live track) plays to EOS with no panic; add a #[ignore] play-to-EOS test. (G19, G20.)

Phase D — Server wiring

  • D1 — Workspace dep soundclouddy = { path = "soundclouddy" } in root Cargo.toml [workspace.dependencies]; add to crabidy-server deps.
  • D2 — settings.rs: "soundcloud" in ALL_PROVIDERS (→8), ProviderToggles.sc, all(), provider_toggles().
  • D3 — provider.rs: sc_client field, sc_owns, sc_provider, build() block reading soundcloud.toml (non-fatal, round-trip settings()), get_lib_root child gated on is_some(), and a routing arm in each dispatch method. (mirrors abs.)
  • D4 — Gate: cargo build -p crabidy-server; browse /soundcloud in the running app (manual//run).

Phase E — Validation & docs

  • E1 — tests/live.rs (#[ignore]): reads SOUNDCLOUD_OAUTH/optional SOUNDCLOUD_CLIENT_ID from env, exercises scrape→search→resolve→stream_url end-to-end; confirms DTOs match live JSON. (G7 live gate.)
  • E2 — Quality sweep: cargo fmt, clippy, machete; tick quality/soundcloud-provider.md. (G23.)
  • E3 — plan/summary.md records deviations from this plan.

Notes

  • Live-verification boundary: Phases A and C1 are fully verifiable offline. B3/B5/C4/D4/E1 need live SoundCloud access (no creds in CI/sandbox) — deliver as code + fixture/mock tests, run the live gate on a real machine.
  • Do not commit secrets; soundcloud.toml stays out of git.