torttube/MILESTONES.md
Kayos e2bbf5f0e4 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.
2026-05-23 11:33:20 -07:00

117 lines
6.2 KiB
Markdown

# torttube milestones
## M0 — Scaffold [current]
- [x] `Sulkta-Coop/torttube` on LAN Gitea
- [x] Layout: `addon/` (Python) + `sidecar/` (Rust workspace)
- [x] GPL-3.0 license headers
- [ ] crafting-table build target produces a static aarch64 sidecar binary
## M1 — sidecar resolve (three-tier)
- [ ] reads `{"op":"resolve","id":"<yt-id>"}` from stdin
- [ ] **Tier 1:** rustypipe → `{"streams":[…],"title":"…","duration_s":N,"source":"rustypipe"}`
- [ ] **Tier 2:** on Tier-1 failure, shell out to `yt-dlp -j <url>` → same JSON shape with `"source":"yt-dlp"`
- [ ] **Tier 3:** new op `{"op":"rip","id":"<yt-id>","dest":"/storage/.kodi/temp/torttube/<id>.<ext>"}` invoked by addon on Tier-1+2 stream failures or 403-mid-play; yt-dlp downloads file, sidecar returns local path
- [ ] typed errors for age-restricted / region-restricted / private (not panics)
- [ ] sig decoding verified against a known-good video
- [ ] DASH manifest URL or per-itag direct stream URL — whichever inputstream.adaptive prefers
## M2 — SponsorBlock
- [ ] `{"op":"sponsorblock","id":"<yt-id>"}` → segments array
- [ ] SHA-256 prefix lookup (privacy-preserving — only send first 4 hex chars)
- [ ] category filter honoured from addon settings (skip / mute / show only)
- [ ] cache per-session
## M3 — Kodi addon plays one video
- [ ] `addon.xml` + `main.py` register as video plugin
- [x] `main.py` handles `plugin://plugin.video.torttube/?action=play&id=<id>` and
`?url=<full-url>` — wired so JSON-RPC `Player.Open` from any LAN client
(phone, HA, curl) triggers resolve + play. See docs/remote-control.md.
- [ ] cross-compile sidecar for aarch64, drop into `bin/` of addon dir
- [ ] install + smoke on LibreELEC RPi at `192.168.0.158`
- [ ] (later) hardcoded list of 3 test videos for in-Kodi navigation
## M4 — search + channel browse [PARTIAL]
- [x] sidecar `search` op via rustypipe `query().search::<VideoItem,_>()`
- [x] root directory listing in Kodi addon (Search + Play by URL entries)
- [x] `?action=search` accepts inline `q=` (for JSON-RPC) or prompts keyboard
- [x] result labels show title · channel · duration · view-count, with
`IsPlayable=true`, thumbnails, video InfoLabels
- [x] verified via JSON-RPC: `Files.GetDirectory` returns 19+ formatted
results for "linus tech tips"
- [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
- [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]
- [x] `SponsorBlockMonitor` (xbmc.Monitor subclass) polls `Player().getTime()`
every 0.5s after playback starts; seeks past each segment exactly
once (UUID dedup); exits cleanly on abort or playback stop
- [x] toast on skip (`SponsorBlock — Skipped <category> (<duration>s)`)
- [x] only segments with `actionType: skip` are honored (mute/etc. ignored
for now — adding those is a multi-pass M5+ pass)
- [ ] category toggles + skip-counter in settings.xml (deferred — defaults
currently: sponsor, selfpromo, interaction skipped automatically)
## M6 — install + cross-compile [DONE]
- [x] cross-compile sidecar for aarch64-musl static via throwaway
`messense/rust-musl-cross:aarch64-musl` container. 6.2MB stripped
static binary. Builds clean from `scripts/build-addon-zip.sh`.
- [x] bundle yt-dlp's universal Python zipapp (~3MB, not the PyInstaller
binary — that breaks on LibreELEC's armhf userspace because the
aarch64 dynamic loader doesn't exist; Python zipapp runs on
`/usr/bin/python3` which is always there).
- [x] zip layout matches Kodi "install from zip" expectations
- [x] addon.zip dropped at `smb://lucy/downloads/torttube/`
- [x] install + smoke recipe documented at `docs/install.md`
- [x] **installed on Livingroom Pi** (`192.168.0.158`) via SSH +
`systemctl restart kodi` + `Addons.SetAddonEnabled`
- [x] **JSON-RPC `Player.Open` smoke verified** — Rick Astley played
end-to-end with audio + video synced (yt-dlp `-f best[ext=mp4]/best`
picked format 18, 360p H.264+AAC progressive)
- [ ] armv7 build for older Pis (deferred — current TV is aarch64-capable
and my static sidecar runs on it even though userspace is armhf)
- [ ] 720p+ playback (DASH WIP — see "M7 — DASH/HD" below) — itag 22 (720p
progressive) is deprecated by YouTube; higher quality needs DASH
manifest generation. Code path exists, gated behind `TORTTUBE_DASH=1`.
## M7 — DASH / HD playback [WIP]
- [x] sidecar `resolve_dash` op returns rustypipe's full
`video_only_streams` + `audio_streams` arrays (16+ representations)
- [x] addon `_build_dash_mpd` constructs valid on-demand MPD with H.264
video reps from 360p → 1080p + best AAC audio rep (XML-escaped URLs,
proper `SegmentBase indexRange` + `Initialization range`)
- [ ] **serve MPD over localhost HTTP** — inputstream.adaptive's libcurl
can't open `file://` URLs. ThreadingHTTPServer + Player monitor
lifecycle is sketched but caused Kodi's "two concurrent busydialogs"
fatal during rapid retries. Needs more work on lifecycle + retry
backoff before re-enabling.
- [ ] handle session-cookie / poToken inheritance — googlevideo URLs may
need the same client signature across MPD + segment fetches
- [ ] graceful fallback to progressive if MPD load fails mid-playback
## Upstream PR work (parallel lane — every bug evaluated for "fix it upstream?")
This isn't a separate milestone, it's a posture. Every sidecar bug we
diagnose: ask "is this rustypipe / NPE / yt-dlp's bug?" — if yes, fix lands
upstream too. Log every filed PR in [docs/upstream.md](docs/upstream.md).
Opening shortlist:
- rustypipe PR #77 — review + help land
- NPE #1357 (JDoc) — smallest credible PR, opens the relationship
- NPE #1444 (typed errors) — clean signal/contract change
- NPE #1360 (refactor link handlers, "help wanted")
- rustypipe ↔ NPE port — anything one project has that the other lacks