Kodi addon for YouTube via rustypipe + SponsorBlock. Replaces the dead plugin.video.youtube on LibreELEC RPi TVs.
Find a file
Kayos 24be9497e9 v1.0.0 — production-quality cleanup pass
Big sweep ahead of tagging v1:

WATCH LATER STALENESS (MED-2 2nd audit) — actually shipped.
- New _refresh_watch_later_item() that load-mutate-saves under the lock
  helper, replacing the metadata for a single id in place.
- 'Refresh metadata' context-menu entry on every Watch Later item.
- _wl_refresh_action handler: validate id, call _resolve_video_metadata
  (which factors out the same logic both wl_add and wl_refresh need),
  patch the on-disk record, refresh the container if the user is
  currently viewing the WL list.
- Bug: this was supposed to ship in the prior sprint but a duplicate
  Edit replaced the wrong block and the _refresh_watch_later_item
  function never actually landed in the file. Smoke caught it:
  Kodi reported 'Error getting plugin://…?action=wl_refresh' because
  the action raised NameError. Now landed properly; verified
  end-to-end after a Kodi restart cleared the cached-addon stub.

MULTI-NIC _lan_ip (MED-7 2nd audit) — fixed.
- gethostbyname_ex now scans local interfaces first and prefers a
  private-range LAN IP (192.168.x.x / 10.x.x.x / 172.16-31.x.x).
- Connect-trick to 8.8.8.8 stays as the fallback for hosts with a
  single default route. 127.0.0.1 is the last resort.
- On hosts with Tailscale / OpenVPN / VPN tunnels as the default
  route, this prevents inputstream.adaptive from getting handed a
  VPN-tunnel IP it can't reach.

REMAINING LOW BATCH (1st + 2nd audit) — landed.
- _CHANNEL_ID_RE check in _add_video_items drops 'Go to channel'
  entries when rustypipe ever hands us a non-UC-shaped id (LOW-1 2nd).
- _redact_query truncates queries before logging (LOW-3 2nd).
- _add_to_watch_later() now returns 'was_full' so the wl_add notify
  can surface 'Watch Later at cap (500) — dropped oldest' (LOW-9 2nd).
- _remove_from_watch_later() returns 'removed' so wl_remove notifies
  'Item was not in Watch Later' on no-op (LOW-7 2nd).
- _add_to_watch_later validates yt_id shape before writing (LOW-6 2nd).
- _record_search collapses whitespace before dedup (LOW-4 2nd).
- Sidecar tokio runtime now flavor='current_thread' — one-shot per
  invocation, saves ~100KB RSS per spawn (LOW-6 1st).
- _MIME_CODEC_RE accepts either quote style (MED-5).
- Response::ok has a debug_assert! tripwire if a handler ever returns
  its own 'ok' key (MED-6).
- _pick_thumbnail defends against rustypipe handing it a string,
  dict, or list-of-non-dicts shape (MED-9 / HIGH-3 redux).

DANGEROUS-FUNCTIONS SCAN — clean.
- Zero shell=True, os.system, os.popen, eval, exec, pickle,
  __import__ across both Python and Rust.
- All subprocess calls list-form, all URL building via urlencode,
  all JSON via json/serde_json.
- xbmc.executebuiltin Container.Update / RunPlugin URLs always go
  through _plugin_url(urlencode) — channel_id additionally regex-
  validated for defense-in-depth.

CODE FEEL — humanized.
- Stripped all 'Audit CRIT-1 (2nd pass)' / 'Audit MED-X' ticket
  prefixes across main.py + sidecar Rust. The 'why' comments stay;
  the audit-trail breadcrumbs go. Code reads like working software,
  not a postmortem trail.
- Section comments (── Search history ──, ── Watch Later ──,
  ── Subscriptions ──) added on the persistence block for navigation.

VERSION — bumped addon.xml to 1.0.0, Cargo.toml workspace to 1.0.0.
Verified live on Livingroom Pi after a Kodi restart: wl_add writes
fresh LTT metadata, manual mutation to 'STALE STUB' detected,
wl_refresh re-fetches and restores the canonical title.
2026-05-23 13:17:27 -07:00
addon/plugin.video.torttube v1.0.0 — production-quality cleanup pass 2026-05-23 13:17:27 -07:00
docs Declare plugin.video.youtube as a Kodi addon dep 2026-05-23 12:15:06 -07:00
scripts M6 DONE — torttube ships, Rick Astley plays fullscreen on the Livingroom Pi 2026-05-23 10:18:26 -07:00
sidecar v1.0.0 — production-quality cleanup pass 2026-05-23 13:17:27 -07:00
.gitignore M0 scaffold — Python addon + Rust sidecar 2026-05-23 08:14:09 -07:00
LICENSE M0 scaffold — Python addon + Rust sidecar 2026-05-23 08:14:09 -07:00
MILESTONES.md M7 DONE via delegation — pv.youtube plays HD with audio 2026-05-23 11:58:12 -07:00
README.md Declare plugin.video.youtube as a Kodi addon dep 2026-05-23 12:15:06 -07:00

torttube

Kodi addon for YouTube via RustyPipe extraction + SponsorBlock segment skipping.

Replaces the dead plugin.video.youtube on LibreELEC RPi TVs after Google required account-linking for the upstream addon.

Architecture

Kodi (LibreELEC, RPi)
  └── plugin.video.torttube     [Python addon — UI, browse, SponsorBlock]
        ├── torttube-sidecar    [Rust binary — JSON-over-stdio]
        │     ├── rustypipe         [Native Rust Innertube for browse]
        │     ├── yt-dlp subprocess [Fallback resolve]
        │     └── sponsorblock      [REST client, SHA-256 prefix lookup]
        └── plugin.video.youtube    [DEPENDENCY — handles HD playback]
              └── inputstream.adaptive  [DASH demux + decode]

plugin.video.youtube is declared as a Kodi addon dependency in addon.xml. When a user installs torttube, Kodi auto-fetches pv.youtube from the official Kodi addon repository — user only manages torttube; the dep is transparent.

torttube does what it's faster at: rustypipe-backed search/channel/playlist browse, SponsorBlock auto-skip via a tight xbmc.Player() monitor loop, JSON-RPC remote-control for share-to-TV. Playback hands off to pv.youtube via plugin://plugin.video.youtube/play/?video_id=<id> — they've spent years getting the DASH-MPD + multi-client Innertube fallback right. Our SponsorBlock monitor runs in parallel because xbmc.Player() is a global accessor that works regardless of which addon initiated playback.

Kodi addons are Python — the engine layer (n-param sig decoding, Innertube, SponsorBlock hashing) lives in a Rust sidecar so we get a single maintained extraction surface and clean aarch64/armv7 cross-compiles.

Three-tier resolve because YouTube actively fights every extractor:

  1. rustypipe (Rust) — preferred. Fast, in-process, no Python dep on the RPi.
  2. yt-dlp subprocess — fallback when rustypipe sig-decoding falls behind YouTube's deobfuscator changes. yt-dlp updates weekly; we shell out, parse -j JSON.
  3. Rip-to-temp — last resort when stream URLs 403 mid-playback (poToken expiry, cookie session mismatch). yt-dlp downloads to /storage/.kodi/temp/torttube/<id>.<ext>, Kodi plays the local file. Temp dir has size cap + age cleanup.

Status

M0 scaffold. Nothing playable yet — see MILESTONES.md.

Upstream — we fight with the FOSS extractor ecosystem, not next to it

YouTube's anti-scraping changes hit every extractor: NewPipe, yt-dlp, Invidious, rustypipe. Every fix we make in our sidecar gets evaluated for "is this upstreamable?" — if yes, the fix lands at the upstream project, not just here.

Active lanes:

  • rustypipe (Rust, codeberg.org/ThetaDev/rustypipe) — maintenance has slowed. Open PR #77 "Some fixes" is unmerged as of 2026-05-23. We will either help land it (review + ping maintainer) or fork to Sulkta-Coop/rustypipe if upstream stays quiet. Forking is the worst case, not the first move.
  • NewPipeExtractor (Java, github.com/TeamNewPipe/NewPipeExtractor) — actively maintained, 177 open issues. We use it as the reference implementation for Innertube behaviour. PRs to NPE land in Rust here via rustypipe, and vice versa.
  • yt-dlp (Python, github.com/yt-dlp/yt-dlp) — the gold standard. We're more consumers than contributors here, but if our rip-to-temp tier surfaces a specific extractor bug we file it.

Issues we're watching:

  • NPE #1339 — n-parameter deobfuscation
  • NPE #1444 — distinguish unavailable vs unextractable
  • NPE #1360 — refactor link handlers (help wanted)
  • NPE #1357 — JDoc checks in PR pipeline (good first issue)
  • rustypipe PR #77 — open as of 2026-05-23, unmerged

Contribution log lives at docs/upstream.md — every PR we file lands there with its outcome.

License

GPL-3.0-or-later. Matches RustyPipe and NewPipeExtractor.