tv-anarchy/docs/operations.md
Natalie 0a4cde36d1 feat(devices): add dependency issue warnings
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-09 21:57:08 -07:00

13 KiB

Operations

How to build, install, configure, and run every component. Schemas referenced here are detailed in 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)

./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)

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)

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

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.
  • Roku (ECP) — transport control of the stick's own playback (play/pause, jump-back, exit, now-playing) over its unauthenticated LAN REST on port 8060; discoverable via SSDP (ST: roku:ecp). Deliberately NOT a library playback destination — a Roku can't open NFS paths; that's the planned dev channel (see roadmap).

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).

A "not up to date" badge flags a stale helper deploy: the device's stats report carries the sha256 of the helper script it runs (helper_sha), and the app compares it against the repo's vendored copy (mcp/src/blacktv/black-tv.sh) — a mismatch (or a pre-stamp helper that reports nothing) means the app may be speaking verbs the device doesn't know. No badge appears when freshness can't be judged (device unreachable, unknown helper, or no repo checkout).

Update & restart service (menu + expanded summary) is the in-app fix: the app base64-pushes the vendored script over the device's own SSH channel, installs it atop the deployed bin (sudo install — atomic), verifies the landed sha256, then restarts the player service through the fresh script. The manual mcp/README.md deploy step remains for hosts the app can't reach.

Each row expands (chevron) into a summary section: backend + endpoints, role/fleet class + services, live connection/playback line, host load (1/5/15-min + mpv decode CPU), the dependent-services line, and the helper-deployment line (deployed vs repo hash, judged) — with the Restart / Update & restart buttons inline, so diagnosing and fixing a wedged device happens in one place.

Dependent services: black-tv stats also reports a deps object — transmission unit state, mpv unit + socket, media-root presence, DRM display state, disk used%/free under the media root. The helper sends raw facts; HostDeps.facts (app-side, one testable place) judges severity: transmission not active / media root missing / disk ≥97% = error, disk ≥90% / TV not connected / stale socket = warning. The row shows a red/orange pill only when something is wrong (a single issue shows its message, several collapse to a count); the expanded summary always shows the full facts line with ⚠ on the interesting ones.

governor (portable-net-tv)

TypeScript/Bun standalone daemon on plum.

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.

Fleet engine (portable-net-tv fleet …)

portable-net-tv fleet status            # registry + duties + probed capacity + 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 repin [--apply]   # execute the re-pin plans (rsync holder→target
                                        #   on the target via ssh; dry-run by default;
                                        #   targets need ssh + mediaRoot in fleet.json)
portable-net-tv fleet reaper [--apply]  # classify healthy|stalled|dead; --apply = safe
                                        #   nudges only (reannounce/verify)
portable-net-tv fleet peers <q>         # peers_for(infohash|title), provenance-tagged
portable-net-tv fleet probe             # ssh df + reachability → rolling capacity state
                                        #   (feeds custody disk-eligibility + status)
portable-net-tv fleet daemon [--apply-nudges] [--interval-min=N]
                                        # periodic tick (default 10 min): duties Δ →
                                        #   floor check → reaper; for launchd
portable-net-tv fleet serve [--port=N] [--token=T]
                                        # HTTP service (default :9094, $FLEET_TOKEN):
                                        #   /health /registry /custody /reaper
                                        #   /peers_for/<infohash>[?title=]

All subcommands take --json. Reads the fleet registry from ~/.config/tv-anarchy/fleet.json (array form; devices.json fallback) — see data-model.md. Read-only by default: mutation only behind --apply/--apply-nudges (repin --apply rsyncs + records the new holding; reaper nudges are idempotent transmission ops; mesh recoveries and re-searches remain printed plans). Engine state under ~/.local/state/tv-anarchy/: fleet-state.json (CLI duty diffs), fleet-daemon-state.json (daemon duty diffs), fleet-probe-state.json (rolling capacity), fleet-holdings.json (recorded re-pins). 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.

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.

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.