youtube: don't reset visitorData / retry VISIONOS on permanent content failures
Some checks failed
gitleaks / scan (push) Failing after 1s
Some checks failed
gitleaks / scan (push) Failing after 1s
A rejected primary response triggered a visitorData cache reset (and, in the full path, a VISIONOS retry) for ANY playability failure. But age-restricted / geo / paid / private / premium / SoundCloud-Go+ / terminated are permanent properties of the video — client- and visitorData-independent — so resetting + retrying VISIONOS just churns the (visitorData-based) cache the VISIONOS path relies on and burns a wasted /player round-trip. The feed enrich hits many such items per refresh, so the dogfood logs showed a visitorData reset before every age-restricted item. Add is_permanent_content_failure() and gate on it: select_primary returns the error immediately (no reset, no VISIONOS) for permanent failures; stream_metadata skips the reset for them. BotDetected (the actual bot-wall), decoys, and network/transient errors stay fully recoverable — reset + VISIONOS as before.
This commit is contained in:
parent
6df61c1f52
commit
d12e92fb60
1 changed files with 81 additions and 5 deletions
|
|
@ -453,12 +453,15 @@ pub fn stream_metadata(video_id: &str) -> Result<StreamInfo, ExtractionError> {
|
|||
android_token.as_ref().map(|t| t.visitor_data.as_str()),
|
||||
)?;
|
||||
|
||||
// A primary-android rejection (playability failure or decoy) is exactly the
|
||||
// symptom a poisoned visitorData would produce on the primary reel call —
|
||||
// and since it aborts extraction the value would otherwise never self-heal.
|
||||
// Drop the cached visitorData so the next extraction fetches a fresh one.
|
||||
// A primary-android rejection is the symptom a poisoned visitorData would
|
||||
// produce, so drop the cache to self-heal — but ONLY for session/transient
|
||||
// failures (bot-wall, decoy). A permanent content property (age/geo/paid/…)
|
||||
// is client- and visitorData-independent, so resetting on it is pointless
|
||||
// churn — and the feed enrich hits many such items per refresh.
|
||||
if let Err(e) = check_playability_status(&player_response) {
|
||||
stream_helper::reset_visitor_data_cache();
|
||||
if !is_permanent_content_failure(&e) {
|
||||
stream_helper::reset_visitor_data_cache();
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
if is_player_response_not_valid(&player_response, video_id) {
|
||||
|
|
@ -720,6 +723,26 @@ fn primary_response_usable(resp: &Value, video_id: &str) -> Result<(), Extractio
|
|||
/// primary_is_visionos)` for the winning client, or the ORIGINAL ANDROID error
|
||||
/// when VISIONOS also fails. `fetch_visionos` is only invoked on the fallback
|
||||
/// path (so a usable ANDROID never triggers a visionOS round-trip).
|
||||
/// A failure that a different client (VISIONOS) or a fresh visitorData cannot
|
||||
/// change — a permanent property of the video/account. For these we skip both
|
||||
/// the visitorData reset and the VISIONOS retry: they'd fail identically and
|
||||
/// only churn the visitorData cache the VISIONOS path itself depends on.
|
||||
/// `BotDetected`, decoy (`Other`), and network/transport errors stay recoverable.
|
||||
fn is_permanent_content_failure(err: &ExtractionError) -> bool {
|
||||
matches!(
|
||||
err,
|
||||
ExtractionError::ContentUnavailable(
|
||||
ContentUnavailable::AgeRestricted
|
||||
| ContentUnavailable::GeoRestricted
|
||||
| ContentUnavailable::Paid
|
||||
| ContentUnavailable::Private
|
||||
| ContentUnavailable::YoutubeMusicPremium
|
||||
| ContentUnavailable::SoundCloudGoPlus
|
||||
| ContentUnavailable::AccountTerminated
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fn select_primary(
|
||||
android_result: Result<Value, ExtractionError>,
|
||||
video_id: &str,
|
||||
|
|
@ -733,6 +756,17 @@ fn select_primary(
|
|||
},
|
||||
Err(e) => e,
|
||||
};
|
||||
// A permanent content property (age/geo/paid/private/premium/terminated)
|
||||
// fails identically on every client — return it immediately rather than
|
||||
// burning a visitorData reset + a second /player round-trip that VISIONOS
|
||||
// can't recover (and which would needlessly churn the visitorData the
|
||||
// VISIONOS path relies on).
|
||||
if is_permanent_content_failure(&android_error) {
|
||||
log::debug!(
|
||||
"[{video_id}] ANDROID unusable ({android_error}); permanent content — skipping VISIONOS"
|
||||
);
|
||||
return Err(android_error);
|
||||
}
|
||||
// ANDROID unusable → drop the (likely poisoned) visitorData, then try
|
||||
// VISIONOS as the primary. On any VISIONOS failure, surface the ORIGINAL
|
||||
// ANDROID error rather than the visionOS one.
|
||||
|
|
@ -1661,6 +1695,48 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cascade_skips_visionos_for_permanent_content_failure() {
|
||||
// A permanent content property (geo-restricted here) fails identically
|
||||
// on every client → NO visitorData reset, NO VISIONOS retry; surface the
|
||||
// original error immediately. This is the fix for the feed enrich
|
||||
// churning visitorData on every age-restricted item.
|
||||
let fallback_ran = std::cell::Cell::new(false);
|
||||
let visionos_ran = std::cell::Cell::new(false);
|
||||
let geo = json!({
|
||||
"playabilityStatus": {
|
||||
"status": "UNPLAYABLE",
|
||||
"reason": "This video is not available in your country"
|
||||
},
|
||||
"videoDetails": {"videoId": "vid"}
|
||||
});
|
||||
let err = select_primary(
|
||||
Ok(geo),
|
||||
"vid",
|
||||
|| fallback_ran.set(true),
|
||||
|| {
|
||||
visionos_ran.set(true);
|
||||
Ok(ok_response("vid", 18))
|
||||
},
|
||||
)
|
||||
.expect_err("a permanent content failure must not be recovered");
|
||||
assert!(
|
||||
matches!(
|
||||
err,
|
||||
ExtractionError::ContentUnavailable(ContentUnavailable::GeoRestricted)
|
||||
),
|
||||
"the original permanent error must be returned, got {err:?}"
|
||||
);
|
||||
assert!(
|
||||
!fallback_ran.get(),
|
||||
"visitorData reset must NOT run for a permanent content failure"
|
||||
);
|
||||
assert!(
|
||||
!visionos_ran.get(),
|
||||
"VISIONOS must NOT be fetched for a permanent content failure"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cascade_falls_back_on_android_fetch_error() {
|
||||
// A hard ANDROID fetch error (not just a playability fail) also cascades.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue