M4 wrap — playlist browse + addon settings + upstream PR-77 notes

Sidecar Playlist op via rustypipe playlist(). Returns playlist metadata
block (id, name, channel, video_count) + items array. Verified live
against LTT's 'Consumer Advocacy' (PL8mG-RkN2uTzwoF72GqeqAJMI-N7scqtI):
returns the single video with full metadata.

Addon ?action=playlist&id=PL... lists items via _add_video_items reuse.
Verified via Files.GetDirectory JSON-RPC.

resources/settings.xml gains a 'dash_enabled' toggle (boolean, default
off). main.py checks ADDON.getSettingBool('dash_enabled') OR the
TORTTUBE_DASH env fallback before attempting the DASH path. Toggle via
Kodi Settings → Add-on settings → torttube, OR via
Addons.SetSettings JSON-RPC.

docs/upstream.md: filed a 'watching' entry for rustypipe PR #77
(Schmiddiii's late-May YouTube parsing fixes) with our independent
test data — player(), search(), and channel_videos() all still work
against current YouTube on 0.11.4, suggesting the PR fixes code paths
torttube doesn't yet exercise. Endorsement comment pending: gated on
creating a Sulkta-Coop codeberg account.

Observation from kodi.log: plugin.video.youtube successfully parsed a
DASH MPD with 26 streams via inputstream.adaptive on this same Pi —
proves DASH is solvable on our setup, just need to match the URL
pattern they use. M7 stabilization carrying forward.

Addon version 0.0.9.
This commit is contained in:
Kayos 2026-05-23 11:33:20 -07:00
parent d463781aae
commit a784321759
7 changed files with 94 additions and 7 deletions

View file

@ -56,6 +56,12 @@ enum Request {
#[serde(default = "default_search_limit")]
limit: u32,
},
/// List a playlist's videos. `id` is a YouTube playlist ID (PL…).
Playlist {
id: String,
#[serde(default = "default_search_limit")]
limit: u32,
},
}
fn default_search_limit() -> u32 {
@ -173,6 +179,10 @@ async fn handle_line(line: &str) -> Response {
Ok(v) => Response::ok(v),
Err(e) => e.into(),
},
Request::Playlist { id, limit } => match resolve::playlist(&id, limit).await {
Ok(v) => Response::ok(v),
Err(e) => e.into(),
},
}
}

View file

@ -76,6 +76,41 @@ pub(crate) async fn channel_videos(channel_id: &str, limit: u32) -> Result<Value
}))
}
/// List a playlist's videos. Returns the same VideoItem shape as search/channel.
pub(crate) async fn playlist(playlist_id: &str, limit: u32) -> Result<Value, HandlerError> {
use rustypipe::client::RustyPipe;
let rp = RustyPipe::new();
let pl = rp
.query()
.playlist(playlist_id)
.await
.map_err(|e| classify_rustypipe_error(&e))?;
let items_json: Vec<Value> = pl
.videos
.items
.iter()
.take(limit as usize)
.filter_map(|v| serde_json::to_value(v).ok())
.collect();
tracing::info!(playlist_id, count = items_json.len(), "playlist ok");
Ok(serde_json::json!({
"source": "rustypipe",
"playlist": {
"id": pl.id,
"name": pl.name,
"description": pl.description,
"video_count": pl.video_count,
"channel": pl.channel,
"thumbnail": pl.thumbnail,
},
"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