Phase 7 — strawcore wrapper now bridges to Sulkta-OSS/strawcore-core

Replaces the rustypipe-backed extraction with calls into the new
NPE-port crate. The UniFFI surface Kotlin sees is unchanged:

  suspend fun search(query: String): List<SearchItem>
  suspend fun streamInfo(input: String): StreamInfo
  suspend fun channelInfo(input: String): ChannelInfo
  fun initLogging()  // also wires the strawcore-core Downloader
  fun helloFromRust(name: String): String

rust/strawcore/
  * Cargo.toml      — dropped rustypipe + rquickjs-sys direct dep;
                      added strawcore-core path dep (../../../strawcore)
  * src/error.rs    — From<strawcore_core::ExtractionError>, mapping
                      ContentUnavailable variants to typed
                      StrawcoreError cases (AgeRestricted, GeoRestricted,
                      Private, RequiresLogin) instead of bucketing all
                      to Extractor
  * src/runtime.rs  — Once-guarded ReqwestDownloader init via
                      NewPipe::init_full
  * src/search.rs   — search() spawn_blocks core search_extractor::search
                      against SearchFilter::Videos
  * src/stream.rs   — stream_info() resolves URL → video_id via
                      strawcore_core::linkhandler::stream, then
                      spawn_blocks core stream_extractor::stream_info,
                      then maps StreamInfo → wrapper DTOs (combined/
                      video_only/audio_only/dash/hls)
  * src/channel.rs  — channel_info() parses input via
                      strawcore_core::linkhandler::channel (handle /
                      custom-url / legacy-user resolution lives in
                      core), then spawn_blocks core channel::channel_info

Build verified: wrapper compiles linking strawcore-core, uniffi-bindgen
generates Kotlin bindings with the same suspend fun + data class
surface Kotlin already consumes. Android NDK cross-compile + APK + on-
device smoke pending (needs build-host container).

This commits onto rollback/vc18-back-to-NPE — the existing Kotlin code
still calls NewPipeExtractor directly. Switching the Kotlin side to
consume the rust wrapper is a separate cutover.
This commit is contained in:
Sulkta 2026-05-24 17:29:23 -07:00
parent c1d7fffb1f
commit fb89b22685
7 changed files with 250 additions and 309 deletions

View file

@ -1,13 +1,13 @@
// Phase U-2 — search via rustypipe, exposed to Kotlin as a suspend fun.
//
// `SearchItem` mirrors the existing Kotlin `StreamItem` field for field so
// `SearchViewModel` can swap NewPipeExtractor for this with a one-line
// change. We map only Video items from rustypipe's result (it also returns
// channels and playlists which we don't need for the search list yet).
// Phase 7 — search via Sulkta-OSS/strawcore-core. Exposed to Kotlin
// as a suspend fun. SearchItem field shape is unchanged from Phase U-2
// so Kotlin callers (SearchViewModel) keep working with no code
// changes.
use strawcore_core::stream::StreamInfoItem;
use strawcore_core::youtube::linkhandler::search::SearchFilter;
use strawcore_core::youtube::search_extractor;
use crate::error::StrawcoreError;
use rustypipe::client::RustyPipe;
use rustypipe::model::YouTubeItem;
#[derive(Debug, Clone, uniffi::Record)]
pub struct SearchItem {
@ -22,44 +22,40 @@ pub struct SearchItem {
pub view_count: i64,
}
fn yt_video_url(id: &str) -> String {
format!("https://www.youtube.com/watch?v={}", id)
}
fn yt_channel_url(id: &str) -> String {
format!("https://www.youtube.com/channel/{}", id)
pub(crate) fn from_core(item: StreamInfoItem) -> SearchItem {
let uploader_url = if item.uploader_url.is_empty() {
None
} else {
Some(item.uploader_url)
};
let thumbnail = item
.thumbnails
.last()
.map(|i| i.url().to_string());
SearchItem {
url: item.url,
title: item.name,
uploader: item.uploader_name,
uploader_url,
thumbnail,
duration_seconds: item.duration_seconds,
view_count: if item.view_count < 0 {
0
} else {
item.view_count
},
}
}
#[uniffi::export(async_runtime = "tokio")]
pub async fn search(query: String) -> Result<Vec<SearchItem>, StrawcoreError> {
log::info!("strawcore::search query={}", query);
let rp = RustyPipe::new();
let results = rp.query().search(query).await?;
let items: Vec<SearchItem> = results
.items
.items
.into_iter()
.filter_map(|item| match item {
YouTubeItem::Video(v) => Some(SearchItem {
url: yt_video_url(&v.id),
title: v.name,
uploader: v
.channel
.as_ref()
.map(|c| c.name.clone())
.unwrap_or_default(),
uploader_url: v.channel.as_ref().map(|c| yt_channel_url(&c.id)),
thumbnail: v.thumbnail.first().map(|t| t.url.clone()),
duration_seconds: v.duration.unwrap_or(0) as i64,
view_count: v.view_count.unwrap_or(0) as i64,
}),
// Channels + playlists are dropped at U-2; future phases can
// surface them as a "channels you might like" row.
_ => None,
})
.collect();
log::info!("strawcore::search returned {} videos", items.len());
Ok(items)
let result = tokio::task::spawn_blocking(move || {
search_extractor::search(&query, SearchFilter::Videos)
})
.await
.map_err(|e| StrawcoreError::Extractor {
msg: format!("join: {e}"),
})??;
Ok(result.videos.into_iter().map(from_core).collect())
}