From e2bbf5f0e414387bc61597d9018f30a948033718 Mon Sep 17 00:00:00 2001 From: Kayos Date: Sat, 23 May 2026 11:33:20 -0700 Subject: [PATCH] =?UTF-8?q?M4=20wrap=20=E2=80=94=20playlist=20browse=20+?= =?UTF-8?q?=20addon=20settings=20+=20upstream=20PR-77=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- MILESTONES.md | 6 ++- addon/plugin.video.torttube/addon.xml | 2 +- addon/plugin.video.torttube/main.py | 40 ++++++++++++++++++- .../resources/settings.xml | 6 ++- docs/upstream.md | 2 +- sidecar/crates/torttube-sidecar/src/main.rs | 10 +++++ .../crates/torttube-sidecar/src/resolve.rs | 35 ++++++++++++++++ 7 files changed, 94 insertions(+), 7 deletions(-) diff --git a/MILESTONES.md b/MILESTONES.md index a9527df..ac5f6ac 100644 --- a/MILESTONES.md +++ b/MILESTONES.md @@ -46,8 +46,10 @@ - [x] channel browse → `{"op":"channel_videos","id":"…"}` — verified 30 videos returned for LTT's UCXuqSBlHAE6Xw-yeJA0Tunw via JSON-RPC - [x] context-menu entry on every result: "Go to " → channel listing -- [ ] playlist browse → `{"op":"playlist","id":"…"}` -- [ ] paginated results (currently capped at limit=30/50) +- [x] playlist browse → `{"op":"playlist","id":"…"}` — verified with + LTT's "Consumer Advocacy" playlist, returns playlist metadata + (name, channel, video_count) + items array +- [ ] paginated results (currently capped at limit=30/50/100) - [ ] search history ## M5 — SponsorBlock skipping [DONE] diff --git a/addon/plugin.video.torttube/addon.xml b/addon/plugin.video.torttube/addon.xml index 2dce120..e4f8846 100644 --- a/addon/plugin.video.torttube/addon.xml +++ b/addon/plugin.video.torttube/addon.xml @@ -1,7 +1,7 @@ diff --git a/addon/plugin.video.torttube/main.py b/addon/plugin.video.torttube/main.py index ee648d2..a963c49 100644 --- a/addon/plugin.video.torttube/main.py +++ b/addon/plugin.video.torttube/main.py @@ -314,7 +314,13 @@ def _play(yt_id: str) -> None: mpd_bytes: bytes | None = None dash_resp: dict[str, Any] = {} - if os.environ.get("TORTTUBE_DASH") == "1": + dash_enabled = False + try: + dash_enabled = ADDON.getSettingBool("dash_enabled") + except Exception: + # Setting might not exist on older configs; treat as off. + pass + if dash_enabled or os.environ.get("TORTTUBE_DASH") == "1": mpd_bytes, dash_resp = _try_dash(yt_id) if mpd_bytes: details = dash_resp.get("details") or {} @@ -638,6 +644,36 @@ def _search_directory(query: str | None = None) -> None: xbmcplugin.endOfDirectory(_HANDLE) +def _playlist_directory(playlist_id: str) -> None: + """List a playlist's videos.""" + try: + resp = _call_sidecar( + {"op": "playlist", "id": playlist_id, "limit": 100}, timeout_s=15 + ) + except Exception as e: + _log(f"playlist failed: {e}", xbmc.LOGERROR) + xbmcgui.Dialog().notification( + "torttube", f"playlist failed: {e}", xbmcgui.NOTIFICATION_ERROR, 4000 + ) + xbmcplugin.endOfDirectory(_HANDLE, succeeded=False) + return + if not resp.get("ok"): + xbmcgui.Dialog().notification( + "torttube", + f"playlist: {resp.get('error', 'unknown')}", + xbmcgui.NOTIFICATION_WARNING, + 4000, + ) + xbmcplugin.endOfDirectory(_HANDLE, succeeded=False) + return + + items = resp.get("items") or [] + pl = resp.get("playlist") or {} + _log(f"playlist {pl.get('name') or playlist_id}: {len(items)} items") + _add_video_items(items) + xbmcplugin.endOfDirectory(_HANDLE) + + def _channel_directory(channel_id: str) -> None: """List a channel's recent videos.""" try: @@ -707,6 +743,8 @@ def main() -> None: _search_directory(query=params.get("q")) elif action == "channel": _channel_directory(params.get("id") or "") + elif action == "playlist": + _playlist_directory(params.get("id") or "") elif action == "play_by_url": _play_by_url_prompt() else: diff --git a/addon/plugin.video.torttube/resources/settings.xml b/addon/plugin.video.torttube/resources/settings.xml index f4de3ff..01d5095 100644 --- a/addon/plugin.video.torttube/resources/settings.xml +++ b/addon/plugin.video.torttube/resources/settings.xml @@ -2,8 +2,10 @@
- - + + + false +
diff --git a/docs/upstream.md b/docs/upstream.md index bd80008..ac3d228 100644 --- a/docs/upstream.md +++ b/docs/upstream.md @@ -34,7 +34,7 @@ _(none yet — opens with M1 development)_ | Project | PR/Issue | Title | Why we care | |---------|----------|-------|-------------| -| rustypipe | [PR #77](https://codeberg.org/ThetaDev/rustypipe/pulls/77) | "Some fixes" | Open 2026-05-23, unmerged. If maintainer stays quiet we may need to help land it or fork. | +| rustypipe | [PR #77](https://codeberg.org/ThetaDev/rustypipe/pulls/77) | "Some fixes" (Schmiddiii) | Targeted fixes for YouTube changes ~2026-05-21: video metadata parsing (channel-tag removal) + duration parsing (thumbnail overlay field renames). +15/-11 across 2 files. Stalled in CI-needs-approval. We've **independently verified** rustypipe-0.11.4 (which predates this PR) still works for `player()`, `search()`, and `channel_videos()` against current YouTube as of 2026-05-23 — Rick Astley video, LTT search returning 19 results, LTT channel browse returning 30 videos. Suggests the breaks PR #77 targets are in code paths we don't exercise. **TODO**: comment on the PR with this test data + offer to mirror to GitHub if codeberg CI stays blocked. Gated on creating a Sulkta-Coop codeberg account. | | NPE | [#1339](https://github.com/TeamNewPipe/NewPipeExtractor/issues/1339) | n-parameter deobfuscation broken | Core to playback. Fix here = fix in our Tier-1. | | NPE | [#1444](https://github.com/TeamNewPipe/NewPipeExtractor/issues/1444) | Distinguish unavailable vs unextractable | Clean typed-error PR target. | | NPE | [#1360](https://github.com/TeamNewPipe/NewPipeExtractor/issues/1360) | Refactor link handlers | "help wanted" label. | diff --git a/sidecar/crates/torttube-sidecar/src/main.rs b/sidecar/crates/torttube-sidecar/src/main.rs index 6cefcc5..8df36cc 100644 --- a/sidecar/crates/torttube-sidecar/src/main.rs +++ b/sidecar/crates/torttube-sidecar/src/main.rs @@ -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(), + }, } } diff --git a/sidecar/crates/torttube-sidecar/src/resolve.rs b/sidecar/crates/torttube-sidecar/src/resolve.rs index dbbf8a3..4a2b9af 100644 --- a/sidecar/crates/torttube-sidecar/src/resolve.rs +++ b/sidecar/crates/torttube-sidecar/src/resolve.rs @@ -76,6 +76,41 @@ pub(crate) async fn channel_videos(channel_id: &str, limit: u32) -> Result Result { + 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 = 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