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

12 KiB
Raw Blame History

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 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.toStreamURLfile:// 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. 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 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.tshealthy | 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 (SSH→mpv on DRM console), transmission (search needs FlareSolverr), display.
  • recommender/ (Python). Title→metadata 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; the staged build order is in roadmap.md.