tv-anarchy/docs/operations.md
Natalie a86e68c525 feat(apps): add fleet engine mesh core integration
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-09 21:23:36 -07:00

203 lines
9.6 KiB
Markdown

# Operations
How to build, install, configure, and run every component. Schemas referenced here
are detailed in [data-model.md](./data-model.md).
## The app
Distribution is two-tier: **dev builds are local** (built on the build box,
installed locally, never published); **releases are durable** and
published to forge.black (Forgejo), where any node installs/updates from them
without a toolchain or source.
### Build & install — dev (the build box, plum)
```sh
./build-install.sh
```
Stamps build identity (git SHA / time → `BuildStamp.swift`), runs `xcodegen`,
builds Release into `build/dd`, and installs `TVAnarchy.app` to the
OS-appropriate location: `/Applications` when writable (the standard macOS
location — admin users, no sudo), else `~/Applications` (Apple's per-user
fallback for non-admin accounts). `TVANARCHY_DEST` overrides; non-macOS fails
loud. A stale copy at the other candidate location is removed on install so
two builds can't drift.
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. (Set `TVANARCHY_DD=path` to relocate the derived data;
`tools/release.sh` uses this to build into a throwaway tmp dir.)
### Cut a release → forge.black (plum)
```sh
tools/release.sh [vX.Y.Z] # tag defaults to v<MARKETING_VERSION>
```
Builds into an ephemeral tmp dir, refuses a dirty tree, tags + pushes over
`forge-wg`, then creates a Forgejo release on `lilith/tv-anarchy` and uploads
the zipped `.app`. Needs a write token: `FORGEJO_TOKEN` env or
`~/.config/tv-anarchy/forgejo-token` (chmod 600) — minted at Forgejo → Settings
→ Applications → Tokens (scope `write:repository`). Bump `MARKETING_VERSION` in
`project.yml` first; the tag must not already exist.
### Install / update — any other node (no toolchain or source)
```sh
tools/update.sh [--force] # first run installs; later runs no-op if current
```
Pulls the latest release from forge.black (`FORGEJO_API` defaults to the
mesh-stable overlay `http://10.9.0.4:3000`), picks the **asset for this
platform**, compares versions, and swaps it into the **OS-appropriate
destination** (`TVANARCHY_DEST` overrides everywhere):
| OS | Release asset | Destination |
|---|---|---|
| macOS | `TVAnarchy-<tag>.zip` | `/Applications` (admin) else `~/Applications`; Gatekeeper quarantine stripped; old-location copy migrated away |
| Ubuntu (classic Linux) | `TVAnarchy-<tag>-linux-<arch>.tar.gz` | `/opt/tv-anarchy` when `/opt` is writable, else `~/.local/opt/tv-anarchy` |
| Bluefin (immutable/ostree Linux) | same as Linux | always `~/.local/opt/tv-anarchy` (`/usr` is read-only; detected via `/run/ostree-booted`) |
| Windows (Git Bash/MSYS) | `TVAnarchy-<tag>-windows-<arch>.zip` | `%LOCALAPPDATA%\Programs\TVAnarchy` (per-user, no elevation) |
| Android (Termux) | `TVAnarchy-<tag>-android.apk` (all ABIs, no arch suffix) | APK → `~/storage/downloads`, handed to the system package installer via `termux-open` (user confirms the prompt — shells can't install packages on Android by design). Detected before generic Linux (Termux's `uname` says Linux). Version = handoff stamp, since Termux can't query installed packages |
| iOS | — | not via this script (no shell): build the `TVAnarchyiOS` scheme in Xcode onto the device, or TestFlight/sideload |
All per-OS logic (OS detection, destination, asset name) lives in ONE place —
`tools/platform.sh` — sourced by `build-install.sh`, `release.sh`, and
`update.sh`; run standalone (curl-piped, no checkout), `update.sh` fetches that
same file from the forge's raw endpoint, so the logic is never duplicated.
Version compare: macOS reads the bundle plist; Linux/Windows read the
`.release-tag` stamp the script writes on install. A release that lacks this
platform's asset fails loud with the exact missing name and the published list —
**today only the macOS asset is published** (cut on plum); Linux/Windows entries
activate the moment a release carries their assets. A read token is needed only
if the repo is private. Then quit/restart the app.
> Unsigned local build: fine across **your own** Macs (the quarantine strip
> handles Gatekeeper). Distributing to other people's machines would want real
> signing/notarization — a later piece.
### 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/devices.json` | Device registry + playback targets (migrates from `hosts.json`, then `~/.config/plumtv/hosts.json`) |
| `~/.local/state/tv-anarchy/settings.json` | App settings (adult gating, previews, media keys, offline cache) |
| `~/.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.
Devices are editable in-app (Devices tab: add/edit/delete, make-active, set
type/services, reload, reset, reveal `devices.json`). The list shows a
per-device system-load badge (low/med/high). Devices with a configured `restart`
command template (black by default) get a **Restart service** menu action that
hard-restarts the host-side player (`black-tv restart`: relaunch the mpv unit,
resuming the live playlist/position; clean teardown when idle/hung).
## 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).
### Fleet engine (`portable-net-tv fleet …`)
```sh
portable-net-tv fleet status # registry + duty assignment + warnings
portable-net-tv fleet duties # assign duties; Δ-log changes since last run
portable-net-tv fleet custody # floor-check every title; print re-pin plans
portable-net-tv fleet reaper # classify torrents healthy|stalled|dead
portable-net-tv fleet reaper --apply # + safe nudges only (reannounce/verify)
portable-net-tv fleet peers <q> # peers_for(infohash|title), provenance-tagged
```
All subcommands take `--json`. Reads the fleet registry from
`~/.config/tv-anarchy/fleet.json` (array form; `devices.json` fallback) — see
[data-model.md](./data-model.md#fleetjson--the-fleet-registry-app-side--governor-policy).
Read-only by default: re-pins, mesh recoveries, and re-searches are printed as
plans; only `reaper --apply` mutates anything (idempotent transmission ops).
Tests: `bun test` in `governor/`.
## 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.