diff --git a/.project/history/20260608_gap-analysis.md b/.project/history/20260608_gap-analysis.md deleted file mode 100644 index c52769d..0000000 --- a/.project/history/20260608_gap-analysis.md +++ /dev/null @@ -1,208 +0,0 @@ -# tv-anarchy — Gap Analysis (built vs. intended) - -**Date:** 2026-06-08 -**Scope:** whole vision — the native app, its helper subprocesses (`governor`, -`mcp`, `recommender`), and the self-healing torrent **fleet/mesh** described in -[`20260608_fleet-manager-mesh-design.md`](./20260608_fleet-manager-mesh-design.md). - ---- - -## 1. Summary - -The **playback/library app layer is essentially complete and production-grade.** -The native macOS app drives multiple playback targets, browses a cached library, -searches and manages torrents, and enriches metadata — all wired to real UI with -no TODO/FIXME/`fatalError` debt. Its helper subprocesses (`governor`, `mcp`, -`recommender`) are working components. - -The **fleet/mesh layer is the gap.** The single largest, most ambitious part of -the design — a self-healing, zombie-resistant torrent mesh with custody floors, -duty assignment, and friend-to-friend federation — exists as a complete 253-line -design spec and a one-paragraph `fleet/README.md`, with **zero implementation.** -Three smaller gaps sit alongside it: the `governor` daemon has not been -generalized into the fleet orchestrator the spec calls for, the MLX title-refiner -is a defined-but-unwired seam, and the Discord notification planes are unbuilt. - ---- - -## 2. Method - -Read on 2026-06-08 against the on-disk state of the main checkout -(`/Users/natalie/Code/@applications/tv-anarchy`): - -- the design spec `.project/history/20260608_fleet-manager-mesh-design.md` (253 lines); -- the top-level `README.md` and each subsystem README (`governor/`, `mcp/`, `fleet/`, `recommender/registry.md`); -- the Swift sources under `Sources/TVAnarchy` and `Sources/TVAnarchyCore`; -- the `governor/` (TS/Bun), `mcp/` (TS/Bun), and `recommender/` (Python) trees. - -**Repo-state caveat (verified, not cosmetic).** The committed `main` tree still -tracks only `Sources` (under the old `PlumTV`/`PlumTVCore` names), `Tests`, -`README.md`, and `project.yml`. The working tree is mid-reorg: it has *deleted* -the `PlumTV*` files and *added*, untracked, the renamed `TVAnarchy*` sources plus -the entire `governor/`, `mcp/`, `recommender/`, `fleet/`, and `tools/` trees. -So the analysis below describes **what exists on disk today**; most of the helper -subsystems and the rename are not yet committed. Committing the reorg is itself a -prerequisite hygiene step, not a feature gap. - ---- - -## 3. Built vs. intended — at a glance - -| Subsystem | Intended role | Status | Gap | -|---|---|---|---| -| **App — playback** (`Sources/TVAnarchyCore/*Target.swift`, `PlayerController`) | Pick a target (VLC / mpv-on-black / QuickTime), drive transport | ✅ Built | — | -| **App — library** (`Library/`) | Cached browser over black's index + live NFS scan | ✅ Built | — | -| **App — downloads** (`Torrents/`) | Search + transmission dashboard | ✅ Built | — | -| **App — metadata** (`Metadata/`) | Filename parse → TMDB/IMDb/keyless enrich + artwork | ✅ Built | MLX refiner seam unwired (§4.3) | -| **App — UI** (`Sources/TVAnarchy/*View.swift`) | Home/Player/Library/Search/Downloads/Metadata/Hosts/Logs | ✅ Built | — | -| **governor/** | Today: bandwidth governor + watch/prefetch daemon. Spec: generalize into fleet orchestrator | ⚠️ Partial | Not generalized to duty-assign / reaper / replication-floor (§4.2) | -| **mcp/** | plum-control-mcp: VLC / black-tv / transmission / display tools | ✅ Built | — | -| **recommender/** | TMDB/IMDb/TVmaze/AniList enrichment + local recs | ✅ Built | — | -| **fleet/** | Host registry, duty assignment, `peers_for()`, `custodians_of()`, custody floor, F2F mesh, private-tracker source | ❌ Design-only | Entire subsystem (§4.1) | -| **Discord planes** | Control/ops + QA/community + release announce; content/availability bot | ❌ Design-only | All planes (§4.4) | -| **Multi-identity / cross-fleet** | Single-fleet foundation generalizes to N identities | ❌ Design-only | Nothing multi-identity (§4.5) | - ---- - -## 4. Detailed gaps - -### 4.1 `fleet/` — the entire mesh subsystem is unbuilt - -**Specced.** The design doc defines a complete system: a host registry -(`Host { class, reachable, always_on, api, capacity, duties[] }`), deterministic -duty assignment (`custody_floor` | `public_swarm_face` | `f2f_relay` | `broadcast`), -a peer-source model (`Source { kind, share_policy, swarm_isolation }`), and two -derived outputs — `peers_for(infohash) → Peer[]` (a holistic meta-tracker) and -`custodians_of(title) → Host[]` (who is obligated to keep a title alive). Zombie -prevention rests on a **custody floor**: watch = auto-seed-with-TTL, an -N-copy replication floor, an always-on backstop per floor, and rolling-baton -custodianship. The spec sequences this into five de-risked stages (see §5). - -**Today.** `fleet/` contains only `README.md`, which says verbatim: *"Not yet -implemented. Build order starts at stage 1 (host registry + duty assignment, -single fleet) per the spec."* No registry, no duty logic, no `peers_for`/ -`custodians_of`, no source model, no mesh transport. - -**Delta.** 100% of the fleet/mesh subsystem. This is the project's defining -feature ("a private tracker made of your friends") and none of it exists in code. - -**Consequence.** The product's differentiator is entirely aspirational. Until at -least stage 1 ships, tv-anarchy is a polished single-host media client, not the -mesh it is pitched as. - -### 4.2 `governor` not generalized into the fleet orchestrator - -**Specced.** The spec wants the governor's bandwidth-arbitration brain extended to -be the mesh orchestrator: run duty-assignment rules deterministically on registry -changes, enforce the replication floor (re-pin when copies drop to N−1 before the -last vanishes), and run a **zombie reaper** that classifies every torrent -`healthy | stalled | dead` and, for dead-but-wanted titles, recovers from the mesh -first and falls back to public re-search. - -**Today.** `governor/` (TS/Bun, ~2.3k LOC: `governor.ts`, `watch.ts`, `bandwidth.ts`, -`jobs.ts`, `keeper.ts`, …) is a working **single-host** service: it follows VLC -playback, maintains the cross-show watch log, prefetches next-N episodes within a -bandwidth budget, and GCs the buffer. None of the orchestration duties exist. - -**Delta.** Duty assignment, replication-floor enforcement, and the zombie reaper. -The bandwidth-budgeting and watch-log foundations are reusable; the orchestration -layer on top is missing. - -**Consequence.** Even once `fleet/` defines the registry, nothing *acts* on it. -The governor is the natural home for that actuation and is the gating component -between a static registry and a self-healing mesh. - -### 4.3 MLX title-refiner — defined seam, no implementation - -**Specced.** README lists a "local (MLX) filename→metadata pipeline" as later-phase -work; `FilenameParser.swift` builds the seam for it. - -**Today.** `Sources/TVAnarchyCore/Metadata/FilenameParser.swift` declares the -protocol `TitleRefiner` (line 87) and `public static var refiner: (any TitleRefiner)?` -(line 10), consulted only for the messy tail when regex yields a <2-char title -(line 31). **No type conforms to `TitleRefiner`, no MLX dependency exists, and -`refiner` is never assigned** — grep across `Sources/` finds only the declaration -and the call site. The pipeline is regex-only. - -**Delta.** The model-backed refiner implementation. The integration point is -clean and ready. - -**Consequence.** Low. Regex handles the vast majority of `SxxEyy` releases; -the refiner only matters for filenames regex can't crack. This is a deliberate -seam, not debt. - -### 4.4 Discord planes — unbuilt - -**Specced.** The spec separates Discord into distinct planes: control/ops + -QA/community + release announcements on a project-owned home server, and a -content/availability surface that **never** touches the public home server — -it rides the private mesh via a bot inside users' *own* servers. Principle: -*"Discord is a surface you notify, not a backbone you route through."* - -**Today.** None of it exists. No bot, no server config, no notify hooks. - -**Delta.** All Discord integration. - -**Consequence.** Deferred-appropriate. These planes are notification/community -surfaces layered on top of a working mesh; they cannot meaningfully precede §4.1. - -### 4.5 Multi-identity / cross-fleet - -**Specced.** The "fleet = the atom" model: one Discord identity, typed devices; -the system "works for one user with zero friends; the mesh is the same code with -more identities." Stage 4 federates across fleets via the friend-mesh + F2F relay. - -**Today.** Nothing multi-identity exists (it cannot, given §4.1). - -**Delta.** Everything beyond a single fleet. - -**Consequence.** Correctly last. The single-fleet foundation must exist and be -battle-tested before federation is meaningful. - ---- - -## 5. Sequenced path to close - -Following the spec's own de-risked stage order, with the cross-cutting tracks -called out: - -1. **Hygiene (prerequisite).** Commit the in-flight reorg: the `PlumTV → TVAnarchy` - rename and the untracked `governor/`, `mcp/`, `recommender/`, `fleet/`, `tools/` - trees. Until this lands, the helper subsystems are not in version control. -2. **Fleet stage 1 — host registry + duty assignment, single fleet** (black + apricot - + plum + phone). No mesh, no Discord. Unifies what is run by hand today. *(§4.1)* -3. **Governor generalization (parallel track to stages 1–3).** Wire duty-assignment, - the replication-floor check, and the zombie reaper onto the registry. *(§4.2)* -4. **Fleet stage 2 — seedbox source + `public_swarm_face` duty.** Adds an always-on - custodian; proves the source model on the zero-risk source first. -5. **Fleet stage 3 — `peers_for()` over local sources** (fleet + seedbox + DHT/public). - Meta-tracker, still single-fleet. -6. **Fleet stage 4 — friend-mesh source + F2F relay.** Federates; custody floor goes - cross-fleet. Multi-identity (§4.5) lands here. -7. **Fleet stage 5 — private-tracker source, default-closed.** LAST and - highest-consequence: `swarm_isolation` is **forced** to `f2f_only` and - un-overridable; `content` share is an account-killer, so it ships only once the - F2F isolation path is battle-tested on safe sources. - -**Parallel, any time:** the MLX `TitleRefiner` (§4.3) — independent, low-risk. -**After a working mesh:** the Discord planes (§4.4). - ---- - -## 6. Non-gaps / explicitly deferred (not defects) - -- **`BlackTVTarget` "missing".** Intentional — the class was retired; `HostKind.blacktv` - is auto-migrated at runtime in `PlayerController.makeTarget()` into an `MpvTarget` - with `CommandsConfig.blackTVDefaults(bin:)`. Black-TV control still works, via mpv. -- **Gift-economy, no ratio enforcement.** Resolved decision (spec, 2026-06-08): ship - gift-economy, build the ratio knob, leave it **off**. Absence of ratio policing is - by design — visibility/social pressure over mechanical bans. -- **No fingerprint-stripping on private shares.** Resolved decision: **warn hard, - strip never** (default). Stripping destroys encode quality; the friend is protected - structurally by `f2f_only` isolation, the source by an explicit warning at the - content-share flip. -- **App doesn't link `governor` / call the MCP server directly.** By design — the app - shells out to the `mcp` CLI bridge (`TorrentService`, `EnrichService`) and to - `recommender` as subprocesses; `governor` runs independently under launchd. -- **Black-TV watch state not on plum.** Intentional — persisted black-local and read - over SSH, never written across the NFS mount. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..9d3358b --- /dev/null +++ b/docs/README.md @@ -0,0 +1,36 @@ +# TV Anarchy — Documentation + +TV Anarchy is a native macOS (SwiftUI) front end for a home media stack centered +on **plum** (the laptop) and **black** (the media server). It unifies what was +previously CLI/MCP-only: pick a playback target, browse a cached library, search +and manage torrents, and enrich metadata — all from one app. A larger, +forward-looking layer (the self-healing torrent **fleet/mesh**) is designed but +not yet built. + +This directory is the project's living documentation. Component-level READMEs stay +next to their code; these documents are the cross-cutting reference. + +## Living docs + +| Doc | What it covers | +|---|---| +| [architecture.md](./architecture.md) | System shape — the shipped app, its helper subprocesses, and the planned mesh layer | +| [data-model.md](./data-model.md) | Config + state schemas in use today, and the planned fleet/mesh data model | +| [operations.md](./operations.md) | Build, install, run, configure, and deploy every component | +| [roadmap.md](./roadmap.md) | Status (built vs. designed) and the de-risked path to the full vision | +| [glossary.md](./glossary.md) | Domain terms (fleet, custody floor, duty, F2F relay, …) | + +## Component docs (next to the code) + +- [`../README.md`](../README.md) — the Swift app: targets, layout, build +- [`../governor/README.md`](../governor/README.md) — `portable-net-tv`: watch tracking + prefetch buffer +- [`../mcp/README.md`](../mcp/README.md) — `plum-control-mcp`: VLC / black-tv / transmission / display tools +- [`../fleet/README.md`](../fleet/README.md) — fleet/mesh placeholder (not yet implemented) +- [`../recommender/`](../recommender/) — Python enrichment + recommendations (`recommend_local.py`, `media_rec/`) + +## Design source of truth + +- [`../.project/history/20260608_fleet-manager-mesh-design.md`](../.project/history/20260608_fleet-manager-mesh-design.md) + — the captured design conversation for the fleet/mesh. The mesh sections of + [architecture.md](./architecture.md) and [data-model.md](./data-model.md) + synthesize it; that file remains the origin record. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..007add1 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,130 @@ +# 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. +- **`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 (local files only). +- **`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 `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 SSH `cat`); + fallback walks the `~/media` NFS mount (with autofs-readiness retry); last + resort is `RegistryIngest` (titles from `registry.md`, episode-less, offline). +- **`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. + +### 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](./roadmap.md)). + +### Persistence locations + +See [operations.md](./operations.md#config--state-locations) 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](./data-model.md#planned-fleetmesh-data-model); the staged +build order is in [roadmap.md](./roadmap.md). diff --git a/docs/data-model.md b/docs/data-model.md new file mode 100644 index 0000000..9d0d90d --- /dev/null +++ b/docs/data-model.md @@ -0,0 +1,192 @@ +# Data Model + +Two sets: the **config + state schemas in use today** (the app and helpers read +and write these), and the **planned fleet/mesh data model** (designed, unbuilt). + +--- + +## In use today + +### `hosts.json` — playback target config + +Path: `~/.config/tv-anarchy/hosts.json` (auto-migrated forward from the pre-rename +`~/.config/plumtv/hosts.json`). Source: `Sources/TVAnarchyCore/HostConfig.swift`. +Written pretty-printed + sorted-keys; seeded on first run with Plum VLC + Black +(mpv) if absent. + +```jsonc +{ + "hosts": [ + { + "id": "plum-vlc", + "name": "Plum VLC", + "kind": "vlc", // vlc | mpv-ipc | quicktime | blacktv(legacy) + "vlc": { "host": "127.0.0.1", "port": 8080 } + }, + { + "id": "black", + "name": "Black TV", + "kind": "mpv-ipc", + "mpv": { + "endpoints": ["lilith@10.0.0.11", "lilith@10.9.0.4"], // LAN first, WG overlay fallback + "socket": "/tmp/mpv.sock", // default + "sudo": true, // root-owned socket → sudo socat + "socat": "socat", // default + "volumeScale": 130 // mpv --volume-max + }, + "commands": { // argv templates for what IPC can't do + "launchShow": ["/usr/local/bin/black-tv","play-show","{query}","{season?}","{episode?}"], + "launchResume": ["/usr/local/bin/black-tv","resume-show","{query}"], + "launchFile": ["/usr/local/bin/black-tv","play","{path}"], + "releases": ["/usr/local/bin/black-tv","releases"], + "resolveRelease": ["/usr/local/bin/black-tv","resolve-release","{releaseId}"], + "stats": ["/usr/local/bin/black-tv","stats"], + "stop": ["/usr/local/bin/black-tv","stop"] + } + } + ] +} +``` + +Notes: +- `kind`: `vlc`, `mpv-ipc`, `quicktime`. `blacktv` is legacy and auto-migrated to + `mpv-ipc` at load; only `mpv-ipc`/`vlc`/`quicktime` are offered in the editor. +- Command-template tokens: `{query}`, `{season?}`, `{episode?}`, `{path}`, + `{releaseId}` (a `nil` template = capability absent). +- VLC password is **not** stored here — it's resolved at runtime from the + governor's config or `$VLC_HTTP_PASSWORD` (see `VLCConfig`). +- `MpvConn` decodes from a minimal `{ "endpoints": [...] }`; every other field + has a default. + +### `config.json` — governor (`portable-net-tv`) + +Path: `~/.config/portable-net-tv/config.json` (source: `governor/README.md`). + +```jsonc +{ + "vlcHttp": { "host": "127.0.0.1", "port": 8080, "password": "" }, + "buffer": { "dir": "/Users/natalie/Movies/net-tv-buffer", "ahead": 3, "minFreeGB": 2 } +} +``` + +`buffer` is optional (defaults: `~/Movies/net-tv-buffer`, `ahead` 3, `minFreeGB` 2). + +### Watch log — append-only JSONL + +Path: `~/.local/state/plum-control-mcp/watched.jsonl` (shared by the governor and +`plum-control-mcp`; read by the app's `WatchHistory`). One event per line: + +```jsonc +{ "ts": 1717800000, "event": "play", "show": "…", "season": 1, "episode": 3, + "label": "…", "path": "…", "resumeSeconds": 920 } +``` + +Only `play`/`resume` events count toward progress. Show name is parsed from the +filename (everything before the `SxxEyy` marker). + +### App-local cache + state + +| Artifact | Path | Writer | +|---|---|---| +| Library snapshot | `~/.local/state/tv-anarchy/library.json` | `LibraryStore` | +| Metadata sidecar | `~/.local/state/tv-anarchy/meta/.json` | `MetaWriter` (mirrored best-effort to `.meta` on black) | +| Artwork cache | frame-grab JPEGs (keyed by path) | `ArtworkService` | +| VLC recents | macOS `org.videolan.vlc` plist (read-only) | VLC | + +### Library models (in-memory / snapshot) + +`CachedShow { name, rootDir, category, kind(series|movie), posterPath, overview, +episodes[], year, seasonCount, episodeCount, addedAt }`, +`CachedEpisode { path, season, episode, label, metaPath }`, +`LibrarySnapshot { shows[], capturedAt, source(scan|registry) }`, +`ContinueItem { title, path, show, season, episode, positionSeconds, lastSeen, +source(watchlog|vlc), posterPath }`. Decoders are tolerant so old snapshots load. + +`ParsedFilename { title, year, season, episode, quality, codec, releaseSource }`, +`MediaMeta { path, parsed, resolvedTitle, mediaType, overview, posterURL, +ratings, genres, enrichedAt }`. + +--- + +## Planned fleet/mesh data model + +Synthesized from +[`../.project/history/20260608_fleet-manager-mesh-design.md`](../.project/history/20260608_fleet-manager-mesh-design.md). +**None of this is implemented.** + +### Entities + +``` +Identity { discord_id, display_name, fleets[] } +Fleet { id, identity, hosts[], sources[], custody_capacity } +Host { ...registry, below... } +Source { ...peer-source, below... } +``` + +`custody_capacity = Σ over always_on hosts of (disk_free × uptime_score)` — whether +a fleet can hold a floor or is a net consumer. Gift-economy mode ignores it for +prioritization; ratio mode (off by default) weights by it. + +### Host registry + +``` +Host { + id, fleet_id + class: server | roamer | consumer | seedbox | broadcast // what it IS + reachable: home_lan | wireguard | public_ip + always_on: bool + on_home_ip: bool // true = public-swarm traffic exposes the home connection + api: transmission_rpc | qbittorrent | utorrent_web | none + capacity: { disk_free, up_bw, uptime_score(∈[0,1], rolling) } + duties: Duty[] // assigned by the manager, never hardcoded — what it DOES +} +Duty = custody_floor | public_swarm_face | f2f_relay | broadcast +``` + +Example fleet: black = server, apricot = secondary always-on, plum = roamer (TTL +seeder), phone = consumer (pure sink, never any duty). + +### Duty-assignment rules (deterministic, run on registry change) + +| Duty | Eligibility | Rule | +|---|---|---| +| `broadcast` | `public_ip && always_on` | exactly ONE per fleet; prefer seedbox > vps-0 > server | +| `f2f_relay` | `always_on && reachable ∈ {wireguard, public_ip}` | broadcast host + any other always-on server | +| `public_swarm_face` | prefer `!on_home_ip` | seedbox FIRST; never a consumer; never on_home_ip if an off-home option exists | +| `custody_floor` | `always_on && disk_free > title_size` | N most-recent eligible holders; ≥1 slot reserved for an always-on node | + +Invariants: a `consumer` never receives a duty (checked first); `public_swarm_face` +prefers off-home-IP so home stays dark; every active title's floor keeps ≥1 +always-on holder. + +### Peer-source model + +``` +Source { + id, fleet_id + kind: dht | public_tracker | private_tracker | friend_mesh | fleet_host | seedbox + api/creds: + share_policy: search_only | content // private_tracker DEFAULT-CLOSED (search_only) + swarm_isolation: f2f_only | open // private_tracker FORCED to f2f_only (un-overridable) +} +``` + +Two policy gates are load-bearing because a private `.torrent` carries an embedded +passkey — a friend announcing it to the private tracker gets the **source** user +banned: +1. `share_policy` gates the registry merge; flipping private → `content` triggers a + consequence-explicit warning naming the tracker. +2. `swarm_isolation = f2f_only` is forced for private sources: content is re-hosted + into the fleet's own WireGuard swarm, served `via: wireguard`, never announced + to the private tracker. Seedbox sources are `content` + `open` (no gates). + +### Derived outputs (the point of everything above) + +``` +peers_for(infohash) → Peer[] // holistic seeder list / user-owned meta-tracker +custodians_of(title) → Host[] // who is obligated to keep it alive +Peer { addr, source_kind, source_id, served_via: public | wireguard } +``` + +`peers_for` unions privates ∪ fleet ∪ friends' fleets ∪ seedbox ∪ DHT/public, +provenance-tagged so the UI can show *why* a peer exists and over what transport. diff --git a/docs/operations.md b/docs/operations.md new file mode 100644 index 0000000..415c0f0 --- /dev/null +++ b/docs/operations.md @@ -0,0 +1,120 @@ +# Operations + +How to build, install, configure, and run every component. Schemas referenced here +are detailed in [data-model.md](./data-model.md). + +## The app + +### Build & install (recommended) + +```sh +./build-install.sh +``` + +Stamps build identity (git SHA / time → `BuildStamp.swift`), runs `xcodegen`, +builds Release, and copies `TVAnarchy.app` to `~/Applications`. The one +irreducible manual step is **quit + relaunch** — a running native app can't be +hot-swapped. The sidebar build stamp (`v · ·