Straw full-audit fix batch (2026-07-04). Confirmed HIGH/MED findings: - Updater self-brick: drop the fdroid.sulkta.com leaf+E7 SPKI pins (LE rotates the leaf every ~90d + intermediates from a pool → a routine renewal was guaranteed to miss both pins and silently kill the only in-app update path). System-CA validation + the install-time APK signature gate remain. Also distinguish "index unreachable" from "up to date", add a 30s callTimeout + bounded index read, guard the API-26 NotificationChannel, and setOnlyAlertOnce. - Request POST_NOTIFICATIONS at runtime so A13+ update/media notifications actually post (was declared but never requested → silent no-op). - isDebuggable=false on the shipped debug variant (closes ADB run-as dump of watch/search history + subs; no package-id cutover). - Crash fixes: distinctBy(url) on moreFromChannel + related (duplicate LazyColumn key); back-handling driven by live nav depth so it survives moveTaskToBack on A12+; PlaylistsStore per-store caps + off-main hydration (hostile import ANR); SponsorBlock staleness fence via NowPlaying.refreshIfCurrent. - CacheCap.nearest() snaps up to the nearest finite cap (defaults no longer resolve to Unlimited/100k on fresh installs). - RYD/SB FFI shims wrap the uniffi call (swallow a core panic per contract). - Rust wrapper: release panic="unwind" (was "abort", which defeated UniFFI catch_unwind → any core panic = whole-app SIGABRT) + a 60s wall-clock timeout on every blocking extractor call (unblocks the caller, bounds thread pileup). Pairs with the strawcore-core reliability fix.
84 lines
3.1 KiB
Rust
84 lines
3.1 KiB
Rust
// Phase 7 — `channel_info(channel_url)` via the new strawcore.
|
|
// Used by ChannelScreen (single-channel view) AND
|
|
// SubscriptionFeedViewModel (which fans out across all subscriptions).
|
|
|
|
use strawcore_core::youtube::channel::{
|
|
channel_info as core_channel_info, channel_videos_continuation as core_channel_continuation,
|
|
ChannelInfo as CoreInfo,
|
|
};
|
|
use strawcore_core::youtube::linkhandler::channel as core_link;
|
|
|
|
use crate::error::StrawcoreError;
|
|
use crate::search::{from_core as search_from_core, page_from_core, Page, SearchItem};
|
|
|
|
#[derive(Debug, Clone, uniffi::Record)]
|
|
pub struct ChannelInfo {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub avatar: Option<String>,
|
|
pub banner: Option<String>,
|
|
/// -1 = unknown / hidden by the channel.
|
|
pub subscriber_count: i64,
|
|
pub description: String,
|
|
/// Latest videos from the channel (Videos tab, newest first).
|
|
pub videos: Vec<SearchItem>,
|
|
/// Token to fetch the next page of the Videos tab via
|
|
/// `channel_videos_continuation`. null = the channel has no more
|
|
/// videos (or YT didn't hand out a continuation).
|
|
pub videos_continuation: Option<String>,
|
|
}
|
|
|
|
#[uniffi::export(async_runtime = "tokio")]
|
|
pub async fn channel_info(input: String) -> Result<ChannelInfo, StrawcoreError> {
|
|
log::info!("strawcore::channel_info input_len={}", input.len());
|
|
crate::runtime::ensure_initialized();
|
|
let identifier = resolve_channel_identifier(&input)?;
|
|
let core = crate::runtime::run_extract("channel_info", move || core_channel_info(identifier))
|
|
.await?;
|
|
Ok(map_channel(core))
|
|
}
|
|
|
|
fn resolve_channel_identifier(
|
|
input: &str,
|
|
) -> Result<core_link::ChannelIdentifier, StrawcoreError> {
|
|
let trimmed = input.trim();
|
|
// Bare channel ID — UC..., 24 chars.
|
|
if trimmed.starts_with("UC") && trimmed.len() == 24 {
|
|
return Ok(core_link::ChannelIdentifier::DirectId(trimmed.into()));
|
|
}
|
|
core_link::parse(trimmed).map_err(|e| StrawcoreError::Unsupported {
|
|
detail: e.to_string(),
|
|
})
|
|
}
|
|
|
|
/// Fetch the next page of a channel's Videos tab from a continuation
|
|
/// token returned on `ChannelInfo.videos_continuation` or a prior Page.
|
|
#[uniffi::export(async_runtime = "tokio")]
|
|
pub async fn channel_videos_continuation(token: String) -> Result<Page, StrawcoreError> {
|
|
log::info!(
|
|
"strawcore::channel_videos_continuation token_len={}",
|
|
token.len()
|
|
);
|
|
crate::runtime::ensure_initialized();
|
|
let page = crate::runtime::run_extract("channel_videos_continuation", move || {
|
|
core_channel_continuation(&token)
|
|
})
|
|
.await?;
|
|
Ok(page_from_core(page))
|
|
}
|
|
|
|
fn map_channel(c: CoreInfo) -> ChannelInfo {
|
|
let avatar = c.avatars.last().map(|i| i.url().to_string());
|
|
let banner = c.banners.last().map(|i| i.url().to_string());
|
|
let videos = c.recent_videos.into_iter().map(search_from_core).collect();
|
|
ChannelInfo {
|
|
id: c.channel_id,
|
|
name: c.name,
|
|
avatar,
|
|
banner,
|
|
subscriber_count: c.subscriber_count,
|
|
description: c.description,
|
|
videos,
|
|
videos_continuation: c.videos_continuation,
|
|
}
|
|
}
|