From 2b3eb8bef47b88f57e52dc9a678c332805847766 Mon Sep 17 00:00:00 2001 From: Cobb Date: Sun, 21 Jun 2026 06:56:06 -0700 Subject: [PATCH] =?UTF-8?q?vc=3D82=20=E2=80=94=20subscription-feed=20enric?= =?UTF-8?q?hment=20via=20lightweight=20stream=5Fmetadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit enrich_feed_item now calls the new strawcore stream_metadata() path (Android /player + videoDetails read only) instead of the full stream_info. The full path ran the JS sig/nsig deobf, an extra WEB /player metadata round-trip, the iOS client, and stream/manifest/caption extraction — then kept only view_count + duration_seconds. Those two come from the same videoDetails the lightweight path reads (populate_microformat never touches them), so the values are identical; the feed just stops paying for the discarded work — ~one heavy round-trip dropped per enriched item per refresh. FFI surface (enrichFeedItem -> EnrichedFeedMetadata) unchanged. Needs strawcore 30f24d2 (pushed first; CI clones strawcore main). --- buildSrc/src/main/kotlin/ProjectConfig.kt | 19 +++++++++++++++++-- rust/strawcore/src/feed.rs | 18 ++++++++++-------- rust/strawcore/src/stream.rs | 18 ++++++++++++++++++ 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/buildSrc/src/main/kotlin/ProjectConfig.kt b/buildSrc/src/main/kotlin/ProjectConfig.kt index e8c09b9ab..e9a69fa95 100644 --- a/buildSrc/src/main/kotlin/ProjectConfig.kt +++ b/buildSrc/src/main/kotlin/ProjectConfig.kt @@ -9,6 +9,21 @@ const val STRAW_SDK_TARGET = 35 // Sulkta fork — Straw // +// vc=82 / 0.1.0-CP — subscription-feed enrichment goes lightweight: +// * Filling in a feed row's view count + duration used to run the FULL +// stream extraction per item (the same path opening a video does): +// Android /player, the JS signature/nsig deobfuscation, an extra WEB +// /player metadata round-trip, plus stream/manifest/caption parsing — +// then threw all of it away except those two numbers. On a refresh +// that touched dozens of items that was dozens of redundant heavy +// round-trips. +// * Now it calls a new strawcore stream_metadata() path that does only +// the Android /player fetch + the videoDetails read those two fields +// come from, skipping the JS eval, the extra WEB round-trip, iOS, and +// stream extraction. The values are identical (they come from the same +// videoDetails the full path used), the feed just stops paying for +// work it discarded. (strawcore 30f24d2.) +// // vc=81 / 0.1.0-CO — perf-audit app-side batch (no behavior change): // * Search: the reactive cache-preview filter no longer runs on the // main thread on every keystroke. It walked the entire cached- @@ -134,6 +149,6 @@ const val STRAW_SDK_TARGET = 35 // vc=19 / 0.1.0-AE — rust pipeline cutover. Extraction via // strawcore-core (Sulkta-Coop/strawcore) via the UniFFI wrapper; no // NewPipeExtractor in the runtime path. -const val STRAW_VERSION_CODE = 81 -const val STRAW_VERSION_NAME = "0.1.0-CO" +const val STRAW_VERSION_CODE = 82 +const val STRAW_VERSION_NAME = "0.1.0-CP" const val STRAW_APPLICATION_ID = "com.sulkta.straw" diff --git a/rust/strawcore/src/feed.rs b/rust/strawcore/src/feed.rs index c50b2942c..75f4edc98 100644 --- a/rust/strawcore/src/feed.rs +++ b/rust/strawcore/src/feed.rs @@ -47,11 +47,13 @@ const YEAR_MAX: i32 = 2200; /// /// built specifically so the subs feed can show 'N views · /// X duration' the way YT does, without paying the full channel_info -/// page-scrape cost on initial paint. The underlying stream_info IS -/// heavier than we'd like (~500ms each, runs JS deobf for play URLs -/// we'll discard) — future opt would be to parse the watch-page HTML -/// JSON state directly for just these two fields. ~100ms savings per -/// call but ~150 lines of HTML/JSON pluck logic. Punted until needed. +/// page-scrape cost on initial paint. Backed by the lightweight +/// `stream::stream_metadata` path (Android `/player` `videoDetails` +/// only) — it skips the JS sig/nsig deobf, the extra WEB `/player` +/// metadata round-trip, the iOS client, and stream/manifest/caption +/// extraction the full `stream_info` runs, all of which the feed would +/// discard. Same two values the full path would return, minus the +/// ~500ms JS/stream tail + the redundant WEB round-trip. #[derive(Debug, Clone, uniffi::Record)] pub struct EnrichedFeedMetadata { pub view_count: i64, @@ -63,10 +65,10 @@ pub async fn enrich_feed_item( video_url: String, ) -> Result { crate::runtime::ensure_initialized(); - let info = crate::stream::stream_info(video_url).await?; + let (view_count, duration_seconds) = crate::stream::stream_metadata(video_url).await?; Ok(EnrichedFeedMetadata { - view_count: info.view_count, - duration_seconds: info.duration_seconds, + view_count, + duration_seconds, }) } diff --git a/rust/strawcore/src/stream.rs b/rust/strawcore/src/stream.rs index f28080f9c..a8b5d1a3e 100644 --- a/rust/strawcore/src/stream.rs +++ b/rust/strawcore/src/stream.rs @@ -7,6 +7,7 @@ use strawcore_core::youtube::linkhandler::stream::extract_video_id; use strawcore_core::youtube::stream_extractor::stream_info as core_stream_info; +use strawcore_core::youtube::stream_extractor::stream_metadata as core_stream_metadata; use crate::error::StrawcoreError; use crate::search::SearchItem; @@ -69,6 +70,23 @@ pub async fn stream_info(input: String) -> Result { Ok(map_stream_info(video_id, core)) } +/// Metadata-only fetch — returns `(view_count, duration_seconds)` from the +/// Android `videoDetails`, skipping the JS-deobf / streams / extra WEB +/// round-trip the full `stream_info` pays. Backs `enrich_feed_item`, which +/// only needs those two fields. Non-negative clamping mirrors +/// `map_stream_info` so the values are identical to the full path's. +pub async fn stream_metadata(input: String) -> Result<(i64, i64), StrawcoreError> { + log::info!("strawcore::stream_metadata input_len={}", input.len()); + crate::runtime::ensure_initialized(); + let video_id = resolve_video_id(&input)?; + let core = tokio::task::spawn_blocking(move || core_stream_metadata(&video_id)) + .await + .map_err(|e| StrawcoreError::Extractor { + msg: format!("join: {e}"), + })??; + Ok((clamp_nonneg(core.view_count), core.duration_seconds.max(0))) +} + fn resolve_video_id(input: &str) -> Result { let trimmed = input.trim(); // Bare 11-char id?