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.
Sidecar ChannelVideos op via rustypipe channel_videos(). Returns the
channel metadata block (id, name, subscribers, banner) alongside the
items array — same VideoItem shape as search.
Addon refactor: _add_video_items is now the shared listing builder.
Both _search_directory and _channel_directory call it. Each video
result gets a 'Go to <channel>' context-menu entry that
Container.Update's to ?action=channel&id=<channel_id> — so from any
search result, the user can drill into that channel's recent uploads
without going back through search.
Smoke verified on the Pi via Files.GetDirectory: LTT channel
(UCXuqSBlHAE6Xw-yeJA0Tunw) returned 30 recent videos.
Addon version 0.0.7.
Sidecar gains the 'search' op via rustypipe's
query().search::<VideoItem,_>() — returns id, title, channel, duration,
thumbnails, view_count. Default limit 25.
Addon root directory is no longer a placeholder notification:
- 'Search' entry → ?action=search → keyboard input → result list →
tap a result to play (each result is a play-action plugin URL).
- 'Play by URL' entry → ?action=play_by_url → keyboard input → PlayMedia.
- ?action=search also accepts inline 'q=…' so JSON-RPC clients can
drive search without going through the on-TV keyboard (useful for
share-to-TV from phone + tests).
- Result labels formatted as 'Title · Channel · Duration · Views',
with thumbnail + Kodi InfoLabels for richer skin views.
Verified via Files.GetDirectory JSON-RPC: 19 well-formatted LTT results
returned for query 'linus tech tips'.
Pending M4: channel browse, playlist browse, pagination, search history.
Addon version 0.0.6.
Sidecar resolve_dash op shipped — returns rustypipe's full video_only_streams
+ audio_streams (16+ representations for NGGYU, from 360p H.264 through 4K
AV1). Addon _build_dash_mpd assembles a valid on-demand MPEG-DASH manifest
filtered to H.264 ≤1080p + best AAC audio.
Two unblocked-by-WIP issues surfaced during integration:
- inputstream.adaptive's libcurl can't open file:// URLs (logged in
docs/upstream.md as an enhancement-target).
- Rapid Player.Open retries can trigger Kodi's 'two concurrent
busydialogs' fatal exit; need lifecycle hardening before re-enabling.
Pivoted to localhost HTTP-server serving (ThreadingHTTPServer on a
port-0 socket, MPD bytes captured in a per-instance handler subclass).
Lifecycle: server.shutdown() runs in a finally block after the
SponsorBlockMonitor watcher exits. Works in isolation but Kodi crashed
under rapid retry conditions — needs more testing.
For v0.0.5: DASH path is gated behind TORTTUBE_DASH=1 env var; default
falls through to the stable yt-dlp progressive 360p path that's been
verified live. M7 milestone added to track the remaining work; PRs
to inputstream.adaptive + Kodi candidates logged in docs/upstream.md.
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 Livingroom 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).
Live install verified end-to-end:
- SSH'd into 192.168.0.158 (LibreELEC, Kodi 20.3 Nexus, kernel aarch64
/ userspace armhf — that's why the static Rust sidecar runs but the
PyInstaller yt-dlp binary couldn't)
- Dropped addon dir into /storage/.kodi/addons/
- systemctl restart kodi → Kodi rescans /storage/.kodi/addons/
- JSON-RPC Addons.SetAddonEnabled flipped enabled:false → true
- Player.Open with plugin URL → 7s yt-dlp resolve → VideoFullScreen.xml,
fullscreen:true, currentwindow 12005, audio+video synced
Fixes that surfaced during the install:
- yt-dlp swap: PyInstaller aarch64 binary needs ld-linux-aarch64.so.1
which LibreELEC doesn't ship. Switched to the universal Python zipapp
(~3MB) which runs on /usr/bin/python3.11. build-addon-zip.sh updated.
- main.py now puts the addon's bin/ dir on PATH so the sidecar's
Command::new('yt-dlp') call resolves to the bundled zipapp.
- Cosmetic fix: resolve.rs's classify_yt_dlp_error preserves the
original error message (was downcasing it for keyword matching and
then using the lowercased copy as the user-facing error).
Caveats logged for later:
- 360p ceiling (yt-dlp '-f best[ext=mp4]' picks itag 18; 720p
progressive itag 22 is deprecated by YouTube; higher quality wants
DASH manifest generation).
- ALSA sink: device 'sysdefault:CARD=vc4hdmi1' fails to open on this
Pi but Kodi auto-falls-back to 'sysdefault' so audio works. Worth
cleaning up in Kodi audio settings later.
MILESTONES + docs/install.md updated with the SSH + JSON-RPC alternate
install path.
Realized during M6 packaging that the rustypipe path returns separate
audio + video DASH streams (Opus 251 + AV1 401 on the smoke video). Kodi
can't sync those without an inputstream.adaptive DASH manifest, which
would need server-side manifest generation — M3+ territory.
Stopgap for shippable M3: new sidecar op resolve_play that asks yt-dlp
for -f best[ext=mp4]/best — one combined audio+video URL Kodi plays as
plain HTTP. ~3-5s overhead vs rustypipe but reliable sync.
main.py _play() now calls resolve_play. resolve still exists for
metadata + browse paths (M4 will use it).
Rebuilt aarch64-musl binary, repackaged plugin.video.torttube-0.0.1.zip
(38.7MB, md5 f2c08aed130b1c1bd231a9b6cbfac93c). Live at:
smb://lucy/downloads/torttube/plugin.video.torttube-0.0.1.zip
scripts/build-addon-zip.sh runs the whole pipeline from a host with ssh
lucy:
- one-shot messense/rust-musl-cross:aarch64-musl container builds the
sidecar static (6.2MB stripped). Doesn't mutate crafting-table.
- fetches yt-dlp_linux_aarch64 from the upstream release page so Tier 2
+ Tier 3 work on the Pi (LibreELEC ships no Python YouTube tools)
- packages everything into plugin.video.torttube.zip with the Kodi
install-from-zip layout
- drops the zip at /mnt/user/downloads/torttube/ on Lucy SMB
Cargo.toml swaps rustypipe to default-features=false +
rustls-tls-webpki-roots so the cross-compile is openssl-free.
addon.xml drops the unused script.module.requests requirement — main.py
only uses Python stdlib + Kodi's own modules.
docs/install.md walks the Kodi UI flow + a smoke curl that fires
Player.Open via JSON-RPC. Pi-side smoke is pending Cobb's install on
192.168.0.158.
main.py now handles the standard Kodi plugin-URL routing:
plugin://plugin.video.torttube/?action=play&id=<yt-id>
plugin://plugin.video.torttube/?action=play&url=<full-url>
Either form calls the sidecar resolve op, picks a stream URL from the
response (rustypipe video_stream preferred, yt-dlp combined fallback),
and hands it to Kodi via xbmcplugin.setResolvedUrl.
URL parser accepts watch?v=, youtu.be/, /shorts/, /embed/, /live/, and
bare 11-char IDs. setResolvedUrl flags inputstream.adaptive for .mpd
and .m3u8 manifests so DASH/HLS streams play with the right demuxer.
This makes 'share to TV' work over Kodi's existing JSON-RPC API on
:8080 — Player.Open with a plugin URL is all the remote client needs.
No new server, no app — Kore / Yatse / curl / HA all already work.
docs/remote-control.md captures the curl recipe + Android share-target
plan for the eventual companion app.
JSON-over-stdio loop on tokio with four ops:
- ping liveness
- resolve Tier 1 rustypipe → Tier 2 yt-dlp -j fallback. Typed
errors (age/region/private/not-found) short-circuit
Tier 2 so we don't double-hit a wall. Pass-through
serialization of player.details + selected streams,
so the Python addon parses what it needs without us
coupling to rustypipe's struct shape.
- rip Tier 3 yt-dlp downloads bestvideo+bestaudio to a
caller-supplied dest_dir, returns the resulting
path + size for the addon to play as a local file.
- sponsorblock SHA-256 prefix lookup (first 4 hex), filter to the
exact video_id locally. Categories default to
[sponsor, selfpromo, interaction]; caller can override.
Smoke ran in crafting-table against dQw4w9WgXcQ — rustypipe 0.11.4
still resolves cleanly in 2026-05, sig decoding intact, both 4K AV1
video and Opus 128kbps audio came back with valid signed URLs.
SponsorBlock returns empty segments for music videos (as expected).
Cobb wants rustypipe primary, yt-dlp fallback, and rip-to-temp as the
last-resort path when streams die mid-play. README expanded to spell
out all three tiers + adds the 'fight YouTube alongside the FOSS
ecosystem' framing. MILESTONES M1 rewritten to cover all three tiers.
New file docs/upstream.md tracks every PR we file against rustypipe /
NPE / yt-dlp with honest outcomes. Opens empty; fills as M1+ surface
real bugs to fix.
Kodi addon (plugin.video.torttube) shell with Cargo workspace for the
rustypipe-backed sidecar binary. No working extraction yet — addon.xml
parses, main.py is a notification stub, sidecar's main.rs prints scaffold
banner. See MILESTONES.md for M1..M6.
License: GPL-3.0-or-later (matches rustypipe + NewPipeExtractor).