Public API methods exposed for downstream consumers (e.g. straw can
call deobf.has_sig() to skip cipher streams without observing an Err).
The internal short-circuit uses the struct field directly so the
methods register as unused at compile time.
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.
When YouTube rotates player.js to a shape our six sig/nsig regex
patterns don't recognise (eg. c2f7551f, May 2026), the whole player
path used to die at extract_fns even for clients that don't need the
sig fn at all (iOS, Android, Tv all get pre-signed stream URLs).
Now sig_fn / nsig_fn extraction is best-effort. Only the signature
timestamp is required — every `needs_deobf` client needs sts in
the request payload, but the actual deobfuscation functions are only
consumed by map_url when a stream URL carries `&s=` or `&n=`.
On failure we log a warning and store an empty string; Deobfuscator
then skips the JS eval, and any deobfuscate_sig/deobfuscate_nsig
call will fail loudly with "sig fn unavailable" instead of crashing
the player.
Keeps the Tv fallback alive even when sig deobf regex breaks.
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.