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>
49 lines
2.1 KiB
Swift
49 lines
2.1 KiB
Swift
import Foundation
|
|
|
|
/// Append-only diagnostics log at `/tmp/tvanarchy.log`, surfaced in-app by the
|
|
/// Logs tab. The app shells out to bun/uv/ssh/ffmpeg whose failures otherwise
|
|
/// vanish into a caught string; this records every subprocess (command, exit,
|
|
/// duration, stderr tail) and controller error. Lines: `HH:mm:ss.SSS [LEVEL] msg`.
|
|
public enum Log {
|
|
public static let path = "/tmp/tvanarchy.log"
|
|
|
|
public enum Level: String, Sendable, CaseIterable { case info = "INFO", warn = "WARN", error = "ERROR" }
|
|
|
|
private static let queue = DispatchQueue(label: "tvanarchy.log")
|
|
|
|
public static func info(_ message: String) { write(.info, message) }
|
|
public static func warn(_ message: String) { write(.warn, message) }
|
|
public static func error(_ message: String) { write(.error, message) }
|
|
/// Back-compat default (info).
|
|
public static func write(_ message: String) { write(.info, message) }
|
|
|
|
public static func write(_ level: Level, _ message: String) {
|
|
let line = "\(stamp()) [\(level.rawValue)] \(message)\n"
|
|
queue.async {
|
|
guard let data = line.data(using: .utf8) else { return }
|
|
let url = URL(fileURLWithPath: path)
|
|
if let h = try? FileHandle(forWritingTo: url) {
|
|
defer { try? h.close() }
|
|
_ = try? h.seekToEnd()
|
|
try? h.write(contentsOf: data)
|
|
} else {
|
|
try? data.write(to: url, options: .atomic)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The last `maxLines` lines (newest at the end), for the Logs panel.
|
|
public static func tail(_ maxLines: Int = 2000) -> [String] {
|
|
guard let s = try? String(contentsOfFile: path, encoding: .utf8) else { return [] }
|
|
let lines = s.split(separator: "\n", omittingEmptySubsequences: true).map(String.init)
|
|
return Array(lines.suffix(maxLines))
|
|
}
|
|
|
|
public static func clear() { try? Data().write(to: URL(fileURLWithPath: path), options: .atomic) }
|
|
|
|
private static func stamp() -> String {
|
|
let f = DateFormatter()
|
|
f.dateFormat = "HH:mm:ss.SSS"
|
|
return f.string(from: Date())
|
|
}
|
|
}
|