vc=86: audit-fix sprint (HIGH H2 + 5 MED/LOW from the 2026-06-21 audit)
- Audio-only toggle no longer drops the max-resolution cap: both the fullscreen button (PlayerScreen) and the detail Audio pill (VideoDetailBody) rebuilt TrackSelectionParameters from a fresh Builder, wiping the data-saver ceiling. Now buildUpon() the existing params so the cap survives. (H2) - Subscriptions Refresh button no longer sticks at "..." forever on a warm restart within the cache TTL: refreshIfStale clears the initial loading seed when it decides nothing needs refreshing. (M2) - Search + Channel result lists get a stable item key (video url) so paging / shorts-filtering stops re-binding rows to new data (re-triggered thumbnail loads, scroll shift). (M3, M4) - IosSafeHttpDataSource: the unknown-length (LENGTH_UNSET) chunk path rolls forward to the next Range chunk at inner-EOF instead of re-reading the exhausted source forever (was truncating playback to the first chunk). (M5) - strawcore channel_feed_rss propagates the real failure (network/HTTP/parse) instead of collapsing every error to an empty list, so a broken fetch is distinguishable from "no new videos" (subscription_feed keeps its per-channel tolerance for fan-out). (M6) - Feed recency: a clock-skewed future upload emits "0 seconds ago" (parses to top) instead of "just now" (which Kotlin's recency parser couldn't read, so the item sank to the bottom). (L4) Deferred to a follow-up: M1 (bg-refresh cache-key mismatch — needs a worker redesign) + M7 (build config-cache wiring). Verified: cargo check/clippy + full Android compileDebugKotlin green.
This commit is contained in:
parent
055c9c6d4f
commit
791975ca4a
8 changed files with 89 additions and 17 deletions
|
|
@ -105,7 +105,13 @@ pub async fn channel_feed_rss(
|
|||
crate::runtime::ensure_initialized();
|
||||
log::info!("strawcore::channel_feed_rss url_len={}", channel_url.len());
|
||||
let client = rss_client()?;
|
||||
Ok(fetch_channel_rss(client, &channel_url).await.unwrap_or_default())
|
||||
// Propagate the real failure (network / HTTP / parse) instead of
|
||||
// collapsing it to an empty Vec. The single-channel caller (the subs
|
||||
// feed) needs to tell "this channel posted nothing" apart from "this
|
||||
// fetch broke" — the prior `.unwrap_or_default()` made the `Result`
|
||||
// a lie. (subscription_feed keeps its own per-channel unwrap_or_default
|
||||
// below — fan-out tolerance is correct there, not here.)
|
||||
fetch_channel_rss(client, &channel_url).await
|
||||
}
|
||||
|
||||
/// Bulk subscription feed fan-out — for callers that want one round-trip
|
||||
|
|
@ -138,20 +144,34 @@ pub async fn subscription_feed(
|
|||
Ok(results.into_iter().flatten().collect())
|
||||
}
|
||||
|
||||
async fn fetch_channel_rss(client: &Client, channel_url: &str) -> Option<Vec<SearchItem>> {
|
||||
let channel_id = extract_channel_id(channel_url)?;
|
||||
async fn fetch_channel_rss(
|
||||
client: &Client,
|
||||
channel_url: &str,
|
||||
) -> Result<Vec<SearchItem>, StrawcoreError> {
|
||||
// A URL we can't pull a channel id from isn't a *failure* — it's an
|
||||
// unsupported shape (e.g. an `@handle`, which RSS can't take). Report
|
||||
// "no videos", not an error, so it doesn't trip the caller's retry.
|
||||
let Some(channel_id) = extract_channel_id(channel_url) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let url = format!("{RSS_BASE}{channel_id}");
|
||||
// .without_url() on the error: the rule is to never let a reqwest
|
||||
// error's URL reach a log/message (these RSS URLs carry no secret, but
|
||||
// we keep the discipline uniform).
|
||||
let resp = client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.ok()?
|
||||
.map_err(|e| StrawcoreError::Network { msg: format!("rss fetch: {}", e.without_url()) })?
|
||||
.error_for_status()
|
||||
.ok()?;
|
||||
.map_err(|e| StrawcoreError::Network { msg: format!("rss status: {}", e.without_url()) })?;
|
||||
// Streaming body read with a hard byte cap — `.text()` reads
|
||||
// unbounded into a String. Shared with the RYD/SB path (net.rs).
|
||||
let body = crate::net::read_capped_body(resp, RSS_MAX_BYTES).await?;
|
||||
let body = crate::net::read_capped_body(resp, RSS_MAX_BYTES)
|
||||
.await
|
||||
.ok_or_else(|| StrawcoreError::Network { msg: "rss body exceeded cap".into() })?;
|
||||
parse_rss(&body, channel_id)
|
||||
.ok_or_else(|| StrawcoreError::Extractor { msg: "rss parse failed".into() })
|
||||
}
|
||||
|
||||
/// Extract the `UCxxx` channel ID from a channel URL. Accepts the
|
||||
|
|
@ -403,7 +423,13 @@ fn iso_to_relative(iso: &str) -> String {
|
|||
// a single skewed item doesn't pin itself at the top of the
|
||||
// feed.
|
||||
if secs > now_secs {
|
||||
return "just now".to_string();
|
||||
// "0 seconds ago", NOT "just now": the Kotlin recencyScore parser
|
||||
// only matches `(\d+)\s+unit(s)?\s+ago`, so "just now" fails to
|
||||
// parse → Long.MIN_VALUE → the item sinks to the BOTTOM of the feed
|
||||
// (the opposite of intent — a fresh upload on a forward-skewed clock
|
||||
// would vanish to the end). "0 seconds ago" parses to score 0 = top,
|
||||
// and matches what format_relative(0) emits for a now-ish timestamp.
|
||||
return "0 seconds ago".to_string();
|
||||
}
|
||||
format_relative(now_secs - secs)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue