Phase 5 — PoTokenProvider trait + stream_extractor wiring
Mirrors NPE PoTokenProvider.java + PoTokenResult.java; defines the
host-injection surface for BotGuard attestation. The Rust crate stays
out of the BotGuard business — embedders (Straw on Android, future
Sulkta CLI via Browserless, etc.) supply their own impl.
src/youtube/potoken/mod.rs
* PoTokenResult { player_request_po_token, streaming_data_po_token,
visitor_data } + ::new + ::single constructors
* PoTokenError (Unavailable, MintFailed) — FIX vs NPE: split 'declined'
(Ok(None)) from 'errored' (Err) so callers can react differently
* trait PoTokenProvider with 4 client-scoped methods; default impl
returns Ok(None) so embedders can override just what they support
* set_po_token_provider / clear_po_token_provider / po_token_provider
static registration via RwLock<Option<Arc<dyn PoTokenProvider>>>
src/youtube/potoken/noop.rs
* NoopPoTokenProvider — safe default
src/youtube/stream_extractor.rs
* resolve_po_token via options-first-then-provider helper
(options_or_provider)
* Android branch: pulls player_request_po_token + visitor_data into
/player body, streams streaming_data_po_token through to URL &pot=
* iOS branch: same shape, gated on fetch_ios_client AND non-empty
provider result
Kotlin side (PoTokenWebView lift into Straw via UniFFI's foreign-trait
bridge) is separate work — strawcore just owns the contract.
Tests: 77 lib unit pass (+4 since Phase 4) + 7 Phase 2 offline + 7
Phase 4 offline = 91 green.
This commit is contained in:
parent
0d08f12b3b
commit
1533157485
4 changed files with 271 additions and 8 deletions
|
|
@ -32,6 +32,7 @@ use crate::stream::{
|
|||
};
|
||||
use crate::youtube::itag::{lookup as itag_lookup, ItagType, MediaFormat};
|
||||
use crate::youtube::js::PlayerManager;
|
||||
use crate::youtube::potoken::{po_token_provider, PoTokenResult};
|
||||
use crate::youtube::stream_helper::{self, generate_content_playback_nonce};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
|
|
@ -64,14 +65,32 @@ pub fn stream_info_with(
|
|||
let localization = NewPipe::preferred_localization();
|
||||
let content_country = NewPipe::preferred_content_country();
|
||||
|
||||
// Resolve po_token via the registered provider if present, falling
|
||||
// back to caller-supplied options. The trait split (Ok(None) vs
|
||||
// Err) lets us treat "provider declined" as "go anonymous" and
|
||||
// "provider errored" as "still try anonymous but log the failure."
|
||||
let provider = po_token_provider();
|
||||
let android_token: Option<PoTokenResult> = options_or_provider(
|
||||
options.android_player_request_pot.as_deref(),
|
||||
options.android_streaming_pot.as_deref(),
|
||||
options.android_visitor_data.as_deref(),
|
||||
|| {
|
||||
provider
|
||||
.as_ref()
|
||||
.and_then(|p| p.get_android_client_po_token(video_id).ok().flatten())
|
||||
},
|
||||
);
|
||||
|
||||
let android_cpn = generate_content_playback_nonce();
|
||||
let player_response = fetch_android(
|
||||
video_id,
|
||||
&localization,
|
||||
&content_country,
|
||||
&android_cpn,
|
||||
options.android_player_request_pot.as_deref(),
|
||||
options.android_visitor_data.as_deref(),
|
||||
android_token
|
||||
.as_ref()
|
||||
.map(|t| t.player_request_po_token.as_str()),
|
||||
android_token.as_ref().map(|t| t.visitor_data.as_str()),
|
||||
)?;
|
||||
|
||||
check_playability_status(&player_response)?;
|
||||
|
|
@ -87,6 +106,21 @@ pub fn stream_info_with(
|
|||
.unwrap_or(Value::Null);
|
||||
|
||||
// Optional iOS — best-effort.
|
||||
let ios_token: Option<PoTokenResult> = if options.fetch_ios_client {
|
||||
options_or_provider(
|
||||
options.ios_player_request_pot.as_deref(),
|
||||
options.ios_streaming_pot.as_deref(),
|
||||
options.ios_visitor_data.as_deref(),
|
||||
|| {
|
||||
provider
|
||||
.as_ref()
|
||||
.and_then(|p| p.get_ios_client_po_token(video_id).ok().flatten())
|
||||
},
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (ios_streaming_data, ios_cpn) = if options.fetch_ios_client {
|
||||
let ios_cpn = generate_content_playback_nonce();
|
||||
match stream_helper::get_ios_player_response(
|
||||
|
|
@ -94,8 +128,8 @@ pub fn stream_info_with(
|
|||
&localization,
|
||||
&content_country,
|
||||
&ios_cpn,
|
||||
options.ios_player_request_pot.as_deref(),
|
||||
options.ios_visitor_data.as_deref(),
|
||||
ios_token.as_ref().map(|t| t.player_request_po_token.as_str()),
|
||||
ios_token.as_ref().map(|t| t.visitor_data.as_str()),
|
||||
) {
|
||||
Ok(r) if !is_player_response_not_valid(&r, video_id) => (
|
||||
r.get("streamingData").cloned().unwrap_or(Value::Null),
|
||||
|
|
@ -107,6 +141,15 @@ pub fn stream_info_with(
|
|||
(Value::Null, None)
|
||||
};
|
||||
|
||||
let android_streaming_pot = android_token
|
||||
.as_ref()
|
||||
.map(|t| t.streaming_data_po_token.clone())
|
||||
.or_else(|| options.android_streaming_pot.clone());
|
||||
let ios_streaming_pot = ios_token
|
||||
.as_ref()
|
||||
.map(|t| t.streaming_data_po_token.clone())
|
||||
.or_else(|| options.ios_streaming_pot.clone());
|
||||
|
||||
let signature_timestamp = PlayerManager::instance()
|
||||
.signature_timestamp(video_id)
|
||||
.unwrap_or(0);
|
||||
|
|
@ -129,21 +172,34 @@ pub fn stream_info_with(
|
|||
video_id,
|
||||
&android_cpn,
|
||||
ios_cpn.as_deref(),
|
||||
options.android_streaming_pot.as_deref(),
|
||||
options.ios_streaming_pot.as_deref(),
|
||||
android_streaming_pot.as_deref(),
|
||||
ios_streaming_pot.as_deref(),
|
||||
)?;
|
||||
populate_manifests(
|
||||
&mut info,
|
||||
&android_streaming_data,
|
||||
&ios_streaming_data,
|
||||
options.android_streaming_pot.as_deref(),
|
||||
options.ios_streaming_pot.as_deref(),
|
||||
android_streaming_pot.as_deref(),
|
||||
ios_streaming_pot.as_deref(),
|
||||
);
|
||||
populate_captions(&mut info, &player_response);
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
fn options_or_provider(
|
||||
opt_player_token: Option<&str>,
|
||||
opt_streaming_token: Option<&str>,
|
||||
opt_visitor: Option<&str>,
|
||||
provider_fn: impl FnOnce() -> Option<PoTokenResult>,
|
||||
) -> Option<PoTokenResult> {
|
||||
// Caller-supplied wins when ALL three are present.
|
||||
if let (Some(p), Some(s), Some(v)) = (opt_player_token, opt_streaming_token, opt_visitor) {
|
||||
return Some(PoTokenResult::new(p, s, v));
|
||||
}
|
||||
provider_fn()
|
||||
}
|
||||
|
||||
fn fetch_android(
|
||||
video_id: &str,
|
||||
localization: &Localization,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue