youtube: fall back to VISIONOS primary when ANDROID is bot-walled
Some checks failed
gitleaks / scan (push) Failing after 5s
Some checks failed
gitleaks / scan (push) Failing after 5s
YouTube's 2026 poToken/BotGuard enforcement now rejects the pot-less ANDROID client outright (playabilityStatus "sign in to confirm you're not a bot"), which aborted the whole extraction since ANDROID was the required primary. stream_info_with() now cascades ANDROID -> VISIONOS for the primary player response: when ANDROID is unusable (fetch error, failed playability, or decoy), retry with VISIONOS (client 101) as the primary. VISIONOS is pot-free, visitorData-based, and needs no JS player -- the current survivor (yt-dlp default; NewPipeExtractor #1508). Only if VISIONOS also fails do we surface the ORIGINAL ANDROID error (keeps the real bot-wall reason visible). The fallback drops the (poisoned) visitorData cache first so VISIONOS mints a fresh one (the ingredient that passes attestation). On the VISIONOS-primary path the primary source carries no poToken (null) and uses the VISIONOS cpn, so no ANDROID pot/cpn is appended to a visionOS URL (would 403); the redundant add-on VISIONOS fetch is skipped. Output shape is unchanged (still the muxed itag-18 the app plays) -- no app or delivery-labeling change. 5 new unit tests.
This commit is contained in:
parent
a21a6e14ab
commit
dd964407cc
3 changed files with 421 additions and 23 deletions
141
examples/emit_json.rs
Normal file
141
examples/emit_json.rs
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
// Differential-verification JSON emitter (Phase-1 harness, NOT shipped).
|
||||
//
|
||||
// Additive example only — adds no production code. Emits strawcore's
|
||||
// extraction for one video id as the harness's normalized JSON schema on
|
||||
// stdout, so it can be diffed against the upstream NewPipe-Extractor (Java)
|
||||
// oracle. See /root/build/npe-diff/ for the driver + differ.
|
||||
//
|
||||
// cargo run --example emit_json -- <VIDEO_ID> [visionos-on]
|
||||
//
|
||||
// Default matches the shipped app path (ExtractOptions::default(), visionOS
|
||||
// OFF). Pass `visionos-on` to fetch the visionOS client (client 101) — that
|
||||
// is what upstream NPE does UNCONDITIONALLY, so `visionos-on` is the
|
||||
// apples-to-apples mode for an NPE fidelity diff.
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use strawcore_core::stream::{AudioStream, DeliveryMethod, StreamInfo, VideoStream};
|
||||
use strawcore_core::youtube::stream_extractor::{stream_info_with, ExtractOptions};
|
||||
use strawcore_core::NewPipe;
|
||||
|
||||
/// Parse the innertube client tag (`&c=WEB` / `&c=ANDROID` / ...) off a
|
||||
/// googlevideo URL — this is the "which client sourced this stream" signal
|
||||
/// that made the visionOS outage visible.
|
||||
fn client_source(url: &str) -> Value {
|
||||
url.split(['&', '?'])
|
||||
.find_map(|kv| kv.strip_prefix("c="))
|
||||
.map(|c| Value::String(c.to_string()))
|
||||
.unwrap_or(Value::Null)
|
||||
}
|
||||
|
||||
fn delivery_str(d: DeliveryMethod) -> &'static str {
|
||||
match d {
|
||||
DeliveryMethod::Progressive => "progressive",
|
||||
DeliveryMethod::Dash => "dash",
|
||||
DeliveryMethod::Hls => "hls",
|
||||
DeliveryMethod::Torrent => "torrent",
|
||||
}
|
||||
}
|
||||
|
||||
fn video_json(s: &VideoStream, kind: &str) -> Value {
|
||||
json!({
|
||||
"kind": kind,
|
||||
"itag": s.itag,
|
||||
"delivery": delivery_str(s.delivery),
|
||||
"format": s.format.extension(),
|
||||
"mime": s.format.mime(),
|
||||
"codec": s.codec,
|
||||
"resolution": if s.resolution.is_empty() { Value::Null } else { json!(s.resolution) },
|
||||
"fps": s.fps,
|
||||
"bitrate": s.bandwidth,
|
||||
"has_url": !s.url.is_empty(),
|
||||
"client_source": client_source(&s.url),
|
||||
})
|
||||
}
|
||||
|
||||
fn audio_json(s: &AudioStream) -> Value {
|
||||
json!({
|
||||
"kind": "audio",
|
||||
"itag": s.itag,
|
||||
"delivery": delivery_str(s.delivery),
|
||||
"format": s.format.extension(),
|
||||
"mime": s.format.mime(),
|
||||
"codec": s.codec,
|
||||
"resolution": Value::Null,
|
||||
"fps": 0,
|
||||
"bitrate": s.average_bitrate_kbps,
|
||||
"has_url": !s.url.is_empty(),
|
||||
"client_source": client_source(&s.url),
|
||||
})
|
||||
}
|
||||
|
||||
fn emit(info: &StreamInfo) -> Value {
|
||||
let mut streams: Vec<Value> = Vec::new();
|
||||
for s in &info.video_streams {
|
||||
streams.push(video_json(s, "muxed"));
|
||||
}
|
||||
for s in &info.video_only_streams {
|
||||
streams.push(video_json(s, "video_only"));
|
||||
}
|
||||
for s in &info.audio_streams {
|
||||
streams.push(audio_json(s));
|
||||
}
|
||||
// Deterministic order so diffs are stable: (kind, itag, delivery).
|
||||
streams.sort_by(|a, b| {
|
||||
(
|
||||
a["kind"].as_str().unwrap_or(""),
|
||||
a["itag"].as_u64().unwrap_or(0),
|
||||
a["delivery"].as_str().unwrap_or(""),
|
||||
)
|
||||
.cmp(&(
|
||||
b["kind"].as_str().unwrap_or(""),
|
||||
b["itag"].as_u64().unwrap_or(0),
|
||||
b["delivery"].as_str().unwrap_or(""),
|
||||
))
|
||||
});
|
||||
|
||||
json!({
|
||||
"source": "strawcore",
|
||||
"video_id": info.video_id,
|
||||
"meta": {
|
||||
"name": info.name,
|
||||
"duration_seconds": info.duration_seconds,
|
||||
"stream_type": format!("{:?}", info.stream_type),
|
||||
"uploader_id": info.uploader_id,
|
||||
"uploader_name": info.uploader_name,
|
||||
"view_count": info.view_count,
|
||||
},
|
||||
"dash_present": info.dash_manifest_url.is_some(),
|
||||
"hls_present": info.hls_manifest_url.is_some(),
|
||||
"counts": {
|
||||
"muxed": info.video_streams.len(),
|
||||
"video_only": info.video_only_streams.len(),
|
||||
"audio": info.audio_streams.len(),
|
||||
},
|
||||
"streams": streams,
|
||||
})
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let vid = args.next().unwrap_or_else(|| "dQw4w9WgXcQ".into());
|
||||
let visionos = args.next().as_deref() == Some("visionos-on");
|
||||
|
||||
let dl =
|
||||
Arc::new(strawcore_core::downloader::ReqwestDownloader::new().expect("build downloader"));
|
||||
NewPipe::init(dl);
|
||||
|
||||
let opts = ExtractOptions {
|
||||
fetch_visionos_client: visionos,
|
||||
..Default::default()
|
||||
};
|
||||
match stream_info_with(&vid, opts) {
|
||||
Ok(info) => {
|
||||
println!("{}", serde_json::to_string_pretty(&emit(&info)).unwrap());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("strawcore extraction FAILED for {vid}: {e:?}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue