tv-anarchy/Sources/TVAnarchyCore/Metadata/MetaWriter.swift
Natalie 83a21ca105 feat(library): optimize scan merging with cached metadata
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-09 22:16:25 -07:00

62 lines
2.9 KiB
Swift

import Foundation
import CryptoKit
/// Persists `.meta` payloads. The plum-local cache (keyed by file path) is the
/// always-on source of truth; mirroring `filename.ext.meta` next to the file on
/// black is opportunistic and guarded it never blocks the UI and tolerates an
/// unreachable host (black is non-ECC ZFS; the cache is what we rely on).
public enum MetaWriter {
public static func metaDir() -> URL {
FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".local/state/tv-anarchy/meta")
}
/// Cache key is the sha256 of the CANONICAL (black-side) path, so lookups hit
/// regardless of whether the caller holds a legacy plum mount path or the
/// black-side form the scanner now emits.
public static func cacheURL(forPath path: String) -> URL {
let digest = SHA256.hash(data: Data(MediaPaths.toRemote(path).utf8))
let hex = digest.map { String(format: "%02x", $0) }.joined()
return metaDir().appendingPathComponent("\(hex).json")
}
@discardableResult
public static func writeCache(_ meta: MediaMeta) -> Bool {
let url = cacheURL(forPath: meta.path)
try? FileManager.default.createDirectory(at: metaDir(), withIntermediateDirectories: true)
let enc = JSONEncoder()
enc.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes]
enc.dateEncodingStrategy = .iso8601
guard let data = try? enc.encode(meta) else { return false }
return (try? data.write(to: url, options: .atomic)) != nil
}
public static func loadCache(forPath path: String) -> MediaMeta? {
guard let data = try? Data(contentsOf: cacheURL(forPath: path)) else { return nil }
let dec = JSONDecoder()
dec.dateDecodingStrategy = .iso8601
return try? dec.decode(MediaMeta.self, from: data)
}
/// Best-effort: write `<remotePath>.meta` on black via ssh (base64 to dodge
/// quoting/stdin issues). Returns false on any failure callers ignore it.
/// `remotePath` must be a black-side path (e.g. under /bigdisk/_/media).
@discardableResult
public static func mirrorToBlack(_ meta: MediaMeta, remotePath: String, endpoint: String) -> Bool {
let enc = JSONEncoder()
enc.outputFormatting = [.withoutEscapingSlashes]
enc.dateEncodingStrategy = .iso8601
guard let data = try? enc.encode(meta) else { return false }
let b64 = data.base64EncodedString()
let remote = remotePath + ".meta"
// printf the base64 locally, pipe over ssh, decode into the sidecar.
let command = "printf %s \(shq(b64)) | /usr/bin/ssh -o BatchMode=yes -o ConnectTimeout=5 "
+ "\(shq(endpoint)) \(shq("base64 -d > " + shq(remote)))"
let r = ProcessRunner.runShell(command, timeout: 20)
return r.ok
}
private static func shq(_ s: String) -> String {
"'" + s.replacingOccurrences(of: "'", with: "'\\''") + "'"
}
}