feat(@applications/tv-anarchy): ✨ improve macOS install logic
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
b44b5a2d1a
commit
913135ca8c
6 changed files with 158 additions and 23 deletions
|
|
@ -1,17 +1,34 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build TVAnarchy (Release) and install it to ~/Applications — the per-user
|
||||
# install location (no root/sudo, still shown in Finder's "Applications"), so the
|
||||
# running app and the built app can't silently drift. The one irreducible manual
|
||||
# step is quitting + relaunching — you can't hot-swap a running native app — and
|
||||
# the sidebar build stamp (v<ver> · <sha> · <time>) makes a stale copy obvious.
|
||||
# Build TVAnarchy (Release) and install it to the OS-appropriate Applications
|
||||
# folder (see resolve_dest), so the running app and the built app can't silently
|
||||
# drift. The one irreducible manual step is quitting + relaunching — you can't
|
||||
# hot-swap a running native app — and the sidebar build stamp
|
||||
# (v<ver> · <sha> · <time>) makes a stale copy obvious.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Where TVAnarchy.app installs. Duplicated in tools/update.sh (kept in sync) so
|
||||
# that script stays curl-able standalone:
|
||||
# TVANARCHY_DEST env → explicit override, used verbatim
|
||||
# macOS, /Applications writable → /Applications (the standard location —
|
||||
# Finder's "Applications", admin group, no sudo)
|
||||
# macOS, not writable (non-admin)→ ~/Applications (Apple's per-user location)
|
||||
# anything else → fail loud; the .app bundle is macOS-only
|
||||
resolve_dest() {
|
||||
if [ -n "${TVANARCHY_DEST:-}" ]; then printf '%s\n' "$TVANARCHY_DEST"; return; fi
|
||||
[ "$(uname -s)" = "Darwin" ] || { echo "✗ TVAnarchy.app is macOS-only (this is $(uname -s))." >&2; return 1; }
|
||||
if [ -w /Applications ]; then
|
||||
printf '/Applications/TVAnarchy.app\n'
|
||||
else
|
||||
printf '%s/Applications/TVAnarchy.app\n' "$HOME"
|
||||
fi
|
||||
}
|
||||
|
||||
# Default DD lives in the repo (build/dd); tools/release.sh overrides it with an
|
||||
# ephemeral tmp dir so a release cut never churns the working build cache.
|
||||
DD="${TVANARCHY_DD:-build/dd}"
|
||||
APP="$DD/Build/Products/Release/TVAnarchy.app"
|
||||
DEST="$HOME/Applications/TVAnarchy.app"
|
||||
DEST="$(resolve_dest)"
|
||||
|
||||
echo "→ stamp build identity (git SHA / time → BuildStamp.swift)"
|
||||
tools/stamp-build.sh
|
||||
|
|
@ -29,6 +46,17 @@ mkdir -p "$(dirname "$DEST")"
|
|||
rm -rf "$DEST"
|
||||
cp -R "$APP" "$DEST"
|
||||
|
||||
# One install location only: drop a stale copy at the other auto candidate so
|
||||
# the launched app can never silently be an old build. Skipped under an explicit
|
||||
# TVANARCHY_DEST (e.g. a test install must not touch the real one).
|
||||
if [ -z "${TVANARCHY_DEST:-}" ]; then
|
||||
for other in "/Applications/TVAnarchy.app" "$HOME/Applications/TVAnarchy.app"; do
|
||||
if [ "$other" != "$DEST" ] && [ -d "$other" ]; then
|
||||
rm -rf "$other" && echo " removed stale copy at $other"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Marketing version is in the plist; SHA / build / time live in the compiled
|
||||
# BuildStamp constant we just generated (the reliable source — see project.yml).
|
||||
VER=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$DEST/Contents/Info.plist")
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ A single protocol (poll / playPause / resume / setVolume / seek / next / previou
|
|||
|
||||
### `PlayerController`
|
||||
|
||||
`@MainActor`/Observable. Owns target CRUD (persisted to `hosts.json`), a
|
||||
`@MainActor`/Observable. Owns target CRUD (persisted to `devices.json`), a
|
||||
single-flight poll loop (cadence varies by tab visibility), optimistic command
|
||||
routing, quality/release switching (refetch only on episode boundary), audio/sub
|
||||
track selection (persisted per series), a sleep-timer state machine, a transfer
|
||||
|
|
@ -83,6 +83,10 @@ Cached-first: load a snapshot instantly, refresh in the background, persist.
|
|||
to drive the Continue-Watching rail and resume positions.
|
||||
- **`LibraryController`** — Home rails (Continue / Recently-Added / per-category),
|
||||
franchise prefix-matching, scan orchestration, launch-request building.
|
||||
- **`ShowGrouping` / `LocalLLMGrouper` / `ContentID`** — groups episodes into
|
||||
shows (with an optional local-LLM pass for messy names) keyed by stable
|
||||
content IDs; **`LibraryConfig`** makes the folder→type mapping user-editable
|
||||
(types can be added/renamed/removed in Setup; each carries an `adult` flag).
|
||||
|
||||
### Downloads pipeline (`Torrents/`)
|
||||
|
||||
|
|
@ -98,10 +102,56 @@ completion callback so finished folders get incrementally indexed.
|
|||
`ArtworkService` (ffmpeg frame-grab fallback). An MLX `TitleRefiner` seam exists
|
||||
but is unwired (see [roadmap.md](./roadmap.md)).
|
||||
|
||||
### Device registry (Devices tab, `DeviceConfig`)
|
||||
|
||||
Playback targets generalized into **devices**: each entry in `devices.json` has a
|
||||
user-facing `type` (cellphone / laptop / storage / seedbox / broadcast-station)
|
||||
that maps onto the planned fleet host classes (consumer / roamer / server /
|
||||
seedbox / broadcast) and presets an overridable `services` set (stream,
|
||||
offline-cache, TTL-seed, custody, public-swarm-face, F2F-relay, mesh-anchor).
|
||||
The Devices list shows a per-device system-load badge (low/med/high, via
|
||||
`HostStats`). This is the **app-side registry** for mesh stage 1; the duty
|
||||
*assignment engine* (governor-side) is still unbuilt — see
|
||||
[roadmap.md](./roadmap.md). Legacy `hosts.json` entries are auto-migrated with
|
||||
inferred types.
|
||||
|
||||
### Adult content (`PornCollectionService`, `AdultView`)
|
||||
|
||||
The `porn-rotation.py` collection logic ported native. Doubly gated: the
|
||||
`ENABLE_ADULT` compile flag and the runtime `pornFeature` setting (off by
|
||||
default — the app ships with adult content concealed; a discreet sidebar toggle
|
||||
reveals the Adult tab). Which library types count as adult is data, not
|
||||
hardcoded: `LibraryConfig` types carry an `adult` flag (the default `porn` type
|
||||
has it).
|
||||
|
||||
### VPN subsystem (`VPN/`)
|
||||
|
||||
OVPN profile parsing + store (`OVPNProfile`, `VPNConfigStore`), credentials in
|
||||
the Keychain (`VPNCredentialStore`, via Security.framework), and a
|
||||
`VPNController` with a settings UI — so a roaming device can reach the overlay
|
||||
without hand-managed tunnels.
|
||||
|
||||
### Settings (`SettingsStore`)
|
||||
|
||||
Small tolerant-decode `settings.json`: adult gating (`pornFeature`,
|
||||
`surfaceAdultOnHome`, `switchToAdultOnlyHome`), hover previews, media-key
|
||||
forwarding (`NowPlayingController`), and offline-cache sizing
|
||||
(`offlineEpisodes`/`offlineShows`).
|
||||
|
||||
### Persistence locations
|
||||
|
||||
See [operations.md](./operations.md#config--state-locations) for the full table.
|
||||
|
||||
## 2b. iOS companion app (`Sources/TVAnarchyiOS`)
|
||||
|
||||
A separate iOS target (with `Tests/TVAnarchyiOSUITests`): VLCKit-based player
|
||||
(`VLCPlayerModel`, `PlayerScreen`), library browsing, on-device downloads
|
||||
(`DownloadManager`/`DownloadsView`), and a remote-control surface
|
||||
(`RemoteView`). It talks HTTP to a bridge (`BridgeClient`/`BridgeSettings`,
|
||||
default port `8787`, optional token) rather than SSH'ing anywhere itself. The
|
||||
bridge **server** is not part of this repo's `mcp/` tree — it lives with
|
||||
`plum-control-mcp`'s deployment on plum.
|
||||
|
||||
## 3. Helper subprocesses
|
||||
|
||||
- **`governor/` (`portable-net-tv`, TS/Bun).** A standalone launchd daemon on
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ 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 to `~/Applications`, never published); **releases are durable** and
|
||||
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.
|
||||
|
||||
|
|
@ -17,7 +17,12 @@ without a toolchain or source.
|
|||
```
|
||||
|
||||
Stamps build identity (git SHA / time → `BuildStamp.swift`), runs `xcodegen`,
|
||||
builds Release into `build/dd`, and copies `TVAnarchy.app` to `~/Applications`.
|
||||
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;
|
||||
|
|
@ -44,7 +49,8 @@ tools/update.sh [--force] # first run installs; later runs no-op if cur
|
|||
|
||||
Pulls the latest release zip from forge.black (`FORGEJO_API` defaults to the
|
||||
mesh-stable overlay `http://10.9.0.4:3000`), compares the tag to the installed
|
||||
`CFBundleShortVersionString`, and if newer swaps it into `~/Applications` and
|
||||
`CFBundleShortVersionString`, and if newer swaps it into the same resolved
|
||||
Applications location (migrating away any copy at the old location) and
|
||||
strips the Gatekeeper quarantine xattr (the build is unsigned). A read token is
|
||||
needed only if the repo is private. Then quit + relaunch.
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,13 @@ Where the project is and the de-risked path to the full vision. Architecture is
|
|||
| App — library browser | ✅ Shipped | black index fast-path + local `MEDIA_ROOTS` walk + registry fallback (no NFS) |
|
||||
| App — downloads (search + transmission) | ✅ Shipped | via `mcp` CLI; search needs FlareSolverr |
|
||||
| App — metadata enrichment + artwork | ✅ Shipped | regex parse + TMDB/IMDb/keyless + ffmpeg frame-grab |
|
||||
| App — all UI (Home/Player/Library/Search/Downloads/Metadata/Hosts/Logs) | ✅ Shipped | wired, no TODO/FIXME/`fatalError` debt |
|
||||
| App — all UI (Home/Player/Library/Search/Downloads/Metadata/Adult/Devices/Logs/Settings) | ✅ Shipped | wired, no TODO/FIXME/`fatalError` debt |
|
||||
| App — device registry (Devices tab) | ✅ Shipped (registry only) | `DeviceConfig`: type→services presets mapping to fleet classes, per-device load badge; duty *engine* still unbuilt (see fleet below) |
|
||||
| App — adult content tab | ✅ Shipped | `porn-rotation.py` ported to `PornCollectionService`; `ENABLE_ADULT` compile flag + runtime `pornFeature` setting, concealed by default |
|
||||
| App — VPN subsystem | ✅ Shipped | OVPN profile/credential stores (Keychain), controller, settings UI |
|
||||
| App — offline cache + Now Playing/media keys | ✅ Shipped | `OfflineCacheController`, `NowPlayingController`, bandwidth policy |
|
||||
| iOS app (`TVAnarchyiOS`) | ✅ Shipped (companion) | VLCKit player, library, downloads, remote control via HTTP bridge (default `:8787`); the bridge *server* is not in this repo's `mcp/` tree |
|
||||
| Distribution (release/update) | ✅ Shipped | `tools/release.sh` → Forgejo on forge.black; `tools/update.sh` installs/updates any node without a toolchain |
|
||||
| `governor` (`portable-net-tv`) | ✅ Shipped (single-host) | watch tracking + prefetch buffer |
|
||||
| `mcp` (`plum-control-mcp`) | ✅ Shipped | VLC / black-tv / transmission / display tools |
|
||||
| `recommender` | ✅ Shipped | enrichment + local recs |
|
||||
|
|
@ -28,10 +34,10 @@ is fully specified and entirely unbuilt.
|
|||
|
||||
### Repo-state note
|
||||
|
||||
The main checkout is mid-reorg: the committed `main` tree still tracks the old
|
||||
`PlumTV`/`PlumTVCore` sources, while the working tree has the renamed `TVAnarchy`
|
||||
sources plus the `governor/`, `mcp/`, `recommender/`, `fleet/`, and `tools/`
|
||||
trees **untracked**. Committing this reorg is the prerequisite to everything below.
|
||||
The `PlumTV → TVAnarchy` rename and the helper subsystems (`governor/`, `mcp/`,
|
||||
`recommender/`, `search/`, `fleet/`, `tools/`) were committed to `main` on
|
||||
2026-06-09 as a series of atomic commits (`41afc1c`…`b44b5a2`). Stage 0
|
||||
(hygiene) below is **done**.
|
||||
|
||||
## Remaining work — detail
|
||||
|
||||
|
|
@ -64,10 +70,15 @@ peer-source policy, `peers_for`/`custodians_of` — is design-only.
|
|||
|
||||
From the design spec. Each stage ships on its own; later stages depend on earlier.
|
||||
|
||||
0. **Hygiene (prerequisite)** — commit the `PlumTV → TVAnarchy` rename and the
|
||||
untracked helper subsystems so they are under version control.
|
||||
0. ~~**Hygiene (prerequisite)**~~ — ✅ done 2026-06-09: the `PlumTV → TVAnarchy`
|
||||
rename and the helper subsystems are committed on `main`.
|
||||
1. **Host registry + duty assignment, single fleet** (black + apricot + plum +
|
||||
phone). No mesh, no Discord. Unifies what's run by hand today.
|
||||
*Partially started on the app side:* the Devices tab + `DeviceConfig`
|
||||
(`devices.json`) already model device **types** (cellphone/laptop/storage/
|
||||
seedbox/broadcast → fleet classes consumer/roamer/server/seedbox/broadcast)
|
||||
with overridable per-device **services** presets. What remains is the
|
||||
governor-side duty *assignment engine* over that registry.
|
||||
*(Run the governor-generalization track alongside stages 1–3.)*
|
||||
2. **Seedbox source + `public_swarm_face` duty.** Adds an always-on custodian;
|
||||
proves the source model on the zero-risk source first. ("Add a seedbox" is the
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# Cut a RELEASE of TVAnarchy and publish it to forge.black (Forgejo).
|
||||
#
|
||||
# Two-tier distribution: day-to-day `./build-install.sh` builds are ephemeral
|
||||
# (tmp, install-to-~/Applications, gone). A release is the durable, shareable
|
||||
# (tmp, install-to-/Applications, gone). A release is the durable, shareable
|
||||
# artifact — built once here, tagged, and uploaded to the Forgejo repo so any
|
||||
# node on the mesh can install/update from it WITHOUT the Xcode toolchain or
|
||||
# source. Nodes pull with `tools/update.sh` (or the documented curl one-liner).
|
||||
|
|
@ -37,7 +37,12 @@ trap 'rm -rf "$DD"' EXIT
|
|||
echo "→ building release into $DD"
|
||||
TVANARCHY_DD="$DD" ./build-install.sh
|
||||
|
||||
APP="$HOME/Applications/TVAnarchy.app"
|
||||
# Same destination resolution as build-install.sh (which we just ran), so we zip
|
||||
# the app it actually installed: TVANARCHY_DEST override → /Applications when
|
||||
# writable → ~/Applications fallback.
|
||||
if [ -n "${TVANARCHY_DEST:-}" ]; then APP="$TVANARCHY_DEST"
|
||||
elif [ -w /Applications ]; then APP="/Applications/TVAnarchy.app"
|
||||
else APP="$HOME/Applications/TVAnarchy.app"; fi
|
||||
[ -d "$APP" ] || { echo "✗ build-install did not produce $APP" >&2; exit 1; }
|
||||
VER="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$APP/Contents/Info.plist")"
|
||||
BUILD="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$APP/Contents/Info.plist" 2>/dev/null || echo 0)"
|
||||
|
|
|
|||
|
|
@ -13,9 +13,26 @@ set -euo pipefail
|
|||
API="${FORGEJO_API:-http://10.9.0.4:3000}"
|
||||
OWNER="${FORGEJO_OWNER:-lilith}"
|
||||
REPO="${FORGEJO_REPO:-tv-anarchy}"
|
||||
DEST="$HOME/Applications/TVAnarchy.app"
|
||||
FORCE=""; [ "${1:-}" = "--force" ] && FORCE=1
|
||||
|
||||
# Where TVAnarchy.app installs. Duplicated from build-install.sh (kept in sync)
|
||||
# so this script stays curl-able standalone:
|
||||
# TVANARCHY_DEST env → explicit override, used verbatim
|
||||
# macOS, /Applications writable → /Applications (the standard location —
|
||||
# Finder's "Applications", admin group, no sudo)
|
||||
# macOS, not writable (non-admin)→ ~/Applications (Apple's per-user location)
|
||||
# anything else → fail loud; the .app bundle is macOS-only
|
||||
resolve_dest() {
|
||||
if [ -n "${TVANARCHY_DEST:-}" ]; then printf '%s\n' "$TVANARCHY_DEST"; return; fi
|
||||
[ "$(uname -s)" = "Darwin" ] || { echo "✗ TVAnarchy.app is macOS-only (this is $(uname -s))." >&2; return 1; }
|
||||
if [ -w /Applications ]; then
|
||||
printf '/Applications/TVAnarchy.app\n'
|
||||
else
|
||||
printf '%s/Applications/TVAnarchy.app\n' "$HOME"
|
||||
fi
|
||||
}
|
||||
DEST="$(resolve_dest)"
|
||||
|
||||
TOKEN="${FORGEJO_TOKEN:-}"
|
||||
TOKEN_FILE="$HOME/.config/tv-anarchy/forgejo-token"
|
||||
[ -z "$TOKEN" ] && [ -f "$TOKEN_FILE" ] && TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")"
|
||||
|
|
@ -33,8 +50,21 @@ print(r["tag_name"], asset["browser_download_url"] if asset else "")
|
|||
[ -n "$URL" ] || { echo "✗ release $TAG has no .zip asset." >&2; exit 1; }
|
||||
|
||||
# --- compare to the installed copy (releases are tagged v<marketing version>)
|
||||
installed="none"
|
||||
[ -d "$DEST" ] && installed="v$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$DEST/Contents/Info.plist" 2>/dev/null || echo '?')"
|
||||
installed="none"; stale=""
|
||||
if [ -d "$DEST" ]; then
|
||||
installed="v$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$DEST/Contents/Info.plist" 2>/dev/null || echo '?')"
|
||||
elif [ -z "${TVANARCHY_DEST:-}" ]; then
|
||||
# No copy at the resolved location — an older install may sit at the other
|
||||
# auto candidate (the layout moved from ~/Applications to /Applications).
|
||||
# Count it for the version line and migrate it away after the install below.
|
||||
# Skipped under an explicit TVANARCHY_DEST (a test install must not touch it).
|
||||
for other in "/Applications/TVAnarchy.app" "$HOME/Applications/TVAnarchy.app"; do
|
||||
if [ "$other" != "$DEST" ] && [ -d "$other" ]; then
|
||||
stale="$other"
|
||||
installed="v$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$other/Contents/Info.plist" 2>/dev/null || echo '?') (at $other)"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [ "$installed" = "$TAG" ] && [ -z "$FORCE" ]; then
|
||||
echo "✓ already on $TAG — up to date (use --force to reinstall)."; exit 0
|
||||
fi
|
||||
|
|
@ -49,11 +79,16 @@ ditto -x -k "$TMP/app.zip" "$TMP/unpacked"
|
|||
src="$(/usr/bin/find "$TMP/unpacked" -maxdepth 2 -name 'TVAnarchy.app' -print -quit)"
|
||||
[ -n "$src" ] || { echo "✗ no TVAnarchy.app inside the release zip." >&2; exit 1; }
|
||||
|
||||
mkdir -p "$HOME/Applications"
|
||||
mkdir -p "$(dirname "$DEST")"
|
||||
rm -rf "$DEST"
|
||||
ditto "$src" "$DEST"
|
||||
# Unsigned build copied across machines → clear Gatekeeper quarantine so it opens.
|
||||
xattr -dr com.apple.quarantine "$DEST" 2>/dev/null || true
|
||||
# One install location only: drop the old copy at the other candidate (if any)
|
||||
# so the launched app can never silently be a stale build.
|
||||
if [ -n "$stale" ]; then
|
||||
rm -rf "$stale" && echo " removed stale copy at $stale"
|
||||
fi
|
||||
|
||||
echo "✓ installed $TAG → $DEST"
|
||||
echo " quit any running TVAnarchy and relaunch to pick this up."
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue