channel_info(url) UniFFI suspend fn. ChannelViewModel + SubscriptionFeedViewModel both swap. NewPipeExtractor (Java) is OUT — zero org.schabi.newpipe classes in the APK now. Cleanup: - NewPipeDownloader.kt deleted (was the OkHttp adapter) - Thumbnails.kt deleted (rustypipe returns full URLs) - NewPipe.init() dropped from StrawApp.onCreate - libs.newpipe.extractor removed from build.gradle.kts - STRAW_USER_AGENT + strawHttpClient() now live in net/Http.kt - RydClient + SponsorBlockClient + PlayerScreen + PlaybackService all read from net/Http.kt instead of the extractor package rustypipe API quirks beat: - channel_videos(id) is the right method (channel() doesn't exist) - ChannelInfo struct = basic metadata; Channel<T> wrapper carries name/avatar/banner + .content is the paginator of videos - description is String (not Option), subscriber_count is Option<u64> End state: strawApp Kotlin is ~UI + thin glue to strawcore. The Rust core handles search / streamInfo / channel / channel_videos via UniFFI suspend fns. Tokio + reqwest + rustls + rquickjs all packed in libstrawcore.so (~6MB per ABI). APK 40MB total.
113 lines
4 KiB
Rust
113 lines
4 KiB
Rust
// Phase U-4 — `channel_info(channel_url)` via rustypipe.
|
|
//
|
|
// Returns channel metadata + the channel's latest videos (the "Videos" tab).
|
|
// Used by ChannelScreen (single-channel view) AND
|
|
// SubscriptionFeedViewModel (which fans out across all subscriptions).
|
|
|
|
use crate::error::StrawcoreError;
|
|
use crate::search::SearchItem;
|
|
use rustypipe::client::RustyPipe;
|
|
|
|
#[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>,
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
/// Channel-id extraction. Accepts:
|
|
/// https://www.youtube.com/channel/UC...
|
|
/// https://www.youtube.com/@handle
|
|
/// https://www.youtube.com/c/handle
|
|
/// https://www.youtube.com/user/handle
|
|
/// bare channel id (UC..., 24 chars)
|
|
fn extract_channel_input(input: &str) -> Result<String, StrawcoreError> {
|
|
let trimmed = input.trim();
|
|
// Bare channel ID — usually 24 chars starting with UC.
|
|
if trimmed.starts_with("UC") && trimmed.len() == 24 {
|
|
return Ok(trimmed.to_string());
|
|
}
|
|
let url = url::Url::parse(trimmed).map_err(|e| StrawcoreError::Unsupported {
|
|
detail: format!("bad URL: {}", e),
|
|
})?;
|
|
let path = url.path().trim_start_matches('/').trim_end_matches('/');
|
|
// /channel/UCxxx — canonical
|
|
if let Some(rest) = path.strip_prefix("channel/") {
|
|
let id = rest.split('/').next().unwrap_or("");
|
|
if !id.is_empty() {
|
|
return Ok(id.to_string());
|
|
}
|
|
}
|
|
// /@handle — rustypipe takes the handle (with @)
|
|
if path.starts_with('@') {
|
|
return Ok(path.split('/').next().unwrap_or(path).to_string());
|
|
}
|
|
// /c/name or /user/name
|
|
for prefix in ["c/", "user/"] {
|
|
if let Some(rest) = path.strip_prefix(prefix) {
|
|
let name = rest.split('/').next().unwrap_or("");
|
|
if !name.is_empty() {
|
|
// Rustypipe channel() takes the channel id or @handle. For
|
|
// legacy /c/ and /user/ URLs we prepend @ as a best-effort.
|
|
return Ok(format!("@{}", name));
|
|
}
|
|
}
|
|
}
|
|
Err(StrawcoreError::Unsupported {
|
|
detail: format!("unsupported channel URL: {}", input),
|
|
})
|
|
}
|
|
|
|
#[uniffi::export(async_runtime = "tokio")]
|
|
pub async fn channel_info(channel_url: String) -> Result<ChannelInfo, StrawcoreError> {
|
|
let key = extract_channel_input(&channel_url)?;
|
|
log::info!("strawcore::channel_info key={}", key);
|
|
let rp = RustyPipe::new();
|
|
|
|
// channel_videos(id) returns Channel<Paginator<VideoItem>> — the
|
|
// Channel<T> wrapper carries name/avatar/banner/etc and `.content`
|
|
// is the paginator of videos. One round-trip gets us everything.
|
|
let channel = rp.query().channel_videos(&key).await?;
|
|
|
|
let videos: Vec<SearchItem> = channel
|
|
.content
|
|
.items
|
|
.into_iter()
|
|
.map(|v| SearchItem {
|
|
url: yt_video_url(&v.id),
|
|
title: v.name.clone(),
|
|
uploader: channel.name.clone(),
|
|
uploader_url: Some(yt_channel_url(&channel.id)),
|
|
thumbnail: v.thumbnail.last().map(|t| t.url.clone()),
|
|
duration_seconds: v.duration.unwrap_or(0) as i64,
|
|
view_count: v.view_count.unwrap_or(0) as i64,
|
|
})
|
|
.collect();
|
|
|
|
let avatar = channel.avatar.last().map(|t| t.url.clone());
|
|
let banner = channel.banner.last().map(|t| t.url.clone());
|
|
|
|
Ok(ChannelInfo {
|
|
id: channel.id,
|
|
name: channel.name,
|
|
avatar,
|
|
banner,
|
|
subscriber_count: channel.subscriber_count.map(|n| n as i64).unwrap_or(-1),
|
|
description: channel.description,
|
|
videos,
|
|
})
|
|
}
|