The 8s watch-history poll ran refresh() on the main actor, which read and
JSON-decoded the unioned watch log THREE times (playedPaths, resumePositions,
episodeProgress each re-read) and called MediaPaths.toRemote() per event — and
every toRemote rebuilt ProcessInfo.environment (~22µs each, the whole env dict
is reconstructed on every access) plus a homeDirectory lookup. A live sample
caught the main thread 100% in this path; the app sat at 78–113% CPU.
- Cache MediaPaths.remoteRoot / mappings (process-constant) → kills the
per-call env-dictionary rebuild storm.
- WatchHistory.derivedState(): read+decode the log ONCE, feed all three
derived computations → 3× fewer reads/decodes per refresh.
- WatchHistoryController.refreshAsync(): the background poll now parses off the
main thread on a utility task and only assigns the small results on main.
Settled CPU drops from ~78% sustained to ~0% idle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On-demand downloads (e.g. goon clips picked in the Adult collection list) now
take priority over the background warmup plan instead of waiting behind it or
starting a competing rsync:
- new priority lane (priorityEpisodes/priorityCount/enqueuePriority) drained
before each plan item and immediately when the cache is idle
- fetchFile routes through the lane when a warmup is already running (was: a
second concurrent rsync that also stomped the queue UI), and awaits completion
- per-episode fetch extracted to fetchOne(), shared by plan + priority drains
- status line shows '⤴︎N prioritized'; prioritizeFetch()/awaitDownload() expose
it for callers
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Defense-in-depth against option injection: a library path beginning with '-'
could be parsed as an ffprobe flag. Paths are always absolute today so it isn't
reachable, but '--' makes it safe regardless. Not command injection: $p is a
double-quoted expansion (contents not re-evaluated) and paths arrive as stdin
data, never on a command line — documented inline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each clip row in the adult collection detail view now shows its runtime, and
the header shows total runtime of the queued set (for planning a session of a
given length). Durations are probed in one background SSH batch via ffprobe on
black (NUL-delimited paths over stdin, so the eporner filenames with spaces/
quotes/brackets pass verbatim), debounced on filter and capped at 400 per batch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tapping a collection card on the Adult page now opens a detail view listing
every clip in that collection (goon, pmv, etc) instead of silently firing the
whole set at a host with nothing cached. Each row shows:
- queued state as a tickable checklist (build a session clip by clip)
- freshness / last-played
- offline-cached state, with a per-clip download-to-offline button
Plus a title filter (find e.g. a specific 'brain rot'/gooner clip), queue-all-
fresh, download-all-queued-offline, and play-queued. Downloads land in the
offline cache where the new star/trash row controls manage them. Quick-play the
old fire path stays on the card context menu.
Core: PornCollectionService.clips()/title() expose the full per-collection clip
list with freshness; PlaylistController gains single-item checklist queue ops
(isQueued/addToQueue/removeFromQueue) and pornClips().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Sample every HostStatsProvider target's load while the Devices tab is
visible (gated like the existing detailed/detailVisible pollers), keyed
by device id in PlayerController.hostStatsByID. DevicesView renders a
capsule badge from load1/cores: <0.6/core low, <1.0/core med, else high.
Only SSH/mpv devices report stats; others show no badge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Renames Sources/PlumTV→TVAnarchy and PlumTVCore→TVAnarchyCore (the rename
the auto-commit service couldn't stage — it git-add'd the old, now-gone
paths and aborted every cycle), and commits the accumulated work:
- Library: black-built index fast path (LibraryIndex + scanFromIndex) with
NFS-walk fallback; incremental --add on download-complete; mtime staleness
gate; loose-file series-collapse fix; determinate scan/index progress.
- Cover art: keyless TVmaze cartoon-vs-live-action disambiguation (type/year).
- Player: sleep timer (timed + end-of-episode); visibility-gated polling.
- Home: Continue Watching cover art + live refresh; Recently Added; adult gate.
- Logs: multi-line selection + copy; truncated giant tx-list errors.
- Hover previews (opt-in) via black ffmpeg + scp.
Also gitignores foreign project trees (governor/mcp/fleet/recommender) that
sit in this directory but belong to their own repos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>