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 crafting-table 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.
153 lines
4.6 KiB
Rust
153 lines
4.6 KiB
Rust
// Phase 7 — `stream_info(url)` via Sulkta-Coop/strawcore-core.
|
|
// Exposed as a suspend fun.
|
|
//
|
|
// StreamInfo/AudioStreamItem/VideoStreamItem field shapes are unchanged
|
|
// from Phase U-3 so Kotlin VideoDetailScreen + PlayerScreen +
|
|
// ResolvedPlayback consume them with zero code changes.
|
|
|
|
use strawcore_core::youtube::linkhandler::stream::extract_video_id;
|
|
use strawcore_core::youtube::stream_extractor::stream_info as core_stream_info;
|
|
|
|
use crate::error::StrawcoreError;
|
|
use crate::search::SearchItem;
|
|
|
|
#[derive(Debug, Clone, uniffi::Record)]
|
|
pub struct StreamInfo {
|
|
pub id: String,
|
|
pub title: String,
|
|
pub uploader: String,
|
|
pub uploader_url: Option<String>,
|
|
pub description: String,
|
|
pub thumbnail: Option<String>,
|
|
pub view_count: i64,
|
|
pub like_count: i64,
|
|
/// Duration in seconds. 0 = live/unknown.
|
|
pub duration_seconds: i64,
|
|
|
|
/// Progressive (audio+video combined). Empty when YT only serves DASH.
|
|
pub combined: Vec<VideoStreamItem>,
|
|
/// DASH/adaptive video-only streams. Pair with `audio_only` via MergingMediaSource.
|
|
pub video_only: Vec<VideoStreamItem>,
|
|
/// DASH/adaptive audio-only streams. Sort by bitrate desc for "best audio".
|
|
pub audio_only: Vec<AudioStreamItem>,
|
|
/// Optional DASH MPD URL. ExoPlayer's DashMediaSource accepts this directly.
|
|
pub dash_mpd_url: Option<String>,
|
|
/// Optional HLS playlist URL. ExoPlayer's HlsMediaSource accepts this directly.
|
|
pub hls_url: Option<String>,
|
|
|
|
/// "Up next" list. Empty for now — populated when we port /next response.
|
|
pub related: Vec<SearchItem>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, uniffi::Record)]
|
|
pub struct VideoStreamItem {
|
|
pub url: String,
|
|
/// e.g. 1080, 720, 480.
|
|
pub height: i32,
|
|
pub bitrate: i64,
|
|
pub mime_type: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, uniffi::Record)]
|
|
pub struct AudioStreamItem {
|
|
pub url: String,
|
|
pub bitrate: i64,
|
|
pub mime_type: String,
|
|
}
|
|
|
|
#[uniffi::export(async_runtime = "tokio")]
|
|
pub async fn stream_info(input: String) -> Result<StreamInfo, StrawcoreError> {
|
|
log::info!("strawcore::stream_info input={}", input);
|
|
let video_id = resolve_video_id(&input)?;
|
|
let video_id_for_call = video_id.clone();
|
|
let core = tokio::task::spawn_blocking(move || core_stream_info(&video_id_for_call))
|
|
.await
|
|
.map_err(|e| StrawcoreError::Extractor {
|
|
msg: format!("join: {e}"),
|
|
})??;
|
|
Ok(map_stream_info(video_id, core))
|
|
}
|
|
|
|
fn resolve_video_id(input: &str) -> Result<String, StrawcoreError> {
|
|
let trimmed = input.trim();
|
|
// Bare 11-char id?
|
|
if trimmed.len() == 11
|
|
&& trimmed
|
|
.chars()
|
|
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
|
{
|
|
return Ok(trimmed.to_string());
|
|
}
|
|
extract_video_id(trimmed).map_err(|e| StrawcoreError::Unsupported {
|
|
detail: e.to_string(),
|
|
})
|
|
}
|
|
|
|
fn map_stream_info(
|
|
video_id: String,
|
|
s: strawcore_core::stream::StreamInfo,
|
|
) -> StreamInfo {
|
|
let combined = s
|
|
.video_streams
|
|
.into_iter()
|
|
.map(video_to_dto)
|
|
.collect();
|
|
let video_only = s
|
|
.video_only_streams
|
|
.into_iter()
|
|
.map(video_to_dto)
|
|
.collect();
|
|
let audio_only = s.audio_streams.into_iter().map(audio_to_dto).collect();
|
|
let uploader_url = if s.uploader_url.is_empty() {
|
|
None
|
|
} else {
|
|
Some(s.uploader_url)
|
|
};
|
|
let thumbnail = s.thumbnails.last().map(|i| i.url().to_string());
|
|
|
|
StreamInfo {
|
|
id: video_id,
|
|
title: s.name,
|
|
uploader: s.uploader_name,
|
|
uploader_url,
|
|
description: s.description,
|
|
thumbnail,
|
|
view_count: clamp_nonneg(s.view_count),
|
|
like_count: clamp_nonneg(s.like_count),
|
|
duration_seconds: s.duration_seconds.max(0),
|
|
combined,
|
|
video_only,
|
|
audio_only,
|
|
dash_mpd_url: s.dash_manifest_url,
|
|
hls_url: s.hls_manifest_url,
|
|
related: Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn clamp_nonneg(n: i64) -> i64 {
|
|
if n < 0 {
|
|
0
|
|
} else {
|
|
n
|
|
}
|
|
}
|
|
|
|
fn video_to_dto(v: strawcore_core::stream::VideoStream) -> VideoStreamItem {
|
|
VideoStreamItem {
|
|
url: v.url,
|
|
height: v.height.map(|h| h as i32).unwrap_or(0),
|
|
bitrate: v.bandwidth.map(|b| b as i64).unwrap_or(0),
|
|
mime_type: v.format.mime().to_string(),
|
|
}
|
|
}
|
|
|
|
fn audio_to_dto(a: strawcore_core::stream::AudioStream) -> AudioStreamItem {
|
|
AudioStreamItem {
|
|
url: a.url,
|
|
bitrate: a
|
|
.average_bitrate_kbps
|
|
.map(|b| (b as i64) * 1000)
|
|
.unwrap_or(0),
|
|
mime_type: a.format.mime().to_string(),
|
|
}
|
|
}
|