# 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 ### `devices.json` — device registry + playback target config Path: `~/.config/tv-anarchy/devices.json` (auto-migrated forward from `~/.config/tv-anarchy/hosts.json`, and before that `~/.config/plumtv/hosts.json`). Source: `Sources/TVAnarchyCore/DeviceConfig.swift`. Written pretty-printed + sorted-keys; seeded on first run with Plum VLC + Black (mpv) if absent. The decoder accepts the legacy top-level `hosts` key alongside `devices`. Each device carries a `type` — `cellphone | laptop | storage | seed | broadcast`, mapping to the planned fleet classes `consumer | roamer | server | seedbox | broadcast` — and an overridable `services` preset (`stream`, `offlineCache`, `ttlSeed`, `custody`, `publicSwarmFace`, `f2fRelay`, `meshAnchor`). Legacy entries without a `type` get one inferred from the player backend. ```jsonc { "devices": [ { "id": "plum-vlc", "name": "Plum VLC", "kind": "vlc", // vlc | mpv-ipc | quicktime | blacktv(legacy) "type": "laptop", // cellphone | laptop | storage | seed | broadcast "services": { "stream": true, "offlineCache": true, "ttlSeed": true }, "vlc": { "host": "127.0.0.1", "port": 8080 } }, { "id": "black", "name": "Black TV", "kind": "mpv-ipc", "type": "storage", "services": { "stream": true, "custody": true }, "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 | |---|---|---| | App settings | `~/.local/state/tv-anarchy/settings.json` | `SettingsStore` (adult gating, hover previews, media-key forwarding, offline-cache sizing; tolerant decode) | | 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). > **Partially landed:** the app's `devices.json` (above) already carries the > class mapping (`DeviceType.fleetClass`) and a `services` preset that mirrors > the duty vocabulary. What does **not** exist is the manager that *assigns* > duties from capacity/reachability — services today are user-set presets, not > computed duties. ### 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.