tv-anarchy/Sources/TVAnarchyCore/VLCLauncher.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

99 lines
No EOL
4.1 KiB
Swift

import Foundation
/// Ensure local VLC is running with its HTTP/Lua interface the app can't poll or
/// control VLC without it. Mirrors governor's `launchVlcHttp` / `vlcHttpConfig`.
/// All blocking process/defaults work runs off the main actor.
public enum VLCLauncher {
public struct HttpConfig: Sendable, Equatable {
public let host: String
public let port: Int
public let password: String
}
private static let vlcBin = "/Applications/VLC.app/Contents/MacOS/VLC"
public static func httpConfig(host: String = "127.0.0.1", port: Int = 8080) -> HttpConfig? {
let pw = VLCConfig.password()
guard !pw.isEmpty else { return nil }
return HttpConfig(host: host, port: port, password: pw)
}
public static func isHttpReachable(_ cfg: HttpConfig) async -> Bool {
var comps = URLComponents()
comps.scheme = "http"
comps.host = cfg.host
comps.port = cfg.port
comps.path = "/requests/status.json"
guard let url = comps.url else { return false }
var req = URLRequest(url: url)
req.timeoutInterval = 3
req.setValue("Basic \(Data(":\(cfg.password)".utf8).base64EncodedString())",
forHTTPHeaderField: "Authorization")
guard let (_, resp) = try? await URLSession.shared.data(for: req),
(resp as? HTTPURLResponse)?.statusCode == 200 else { return false }
return true
}
/// Launch VLC (or relaunch when HTTP is down) and wait for the interface.
@discardableResult
public static func ensureRunning(_ cfg: HttpConfig) async -> Bool {
if await isHttpReachable(cfg) { return true }
await launchOffMain(cfg)
for _ in 0..<24 {
try? await Task.sleep(for: .milliseconds(500))
if await isHttpReachable(cfg) {
// Give the freshly (re)launched VLC a moment to fully initialize the
// HTTP interface and be ready to accept play commands (in_play etc).
try? await Task.sleep(for: .milliseconds(300))
return true
}
}
return false
}
/// Quit/relaunch + defaults never call from @MainActor (blocks the UI).
private static func launchOffMain(_ cfg: HttpConfig) async {
await Task.detached(priority: .utility) {
if isProcessRunning() { quitSync() }
persistDefaults(cfg)
spawnWithHttp(cfg)
}.value
}
private static func isProcessRunning() -> Bool {
ProcessRunner.run("/usr/bin/pgrep", ["-x", "VLC"]).ok
}
private static func quitSync() {
_ = ProcessRunner.runShell(
"osascript -e 'tell application \"VLC\" to quit' 2>/dev/null || true", timeout: 6)
for _ in 0..<10 where isProcessRunning() {
Thread.sleep(forTimeInterval: 0.2)
}
}
private static func persistDefaults(_ cfg: HttpConfig) {
_ = ProcessRunner.run("/usr/bin/defaults", ["write", "org.videolan.vlc", "extraintf", "-string", "http"])
_ = ProcessRunner.run("/usr/bin/defaults", ["write", "org.videolan.vlc", "http-host", "-string", cfg.host])
_ = ProcessRunner.run("/usr/bin/defaults", ["write", "org.videolan.vlc", "http-port", "-int", String(cfg.port)])
_ = ProcessRunner.run("/usr/bin/defaults", ["write", "org.videolan.vlc", "http-password", "-string", cfg.password])
}
private static func spawnWithHttp(_ cfg: HttpConfig) {
guard FileManager.default.fileExists(atPath: vlcBin) else {
Log.error("VLC not installed at \(vlcBin)")
return
}
let args = [
"-a", "VLC", "--args",
"--extraintf", "http",
"--http-host", cfg.host,
"--http-port", String(cfg.port),
"--http-password", cfg.password,
"--no-video-title-show",
]
let r = ProcessRunner.run("/usr/bin/open", args)
if r.ok { Log.info("launched VLC with HTTP on \(cfg.host):\(cfg.port)") }
else { Log.error("failed to launch VLC: \(r.stderr.trimmingCharacters(in: .whitespacesAndNewlines))") }
}
}