18 KiB
Status & Roadmap
Where the project is and the de-risked path to the full vision. Architecture is in
architecture.md; the mesh design origin is
../.project/history/20260608_fleet-manager-mesh-design.md.
Status at a glance
| Area | Status | Note |
|---|---|---|
| App — playback (VLC / mpv / QuickTime) | ✅ Shipped | blacktv retired → mpv; no NFS — local players play downloads, else route to black |
| App — library browser | ✅ Shipped | black index fast-path + local MEDIA_ROOTS walk + registry fallback (no NFS) |
| App — downloads (search + transmission) | ✅ Shipped | via mcp CLI; search needs FlareSolverr |
| App — metadata enrichment + artwork | ✅ Shipped | regex parse + TMDB/IMDb/keyless + ffmpeg frame-grab |
| App — all UI (Home/Player/Library/Search/Downloads/Metadata/Adult/Devices/Logs/Settings) | ✅ Shipped | wired, no TODO/FIXME/fatalError debt |
| App — device registry (Devices tab) | ✅ Shipped (registry only) | DeviceConfig: type→services presets mapping to fleet classes, per-device load badge; duty engine still unbuilt (see fleet below) |
| App — adult content tab | ✅ Shipped | porn-rotation.py ported to PornCollectionService; ENABLE_ADULT compile flag + runtime pornFeature setting, concealed by default |
| App — VPN subsystem | ✅ Shipped | OVPN profile/credential stores (Keychain), controller, settings UI |
| App — offline cache + Now Playing/media keys | ✅ Shipped | OfflineCacheController, NowPlayingController, bandwidth policy |
iOS app (TVAnarchyiOS) |
✅ Shipped (companion) | VLCKit player, library, downloads, remote control via HTTP bridge (default :8787); the bridge server is not in this repo's mcp/ tree |
| Distribution (release/update) | ✅ Shipped (macOS) / 🟡 scaffolded (rest) | tools/release.sh → Forgejo; tools/update.sh resolves per-OS asset+dest (mac/linux/windows/android/iOS) via the single-source tools/platform.sh. Only the macOS asset can exist until the client is cross-platform (see north star) |
| Universal client (one app, all devices) | ❌ Designed | the spine: blackd → thin web client → wrap (Tauri/Capacitor) → retire Swift UIs; adult becomes a backend-entitled module. See "North star" |
governor (portable-net-tv) |
✅ Shipped (single-host) | watch tracking + prefetch buffer |
mcp (tv-anarchy-mcp) |
✅ Shipped | VLC / black-tv / transmission / display tools |
recommender |
✅ Shipped | enrichment + local recs |
| MLX title refiner | ✅ Shipped | LocalLLMTitleRefiner → media_rec/title_refiner.py (MLX Qwen); cached, self-disabling, wired at app startup |
governor → fleet orchestrator (stage 1) |
✅ Shipped | governor/src/fleet/: registry ingest, duty assignment, custody floor-check, zombie reaper, re-pin actuation (fleet repin --apply: rsync holder→target + recorded holdings), capacity probes (fleet probe: ssh df + EWMA uptime), periodic daemon (fleet daemon), research feed into mcp search |
peers_for / custodians_of (stage 3 core) |
✅ Shipped (single-fleet) | source model + both policy gates enforced; unions fleet ∪ seedbox ∪ live DHT, provenance-tagged; served over HTTP by fleet serve (/peers_for/<hash>, bearer-token) — runnable on any node until a real broadcast host exists |
| Fleet WireGuard fabric (plane 1) | ❌ Designed | blocked on the 10.9.0.4 open question + root on each node |
| Seedbox source + off-home face | ❌ Blocked external | engine supports the class/duty today; needs an actual provisioned seedbox |
| Friend-mesh / F2F relay (stage 4) | ❌ Designed | needs other fleets to exist |
| Private-tracker source (stage 5) | ❌ Designed | gates already enforced in peers.ts; ships last, after F2F is battle-tested |
| Discord planes | ❌ Designed | control/QA/announce + availability bot; needs bot tokens/servers |
| Multi-identity / cross-fleet | ❌ Designed | single-fleet foundation must land first |
Verdict: the media-client layer is complete and production-grade. The
fleet/mesh layer now has its single-fleet core (registry → duties → custody →
reaper → peers_for) implemented and tested in the governor; what remains is
actuation (cross-host copies), the WG fabric, and every stage that requires
infrastructure outside this repo (seedbox, friends, Discord).
North star — one client, every device
The goal: one app we install on every device — iOS, Android, macOS, Ubuntu, Bluefin, Windows — controlling the display endpoints (Roku, smart TVs) it can't run on, with the adult feature as an opt-in package on entitled devices only.
Why today's app can't be that. It is Swift + SwiftUI/AppKit/UIKit + VLCKit —
structurally Apple-only (29 UI files; the core leans on Observation/AppKit/
MediaPlayer). It reaches only 2 of the 6 install targets. The per-platform
branches in tools/update.sh (linux/windows/android) are honest scaffolding for
release assets a Swift+SwiftUI build cannot produce. No amount of packaging
fixes that; the client technology is the constraint.
The unlock is already on the roadmap. ~20% of TVAnarchyCore exists only to
reach black over ssh/Process, and most of the rest fronts state that truly
lives on black (the index, the watchlog, transmission, porn-rotation). Once
blackd (below) exposes those as HTTP, the client keeps almost no logic — it
becomes browse + control + a <video>. A thin client is portable; a fat one
isn't. So the dependency order is: server-side consolidation first, universal
client second. They're one program, not two.
Client decision (the one real fork). To be genuinely one codebase across all six:
- Recommended — web/PWA, wrapped: one TypeScript app; Tauri for
macOS/Windows/Linux installs, Capacitor for iOS/Android, raw PWA in any
browser (covers Bluefin and anything else for free). Talks only HTTP+WS to
blackd. Reuses the workspace's TS toolchain (governor/mcp). Roku/TVs are controlled, not install sites. Cost: the Swift macOS/iOS UIs become legacy and are retired at parity — a real rewrite, paid down by the logic moving server-side where it belongs. Tradeoff to weigh: native VLCKit offline playback is best-in-class today; a PWA's offline story (service worker- Capacitor filesystem on mobile) is good but not equal — this is the one capability the pivot risks.
- Pragmatic alternative: keep Swift for Apple (where it's excellent), ship the web client only for the non-Apple platforms. Two clients, not one — satisfies reach but not the literal "one app" goal.
Adult as a package (honoring "some include it"). In the web-client world the
adult feature becomes a lazily-loaded module gated by a per-device entitlement
the backend issues — clients are byte-identical everywhere; black serves the
adult module and adult library rows only to entitled devices. That's strictly
stronger than today's client compile flag (an un-entitled install never holds
adult data to leak). The existing ENABLE_ADULT compile-strip survives as a
store-safe variant for the one case that needs zero adult code present
(an app-store submission) — cut from the same source via the asset-suffix
dimension the release pipeline already supports.
Sequence (each phase ships): 1) blackd — HTTP/WS service plane on black
(player, index, /media, verbs); 2) thin web client against it
(browse + control + player) reaching parity with the Swift app feature-by-
feature; 3) wrap (Tauri/Capacitor) + entitlement-gated adult module;
4) retire the Swift UIs and the plum iOS bridge. The /media plane in (1)
is the same one the Roku channel needs — bought once.
Open decision for the operator: commit to the web-thin-client rewrite (true one-app, accept the offline-playback tradeoff), or hold Swift-for-Apple + web-for-the-rest (reach without a single codebase). Everything before the client rewrite —
blackd, the/mediaplane — is correct either way, so the fork can be deferred until phase 2 without stalling phase 1.
Repo-state note
The PlumTV → TVAnarchy rename and the helper subsystems (governor/, mcp/,
recommender/, search/, fleet/, tools/) were committed to main on
2026-06-09 as a series of atomic commits (41afc1c…b44b5a2). Stage 0
(hygiene) below is done.
Remaining work — detail
MLX title refiner — ✅ done 2026-06-09
LocalLLMTitleRefiner (Swift) shells into media_rec/title_refiner.py (MLX
Qwen 1.5B, same model as the show grouper), consulted only for degenerate
(<2-char) regex titles. Results are disk-cached
(~/.local/state/tv-anarchy/title-refinements.json); two consecutive
subprocess failures disable it for the session so a scan never pays repeated
timeouts when MLX is absent. Wired at app startup. (The seam itself had a
wiring bug — the raw-name fallback ran before the refiner check, making it
unreachable — fixed in the same change.)
governor → fleet orchestrator — ✅ engine + read-only actuation done 2026-06-09
governor/src/fleet/ implements stage 1: registry ingest (the app-side
fleet.json array is authoritative, devices.json the fallback), deterministic
duty assignment with the spec's invariants, the N-copy custody floor-check with
rolling-baton custodianship + re-pin planning, and the zombie reaper
(healthy | stalled | dead, mesh-first recovery, public re-search fallback).
CLI: portable-net-tv fleet status|duties|custody|repin|reaper|peers|probe|daemon|serve
(all --json). reaper --apply performs only safe idempotent transmission
nudges (reannounce/verify).
Completed 2026-06-09 (later, parallel agent team): the actuation layer —
fleet repin --apply executes re-pin plans (rsync on the target pulling from
the holder, recorded holdings feed back into the floor-check), fleet probe
measures disk/uptime over ssh (EWMA score; feeds custody disk-eligibility),
fleet daemon runs the duties→floor→reaper tick periodically for launchd, and
fleet serve exposes /registry /custody /reaper /peers_for/<hash> over HTTP
with optional bearer auth (the broadcast-host service, runnable on any node).
Research actions build real mcp search invocations (fleet repin/reaper
plans name them; execution behind apply flags). Remaining inside stage 1:
nothing — what's left is infrastructure (targets need ssh + mediaRoot in
fleet.json for repin to have somewhere to copy to) and the launchd plist
itself.
fleet WireGuard fabric (plane 1) — blocked on a decision
The spec promotes a scoped fleet WG mesh (tva0, fleet-subnet-only
AllowedIPs, collision-probe) to a foundational stage, but its design is
explicitly blocked on the open question of what 10.9.0.4 is today (general
overlay vs ad-hoc tv-anarchy overlay), and bring-up needs root on every node.
Decide, then build.
stages 2/4/5 + Discord — blocked on external infrastructure
The engine already models seedboxes (class, duty preference, source defaults) and enforces the private-tracker gates; what doesn't exist is the infrastructure: a provisioned seedbox, friends' fleets for F2F, private-tracker credentials, Discord bots/servers. None of this is code in this repo until those exist.
blackd — black as a real service (no ssh on the control path) — planned milestone
This is phase 1 of the universal client (see north star): it's the
prerequisite that thins the client enough to be portable, not merely an ssh
cleanup. ssh-as-transport is the app's load-bearing design debt: player control is a
fresh ssh → sudo socat → /tmp/mpv.sock pipeline per command (MpvTarget),
the library index is ssh cat, launch/stats/releases/restart are ssh-invoked
black-tv verbs (whose deploy drift required the helper_sha badge), and the
offline cache rsyncs over ssh. Consequences: every keyless client needs a proxy
(the iOS app exists only via the plum :8787 bridge; a Roku/web client can't
ssh at all), every capable client reimplements ssh plumbing, and the sudo socat hack exists purely because the far socket is root-owned. The
counter-examples are already in the stack: transmission (HTTP RPC :9091) and
the Roku's ECP are the robust integrations.
Design: one daemon on black — HTTP on LAN + WG overlay, bearer token:
/player/*+ WebSocket events — local unix-socket access to mpv (root problem dissolves; no per-command process spawn)/library/index— replacesssh cat/media/<path>(HTTP range now, on-demand HLS remux later) — the SAME media plane the Roku channel / web client milestone needs; one investment/launch,/stats,/releases,/restart— theblack-tvverbs/version— replaces helper_sha drift detection
Migration (each phase shippable): 1) daemon wraps the existing pieces
(systemd unit; deploy via the existing helper mechanism); 2) BlackdTarget
(PlayerTarget/MediaLaunchable over HTTP, like VLC's) preferred with ssh
fallback; 3) retire ssh paths + the plum iOS bridge (iOS talks to black
directly). ssh remains for deploys/admin only.
Roku dev channel — planned milestone
The living-room display has a Roku Streaming Stick 4K (10.0.0.233, ECP)
alongside black's HDMI input. ECP transport control is done (HostKind
.roku / RokuTarget — pause/jump-back/exit/now-playing from the Devices
tab). The channel itself — a couch-native TVAnarchy UI on the Roku — needs:
- HTTP media plane on black — Roku only plays HTTP(S) streams; today the media is NFS/ssh. A file server plus on-demand ffmpeg remux/transcode→HLS for the incompatible tail (mkv with FLAC/Opus audio, PGS subs). This is the heavy piece and is useful beyond Roku (any future web/TV client).
- BrightScript/SceneGraph client — library browse (black index over HTTP)
- Video node playback. New codebase, no Swift reuse.
- Release pipeline —
TVAnarchy-<tag>-roku.zipasset + a push-deploy step (dev channels are uploaded TO the stick:curl --digest -u rokudev:<pass> -F archive=@channel.zip http://10.0.0.233/plugin_install). One dev-channel slot, persists indefinitely; dev mode enable = ECP keypress sequence.
Store publishing is out: Roku killed non-certified channels (2024) and
certification would reject the torrent surfaces regardless of ENABLE_ADULT.
Sideload-only, which the forge model already fits.
Build order (de-risked; each stage independently shippable)
From the design spec. Each stage ships on its own; later stages depend on earlier.
Hygiene (prerequisite)— ✅ done 2026-06-09: thePlumTV → TVAnarchyrename and the helper subsystems are committed onmain.Host registry + duty assignment, single fleet— ✅ done 2026-06-09 (engine): app-side registry (Devices tab,fleet.json) + governor-side duty engine, custody floor-check, and reaper (governor/src/fleet/). Remaining inside stage 1: re-pin actuation (cross-host copy execution) and runningfleet duties/reaperunder launchd instead of on demand.- Seedbox source +
public_swarm_faceduty. Adds an always-on custodian; proves the source model on the zero-risk source first. ("Add a seedbox" is the single most important onboarding step for non-power-users — most multi-device fleets have zero always-on nodes otherwise.) — ✅ core done 2026-06-09:peers_for()over local sourcesfleet peers <infohash|title>unions fleet holders ∪ seedbox ∪ live DHT peers, provenance-tagged, with both private-tracker policy gates enforced (search_onlydefault-closed;f2f_onlyforced un-overridably). Remaining: a broadcast host to servepeers_forto other devices (today it's a local CLI query).- Friend-mesh source + F2F relay. Federates; the custody floor goes cross-fleet. Multi-identity lands here.
- Private-tracker source, default-closed. LAST and highest-consequence:
swarm_isolationforced tof2f_only, un-overridable;contentshare is an account-killer for the source user. Ship only once F2F isolation is battle-tested on safe sources.
Parallel, any time: MLX — ✅ done 2026-06-09.
After a working mesh: the Discord planes — control/ops + QA/community +
release announcements (project home server) and content/availability (private meshTitleRefiner
- a bot inside users' own servers, never the public home server). Discord is a surface you notify, not a backbone you route through: custody/reaper/F2F run over WireGuard regardless, and a Discord ban must not take the mesh down.
Non-goals / explicitly deferred (not defects)
BlackTVTargetremoved. Intentional — auto-migrated at runtime toMpvTargetwithCommandsConfig.blackTVDefaults(bin:). Black-TV control still works.- No ratio enforcement. Resolved decision (2026-06-08): ship gift-economy,
build the
custody_capacityknob, leave it off. First lever is visibility, not enforcement; hard ratio is a per-group last resort. Ratio anxiety is the pain being sold against — "minus the ratio police." - No fingerprint-stripping on private shares. Resolved decision: warn hard,
strip never by default. Stripping re-encodes and destroys the quality that
makes a private release valuable; the friend is protected structurally by
f2f_only, the source by the explicit warning. Strip is a per-release opt-in only, never auto. - App doesn't link
governoror call the MCP server directly. By design — it shells out to themcpCLI andrecommenderas subprocesses;governorruns independently under launchd. - Black-TV watch state not on plum. Intentional — black-local, read over SSH, never written across NFS.
- Watch parties (if built) = synchronized local playback; Discord carries only voice + timestamps, never streamed copyrighted video.