7.1 KiB
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
mcpCLI bridge and the Pythonrecommender. The app spawns them under a login shell sobun/uvresolve onPATH.
┌────────────────────────────────────────────┐
│ 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.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%CPUfor the player chart.QuickTimeTarget— local, zero-install, via AppleScript (local files only).HostKind.blacktvis retired: it is auto-migrated at runtime inPlayerController.makeTarget()into anMpvTargetseeded withCommandsConfig.blackTVDefaults(bin:). Black-TV control still works, via mpv.
PlayerController
@MainActor/Observable. Owns target CRUD (persisted to hosts.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— fast path parses black's prebuilt index (one SSHcat); fallback walks the~/mediaNFS mount (with autofs-readiness retry); last resort isRegistryIngest(titles fromregistry.md, episode-less, offline).LibraryIndex— fetches/rebuilds black'sindex.tsvwithnice/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.
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). An MLX TitleRefiner seam exists
but is unwired (see roadmap.md).
Persistence locations
See operations.md for the full table.
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. In the mesh design this same bandwidth-arbitration brain is the intended fleet orchestrator (not yet built).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. Planned mesh layer (designed, unbuilt)
The fleet/mesh turns the single-host client into "a private tracker made of your friends." 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.