vc=82 — subscription-feed enrichment via lightweight stream_metadata

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).
This commit is contained in:
Sulkta 2026-06-21 06:56:06 -07:00
parent f51b4fbb77
commit a636977512
3 changed files with 45 additions and 10 deletions

View file

@ -9,6 +9,21 @@ const val STRAW_SDK_TARGET = 35
// Sulkta fork — Straw // 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): // 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 // * Search: the reactive cache-preview filter no longer runs on the
// main thread on every keystroke. It walked the entire cached- // 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 // vc=19 / 0.1.0-AE — rust pipeline cutover. Extraction via
// strawcore-core (Sulkta-OSS/strawcore) via the UniFFI wrapper; no // strawcore-core (Sulkta-OSS/strawcore) via the UniFFI wrapper; no
// NewPipeExtractor in the runtime path. // NewPipeExtractor in the runtime path.
const val STRAW_VERSION_CODE = 81 const val STRAW_VERSION_CODE = 82
const val STRAW_VERSION_NAME = "0.1.0-CO" const val STRAW_VERSION_NAME = "0.1.0-CP"
const val STRAW_APPLICATION_ID = "com.sulkta.straw" const val STRAW_APPLICATION_ID = "com.sulkta.straw"

View file

@ -47,11 +47,13 @@ const YEAR_MAX: i32 = 2200;
/// ///
/// built specifically so the subs feed can show 'N views · /// built specifically so the subs feed can show 'N views ·
/// X duration' the way YT does, without paying the full channel_info /// X duration' the way YT does, without paying the full channel_info
/// page-scrape cost on initial paint. The underlying stream_info IS /// page-scrape cost on initial paint. Backed by the lightweight
/// heavier than we'd like (~500ms each, runs JS deobf for play URLs /// `stream::stream_metadata` path (Android `/player` `videoDetails`
/// we'll discard) — future opt would be to parse the watch-page HTML /// only) — it skips the JS sig/nsig deobf, the extra WEB `/player`
/// JSON state directly for just these two fields. ~100ms savings per /// metadata round-trip, the iOS client, and stream/manifest/caption
/// call but ~150 lines of HTML/JSON pluck logic. Punted until needed. /// 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)] #[derive(Debug, Clone, uniffi::Record)]
pub struct EnrichedFeedMetadata { pub struct EnrichedFeedMetadata {
pub view_count: i64, pub view_count: i64,
@ -63,10 +65,10 @@ pub async fn enrich_feed_item(
video_url: String, video_url: String,
) -> Result<EnrichedFeedMetadata, StrawcoreError> { ) -> Result<EnrichedFeedMetadata, StrawcoreError> {
crate::runtime::ensure_initialized(); 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 { Ok(EnrichedFeedMetadata {
view_count: info.view_count, view_count,
duration_seconds: info.duration_seconds, duration_seconds,
}) })
} }

View file

@ -7,6 +7,7 @@
use strawcore_core::youtube::linkhandler::stream::extract_video_id; 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_info as core_stream_info;
use strawcore_core::youtube::stream_extractor::stream_metadata as core_stream_metadata;
use crate::error::StrawcoreError; use crate::error::StrawcoreError;
use crate::search::SearchItem; use crate::search::SearchItem;
@ -69,6 +70,23 @@ pub async fn stream_info(input: String) -> Result<StreamInfo, StrawcoreError> {
Ok(map_stream_info(video_id, core)) 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<String, StrawcoreError> { fn resolve_video_id(input: &str) -> Result<String, StrawcoreError> {
let trimmed = input.trim(); let trimmed = input.trim();
// Bare 11-char id? // Bare 11-char id?