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:
parent
d463781aae
commit
a784321759
7 changed files with 94 additions and 7 deletions
|
|
@ -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>" → 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]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<addon id="plugin.video.torttube"
|
||||
name="torttube"
|
||||
version="0.0.7"
|
||||
version="0.0.9"
|
||||
provider-name="Sulkta-Coop">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="3.0.0"/>
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
<settings version="1">
|
||||
<section id="torttube">
|
||||
<category id="general" label="General">
|
||||
<group id="placeholder">
|
||||
<!-- M2+ — SponsorBlock category toggles, sidecar path override, etc. -->
|
||||
<group id="quality" label="Quality">
|
||||
<setting id="dash_enabled" type="boolean" label="Enable DASH (up to 1080p H.264) — WIP, may not work">
|
||||
<default>false</default>
|
||||
</setting>
|
||||
</group>
|
||||
</category>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue