tv-anarchy/docs/architecture.md
Natalie a86e68c525 feat(apps): add fleet engine mesh core integration
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-09 21:23:36 -07:00

211 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Architecture
Two layers exist: a **shipped media-client layer** (the app + helper subprocesses)
and a **planned mesh layer** (the fleet/self-healing torrent system). Only the
first is implemented; see [roadmap.md](./roadmap.md) for status.
## 1. Design principle: two planes
The app is deliberately split so that no heavy runtime dependency lands in the
native binary:
- **Control plane — native Swift.** Transport (play/pause/seek/volume), target
selection, and UI. Talks to players directly: HTTP to VLC's Lua interface,
JSON IPC over SSH to mpv on black, AppleScript to local QuickTime. Zero new
runtime deps.
- **Data plane — helper subprocesses.** Anything heavy (torrent search,
transmission RPC, metadata enrichment) is shelled out to existing, tested
projects: the `mcp` CLI bridge and the Python `recommender`. The app spawns
them under a login shell so `bun`/`uv` resolve on `PATH`.
```
┌────────────────────────────────────────────┐
│ TV Anarchy.app (Swift) │
│ Sources/TVAnarchy (SwiftUI views) │
│ Sources/TVAnarchyCore (logic, no UI) │
└───────┬───────────────┬──────────────┬──────┘
control plane (direct) │ │ data plane (subprocess)
┌────────────────────────┼───────────┐ │
▼ ▼ ▼ ▼
VLC HTTP/Lua mpv JSON-IPC QuickTime mcp CLI ──▶ transmission RPC
(127.0.0.1:8080) over SSH (AppleScript) (bun) torrent search
to black recommender ──▶ TMDB/IMDb/
(mpv → DRM) (uv/python) TVmaze/AniList
background, independent: governor (portable-net-tv) — launchd daemon on plum
```
## 2. Shipped app (`Sources/TVAnarchyCore`, `Sources/TVAnarchy`)
### Playback targets — the `PlayerTarget` protocol
A single protocol (poll / playPause / resume / setVolume / seek / next / previous
/ stop) with capability sub-protocols (`MediaLaunchable`, `Enqueueable`,
`TrackSelectable`, `QualitySwitchable`, `HostStatsProvider`). Implementations:
- **`VLCTarget`** — HTTP/Lua to plum's VLC. Volume normalized (256 → 100%), track
enumeration + language-preference application, playlist control. Plays only files
downloaded locally (`MediaPaths.toStreamURL``file://` of an offline-cache copy)
— no NFS, and VLC's sftp access is broken on macOS. A not-downloaded item is
routed to black by `PlayerController.routedTarget(for:)` for that one play.
- **`MpvTarget`** — generic mpv over SSH JSON-IPC with request-id batching;
launch/library/stats/teardown are *delegated* to per-host command templates
(`CommandsConfig`). Reports decode `%CPU` for the player chart.
- **`QuickTimeTarget`** — local, zero-install, via AppleScript; like VLC it plays
only locally-downloaded copies (else the item routes to black).
- **`HostKind.blacktv`** is retired: it is auto-migrated at runtime in
`PlayerController.makeTarget()` into an `MpvTarget` seeded with
`CommandsConfig.blackTVDefaults(bin:)`. Black-TV control still works, via mpv.
### `PlayerController`
`@MainActor`/Observable. Owns target CRUD (persisted to `devices.json`), a
single-flight poll loop (cadence varies by tab visibility), optimistic command
routing, quality/release switching (refetch only on episode boundary), audio/sub
track selection (persisted per series), a sleep-timer state machine, a transfer
queue, and throttled status-cache persistence.
### Library pipeline (`Library/`)
Cached-first: load a snapshot instantly, refresh in the background, persist.
- **`LibraryScanner`** — primary path parses black's prebuilt index (one SSH `cat`,
black-side paths kept canonical, no NFS); fallback walks a structured local
`MEDIA_ROOTS` dir if configured; last resort is `RegistryIngest` (titles from
`registry.md`, episode-less, offline).
- **`DownloadsIndex`** — filename index of `OfflineCacheController`'s download dir,
so a downloaded episode plays on the local player (VLC/QuickTime) while anything
else routes to black. Refreshed on library refresh + after a cache run.
- **`LibraryIndex`** — fetches/rebuilds black's `index.tsv` with `nice`/`ionice`
(black seeds 200+ torrents); reports determinate progress.
- **`WatchHistory`** — unions the plum watch log
(`~/.local/state/plum-control-mcp/watched.jsonl`) with VLC recents (macOS plist)
to drive the Continue-Watching rail and resume positions.
- **`LibraryController`** — Home rails (Continue / Recently-Added / per-category),
franchise prefix-matching, scan orchestration, launch-request building.
- **`ShowGrouping` / `LocalLLMGrouper` / `ContentID`** — groups episodes into
shows (with an optional local-LLM pass for messy names) keyed by stable
content IDs; **`LibraryConfig`** makes the folder→type mapping user-editable
(types can be added/renamed/removed in Setup; each carries an `adult` flag).
### Downloads pipeline (`Torrents/`)
`TorrentService` shells to the `mcp` CLI for search + transmission RPC;
`DownloadsController` runs an adaptive-cadence transmission dashboard and fires a
completion callback so finished folders get incrementally indexed.
### Metadata pipeline (`Metadata/`)
`FilenameParser` (regex: title/year/SxxEyy/quality/codec/source) →
`EnrichService` (subprocess to `recommender`, provider routed by category) →
`MetaWriter` (path-digest `.meta` sidecars, best-effort black mirror) and
`ArtworkService` (ffmpeg frame-grab fallback). Degenerate (<2-char) regex
titles are refined by `LocalLLMTitleRefiner` a subprocess to
`media_rec/title_refiner.py` (local MLX Qwen, same model as the show grouper),
disk-cached per filename and self-disabling after consecutive failures so a
scan never blocks on a missing model. Wired at app startup.
### Device registry (Devices tab, `DeviceConfig`)
Playback targets generalized into **devices**: each entry in `devices.json` has a
user-facing `type` (cellphone / laptop / storage / seedbox / broadcast-station)
that maps onto the planned fleet host classes (consumer / roamer / server /
seedbox / broadcast) and presets an overridable `services` set (stream,
offline-cache, TTL-seed, custody, public-swarm-face, F2F-relay, mesh-anchor).
The Devices list shows a per-device system-load badge (low/med/high, via
`HostStats`). This is the **app-side registry** for mesh stage 1; the duty
*assignment engine* (governor-side) is still unbuilt see
[roadmap.md](./roadmap.md). Legacy `hosts.json` entries are auto-migrated with
inferred types.
### Adult content (`PornCollectionService`, `AdultView`)
The `porn-rotation.py` collection logic ported native. Doubly gated: the
`ENABLE_ADULT` compile flag and the runtime `pornFeature` setting (off by
default the app ships with adult content concealed; a discreet sidebar toggle
reveals the Adult tab). Which library types count as adult is data, not
hardcoded: `LibraryConfig` types carry an `adult` flag (the default `porn` type
has it).
### VPN subsystem (`VPN/`)
OVPN profile parsing + store (`OVPNProfile`, `VPNConfigStore`), credentials in
the Keychain (`VPNCredentialStore`, via Security.framework), and a
`VPNController` with a settings UI so a roaming device can reach the overlay
without hand-managed tunnels.
### Settings (`SettingsStore`)
Small tolerant-decode `settings.json`: adult gating (`pornFeature`,
`surfaceAdultOnHome`, `switchToAdultOnlyHome`), hover previews, media-key
forwarding (`NowPlayingController`), and offline-cache sizing
(`offlineEpisodes`/`offlineShows`).
### Persistence locations
See [operations.md](./operations.md#config--state-locations) for the full table.
## 2b. iOS companion app (`Sources/TVAnarchyiOS`)
A separate iOS target (with `Tests/TVAnarchyiOSUITests`): VLCKit-based player
(`VLCPlayerModel`, `PlayerScreen`), library browsing, on-device downloads
(`DownloadManager`/`DownloadsView`), and a remote-control surface
(`RemoteView`). It talks HTTP to a bridge (`BridgeClient`/`BridgeSettings`,
default port `8787`, optional token) rather than SSH'ing anywhere itself. The
bridge **server** is not part of this repo's `mcp/` tree it lives with
`plum-control-mcp`'s deployment on plum.
## 3. Helper subprocesses
- **`governor/` (`portable-net-tv`, TS/Bun).** A standalone launchd daemon on
plum: follows VLC playback, appends to the shared watch log, prefetches the
next *N* episodes within a bandwidth budget, and GCs the buffer. The app does
**not** invoke it; it runs on its own. It also hosts the **fleet engine**
(`src/fleet/`) the implemented single-fleet core of the mesh design:
- `registry.ts` joins the app-side fleet registry (`fleet.json`,
authoritative when present) / `devices.json` (fallback) into `FleetHost`
records;
- `duties.ts` deterministic duty assignment (broadcast / f2f_relay /
public_swarm_face) with the spec invariants (a consumer never gets a duty;
home-IP exposure warned);
- `custody.ts` N-copy floor-check, rolling-baton custodianship,
`custodians_of`, re-pin planning;
- `reaper.ts` `healthy | stalled | dead` classification + mesh-first
recovery planning;
- `peers.ts` the source model with both private-tracker policy gates and
`peers_for` (fleet seedbox live DHT, provenance-tagged);
- `transmission.ts` ssh-tunneled JSON-RPC view of black's daemon.
CLI: `portable-net-tv fleet status|duties|custody|reaper [--apply]|peers <q>`.
Read-only by default; `reaper --apply` runs only idempotent
reannounce/verify nudges. Re-pins and re-sourcing are printed plans
cross-host actuation is the remaining stage-1 work.
- **`mcp/` (`plum-control-mcp`, TS/Bun).** An MCP stdio server *and* a CLI bridge.
The app uses the CLI (`TorrentService`, `EnrichService`); MCP clients (Claude)
use the server. Domains: VLC, black-tv (SSHmpv on DRM console), transmission
(search needs FlareSolverr), display.
- **`recommender/` (Python).** Titlemetadata resolution
(TMDB/IMDb/TVmaze/AniList, routed by category) and local recommendations
(`recommend_local.py`, keyless). Invoked only during indexing/enrichment.
## 4. Mesh layer (single-fleet core implemented; federation designed)
The fleet/mesh turns the single-host client into "a private tracker made of your
friends." The single-fleet core (registry duties custody floor reaper
`peers_for`) is implemented in the governor's fleet engine 3); everything
cross-fleet below F2F relay, friend sources, Discord planes remains design.
Two graphs ride one Discord identity layer:
- **Custody graph** narrow, trust-bounded (1° friends + always-on nodes). Holds
the seeder floor; the zombie-prevention guarantee lives here.
- **Discovery/signal graph** wide, six-degrees, anonymized + per-fleet-deduped.
Carries popularity signal and relays friend-to-friend (F2F) requests.
The unit is the **fleet** (one identity, typed devices), not a person or device
so the system works for a single user with zero friends and the mesh is the same
code with more identities. A **broadcast** node (seedbox/vps-0) anchors F2F
rendezvous, holds the aggregated peer registry, runs the Discord bridge, and is
optionally the only node touching public swarms (keeping home connections dark).
The **governor** (generalized) assigns duties and enforces the custody floor /
zombie reaper. Full entities, duty-assignment rules, and the peer-source policy
are in [data-model.md](./data-model.md#planned-fleetmesh-data-model); the staged
build order is in [roadmap.md](./roadmap.md).