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>
65 lines
3.6 KiB
Swift
65 lines
3.6 KiB
Swift
import Foundation
|
|
|
|
/// Path policy for the library. The library is identified by **storage-side absolute
|
|
/// paths** (`/bigdisk/_/media/…`) everywhere — that's what the storage index emits
|
|
/// and what remote players expect. This project does NOT depend on the NFS `~/media`
|
|
/// mount: a local player opens *downloaded* copies (fetched from the storage server
|
|
/// on demand when away). Pick a remote HDMI player explicitly to play at home — the
|
|
/// app never silently hijacks it when a local player is selected.
|
|
///
|
|
/// `toRemote` stays as a normalizer so any *legacy* laptop mount path left in a
|
|
/// persisted cache or watch-history key still resolves to its storage-side form
|
|
/// (it's the identity on paths that are already storage-side).
|
|
public enum MediaPaths {
|
|
/// Storage-side media root (env-overridable, matches the TS bridge default).
|
|
/// Cached once: the environment is fixed for the process lifetime, and
|
|
/// `ProcessInfo.environment` rebuilds the entire env dictionary on every access
|
|
/// (~22µs), which `toRemote` would otherwise pay thousands of times per watch-log
|
|
/// refresh. See the watch-history poll hot path.
|
|
public static let remoteRoot: String =
|
|
ProcessInfo.processInfo.environment["BLACK_MEDIA_ROOT"] ?? "/bigdisk/_/media"
|
|
|
|
/// Legacy laptop mount prefix → storage-side absolute prefix. Longest first so
|
|
/// `~/_/bigdisk/_/media` wins over `~/_/bigdisk`. Only relevant for stale paths
|
|
/// persisted before the mount was dropped — fresh scans are already storage-side.
|
|
/// Cached once (the home dir and root are process-constant) for the same reason.
|
|
private static let mappings: [(plum: String, remote: String)] = {
|
|
let home = FileManager.default.homeDirectoryForCurrentUser.path
|
|
return [
|
|
(home + "/_/bigdisk/_/media", remoteRoot),
|
|
(home + "/media", remoteRoot),
|
|
(home + "/_/bigdisk", "/bigdisk"),
|
|
]
|
|
}()
|
|
|
|
/// Normalize any path to its storage-side absolute form. Already-canonical paths and
|
|
/// anything we don't manage pass through unchanged (so it's the identity on the
|
|
/// canonical paths the scanner now produces).
|
|
public static func toRemote(_ path: String) -> String {
|
|
let p = path.hasPrefix("file://") ? String(path.dropFirst(7)) : path
|
|
for m in mappings where p.hasPrefix(m.plum) {
|
|
return m.remote + String(p.dropFirst(m.plum.count))
|
|
}
|
|
return p
|
|
}
|
|
|
|
/// URL a local player opens for a library item. VLC can only open a
|
|
/// file it has on disk — its sftp access is broken on macOS, and there is no
|
|
/// NFS mount — so this resolves to a local downloaded copy (`file://`) when one
|
|
/// exists. For a file with no local copy, `PlayerController` downloads from
|
|
/// black first (single launch) rather than handing a dead path here.
|
|
/// An already-resolved URL passes through unchanged.
|
|
public static func toStreamURL(_ path: String) -> String {
|
|
if path.hasPrefix("file://") || path.hasPrefix("http://")
|
|
|| path.hasPrefix("https://") || path.hasPrefix("sftp://") { return path }
|
|
if let local = localCopy(of: path) { return "file://" + local }
|
|
return "file://" + path // best-effort; reroute should have caught this
|
|
}
|
|
|
|
/// Path of a downloaded local copy of `path`, matched by filename through
|
|
/// `DownloadsIndex` (media-fetch preserves names but not the tree layout). nil
|
|
/// when nothing matching is downloaded. Never touches `~/media`.
|
|
public static func localCopy(of path: String) -> String? {
|
|
DownloadsIndex.shared.localPath(for: path)
|
|
}
|
|
}
|