CRIT-1: ExtractionError::Deobfuscation is now switchable.
Deobfuscator gains has_sig()/has_nsig() — deobfuscate_sig/_nsig
short-circuit with a recognisable error class so cipher streams
on the wrong client fall through to the next client in the chain
instead of killing the whole call.
CRIT-2: Soft-failed DeobfData now caches with a 1-hour retry instead of
living for 24h. Re-extraction kicks in automatically once YT
rotates back to a player.js shape we recognise — no more
wall-clock-day-of-poisoned-cache.
HIGH-1: Reporter now emits a Level::WRN `extract_deobf_soft_fail` report
on partial extraction. straw / torttube get an artefact when
sig/nsig regex starts missing.
HIGH-2: player_client_order branches on opts.auth. With botguard
+ authed-cookie users, Desktop is now position 2 (where their
cookie maps to an OAuth session) instead of position 4.
HIGH-3: Android dropped from the default order. needs_po_token doesn't
flag Android, so requests were firing unsigned and tripping
YT's bot-check rejection — which is also not switchable.
Re-add when a real po_token strategy lands.
MED-1: Comment in needs_deobf softened — the iOS/Android-no-deobf
property is a current YT behaviour, not a permanent protocol.
MED-2: Cargo.toml workspace pin bumped 0.11.4 → 0.11.5 so it matches
the package version (avoids future 0.12.x bump surprises).
MED-3: Smoke test fixture uses an isolated per-process scratch dir
instead of the repo root, avoiding cache-race with
tests/youtube.rs (which uses CARGO_MANIFEST_DIR and could
wipe OAuth tokens).
LOW-1: Misleading "dead-code fallback" comment in extract_fns replaced
with the actual behaviour description.
LOW-2: get_deobf_data uses read-then-write — concurrent player calls
on warm cache no longer serialise on the write lock.
LOW-3: Smoke test catches IpBan via exact UnavailabilityReason match
instead of substring "Sign in/IpBan/bot" — a real regression
won't silently pass anymore.
LOW-4: TV smoke test now asserts !audio_streams.is_empty() too,
matching iOS / default-order tests.
LOW-5: needs_deobf comment notes YT's historical n= experiments on
Android — sets expectation for future review passes.
Smoke-tested against current YT player c2f7551f (May 2026):
test ios_player_returns_streams ........... ok
test default_client_order_returns_streams . ok (audio Range-GET 206 Partial Content, 1024 bytes)
test tv_player_returns_streams ............ ok (or env-skipped on IP-banned egress)
Fork changes since upstream v0.11.4:
- client::ClientType::needs_deobf: skip player.js deobf for Android too
- client::player::player_client_order: prefer iOS first (no botguard),
iOS/Android/Tv/Desktop (with botguard)
- deobfuscate::DeobfData::extract_fns: soft-fail sig_fn/nsig_fn extraction
so Tv/Desktop callers keep working when YT rotates player.js to a shape
our regex doesn't recognise — only the load-bearing sig_timestamp is
required for the request payload
- tests/sulkta_smoke.rs: end-to-end sanity covering iOS, Tv, default-order
and a Range-GET probe to confirm YT actually serves the audio bytes
YouTube googlevideo CDN 403s HEAD requests + 403s requests with a
non-client User-Agent. Use the iOS client UA on the probe so the CDN
treats it as the same client that requested the URL.
Exercises the patched default client_order + soft-fail DeobfData
end-to-end against current YouTube. Verifies:
1. iOS player_from_client returns streams (no deobf path).
2. TV player_from_client returns streams (deobf path with soft-fail).
3. default-clients player() picks iOS primary and a returned audio
URL HEADs to a 2xx/3xx (i.e. YouTube CDN accepts it).
Lives alongside the upstream tests/youtube.rs so we don't fork their
big snapshot-based test suite, but stays standalone so a single
`cargo test --test sulkta_smoke` exercises just the load-bearing
playback path for our consumers (straw, future torttube).