import Foundation /// The persisted sub/dub choices: a global default plus per-series overrides. /// Keyed by normalized show name so a choice survives across episodes and app /// restarts. anime defaults to `sub` at resolve time even without an override. public struct TrackPreferences: Codable, Sendable, Equatable { public var globalDefault: DubSub public var perSeries: [String: DubSub] public init(globalDefault: DubSub = .dub, perSeries: [String: DubSub] = [:]) { self.globalDefault = globalDefault self.perSeries = perSeries } static func key(_ name: String) -> String { name.lowercased().trimmingCharacters(in: .whitespaces) } /// Resolve the effective choice: explicit per-series override wins; otherwise /// anime defaults to `sub`; otherwise the global default (`dub` — native audio, /// no forced subs, which is right for English-language content). public func choice(series: String?, category: String?) -> DubSub { if let series, let explicit = perSeries[Self.key(series)] { return explicit } if category == "anime" { return .sub } return globalDefault } public mutating func setSeries(_ name: String, _ choice: DubSub) { perSeries[Self.key(name)] = choice } } /// JSON store at `~/.local/state/tv-anarchy/track-prefs.json`. Mirrors /// PlayerStatusCache's load/save shape. public enum TrackPreferenceStore { private static var url: URL { FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent(".local/state/tv-anarchy/track-prefs.json") } public static func load() -> TrackPreferences { guard let data = try? Data(contentsOf: url), let prefs = try? JSONDecoder().decode(TrackPreferences.self, from: data) else { return TrackPreferences() } return prefs } public static func save(_ prefs: TrackPreferences) { guard let data = try? JSONEncoder().encode(prefs) else { return } try? FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true) try? data.write(to: url, options: .atomic) } }