tv-anarchy/Sources/TVAnarchyCore/Log.swift
Natalie 92b38b1bae refactor(tv-anarchy): rename PlumTV→TVAnarchy and land session work
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>
2026-06-08 22:04:22 -07:00

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())
}
}