M4 — channel browse + context-menu drill-down

Sidecar ChannelVideos op via rustypipe channel_videos(). Returns the
channel metadata block (id, name, subscribers, banner) alongside the
items array — same VideoItem shape as search.

Addon refactor: _add_video_items is now the shared listing builder.
Both _search_directory and _channel_directory call it. Each video
result gets a 'Go to <channel>' context-menu entry that
Container.Update's to ?action=channel&id=<channel_id> — so from any
search result, the user can drill into that channel's recent uploads
without going back through search.

Smoke verified on the Pi via Files.GetDirectory: LTT channel
(UCXuqSBlHAE6Xw-yeJA0Tunw) returned 30 recent videos.

Addon version 0.0.7.
This commit is contained in:
Kayos 2026-05-23 11:24:59 -07:00
parent 1b18c67fff
commit d463781aae
5 changed files with 143 additions and 38 deletions

View file

@ -50,6 +50,12 @@ enum Request {
#[serde(default = "default_search_limit")]
limit: u32,
},
/// List a channel's recent videos. `id` is a YouTube channel ID (UC…).
ChannelVideos {
id: String,
#[serde(default = "default_search_limit")]
limit: u32,
},
}
fn default_search_limit() -> u32 {
@ -163,6 +169,10 @@ async fn handle_line(line: &str) -> Response {
Ok(v) => Response::ok(v),
Err(e) => e.into(),
},
Request::ChannelVideos { id, limit } => match resolve::channel_videos(&id, limit).await {
Ok(v) => Response::ok(v),
Err(e) => e.into(),
},
}
}

View file

@ -39,6 +39,43 @@ pub(crate) async fn search(query: &str, limit: u32) -> Result<Value, HandlerErro
}))
}
/// List a channel's recent videos. Returns the same VideoItem shape as
/// `search`, plus channel metadata (name, subscribers, description, banner).
pub(crate) async fn channel_videos(channel_id: &str, limit: u32) -> Result<Value, HandlerError> {
use rustypipe::client::RustyPipe;
let rp = RustyPipe::new();
let ch = rp
.query()
.channel_videos(channel_id)
.await
.map_err(|e| classify_rustypipe_error(&e))?;
let items_json: Vec<Value> = ch
.content
.items
.iter()
.take(limit as usize)
.filter_map(|v| serde_json::to_value(v).ok())
.collect();
tracing::info!(channel_id, count = items_json.len(), "channel_videos ok");
Ok(serde_json::json!({
"source": "rustypipe",
"channel": {
"id": ch.id,
"name": ch.name,
"description": ch.description,
"subscribers": ch.subscriber_count,
"video_count": ch.video_count,
"avatar": ch.avatar,
"banner": ch.banner,
},
"items": items_json,
}))
}
/// DASH-ready resolve: returns rustypipe's full `video_only_streams` +
/// `audio_streams` arrays + `details`. The Python addon builds an MPD
/// from these and hands it to inputstream.adaptive — unlocks 1080p+ via