tv-anarchy/docs/operations.md
Natalie ca1871f5dd feat(@applications/tv-anarchy): add roku device support
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-09 21:37:34 -07:00

12 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. Redeploy per mcp/README.md to clear it. No badge appears when freshness can't be judged (device unreachable, unknown helper, or no repo checkout).

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.