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).
This commit is contained in:
Kayos 2026-05-23 08:14:09 -07:00
commit 238dfb8391
11 changed files with 231 additions and 0 deletions

20
.gitignore vendored Normal file
View file

@ -0,0 +1,20 @@
# Rust
target/
**/*.rs.bk
# Python / Kodi
__pycache__/
*.pyc
*.pyo
*.egg-info/
.pytest_cache/
# Editor + OS
.vscode/
.idea/
*.swp
.DS_Store
# Build artifacts
*.zip
dist/

9
LICENSE Normal file
View file

@ -0,0 +1,9 @@
torttube is licensed under the GNU General Public License version 3 or
(at your option) any later version. SPDX-License-Identifier: GPL-3.0-or-later.
The full text of the GPL-3.0 is available at
https://www.gnu.org/licenses/gpl-3.0.txt and will be bundled with the
addon.zip release artifact. This file is a pointer placeholder for the
in-repo development tree.
Copyright (C) 2026 Sulkta-Coop and contributors.

58
MILESTONES.md Normal file
View file

@ -0,0 +1,58 @@
# 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
- [ ] reads `{"op":"resolve","id":"<yt-id>"}` from stdin
- [ ] calls rustypipe → returns `{"streams":[…],"title":"…","duration_s":N}`
- [ ] handles age-restricted + region-restricted as typed errors, 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
- [ ] hardcoded list of 3 test videos
- [ ] select → sidecar `resolve` → stream URL → Kodi player
- [ ] verified end-to-end on LibreELEC RPi at `192.168.0.158`
## 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
- [ ] background thread on `Player()` polls position
- [ ] when position enters a skip segment → `xbmc.Player().seekTime(end)`
- [ ] toast on skip + skip-counter in settings
- [ ] category toggles in `settings.xml`
## M6 — install + cross-compile
- [ ] crafting-table builds `torttube-sidecar.aarch64` + `.armv7`
- [ ] `addon.zip` ships with platform detect via `xbmc.getCondVisibility('system.platform.linux.raspberrypi')`
- [ ] one-shot install path documented for LibreELEC `/storage/.kodi/`
## Upstream PR targets (parallel, opportunistic)
Pick one as we hit it organically — don't gate milestones on these.
- NPE #1357 (JDoc) — smallest, just to land a credible PR
- NPE #1444 (typed errors) — clean signal/contract change
- rustypipe — file issues + PRs on codeberg as we hit gaps

43
README.md Normal file
View file

@ -0,0 +1,43 @@
# torttube
Kodi addon for YouTube via [RustyPipe](https://codeberg.org/ThetaDev/rustypipe)
extraction + [SponsorBlock](https://sponsor.ajay.app/) 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, settings]
└── torttube-sidecar [Rust binary — Innertube extraction,
SponsorBlock fetch, JSON-over-stdio]
├── rustypipe [YouTube Innertube client + sig decode]
└── sponsorblock [REST API client, SHA-256 prefix lookup]
```
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.
## Status
M0 scaffold. Nothing playable yet — see [MILESTONES.md](MILESTONES.md).
## Upstream contributions
NewPipeExtractor (Java) is the canonical reference for Innertube behaviour.
When we hit a corner case RustyPipe doesn't cover we look at NPE for the
fix, then either port it to RustyPipe (Rust PR) or fix NPE directly (Java
PR). Either way upstream lands a real improvement.
Issues we're watching:
- [NPE #1339](https://github.com/TeamNewPipe/NewPipeExtractor/issues/1339) — n-parameter deobfuscation
- [NPE #1444](https://github.com/TeamNewPipe/NewPipeExtractor/issues/1444) — distinguish unavailable vs unextractable
- [NPE #1360](https://github.com/TeamNewPipe/NewPipeExtractor/issues/1360) — refactor link handlers (help wanted)
- [NPE #1357](https://github.com/TeamNewPipe/NewPipeExtractor/issues/1357) — JDoc checks in PR pipeline (good first issue)
## License
GPL-3.0-or-later. Matches RustyPipe and NewPipeExtractor.

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.torttube"
name="torttube"
version="0.0.1"
provider-name="Sulkta-Coop">
<requires>
<import addon="xbmc.python" version="3.0.0"/>
<import addon="script.module.requests" version="2.22.0"/>
<import addon="inputstream.adaptive" version="2.0.0" optional="true"/>
</requires>
<extension point="xbmc.python.pluginsource" library="main.py">
<provides>video</provides>
</extension>
<extension point="xbmc.addon.metadata">
<summary lang="en_gb">YouTube via RustyPipe + SponsorBlock</summary>
<description lang="en_gb">Browse, search, and play YouTube videos without an account. Backed by a native RustyPipe sidecar binary. SponsorBlock segments are skipped automatically.</description>
<license>GPL-3.0-or-later</license>
<source>http://192.168.0.5:3001/Sulkta-Coop/torttube</source>
<platform>linux</platform>
<language>en</language>
</extension>
</addon>

View file

@ -0,0 +1,24 @@
# torttube — Kodi YouTube addon
# SPDX-License-Identifier: GPL-3.0-or-later
"""M0 scaffold entry point. Sidecar handoff lands in M1+."""
import sys
import xbmcgui
import xbmcplugin
_HANDLE = int(sys.argv[1]) if len(sys.argv) > 1 else -1
def main() -> None:
xbmcgui.Dialog().notification(
"torttube",
"M0 scaffold — playback wires up in M3",
xbmcgui.NOTIFICATION_INFO,
2500,
)
xbmcplugin.endOfDirectory(_HANDLE)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,7 @@
# torttube English (en_GB) strings
# SPDX-License-Identifier: GPL-3.0-or-later
msgid ""
msgstr ""
"Project-Id-Version: torttube\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Language: en_gb\n"

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<settings version="1">
<section id="torttube">
<category id="general" label="General">
<group id="placeholder">
<!-- M2+ — SponsorBlock category toggles, sidecar path override, etc. -->
</group>
</category>
</section>
</settings>

15
sidecar/Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[workspace]
resolver = "2"
members = ["crates/torttube-sidecar"]
[workspace.package]
version = "0.0.1"
edition = "2021"
license = "GPL-3.0-or-later"
authors = ["Cobb <cobb@sulkta.com>"]
repository = "http://192.168.0.5:3001/Sulkta-Coop/torttube"
[profile.release]
lto = true
codegen-units = 1
strip = true

View file

@ -0,0 +1,14 @@
[package]
name = "torttube-sidecar"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
[[bin]]
name = "torttube-sidecar"
path = "src/main.rs"
[dependencies]
# M1 — rustypipe (codeberg.org/ThetaDev/rustypipe), tokio, serde, serde_json, reqwest

View file

@ -0,0 +1,9 @@
// torttube-sidecar — JSON-over-stdio bridge between Kodi (Python) and YouTube extraction.
// SPDX-License-Identifier: GPL-3.0-or-later
//
// Reads one JSON request per line from stdin, writes one JSON response per line to stdout.
// M0 scaffold — handlers land in M1+ (resolve, sponsorblock, search, channel, playlist).
fn main() {
eprintln!("torttube-sidecar M0 scaffold — see MILESTONES.md");
}