Commit graph

12 commits

Author SHA1 Message Date
a784321759 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
d463781aae M4 — channel browse + context-menu drill-down
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.
2026-05-23 11:24:59 -07:00
1b18c67fff M4 partial — Search shipped, browse UI live on the Pi
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.
2026-05-23 11:20:41 -07:00
45e1306bf3 DASH HD playback — WIP behind TORTTUBE_DASH=1 + upstream notes
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.
2026-05-23 11:14:56 -07:00
f610965fcf M5 DONE — SponsorBlock auto-skip wired + verified on the TV
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).
2026-05-23 10:23:07 -07:00
284fe5fde7 M6 DONE — torttube ships, Rick Astley plays fullscreen on the Livingroom Pi
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.
2026-05-23 10:18:26 -07:00
283693525c addon: switch play action to resolve_play (yt-dlp combined format)
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
2026-05-23 08:59:25 -07:00
f4ceae3b70 M6 — cross-compile aarch64-musl + addon.zip + install docs
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.
2026-05-23 08:54:46 -07:00
9b2a47c909 addon: wire play action + remote-control via Kodi JSON-RPC
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.
2026-05-23 08:37:57 -07:00
7add3cb469 M1 sidecar — resolve (rustypipe + yt-dlp), rip, sponsorblock
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).
2026-05-23 08:30:41 -07:00
76bd1d970e M0 docs — lock three-tier resolve + upstream-contribution posture
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.
2026-05-23 08:20:50 -07:00
238dfb8391 M0 scaffold — Python addon + Rust sidecar
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).
2026-05-23 08:14:09 -07:00