tv-anarchy/Sources/TVAnarchyCore/Library/SettingsStore.swift

91 lines
5.1 KiB
Swift

import Foundation
/// User-facing app preferences that aren't host/franchise specific. Persisted to
/// `~/.local/state/tv-anarchy/settings.json`. Kept deliberately small one file,
/// tolerant decode, sensible defaults so adding a preference is one field here
/// plus its wiring, no migration.
public struct AppSettings: Codable, Sendable, Equatable {
/// When false (the default), the Home screen hides everything from the `porn`
/// media dir its category rails, plus any adult item in Continue Watching or
/// Recently Added. Independent of the Library tab's own porn browse toggle, so
/// you can deliberately browse adult content in Library without it leaking onto
/// the landing screen.
public var surfaceAdultOnHome: Bool
/// When true, hovering a poster plays a short muted preview clip (built on
/// black, seeked past the intro, cached). Off by default it triggers ffmpeg
/// work on black, so it's opt-in.
public var hoverPreviews: Bool
/// When true (the default), the Mac's system transport media keys, Control
/// Center, lock screen, AirPods drives whatever TVAnarchy is playing, and
/// Now Playing reflects it. Turn off to let the media keys fall through to
/// other apps. (Part B media-control forwarding; see NowPlayingController.)
public var forwardMediaKeys: Bool
/// Offline cache (cellphone/laptop devices): pull the next `offlineEpisodes`
/// unwatched episodes of the most-recent `offlineShows` shows to local disk.
/// Off by default it rsyncs from black. `offlineFromContinueWatching` picks
/// the source rail (continue-watching vs recently-added).
public var offlineEpisodes: Int
public var offlineShows: Int
public var offlineFromContinueWatching: Bool
/// When true (default), dynamically combine split/duplicate library entries of
/// one show into a single show via the cheap clusterer + the local-LLM reasoner
/// (the Dandadan case). Decisions are cached on disk, so it runs once per cluster.
public var combineSplitShows: Bool
/// When true, use the local MLX LLM (a separate download) to reason over
/// ambiguous show clusters. Off by default the shipped `DeterministicGrouper`
/// (year + episode/season structure, zero MB) handles the real cases; the LLM is
/// an optional enhancement for the rare same-year ambiguous tail.
public var useLLMGrouper: Bool
/// Notify (Notification Center + in-app banner) when a download is ready to watch
/// or gets stuck. On by default.
public var notifyDownloads: Bool
public init(surfaceAdultOnHome: Bool = false, hoverPreviews: Bool = false,
forwardMediaKeys: Bool = true, offlineEpisodes: Int = 3,
offlineShows: Int = 5, offlineFromContinueWatching: Bool = true,
combineSplitShows: Bool = true, useLLMGrouper: Bool = false,
notifyDownloads: Bool = true) {
self.surfaceAdultOnHome = surfaceAdultOnHome
self.hoverPreviews = hoverPreviews
self.forwardMediaKeys = forwardMediaKeys
self.offlineEpisodes = offlineEpisodes
self.offlineShows = offlineShows
self.offlineFromContinueWatching = offlineFromContinueWatching
self.combineSplitShows = combineSplitShows
self.useLLMGrouper = useLLMGrouper
self.notifyDownloads = notifyDownloads
}
public init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
surfaceAdultOnHome = try c.decodeIfPresent(Bool.self, forKey: .surfaceAdultOnHome) ?? false
hoverPreviews = try c.decodeIfPresent(Bool.self, forKey: .hoverPreviews) ?? false
forwardMediaKeys = try c.decodeIfPresent(Bool.self, forKey: .forwardMediaKeys) ?? true
offlineEpisodes = try c.decodeIfPresent(Int.self, forKey: .offlineEpisodes) ?? 3
offlineShows = try c.decodeIfPresent(Int.self, forKey: .offlineShows) ?? 5
offlineFromContinueWatching = try c.decodeIfPresent(Bool.self, forKey: .offlineFromContinueWatching) ?? true
combineSplitShows = try c.decodeIfPresent(Bool.self, forKey: .combineSplitShows) ?? true
useLLMGrouper = try c.decodeIfPresent(Bool.self, forKey: .useLLMGrouper) ?? false
notifyDownloads = try c.decodeIfPresent(Bool.self, forKey: .notifyDownloads) ?? true
}
}
public enum SettingsStore {
private static var url: URL {
FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".local/state/tv-anarchy/settings.json")
}
public static func load() -> AppSettings {
guard let d = try? Data(contentsOf: url),
let s = try? JSONDecoder().decode(AppSettings.self, from: d) else { return AppSettings() }
return s
}
public static func save(_ s: AppSettings) {
guard let d = try? JSONEncoder().encode(s) else { return }
try? FileManager.default.createDirectory(at: url.deletingLastPathComponent(),
withIntermediateDirectories: true)
try? d.write(to: url, options: .atomic)
}
}