# 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 `. 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](./data-model.md#planned-fleetmesh-data-model); the staged build order is in [roadmap.md](./roadmap.md).