import Foundation import SwiftUI import UniformTypeIdentifiers /// What an export file holds: stem playlists (with each song's mix) and the non-secret sound settings. /// Plex and stem server tokens and URLs are never included. struct StemifyExport: Codable { static let format = "stemify-export" static let version = 1 struct Prefs: Codable { var stemQuality: String? var keepStemMix: Bool? var karaokeVocals: Int? var karaokeSoftDb: Int? var playbackSpeed: Double? var downloadsWifiOnly: Bool? } var format = StemifyExport.format var version = StemifyExport.version var exported = Date() var playlists: [StemPlaylist] var prefs: Prefs @MainActor static func capture(_ lists: StemPlaylists, _ s: Settings) -> StemifyExport { StemifyExport(playlists: lists.lists, prefs: Prefs( stemQuality: s.stemQuality.rawValue, keepStemMix: s.keepStemMix, karaokeVocals: s.karaokeVocals, karaokeSoftDb: s.karaokeSoftDb, playbackSpeed: s.playbackSpeed, downloadsWifiOnly: s.downloadsWifiOnly)) } func encoded() throws -> Data { let e = JSONEncoder() e.outputFormatting = [.prettyPrinted, .sortedKeys] e.dateEncodingStrategy = .iso8601 return try e.encode(self) } static func decode(_ data: Data) throws -> StemifyExport { let d = JSONDecoder() d.dateDecodingStrategy = .iso8601 let x = try d.decode(StemifyExport.self, from: data) guard x.format == Self.format else { throw BackupError.notAnExport } guard x.version <= Self.version else { throw BackupError.newerVersion } return x } @MainActor func apply(to s: Settings) { if let v = prefs.stemQuality.flatMap(StemQuality.init(rawValue:)) { s.stemQuality = v } if let v = prefs.keepStemMix { s.keepStemMix = v } if let v = prefs.karaokeVocals { s.karaokeVocals = v } if let v = prefs.karaokeSoftDb { s.karaokeSoftDb = v } if let v = prefs.playbackSpeed { s.playbackSpeed = v } if let v = prefs.downloadsWifiOnly { s.downloadsWifiOnly = v } } } enum BackupError: LocalizedError { case notAnExport, newerVersion var errorDescription: String? { switch self { case .notAnExport: "That file isn't a Stemify export." case .newerVersion: "That export is from a newer version of Stemify." } } } /// Where an export goes and comes from. The Files sheet covers local storage and iCloud Drive; /// a direct iCloud (ubiquity container / CloudKit) backend would be another conformer. protocol BackupDestination { func write(_ data: Data, name: String) async throws func read() async throws -> Data } /// The export as a document for fileExporter, which also reaches iCloud Drive through Files. struct ExportDocument: FileDocument { static var readableContentTypes: [UTType] { [.json] } var data: Data init(data: Data = Data()) { self.data = data } init(configuration: ReadConfiguration) throws { data = configuration.file.regularFileContents ?? Data() } func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { FileWrapper(regularFileWithContents: data) } }