Surface the existing pin (keep-from-cull) and per-file delete actions as visible inline buttons on each offline cache row instead of context-menu-only: a star toggles protection from auto-cull (and restore-if-missing), a trash culls that file early. Aligns wording/icons to the star metaphor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
351 lines
17 KiB
Markdown
351 lines
17 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.
|
|
|
|
### Daily dev loop (recommended)
|
|
|
|
Use the `./run` task runner for the common actions (it wraps stamp-build, xcodegen,
|
|
xcodebuild, install-via-platform.sh, relaunch, and helper subprojects). See
|
|
`./run help` and the script header for the full list and details.
|
|
|
|
```sh
|
|
./run # or ./run dev — Debug build + install to Applications + relaunch
|
|
./run test
|
|
./run clean
|
|
./run generate
|
|
./run typecheck
|
|
./run governor ...
|
|
./run mcp
|
|
./run bridge # HTTP bridge for iOS (default :8787)
|
|
./run test:all
|
|
./run deploy
|
|
./run deploy:phone
|
|
```
|
|
|
|
This is the normal iteration loop for macOS (Debug, uses `build/dd` derived data
|
|
by default; `TVANARCHY_DD=...` to move it). It installs over the user-visible
|
|
copy in /Applications (or ~/Applications) so Spotlight / dock launches the fresh
|
|
build, then quits any running instance and re-opens it. The sidebar build stamp
|
|
makes it obvious when you're running a fresh one.
|
|
|
|
### Build & install — dev (the build box, plum)
|
|
|
|
For a Release-style dev build (no relaunch, matches what `update.sh` will see):
|
|
|
|
```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
|
|
|
|
Prefer `./run dev` (or the other `./run` targets) for the common flows. For
|
|
one-off manual steps:
|
|
|
|
```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). See `./run help` (or the script source) for the full curated list and one-line
|
|
descriptions. Highlights for subprojects (no need to cd + bun manually):
|
|
|
|
- `./run governor fleet status|duties|...` — runs the governor CLI
|
|
- `./run mcp` — the stdio MCP server
|
|
- `./run bridge` — the HTTP bridge (iOS remote / torrent control)
|
|
- `./run typecheck` — tsc in governor + mcp
|
|
- `./run test:all` — Xcode tests + all helper test suites
|
|
|
|
(You can still run the subprojects directly: `(cd governor && bun run ...)` etc.)
|
|
|
|
### 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/tv-anarchy/watched.jsonl` (+ `black-watched.jsonl` mirror) | Shared watch log (see architecture.md for protocol, writers, 0.92 rule) |
|
|
| `~/.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.
|
|
|
|
```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 + 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]
|
|
|
|
**Sample launchd plist** (save as `~/Library/LaunchAgents/com.tv-anarchy.fleet-daemon.plist`,
|
|
then `launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.tv-anarchy.fleet-daemon.plist`):
|
|
|
|
```xml
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>Label</key><string>com.tv-anarchy.fleet-daemon</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>/opt/homebrew/bin/bun</string>
|
|
<string>run</string>
|
|
<string>/Users/natalie/Code/@applications/tv-anarchy/governor/src/index.ts</string>
|
|
<string>fleet</string>
|
|
<string>daemon</string>
|
|
<string>--apply-nudges</string>
|
|
</array>
|
|
<key>RunAtLoad</key><true/>
|
|
<key>KeepAlive</key><true/>
|
|
<key>WorkingDirectory</key><string>/Users/natalie/Code/@applications/tv-anarchy/governor</string>
|
|
<key>EnvironmentVariables</key>
|
|
<dict>
|
|
<key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
<key>HOME</key><string>/Users/natalie</string>
|
|
</dict>
|
|
<key>StandardOutPath</key><string>/Users/natalie/.local/state/tv-anarchy/fleet-daemon.log</string>
|
|
<key>StandardErrorPath</key><string>/Users/natalie/.local/state/tv-anarchy/fleet-daemon.log</string>
|
|
</dict>
|
|
</plist>
|
|
```
|
|
|
|
Edit paths for your install. Use `--interval-min=5` for testing. Logs via Console.app or the file. Safe nudges only; research/repin stay printed plans unless you extend the daemon.
|
|
|
|
# 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](./data-model.md#fleetjson--the-fleet-registry-app-side--governor-policy).
|
|
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 (`tv-anarchy-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 tv-anarchy # 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`.
|
|
|
|
## iOS app + bridge
|
|
|
|
- **Bridge** runs on black as a `systemd --user` unit (`tvanarchy-bridge`,
|
|
port 8787, token auth; env at `~/.config/tvanarchy-bridge/bridge.env`).
|
|
Redeploy from plum with `mcp/deploy/redeploy.sh` — it also syncs
|
|
`~/.config/tv-anarchy/{devices,fleet}.json` to black, which is what the
|
|
Remote tab's `/remote/targets` serves.
|
|
- **Phone** installs are dev-signed with a free Personal Team profile that
|
|
**expires every 7 days** — run `tools/deploy-phone.sh` weekly (builds to the
|
|
device so automatic signing can mint the profile, installs via `devicectl`).
|
|
- Library scans run in a worker (boot + every 15 min); a request never
|
|
triggers a walk. `?refresh=1` kicks a background rescan and returns the
|
|
current snapshot. /bigdisk is mergerfs (FUSE): per-entry stats are what made
|
|
scans take minutes, so the walk only stats video files — keep it that way.
|
|
|
|
## Verification after changes
|
|
|
|
- App: `./run` (or `./run dev`), relaunch if needed, confirm the sidebar build
|
|
stamp (vX · sha · time) updated. Or `./run update:plum` for a Release-style
|
|
build.
|
|
- Helpers: `./run typecheck` (or `./run test:all` for full).
|
|
- governor/mcp: also `bun run typecheck` / `bun test` directly if iterating inside
|
|
the package.
|
|
- recommender: run an `enrich` for a known title and confirm JSON output.
|
|
- iOS: `./run deploy:phone` (or `tools/deploy-phone.sh`), then on the phone
|
|
confirm Library loads and a downloaded episode plays in airplane mode.
|