SponsorBlockMonitor (xbmc.Monitor subclass) attaches after setResolvedUrl:
- fetches segments from sidecar via the existing sponsorblock op
(SHA-256 prefix lookup, defaults to sponsor + selfpromo + interaction
categories)
- waits up to 30s for playback to actually start, then polls
Player.getTime() every 0.5s
- when position enters a skip segment, calls seekTime(end) and shows
a 'SponsorBlock — Skipped <category> (<duration>s)' toast
- UUIDs are remembered so a manual rewind into a previously-skipped
segment doesn't trigger again
- exits cleanly on playback stop or Kodi shutdown
Live-verified on the LibreELEC Pi with LTT 2T8x5antlnc ('Trump Phone'),
which has two locked sponsor segments. Sought to 1:45, the monitor
fired at 108.3s and seeked to 128.4s — log line:
[torttube] sponsorblock skip: sponsor 108.3-128.4 (20s)
addon.xml v0.0.1 → v0.0.2.
Deferred for v0.0.3+: settings.xml category toggles + a skip-counter,
support for non-skip action types (mute, full, poi).
89 lines
4.5 KiB
Markdown
89 lines
4.5 KiB
Markdown
# torttube milestones
|
|
|
|
## M0 — Scaffold [current]
|
|
|
|
- [x] `Sulkta/torttube`
|
|
- [x] Layout: `addon/` (Python) + `sidecar/` (Rust workspace)
|
|
- [x] GPL-3.0 license headers
|
|
- [ ] the build container 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 `kodi-host`
|
|
- [ ] (later) hardcoded list of 3 test videos for in-Kodi navigation
|
|
|
|
## M4 — search + channel browse
|
|
|
|
- [ ] search box → sidecar `{"op":"search","q":"…"}` → results
|
|
- [ ] channel browse → `{"op":"channel","id":"…"}`
|
|
- [ ] playlist browse → `{"op":"playlist","id":"…"}`
|
|
- [ ] result thumbnails + duration + uploader
|
|
|
|
## 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://fileserver/downloads/torttube/`
|
|
- [x] install + smoke recipe documented at `docs/install.md`
|
|
- [x] **installed on a LibreELEC Pi** (`kodi-host`) 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 (deferred to M5+) — itag 22 (720p progressive) is
|
|
deprecated by YouTube; higher quality needs DASH manifest generation
|
|
|
|
## 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
|