8.1 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 |
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 tohttp://127.0.0.1:8080/requests/…. - Black (mpv) — mpv is driven straight to the DRM console (no X) over SSH JSON
IPC; the
commandstemplates 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).
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.
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_showsscansMEDIA_ROOTS(default~/media) forSxxEyyfiles.- 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(TSVsize⇥mtime⇥path), fetched byLibraryIndexvia one SSHcat; rebuilt withnice/ionicebecause black seeds 200+ torrents. - Registry: black's
.registry.mddump →recommender/registry.md.
Verification after changes
- App:
./build-install.sh, relaunch, confirm the build stamp updated. - governor/mcp:
bun run typecheckin each package. - recommender: run an
enrichfor a known title and confirm JSON output.