import Foundation import CryptoKit import Darwin import ffmpegkit import SpotiFLACBackend struct CoreFFmpegCommand: Decodable { let command_id: String let arguments: [String] var output_path: String? = nil } final class CoreFFmpegExecution { let wait: (Int64) throws -> [CoreFFmpegCommand] let active: (String) throws -> Bool let complete: (String, Bool, String, String) throws -> Void let close: () -> Void private let lock = NSLock() private var running = true init(wait: @escaping (Int64) throws -> [CoreFFmpegCommand], active: @escaping (String) throws -> Bool, complete: @escaping (String, Bool, String, String) throws -> Void, close: @escaping () -> Void = {}) { self.wait = wait self.active = active self.complete = complete self.close = close } func run(execute: @escaping ([String], @escaping () -> Bool) -> (Bool, String) = executeCoreFFmpeg, block: () throws -> String) rethrows -> String { DispatchQueue.global(qos: .userInitiated).async { defer { self.close() } while self.isRunning() { guard let commands = try? self.wait(1_000) else { return } // A different operation may own any command in this claimed batch. for command in commands { let cancelled = { (try? self.active(command.command_id)) != true } let result: (Bool, String) if cancelled() { result = (false, "cancelled") } else if command.arguments.isEmpty { result = (false, "FFmpeg arguments are empty") } else { result = executeCoreFFmpegCommand(command: command, cancelled: cancelled, execute: execute) } try? self.complete(command.command_id, result.0, result.1, result.0 ? "" : result.1) } } } defer { lock.lock() running = false lock.unlock() } return try block() } private func isRunning() -> Bool { lock.lock() defer { lock.unlock() } return running } } private enum CoreFFmpegStaging { static let lock = NSLock() static var directories = Set() static let prefix = ".spotiflac-ffmpeg-\(Bundle.main.bundleIdentifier ?? "spotiflac")-" static func create(for target: URL) throws -> URL { let directory = target.deletingLastPathComponent().resolvingSymlinksInPath() lock.lock() defer { lock.unlock() } // One app process owns these stages. Sweep before its first command // in this directory; subsequent/concurrent commands may own live files. if !directories.contains(directory.path) { for file in try FileManager.default.contentsOfDirectory( at: directory, includingPropertiesForKeys: [.isRegularFileKey, .isSymbolicLinkKey] ) where file.lastPathComponent.hasPrefix(prefix) { let suffix = file.lastPathComponent.dropFirst(prefix.count) let token = String(suffix.prefix(36)) let values = try file.resourceValues(forKeys: [.isRegularFileKey, .isSymbolicLinkKey]) if UUID(uuidString: token) != nil, suffix.dropFirst(36).hasPrefix("."), values.isRegularFile == true, values.isSymbolicLink != true { try FileManager.default.removeItem(at: file) } } directories.insert(directory.path) } let staged = directory.appendingPathComponent("\(prefix)\(UUID().uuidString).\(target.pathExtension)") try Data().write(to: staged, options: .withoutOverwriting) return staged } } func executeCoreFFmpegCommand(command: CoreFFmpegCommand, cancelled: @escaping () -> Bool, execute: ([String], @escaping () -> Bool) -> (Bool, String)) -> (Bool, String) { if cancelled() { return (false, "cancelled") } guard let output = command.output_path, !output.isEmpty else { return execute(command.arguments, cancelled) } guard command.arguments.last == output else { return (false, "FFmpeg output does not match command") } let target = URL(fileURLWithPath: output) do { let staged = try CoreFFmpegStaging.create(for: target) defer { try? FileManager.default.removeItem(at: staged) } var arguments = command.arguments arguments[arguments.count - 1] = staged.path let result = execute(arguments, cancelled) if cancelled() { return (false, "cancelled") } if !result.0 { return result } // POSIX rename replaces the destination atomically on the same filesystem. guard rename(staged.path, target.path) == 0 else { throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno)) } return result } catch { return (false, error.localizedDescription) } } private func executeCoreFFmpeg(arguments: [String], cancelled: @escaping () -> Bool) -> (Bool, String) { if cancelled() { return (false, "cancelled") } let finished = DispatchSemaphore(value: 0) guard let session = FFmpegKit.execute(withArgumentsAsync: arguments, withCompleteCallback: { _ in finished.signal() }) else { return (false, "FFmpeg session could not start") } while finished.wait(timeout: .now() + 0.2) == .timedOut { if cancelled() { session.cancel() _ = finished.wait(timeout: .now() + 5) return (false, "cancelled") } } if cancelled() { return (false, "cancelled") } return (ReturnCode.isSuccess(session.getReturnCode()), session.getOutput() ?? "") } /// Owns one operation's directory access through finalization. final class CoreDirectoryScope { private let lock = NSLock() private var release: (() -> Void)? init(_ release: @escaping () -> Void) { self.release = release } func close() { lock.lock() defer { lock.unlock() } let action = release release = nil action?() } deinit { close() } } /// Native migration boundary; each process selects one stateful backend. protocol CoreBackend { var implementation: String { get } var routesApplication: Bool { get } func invokeApplication(method: String, arguments: Any?) throws -> Any? func completeAuthCallback(state: String, code: String, sessionGrant: Bool, onResolved: (String) -> Void) throws func openDownloadProgress() throws -> CoreDownloadProgress func cancelActiveDownloads() throws -> String func downloadByStrategy(requestJson: String) throws -> String func runPostProcessing(inputJson: String, metadataJson: String) throws -> String func buildFilename(template: String, metadataJson: String) throws -> String func sanitizeFilename(filename: String) -> String func fileMetadataImplementation(path: String) -> String func readFileMetadata(path: String, hint: String) throws -> String func checkHiResAuthenticity(path: String, optionsJson: String) throws -> String func editFileMetadata(path: String, metadataJson: String) throws -> String func reEnrichFile(requestJson: String) throws -> String func rewriteSplitArtistTags(path: String, artist: String, albumArtist: String) throws -> String func extractCoverToFile(audioPath: String, outputPath: String) throws func writeM4aFreeformTags(path: String, metadataJson: String) throws -> String func ensureAc4Config(path: String, reference: String) throws -> String func writeAc4Metadata(path: String, metadataJson: String, coverPath: String) throws -> String func setLibraryCoverCacheDirectory(path: String) throws func scanLibraryFolder(folder: String) throws -> String func scanLibraryFolderToNdjsonFile(folder: String, output: String) throws -> Int func scanLibraryFolderIncremental(folder: String, existing: String) throws -> String func getLibraryScanProgress() throws -> String func cancelLibraryScan() throws func parseCueSheet(path: String, audioDirectory: String) throws -> String func openDownloadDirectory(path: String) throws -> CoreDirectoryScope func createTemporaryMediaFile(prefix: String, suffix: String) throws -> URL func downloadCoverToFileSized(url: String, outputPath: String, maxDimension: Int64) throws func releaseIdleResources() throws func releaseMemoryUnderPressure() throws } extension CoreBackend { var routesApplication: Bool { false } func invokeApplication(method: String, arguments: Any?) throws -> Any? { throw NSError(domain: "CoreBackend", code: 1, userInfo: [NSLocalizedDescriptionKey: "Application routing is unavailable for \(method)"]) } } final class RustCoreBackend: CoreBackend { static let shared = RustCoreBackend() let implementation = "rust" let routesApplication = true private let root = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0].appendingPathComponent("rust-core-pilot", isDirectory: true) private let ownerLock = NSLock() private var manager: ExtensionManager? private var repository: ExtensionRepository? private var requests: CancellationRegistry? private var identity: [String]? private var loggingEnabled = false private var allowPrivateNetwork = false private var allowHttpFallback = false private var fallbackProviders: [String]? private var directoryScopes: [String: CoreDirectoryScope] = [:] private var libraryCoverScope: CoreDirectoryScope? private init() {} private func withFFmpegCommands(_ current: ExtensionManager, block: () throws -> String) throws -> String { let commands = try current.environment().ffmpegCommands() let execution = CoreFFmpegExecution(wait: { timeout in try JSONDecoder().decode([CoreFFmpegCommand].self, from: Data(commands.waitPending(timeoutMs: timeout).utf8)) }, active: { id in try !commands.getCommand(commandId: id).isEmpty }, complete: { id, success, output, error in _ = try commands.complete(commandId: id, success: success, output: output, error: error) }) return try execution.run(block: block) } func downloadByStrategy(requestJson: String) throws -> String { let current = try owner() return try withFFmpegCommands(current) { try current.downloadByStrategy(requestJson: requestJson) } } func runPostProcessing(inputJson: String, metadataJson: String) throws -> String { let current = try owner() return try withFFmpegCommands(current) { try current.runPostProcessing(inputJson: inputJson, metadataJson: metadataJson, timeoutMs: 120_000) } } private func owner() throws -> ExtensionManager { ownerLock.lock() defer { ownerLock.unlock() } guard let manager = manager else { throw failure("Rust backend is not initialized") } return manager } private func requestRegistryLocked() -> CancellationRegistry { if let requests = requests { return requests } let created = CancellationRegistry(domain: .extensionRequest) requests = created return created } private func acquireRequest(_ id: String) throws -> (ExtensionManager, RequestLease) { ownerLock.lock() defer { ownerLock.unlock() } guard let manager = manager else { throw failure("Rust backend is not initialized") } return (manager, try requestRegistryLocked().acquire(id: id)) } private func repositoryOwner() throws -> ExtensionRepository { ownerLock.lock() defer { ownerLock.unlock() } guard let repository = repository else { throw failure("Extension repository is not initialized") } return repository } private func initializeRepository(_ cachePath: String) throws { ownerLock.lock() defer { ownerLock.unlock() } guard let manager = manager else { throw failure("Rust backend is not initialized") } if repository != nil { return } guard cachePath.hasPrefix("/") else { throw failure("Repository cache directory must be absolute") } let cache = URL(fileURLWithPath: cachePath).resolvingSymlinksInPath().standardizedFileURL.path repository = try ExtensionRepository(manager: manager, cacheDirectory: cache) } private func failure(_ message: String) -> NSError { NSError(domain: "RustCoreBackend", code: 1, userInfo: [NSLocalizedDescriptionKey: message]) } private func directoryAliases(_ value: String, sources: String, data: String) throws -> [String] { guard value.hasPrefix("/") else { throw failure("Output directories must be absolute") } let url = URL(fileURLWithPath: value) let path = url.resolvingSymlinksInPath().standardizedFileURL.path guard path != "/", ![sources, data].contains(where: { path == $0 || path.hasPrefix($0 + "/") || $0.hasPrefix(path + "/") }) else { throw failure("Output directory overlaps extension storage") } let original = url.standardizedFileURL.path return path == original ? [path] : [path, original] } private func openDownloadDirectoryLocked(path: String) throws -> CoreDirectoryScope { guard let manager = manager, let storage = identity else { throw failure("Rust backend is not initialized") } let paths = try directoryAliases(path, sources: storage[0], data: storage[1]) let scope = try manager.environment().grantDownloadDirectories(directories: paths) return CoreDirectoryScope { scope.release() } } func openDownloadDirectory(path: String) throws -> CoreDirectoryScope { ownerLock.lock() defer { ownerLock.unlock() } return try openDownloadDirectoryLocked(path: path) } private func withLibraryDirectories(_ paths: [String], block: (ExtensionManager) throws -> T) throws -> T { let (current, scope): (ExtensionManager, CoreDirectoryScope) = try { ownerLock.lock() defer { ownerLock.unlock() } guard let manager = manager, let storage = identity else { throw failure("Rust backend is not initialized") } let aliases = try paths.flatMap { try directoryAliases($0, sources: storage[0], data: storage[1]) } let grant = try manager.environment().grantDownloadDirectories(directories: Array(Set(aliases))) return (manager, CoreDirectoryScope { grant.release() }) }() defer { scope.close() } // Progress and cancellation must remain available while the scan runs. return try block(current) } func setLibraryCoverCacheDirectory(path: String) throws { ownerLock.lock() defer { ownerLock.unlock() } guard let manager = manager else { throw failure("Rust backend is not initialized") } guard path.isEmpty || path.hasPrefix("/") else { throw failure("Library cover directory must be absolute") } let directory = path.isEmpty ? nil : URL(fileURLWithPath: path).resolvingSymlinksInPath().standardizedFileURL if let directory = directory { try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) } let next = try directory.map { try openDownloadDirectoryLocked(path: $0.path) } do { try manager.setLibraryCoverCacheDirectory(directory: directory?.path ?? "") } catch { next?.close(); throw error } libraryCoverScope?.close() libraryCoverScope = next } func scanLibraryFolder(folder: String) throws -> String { try withLibraryDirectories([folder]) { try $0.scanLibraryFolder(folder: URL(fileURLWithPath: folder).resolvingSymlinksInPath().standardizedFileURL.path, lease: nil) } } func scanLibraryFolderToNdjsonFile(folder: String, output: String) throws -> Int { guard output.hasPrefix("/"), URL(fileURLWithPath: output).pathExtension.lowercased() == "ndjson" else { throw failure("Library scan output must be an absolute NDJSON path") } return try withLibraryDirectories([folder]) { current in // Application support also contains extension storage. Stage inside // the existing private root instead of granting that whole directory. let staged = root.appendingPathComponent("files", isDirectory: true) .appendingPathComponent("library_scan_\(UUID().uuidString).ndjson") defer { try? FileManager.default.removeItem(at: staged) } let count = try current.scanLibraryFolderToNdjsonFile( folder: URL(fileURLWithPath: folder).resolvingSymlinksInPath().standardizedFileURL.path, output: staged.resolvingSymlinksInPath().path, lease: nil) guard rename(staged.path, output) == 0 else { throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno)) } return Int(count) } } func scanLibraryFolderIncremental(folder: String, existing: String) throws -> String { try withLibraryDirectories([folder]) { try $0.scanLibraryFolderIncremental(folder: URL(fileURLWithPath: folder).resolvingSymlinksInPath().standardizedFileURL.path, existingJson: existing, lease: nil) } } func getLibraryScanProgress() throws -> String { ownerLock.lock() let current = manager ownerLock.unlock() return try current?.getLibraryScanProgress() ?? "{}" } func cancelLibraryScan() throws { ownerLock.lock() let current = manager ownerLock.unlock() try current?.cancelLibraryScan() } func parseCueSheet(path: String, audioDirectory: String) throws -> String { let cue = URL(fileURLWithPath: path).resolvingSymlinksInPath().standardizedFileURL let audio = audioDirectory.isEmpty ? cue.deletingLastPathComponent() : URL(fileURLWithPath: audioDirectory).resolvingSymlinksInPath().standardizedFileURL return try withLibraryDirectories([cue.deletingLastPathComponent().path, audio.path]) { try $0.parseCueFileJson(path: cue.path, audioDirectory: audio.path, lease: nil) } } func createTemporaryMediaFile(prefix: String, suffix: String) throws -> URL { _ = try owner() let file = root.appendingPathComponent("files", isDirectory: true) .appendingPathComponent("\(prefix)\(UUID().uuidString)\(suffix)") try Data().write(to: file, options: .withoutOverwriting) return file } func downloadCoverToFileSized(url: String, outputPath: String, maxDimension: Int64) throws { try owner().downloadCoverToFileSized(url: url, outputPath: URL(fileURLWithPath: outputPath).resolvingSymlinksInPath().standardizedFileURL.path, maxDimension: maxDimension, lease: nil) } func releaseIdleResources() throws { try owner().releaseMemory(underPressure: false) } func releaseMemoryUnderPressure() throws { try owner().releaseMemory(underPressure: true) } private func initializeOwner(_ arguments: [String: Any]) throws { guard let sourcePath = arguments["extensions_dir"] as? String, let dataPath = arguments["data_dir"] as? String, let key = arguments["master_key"] as? String else { throw failure("Extension storage arguments are required") } guard sourcePath.hasPrefix("/"), dataPath.hasPrefix("/") else { throw failure("Extension storage directories must be absolute") } let sources = URL(fileURLWithPath: sourcePath).resolvingSymlinksInPath().standardizedFileURL.path let data = URL(fileURLWithPath: dataPath).resolvingSymlinksInPath().standardizedFileURL.path let keyHash = SHA256.hash(data: Data(key.utf8)).map { String(format: "%02x", $0) }.joined() let requested = [sources, data, keyHash] ownerLock.lock() defer { ownerLock.unlock() } let files = root.appendingPathComponent("files", isDirectory: true) try FileManager.default.createDirectory(at: files, withIntermediateDirectories: true) guard arguments["allowed_directories"] == nil || arguments["allowed_directories"] is [String] else { throw failure("Invalid output directories") } var allowedDirectories = [files.resolvingSymlinksInPath().path, files.standardizedFileURL.path] for value in arguments["allowed_directories"] as? [String] ?? [] { for alias in try directoryAliases(value, sources: sources, data: data) where !allowedDirectories.contains(alias) { allowedDirectories.append(alias) } } if manager != nil { guard identity == requested else { throw failure("Rust backend is already initialized with different storage") } try manager!.environment().setAllowedDownloadDirectories(directories: allowedDirectories) return } let created = try ExtensionManager.withLyricsSettings( sourceDirectory: sources, dataDirectory: data, masterKey: key, appVersion: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1", timeoutMs: 30000, providersJson: arguments["lyrics_providers_json"] as? String ?? "[]", optionsJson: arguments["lyrics_options_json"] as? String ?? "{}" ) do { let environment = created.environment() try environment.setAllowedDownloadDirectories(directories: allowedDirectories) try environment.setAllowPrivateNetwork(allow: allowPrivateNetwork) try environment.setNetworkCompatibilityOptions(allowHttp: allowHttpFallback, insecureTls: false) try environment.logBuffer().setEnabled(enabled: loggingEnabled) try created.setFallbackProviders(ids: fallbackProviders) } catch { created.shutdown() throw error } identity = requested manager = created } private func shutdownOwner() { ownerLock.lock() defer { ownerLock.unlock() } requests?.shutdown() requests = nil repository?.shutdown() repository = nil manager?.shutdown() manager = nil directoryScopes.values.forEach { $0.close() } directoryScopes.removeAll() libraryCoverScope?.close() libraryCoverScope = nil identity = nil } func fileMetadataImplementation(path: String) -> String { "rust" } func readFileMetadata(path: String, hint: String) throws -> String { try SpotiFLACBackend.readFileMetadata(path: path, hint: hint, lease: nil) } func checkHiResAuthenticity(path: String, optionsJson: String) throws -> String { try SpotiFLACBackend.checkHiresAuthenticity(path: path, optionsJson: optionsJson, lease: nil) } func editFileMetadata(path: String, metadataJson: String) throws -> String { try owner().editFileMetadata(path: URL(fileURLWithPath: path).resolvingSymlinksInPath().standardizedFileURL.path, metadataJson: metadataJson, lease: nil) } private func mediaPath(_ path: String) -> String { path.isEmpty ? "" : URL(fileURLWithPath: path).resolvingSymlinksInPath().standardizedFileURL.path } func reEnrichFile(requestJson: String) throws -> String { guard var request = try JSONSerialization.jsonObject(with: Data(requestJson.utf8)) as? [String: Any] else { throw failure("Re-enrich request must be an object") } if request["preview_only"] as? Bool != true, let path = request["file_path"] as? String, path.hasPrefix("/") { request["file_path"] = mediaPath(path) } let data = try JSONSerialization.data(withJSONObject: request) return try owner().reenrichFile(requestJson: String(decoding: data, as: UTF8.self), lease: nil) } func rewriteSplitArtistTags(path: String, artist: String, albumArtist: String) throws -> String { try owner().rewriteSplitArtistTags(path: mediaPath(path), artist: artist, albumArtist: albumArtist, lease: nil) } func extractCoverToFile(audioPath: String, outputPath: String) throws { try owner().extractCoverToFile(audioPath: mediaPath(audioPath), outputPath: mediaPath(outputPath), lease: nil) } func writeM4aFreeformTags(path: String, metadataJson: String) throws -> String { try owner().writeM4aFreeformTags(path: mediaPath(path), metadataJson: metadataJson, lease: nil) } func ensureAc4Config(path: String, reference: String) throws -> String { try owner().ensureAc4Config(path: mediaPath(path), reference: mediaPath(reference), lease: nil) } func writeAc4Metadata(path: String, metadataJson: String, coverPath: String) throws -> String { try owner().writeAc4Metadata(path: mediaPath(path), metadataJson: metadataJson, coverPath: mediaPath(coverPath), lease: nil) } func completeAuthCallback(state: String, code: String, sessionGrant: Bool, onResolved: (String) -> Void) throws { let current = try owner() let environment = current.environment() let id = try (sessionGrant ? environment.resolveCallbackState(state: state) : environment.consumeCallbackState(state: state)) onResolved(id) if sessionGrant { try completeSessionGrant(current, id: id, grant: code) } else { try environment.setAuthCode(extensionId: id, code: code) _ = try current.invokeAction(extensionId: id, action: "completeSpotifyLogin") } } func openDownloadProgress() throws -> CoreDownloadProgress { let subscription = try owner().environment().downloadState().subscribeProgress() return CoreDownloadProgress( wait: { try subscription.waitDelta(since: $0, timeoutMs: $1) }, close: { subscription.stop() } ) } func cancelActiveDownloads() throws -> String { let ids = try owner().environment().downloadState().cancelActiveDownloads() return String(decoding: try JSONSerialization.data(withJSONObject: ids), as: UTF8.self) } private func completeSessionGrant(_ current: ExtensionManager, id: String, grant: String) throws { try current.environment().setSessionGrant(extensionId: id, grant: grant) let response = try current.invokeAction(extensionId: id, action: "completeGrant") try requireSuccessfulExtensionAction(extensionId: id, actionName: "completeGrant", response: response) } func invokeApplication(method: String, arguments: Any?) throws -> Any? { let args = arguments as? [String: Any] ?? [:] func string(_ key: String, _ fallback: String = "") -> String { args[key] as? String ?? fallback } func ids(_ raw: String) throws -> [String] { guard let value = try JSONSerialization.jsonObject(with: Data(raw.utf8)) as? [String] else { throw failure("Expected an array of extension IDs") } return value } switch method { case "cancelExtensionRequest": ownerLock.lock() defer { ownerLock.unlock() } try requestRegistryLocked().cancel(id: string("request_id")) return nil case "customSearchWithExtension", "getExtensionHomeFeed": let (current, lease) = try acquireRequest(string("request_id")) defer { lease.release() } if method == "customSearchWithExtension" { return try current.customSearchJson(extensionId: string("extension_id"), query: string("query"), optionsJson: string("options"), lease: lease) } return try current.getExtensionHomeFeedJson(extensionId: string("extension_id"), lease: lease) case "initExtensionSystem": try initializeOwner(args); return nil case "setDownloadDirectory": ownerLock.lock() defer { ownerLock.unlock() } guard let manager = manager, let storage = identity else { throw failure("Rust backend is not initialized") } let files = root.appendingPathComponent("files", isDirectory: true) var paths = [files.resolvingSymlinksInPath().path, files.standardizedFileURL.path] let path = string("path") if !path.isEmpty { paths += try directoryAliases(path, sources: storage[0], data: storage[1]) } try manager.environment().setAllowedDownloadDirectories(directories: paths) return nil case "acquireDownloadDirectory": ownerLock.lock() defer { ownerLock.unlock() } let scope = try openDownloadDirectoryLocked(path: string("path")) let token = UUID().uuidString directoryScopes[token] = scope return token case "releaseDownloadDirectory": ownerLock.lock() let scope = directoryScopes.removeValue(forKey: string("token")) ownerLock.unlock() scope?.close() return nil case "initExtensionRepo": try initializeRepository(string("cache_dir")); return nil case "getRepoRegistryUrl": return try repositoryOwner().registryUrl() case "setRepoRegistryUrl": try repositoryOwner().setRegistryUrl(url: string("registry_url")); return nil case "clearRepoRegistryUrl": try repositoryOwner().clearRegistryUrl(); return nil case "getRepoExtensions": return try repositoryOwner().extensions(forceRefresh: args["force_refresh"] as? Bool ?? false) case "searchRepoExtensions": return try repositoryOwner().search(query: string("query"), category: string("category")) case "getRepoCategories": return String(decoding: try JSONSerialization.data(withJSONObject: repositoryOwner().categories()), as: UTF8.self) case "downloadRepoExtension": return try repositoryOwner().download(extensionId: string("extension_id"), destinationDirectory: string("dest_dir")) case "clearRepoCache": try repositoryOwner().clearCache(); return nil case "cleanupExtensions": shutdownOwner(); return nil case "buildFilename": return try buildFilename(template: string("template"), metadataJson: string("metadata", "{}")) case "sanitizeFilename": return sanitizeFilename(filename: string("filename")) case "readFileMetadata": return try readFileMetadata(path: string("file_path"), hint: string("display_name")) case "editFileMetadata": return try editFileMetadata(path: string("file_path"), metadataJson: string("metadata_json", "{}")) case "getLogsSince": ownerLock.lock() defer { ownerLock.unlock() } return try manager?.environment().logBuffer().since(index: (args["index"] as? NSNumber)?.int64Value ?? 0) ?? "{\"logs\":[],\"next_index\":0}" case "clearLogs": ownerLock.lock() defer { ownerLock.unlock() } try manager?.environment().logBuffer().clear() return nil case "setLoggingEnabled", "setAllowPrivateNetwork", "setDownloadFallbackExtensionIds", "setLyricsProviders", "setLyricsFetchOptions", "setNetworkCompatibilityOptions", "setSongLinkNetworkOptions": ownerLock.lock() defer { ownerLock.unlock() } switch method { case "setLoggingEnabled": let enabled = args["enabled"] as? Bool ?? false try manager?.environment().logBuffer().setEnabled(enabled: enabled) loggingEnabled = enabled case "setAllowPrivateNetwork": let allowed = args["allowed"] as? Bool ?? false try manager?.environment().setAllowPrivateNetwork(allow: allowed) allowPrivateNetwork = allowed case "setNetworkCompatibilityOptions", "setSongLinkNetworkOptions": let allowed = args["allow_http"] as? Bool ?? false let insecureTLS = args["insecure_tls"] as? Bool ?? false try manager?.environment().setNetworkCompatibilityOptions(allowHttp: allowed, insecureTls: insecureTLS) allowHttpFallback = allowed case "setDownloadFallbackExtensionIds": let raw = string("extension_ids").trimmingCharacters(in: .whitespacesAndNewlines) let value = raw.isEmpty || raw == "null" ? nil : try ids(raw) try manager?.setFallbackProviders(ids: value) fallbackProviders = value case "setLyricsProviders": let raw = string("providers_json", "[]") if let manager = manager { try manager.setLyricsProvidersJson(providersJson: raw) } else { _ = try ids(raw) } return "{\"success\":true}" case "setLyricsFetchOptions": let raw = string("options_json", "{}") if let manager = manager { try manager.setLyricsFetchOptionsJson(optionsJson: raw) } else { _ = try JSONSerialization.jsonObject(with: Data(raw.utf8)) } return "{\"success\":true}" default: break } return nil default: break } let current = try owner() switch method { case "loadExtensionsFromDir": let path = URL(fileURLWithPath: string("dir_path")).resolvingSymlinksInPath().standardizedFileURL.path ownerLock.lock() let matches = identity?.first == path ownerLock.unlock() guard matches else { throw failure("Extension source directory does not match the initialized owner") } return try current.loadAll() case "loadExtensionFromPath": return try current.install(packagePath: string("file_path")) case "upgradeExtension": return try current.upgrade(packagePath: string("file_path")) case "checkExtensionUpgrade": return try current.checkUpgrade(packagePath: string("file_path")) case "getInstalledExtensions": return try current.installed() case "setExtensionEnabled": try current.setEnabled(extensionId: string("extension_id"), enabled: args["enabled"] as? Bool ?? false); return nil case "unloadExtension": try current.unload(extensionId: string("extension_id")); return nil case "removeExtension": try current.remove(extensionId: string("extension_id")); return nil case "getExtensionSettings": return try current.environment().settings(extensionId: string("extension_id")) case "setExtensionSettings": try current.updateSettings(extensionId: string("extension_id"), settingsJson: string("settings", "{}")); return nil case "invokeExtensionAction": return try current.invokeAction(extensionId: string("extension_id"), action: string("action")) case "checkExtensionHealth": return try current.checkExtensionHealthJson(extensionId: string("extension_id")) case "searchTracksWithMetadataProviders": return try current.searchMetadataProviders(query: string("query"), limit: (args["limit"] as? NSNumber)?.int64Value ?? 20, includeExtensions: args["include_extensions"] as? Bool ?? true, itemId: "", timeoutMs: 30000) case "searchTracksWithMetadataProvider": return try current.searchMetadataProvider(extensionId: string("extension_id"), query: string("query"), limit: (args["limit"] as? NSNumber)?.int64Value ?? 20, timeoutMs: 30000) case "getProviderMetadata": return try current.getProviderMetadataJson(providerId: string("provider_id"), resourceType: string("resource_type"), resourceId: string("resource_id"), lease: nil) case "findCollectionAcrossExtensions": return try current.findCollectionAcrossExtensionsJson(requestJson: arguments as? String ?? "{}", lease: nil) case "enrichTrackWithExtension": return try current.enrichTrackJson(extensionId: string("extension_id"), trackJson: string("track", "{}")) case "handleURLWithExtension": return try current.handleUrlJson(url: string("url")) case "findURLHandler": return try current.findUrlHandler(url: string("url")) ?? "" case "searchDeezerByISRC": return try current.searchDeezerByIsrcForItemId(isrc: string("isrc"), itemId: string("item_id"), lease: nil) case "getDeezerExtendedMetadata": return try current.getDeezerExtendedMetadata(trackId: string("track_id"), lease: nil) case "convertSpotifyToDeezer": return try current.convertSpotifyToDeezer(resourceType: string("resource_type"), spotifyId: string("spotify_id"), lease: nil) case "getSpotifyIDFromDeezerTrack": return try current.getSpotifyIdFromDeezerTrack(trackId: string("deezer_track_id"), lease: nil) case "getTidalURLFromDeezerTrack": return try current.getTidalUrlFromDeezerTrack(trackId: string("deezer_track_id"), lease: nil) case "getTrackPlatformLinks": return try current.getTrackPlatformLinksJson(spotifyId: string("spotify_id"), isrc: string("isrc"), lease: nil) case "fetchMusicBrainzTags": let genre = (try? current.fetchMusicBrainzGenreByIsrc(isrc: string("isrc"), lease: nil)) ?? "" let albumArtist = (try? current.fetchMusicBrainzAlbumArtistByIsrc(isrc: string("isrc"), albumName: string("album_name"), lease: nil)) ?? "" return String(decoding: try JSONSerialization.data(withJSONObject: ["genre": genre, "album_artist": albumArtist]), as: UTF8.self) case "getTrackCacheSize": return Int(try current.getTrackCacheSize()) case "readAudioMetadata": return try current.readAudioMetadata(path: string("file_path"), hint: "", cacheKey: "", lease: nil) case "clearTrackCache": try current.clearTrackIdCache(); return nil case "setMetadataLanguage": try current.setMetadataLanguage(tag: string("tag")); return nil case "downloadByStrategy", "downloadWithExtensions": guard let request = arguments as? String else { throw NSError(domain: "CoreBackend", code: 1, userInfo: [NSLocalizedDescriptionKey: "Download request must be a JSON string"]) } return try withFFmpegCommands(current) { if method == "downloadWithExtensions" { return try current.downloadWithExtensionsJson(requestJson: request) } return try current.downloadByStrategy(requestJson: request) } case "getAllDownloadProgress": return try current.environment().downloadState().allProgress() case "cleanupConnections": try current.environment().cleanupConnections(); return nil case "clearItemProgress": try current.environment().downloadState().clearItemProgress(itemId: string("item_id")); return nil case "cancelDownload": try current.environment().downloadState().cancelDownload(itemId: string("item_id")); return nil case "resetDownloadCancel": try current.environment().downloadState().resetDownloadCancel(itemId: string("item_id")); return nil case "getExtensionPendingAuth": return try current.getExtensionPendingAuthJson(extensionId: string("extension_id")) case "setExtensionAuthCode": try current.environment().setAuthCode(extensionId: string("extension_id"), code: string("auth_code")); return nil case "completeExtensionSessionGrant": try completeSessionGrant(current, id: string("extension_id"), grant: string("grant")) return true case "setExtensionTokens": try current.environment().setAuthTokens(extensionId: string("extension_id"), accessToken: string("access_token"), refreshToken: string("refresh_token"), expiresIn: (args["expires_in"] as? NSNumber)?.int64Value ?? 0) return nil case "clearExtensionPendingAuth": try current.environment().clearPendingAuth(extensionId: string("extension_id")); return nil case "isExtensionAuthenticated": return try current.environment().isAuthenticated(extensionId: string("extension_id")) case "getAllPendingAuthRequests": return try current.environment().allPendingAuth() case "getLyricsLRC", "getLyricsLRCWithSource", "fetchAndSaveLyrics": let request = LyricsRequest( spotifyId: string("spotify_id"), track: string("track_name"), artist: string("artist_name"), filePath: string(method == "fetchAndSaveLyrics" ? "audio_file_path" : "file_path"), durationMs: (args["duration_ms"] as? NSNumber)?.int64Value ?? 0 ) if method == "getLyricsLRC" { return try current.getLyricsLrc(request: request, lease: nil) } if method == "getLyricsLRCWithSource" { return try current.getLyricsLrcWithSource(request: request, lease: nil) } try current.fetchAndSaveLyrics(request: request, outputPath: string("output_path"), lease: nil) return "{\"success\":true}" case "getPendingFFmpegCommand": return try current.environment().ffmpegCommands().getCommand(commandId: string("command_id")) case "getAllPendingFFmpegCommands": return try current.environment().ffmpegCommands().pending() case "setFFmpegCommandResult": _ = try current.environment().ffmpegCommands().complete( commandId: string("command_id"), success: args["success"] as? Bool ?? false, output: string("output"), error: string("error") ) return nil case "runPostProcessingV2": return try withFFmpegCommands(current) { try current.runPostProcessing(inputJson: string("input"), metadataJson: string("metadata"), timeoutMs: 120_000) } case "embedLyricsToFile": return try current.embedLyricsToFile(path: string("file_path"), lyrics: string("lyrics"), lease: nil) case "setProviderPriority", "setMetadataProviderPriority": try current.setProviderPriority(kind: method == "setProviderPriority" ? "download" : "metadata", ids: ids(string("priority", "[]"))) return nil case "getProviderPriority", "getMetadataProviderPriority": let priorities = try JSONSerialization.jsonObject(with: Data(current.providerPriorities().utf8)) as! [String: Any] let value = priorities[method == "getProviderPriority" ? "download" : "metadata"] as? [String] ?? [] return String(decoding: try JSONSerialization.data(withJSONObject: value), as: UTF8.self) case "getLyricsProviders": return try current.getLyricsProvidersJson() case "getLyricsFetchOptions": return try current.getLyricsFetchOptionsJson() case "getAvailableLyricsProviders": return try current.getAvailableLyricsProvidersJson() default: throw failure("Rust application method is not connected yet: \(method)") } } func buildFilename(template: String, metadataJson: String) throws -> String { try SpotiFLACBackend.buildFilename(template: template, metadataJson: metadataJson) } func sanitizeFilename(filename: String) -> String { SpotiFLACBackend.sanitizeFilename(filename: filename) } } func requireSuccessfulExtensionAction(extensionId: String, actionName: String, response: String?) throws { let text = response ?? "" guard let data = text.data(using: .utf8), let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { throw NSError( domain: "SpotiFLAC", code: 1, userInfo: [NSLocalizedDescriptionKey: "Extension \(actionName) for \(extensionId) returned invalid JSON: \(String(text.prefix(240)))"] ) } if (obj["success"] as? Bool) == true { return } let error = (obj["error"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? (obj["message"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? String(text.prefix(240)) throw NSError( domain: "SpotiFLAC", code: 2, userInfo: [NSLocalizedDescriptionKey: "Extension \(actionName) failed for \(extensionId): \(error)"] ) } func createCoreBackend() -> CoreBackend { return RustCoreBackend.shared }