NavidromeApp/Shared/Storage/LibraryCache.swift

127 lines
4.2 KiB
Swift
Raw Normal View History

import Foundation
import Network
/// Caches library data (albums, artists, playlists, songs) to disk for offline browsing.
/// Also monitors network connectivity.
class LibraryCache: ObservableObject {
static let shared = LibraryCache()
@Published var isOffline = false
private let fileManager = FileManager.default
private let cacheDir: URL
private let monitor = NWPathMonitor()
private let monitorQueue = DispatchQueue(label: "com.navidromeplayer.network")
private init() {
let caches = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
cacheDir = caches.appendingPathComponent("LibraryCache", isDirectory: true)
try? fileManager.createDirectory(at: cacheDir, withIntermediateDirectories: true)
// Allow background access prevents -54 sandbox errors
try? (cacheDir as NSURL).setResourceValue(
URLFileProtection.completeUntilFirstUserAuthentication,
forKey: .fileProtectionKey
)
// Set file protection to allow background access prevents -54 sandbox errors
try? (cacheDir as NSURL).setResourceValue(
URLFileProtection.completeUntilFirstUserAuthentication,
forKey: .fileProtectionKey
)
// Monitor network status
monitor.pathUpdateHandler = { [weak self] path in
DispatchQueue.main.async {
self?.isOffline = (path.status != .satisfied)
}
}
monitor.start(queue: monitorQueue)
}
deinit {
monitor.cancel()
}
// MARK: - Generic Cache Read/Write
private func cacheURL(for key: String) -> URL {
cacheDir.appendingPathComponent("\(key).json")
}
func save<T: Encodable>(_ data: T, key: String) {
if let encoded = try? JSONEncoder().encode(data) {
try? encoded.write(to: cacheURL(for: key), options: .atomic)
}
}
func load<T: Decodable>(_ type: T.Type, key: String) -> T? {
let url = cacheURL(for: key)
guard let data = try? Data(contentsOf: url) else { return nil }
return try? JSONDecoder().decode(type, from: data)
}
// MARK: - Typed Helpers
func cacheAlbums(_ albums: [Album], key: String = "recent_albums") {
save(albums, key: key)
}
func loadAlbums(key: String = "recent_albums") -> [Album]? {
load([Album].self, key: key)
}
func cacheArtists(_ artists: [ArtistIndex]) {
save(artists, key: "artists")
}
func loadArtists() -> [ArtistIndex]? {
load([ArtistIndex].self, key: "artists")
}
func cachePlaylists(_ playlists: [Playlist]) {
save(playlists, key: "playlists")
}
func loadPlaylists() -> [Playlist]? {
load([Playlist].self, key: "playlists")
}
func cacheAlbumDetail(_ album: AlbumWithSongs) {
save(album, key: "album_\(album.id)")
}
func loadAlbumDetail(id: String) -> AlbumWithSongs? {
load(AlbumWithSongs.self, key: "album_\(id)")
}
func cacheArtistDetail(_ artist: ArtistWithAlbums) {
save(artist, key: "artist_\(artist.id)")
}
func loadArtistDetail(id: String) -> ArtistWithAlbums? {
load(ArtistWithAlbums.self, key: "artist_\(id)")
}
func cacheRadioStations(_ stations: [RadioStation]) {
save(stations, key: "radio_stations")
}
func loadRadioStations() -> [RadioStation]? {
load([RadioStation].self, key: "radio_stations")
}
// MARK: - Download Status Check
/// Returns set of song IDs that are downloaded for offline playback
func downloadedSongIds() -> Set<String> {
let key = "offline_catalog"
guard let data = UserDefaults.standard.data(forKey: key),
let songs = try? JSONDecoder().decode([DownloadedSong].self, from: data) else {
return []
}
return Set(songs.map { $0.id })
}
// MARK: - Clear
func clearAll() {
if let files = try? fileManager.contentsOfDirectory(at: cacheDir, includingPropertiesForKeys: nil) {
for file in files { try? fileManager.removeItem(at: file) }
}
}
}