2026-06-08 22:07:16 -07:00
# Operations
How to build, install, configure, and run every component. Schemas referenced here
are detailed in [data-model.md ](./data-model.md ).
## The app
2026-06-09 19:51:12 -07:00
Distribution is two-tier: **dev builds are local** (built on the build box,
2026-06-09 20:50:54 -07:00
installed locally, never published); **releases are durable** and
2026-06-09 19:51:12 -07:00
published to forge.black (Forgejo), where any node installs/updates from them
without a toolchain or source.
### Build & install — dev (the build box, plum)
2026-06-08 22:07:16 -07:00
```sh
./build-install.sh
```
Stamps build identity (git SHA / time → `BuildStamp.swift` ), runs `xcodegen` ,
2026-06-09 20:50:54 -07:00
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.
2026-06-09 19:51:12 -07:00
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
```
2026-06-09 21:04:19 -07:00
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) |
2026-06-09 21:10:47 -07:00
| 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 |
2026-06-09 21:04:19 -07:00
| iOS | — | not via this script (no shell): build the `TVAnarchyiOS` scheme in Xcode onto the device, or TestFlight/sideload |
2026-06-09 21:17:10 -07:00
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.
2026-06-09 21:04:19 -07:00
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.
2026-06-09 19:51:12 -07:00
> 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.
2026-06-08 22:07:16 -07:00
### 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 |
|---|---|
2026-06-09 20:57:51 -07:00
| `~/.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) |
2026-06-08 22:07:16 -07:00
| `~/.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.
2026-06-09 20:57:51 -07:00
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).
2026-06-08 22:07:16 -07:00
## 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 ).
## 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.