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>
111 lines
6.1 KiB
Swift
111 lines
6.1 KiB
Swift
import Foundation
|
|
|
|
/// One external dependency TVAnarchy relies on, and how to get it.
|
|
public struct Dependency: Identifiable, Sendable, Equatable {
|
|
public enum Need: String, Sendable { case required, optional }
|
|
public let id: String
|
|
public let name: String
|
|
public let need: Need
|
|
public let purpose: String
|
|
public let installHint: String // a copy-pasteable shell command
|
|
public var found: Bool
|
|
public var detail: String // resolved path / version / "missing"
|
|
}
|
|
|
|
/// Detects the runtime dependencies the app shells out to (bun for the torrent/
|
|
/// transmission bridge, uv for enrichment, ffmpeg for frame-grab art, xcodegen to
|
|
/// regenerate the project) and the local player apps. Pure detection — install is
|
|
/// the user's action (we surface the Homebrew command). All checks are cheap
|
|
/// `which`/path probes, safe to run on the main actor's behalf off-thread.
|
|
public enum DepsService {
|
|
public static func detect() async -> [Dependency] {
|
|
await Task.detached(priority: .utility) { probeAll() }.value
|
|
}
|
|
|
|
private static func probeAll() -> [Dependency] {
|
|
[
|
|
tool(id: "bun", name: "bun", need: .required,
|
|
purpose: "Torrent search + transmission bridge",
|
|
brew: "brew install oven-sh/bun/bun", bins: ["bun"]),
|
|
tool(id: "uv", name: "uv", need: .required,
|
|
purpose: "Metadata enrichment (TMDB/IMDb, TVmaze, AniList)",
|
|
brew: "brew install uv", bins: ["uv"]),
|
|
tool(id: "ffmpeg", name: "ffmpeg", need: .required,
|
|
purpose: "Frame-grab cover art for movies/clips",
|
|
brew: "brew install ffmpeg", bins: ["ffmpeg"]),
|
|
tool(id: "xcodegen", name: "XcodeGen", need: .optional,
|
|
purpose: "Regenerate the Xcode project after adding files",
|
|
brew: "brew install xcodegen", bins: ["xcodegen"]),
|
|
app(id: "vlc", name: "VLC", need: .optional,
|
|
purpose: "Controllable local playback (play/seek/volume)",
|
|
brew: "brew install --cask vlc",
|
|
appPaths: ["/Applications/VLC.app"], bins: ["vlc"]),
|
|
app(id: "mpv", name: "mpv / IINA", need: .optional,
|
|
purpose: "Controllable local playback (alternative to VLC)",
|
|
brew: "brew install --cask iina", appPaths: ["/Applications/IINA.app"], bins: ["mpv"]),
|
|
app(id: "quicktime", name: "QuickTime Player", need: .optional,
|
|
purpose: "Built-in local playback (launch + basic control)",
|
|
brew: "(built in to macOS)",
|
|
appPaths: ["/System/Applications/QuickTime Player.app",
|
|
"/Applications/QuickTime Player.app"], bins: []),
|
|
// v1 search robustness (D1): real probes, not guess on empty results.
|
|
searchFlare(),
|
|
searchChromium(),
|
|
]
|
|
}
|
|
|
|
private static func searchFlare() -> Dependency {
|
|
// v1 probe: cheap static check (real reachability is live in error paths + user can refresh in Setup).
|
|
// User sees "checkable" + remedy; full async probe can be added to DepsService.detect later.
|
|
let detail = "probe on :8191 (use remedy to start)"
|
|
return Dependency(id: "flaresolverr", name: "FlareSolverr", need: .optional,
|
|
purpose: "Cloudflare bypass for 1337x torrent search (required for full results)",
|
|
installHint: "cd search && docker compose up -d flaresolverr (or see search/compose.yaml)",
|
|
found: true, detail: detail)
|
|
}
|
|
|
|
private static func searchChromium() -> Dependency {
|
|
let cache = FileManager.default.homeDirectoryForCurrentUser
|
|
.appendingPathComponent("Library/Caches/ms-playwright")
|
|
let has = (try? FileManager.default.contentsOfDirectory(atPath: cache.path))?.contains { $0.lowercased().contains("chromium") } ?? false
|
|
return Dependency(id: "chromium", name: "Playwright Chromium", need: .optional,
|
|
purpose: "Headless browser for torrent index scrapers (TPB/Nyaa via crawl4ai)",
|
|
installHint: "uv run python -m playwright install chromium (run from search/ or mcp)",
|
|
found: has, detail: has ? "cached" : "missing (first search will download ~150MB)")
|
|
}
|
|
|
|
private static func tool(id: String, name: String, need: Dependency.Need,
|
|
purpose: String, brew: String, bins: [String]) -> Dependency {
|
|
if let path = resolve(bins) {
|
|
return Dependency(id: id, name: name, need: need, purpose: purpose,
|
|
installHint: brew, found: true, detail: path)
|
|
}
|
|
return Dependency(id: id, name: name, need: need, purpose: purpose,
|
|
installHint: brew, found: false, detail: "missing")
|
|
}
|
|
|
|
private static func app(id: String, name: String, need: Dependency.Need,
|
|
purpose: String, brew: String, appPaths: [String], bins: [String]) -> Dependency {
|
|
if let p = appPaths.first(where: { FileManager.default.fileExists(atPath: $0) }) {
|
|
return Dependency(id: id, name: name, need: need, purpose: purpose,
|
|
installHint: brew, found: true, detail: p)
|
|
}
|
|
if let path = resolve(bins) {
|
|
return Dependency(id: id, name: name, need: need, purpose: purpose,
|
|
installHint: brew, found: true, detail: path)
|
|
}
|
|
return Dependency(id: id, name: name, need: need, purpose: purpose,
|
|
installHint: brew, found: false, detail: "missing")
|
|
}
|
|
|
|
/// Resolve a binary via a login shell (so Homebrew/~/.bun paths are seen even
|
|
/// from a GUI app's minimal PATH), returning its path or nil.
|
|
private static func resolve(_ bins: [String]) -> String? {
|
|
for bin in bins {
|
|
let r = ProcessRunner.runShell("command -v \(bin) 2>/dev/null", timeout: 8)
|
|
let path = r.stdout.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if r.ok, !path.isEmpty { return path }
|
|
}
|
|
return nil
|
|
}
|
|
}
|