tv-anarchy/docs/data-model.md
Natalie f2ce865cb8 docs(@applications/tv-anarchy): 📝 update app description and architecture details
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-09 20:57:51 -07:00

210 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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": "<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/<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).
> **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: <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.