docs(@applications): 📝 update architecture and data model docs
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
92b38b1bae
commit
5b47531b5c
5 changed files with 478 additions and 208 deletions
|
|
@ -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.
|
||||
36
docs/README.md
Normal file
36
docs/README.md
Normal file
|
|
@ -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.
|
||||
130
docs/architecture.md
Normal file
130
docs/architecture.md
Normal file
|
|
@ -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).
|
||||
192
docs/data-model.md
Normal file
192
docs/data-model.md
Normal file
|
|
@ -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": "<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/<sha256(path)>.json` | `MetaWriter` (mirrored best-effort to `<remotePath>.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: <opaque, encrypted at rest>
|
||||
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.
|
||||
120
docs/operations.md
Normal file
120
docs/operations.md
Normal file
|
|
@ -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<ver> · <sha> · <time>`) makes a stale
|
||||
copy obvious.
|
||||
|
||||
### Build manually / in Xcode
|
||||
|
||||
```sh
|
||||
brew install xcodegen # if needed
|
||||
xcodegen generate # project.yml → TVAnarchy.xcodeproj (generated, git-ignored)
|
||||
xcodebuild -scheme TVAnarchy -destination 'platform=macOS' build
|
||||
# or open TVAnarchy.xcodeproj and Run
|
||||
```
|
||||
|
||||
`project.yml` is the source of truth; the `.xcodeproj` is generated. Local dev is
|
||||
unsigned (no sandbox/hardened runtime — the app needs `Process`/ssh and
|
||||
localhost+overlay networking).
|
||||
|
||||
### Config & state locations
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `~/.config/tv-anarchy/hosts.json` | Playback targets (migrates from `~/.config/plumtv/hosts.json`) |
|
||||
| `~/.config/portable-net-tv/config.json` | Governor config **and** the VLC HTTP password source |
|
||||
| `~/.local/state/plum-control-mcp/watched.jsonl` | Shared watch log |
|
||||
| `~/.local/state/tv-anarchy/library.json` | Cached library snapshot |
|
||||
| `~/.local/state/tv-anarchy/meta/` | Metadata sidecars (path-digest keyed) |
|
||||
| `$VLC_HTTP_PASSWORD` | Alternate VLC password source |
|
||||
|
||||
## Playback targets
|
||||
|
||||
- **Plum VLC** — enable VLC's HTTP/Lua interface (Preferences → All → Interface →
|
||||
Main interfaces → Lua), set the password (read from the governor config or
|
||||
`$VLC_HTTP_PASSWORD`). App talks to `http://127.0.0.1:8080/requests/…`.
|
||||
- **Black (mpv)** — mpv is driven straight to the DRM console (no X) over SSH JSON
|
||||
IPC; the `commands` templates delegate launch/library/stats/teardown to
|
||||
`/usr/local/bin/black-tv`. Endpoints try LAN (`10.0.0.11`) then the WG overlay
|
||||
(`10.9.0.4`) because the LAN address flaps.
|
||||
- **QuickTime** — local, zero-install; no setup.
|
||||
|
||||
Hosts are editable in-app (Hosts tab: add/edit/delete, make-active, reload, reset,
|
||||
reveal `hosts.json`).
|
||||
|
||||
## governor (`portable-net-tv`)
|
||||
|
||||
TypeScript/Bun standalone daemon on plum.
|
||||
|
||||
```sh
|
||||
cd governor
|
||||
bun install
|
||||
portable-net-tv watch # daemon: follow VLC, log watches, prefetch next N eps
|
||||
portable-net-tv next # print per-show progress
|
||||
```
|
||||
|
||||
Runs as a launchd background agent (Apple Events to VLC are blocked there, which
|
||||
is why it reads VLC over HTTP, not AppleScript). Config: see
|
||||
[data-model.md](./data-model.md#configjson--governor-portable-net-tv).
|
||||
|
||||
## mcp (`plum-control-mcp`)
|
||||
|
||||
TypeScript/Bun. Serves both an MCP stdio server (for Claude) and the CLI bridge
|
||||
the app shells out to.
|
||||
|
||||
```sh
|
||||
cd mcp
|
||||
bun install
|
||||
bun run typecheck
|
||||
claude mcp add plum-control # register the MCP server with Claude Code
|
||||
```
|
||||
|
||||
Requirements/constraints:
|
||||
- macOS only (NSScreen via osascript-jxa; reads `org.videolan.vlc.plist`).
|
||||
- VLC must be running with the Web interface enabled.
|
||||
- Torrent search needs **FlareSolverr** on `localhost:8191` (TPB/Nyaa/1337x).
|
||||
- `media_list_shows` scans `MEDIA_ROOTS` (default `~/media`) for `SxxEyy` files.
|
||||
- Black-TV watch state is black-local (`/usr/local/share/black-tv/`), read over
|
||||
SSH, never written to plum/NFS.
|
||||
|
||||
## recommender
|
||||
|
||||
Python (managed with `uv`). Invoked by the app's `EnrichService` during
|
||||
indexing/enrichment; not on the playback path.
|
||||
|
||||
```sh
|
||||
cd recommender
|
||||
uv run python -m media_rec.enrich "<title>" [year] [--category <cat>]
|
||||
uv run python recommend_local.py # keyless local recommendations (preferred)
|
||||
uv run python recommend.py # TMDB-backed (needs an API key)
|
||||
```
|
||||
|
||||
Provider routing by category: anime → AniList, tv/cartoons → TVmaze, fallback →
|
||||
TMDB; degrades gracefully on missing keys/failures. `registry.md` is a generated
|
||||
snapshot of black's library used as the offline title source.
|
||||
|
||||
## Black-side artifacts
|
||||
|
||||
- Prebuilt index: `/bigdisk/_/media/_tools/index.tsv` (TSV `size⇥mtime⇥path`),
|
||||
fetched by `LibraryIndex` via one SSH `cat`; rebuilt with `nice`/`ionice`
|
||||
because black seeds 200+ torrents.
|
||||
- Registry: black's `.registry.md` dump → `recommender/registry.md`.
|
||||
|
||||
## Verification after changes
|
||||
|
||||
- App: `./build-install.sh`, relaunch, confirm the build stamp updated.
|
||||
- governor/mcp: `bun run typecheck` in each package.
|
||||
- recommender: run an `enrich` for a known title and confirm JSON output.
|
||||
Loading…
Add table
Reference in a new issue