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.
Use 0.11.5 instead of 0.11.4-sulkta.1 so the in-workspace
rustypipe-downloader / rustypipe-cli crates (which require
`rustypipe = ^0.11.4`) keep resolving. The original upstream rev
on codeberg is at 0.11.4; we tag this internal release as 0.11.5
to keep cargo happy without needing to bump dependents.
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
Since YouTube keeps changing the nsig function signature and a generic regex may match at multiple places, I changed the extraction logic to search for multiple matches if necessary and test the extracted deobfuscation functions.
I also found out that if the deobfuscation fails for all streams, fetching the player still returns a successful result with no streams, suggesting that the video is not available. So I changed the mapper to throw an ExtractionError if no streams are mapped successfully.