tv-anarchy/Sources/TVAnarchyCore/MediaPaths.swift
Natalie 4a2ceb9781 feat(offline): inline star-to-keep and trash-to-cull on cache rows
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>
2026-06-30 00:12:41 -04:00

61 lines
3.2 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).
public static var 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.
private static var 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)
}
}