mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-22 19:07:16 +02:00
Add set/get/clear store registry URL method channel handlers on Android, iOS, and Go backend so users can configure a custom extension repository. Store tab now shows a setup screen when no registry URL is configured, with a cleaner layout (removed redundant description and helper text) and visible TextField borders for dark theme. Minor comment and formatting cleanups across several files.
1094 lines
45 KiB
Swift
1094 lines
45 KiB
Swift
import Flutter
|
|
import UIKit
|
|
import Gobackend // Import Go framework
|
|
|
|
@main
|
|
@objc class AppDelegate: FlutterAppDelegate {
|
|
private let CHANNEL = "com.zarz.spotiflac/backend"
|
|
private let DOWNLOAD_PROGRESS_STREAM_CHANNEL = "com.zarz.spotiflac/download_progress_stream"
|
|
private let LIBRARY_SCAN_PROGRESS_STREAM_CHANNEL = "com.zarz.spotiflac/library_scan_progress_stream"
|
|
private let streamQueue = DispatchQueue(label: "com.zarz.spotiflac.progress_stream", qos: .utility)
|
|
private var downloadProgressTimer: DispatchSourceTimer?
|
|
private var downloadProgressEventSink: FlutterEventSink?
|
|
private var lastDownloadProgressPayload: String?
|
|
private var libraryScanProgressTimer: DispatchSourceTimer?
|
|
private var libraryScanProgressEventSink: FlutterEventSink?
|
|
private var lastLibraryScanProgressPayload: String?
|
|
|
|
/// Currently accessed security-scoped URL for library folder
|
|
private var activeSecurityScopedURL: URL?
|
|
|
|
override func application(
|
|
_ application: UIApplication,
|
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
|
) -> Bool {
|
|
|
|
let controller = window?.rootViewController as! FlutterViewController
|
|
let channel = FlutterMethodChannel(
|
|
name: CHANNEL,
|
|
binaryMessenger: controller.binaryMessenger
|
|
)
|
|
let downloadProgressEvents = FlutterEventChannel(
|
|
name: DOWNLOAD_PROGRESS_STREAM_CHANNEL,
|
|
binaryMessenger: controller.binaryMessenger
|
|
)
|
|
let libraryScanProgressEvents = FlutterEventChannel(
|
|
name: LIBRARY_SCAN_PROGRESS_STREAM_CHANNEL,
|
|
binaryMessenger: controller.binaryMessenger
|
|
)
|
|
|
|
channel.setMethodCallHandler { [weak self] call, result in
|
|
self?.handleMethodCall(call: call, result: result)
|
|
}
|
|
downloadProgressEvents.setStreamHandler(
|
|
ClosureStreamHandler(
|
|
onListen: { [weak self] _, events in
|
|
self?.startDownloadProgressStream(events)
|
|
return nil
|
|
},
|
|
onCancel: { [weak self] _ in
|
|
self?.stopDownloadProgressStream()
|
|
return nil
|
|
}
|
|
)
|
|
)
|
|
libraryScanProgressEvents.setStreamHandler(
|
|
ClosureStreamHandler(
|
|
onListen: { [weak self] _, events in
|
|
self?.startLibraryScanProgressStream(events)
|
|
return nil
|
|
},
|
|
onCancel: { [weak self] _ in
|
|
self?.stopLibraryScanProgressStream()
|
|
return nil
|
|
}
|
|
)
|
|
)
|
|
|
|
GeneratedPluginRegistrant.register(with: self)
|
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
|
}
|
|
|
|
deinit {
|
|
stopDownloadProgressStream()
|
|
stopLibraryScanProgressStream()
|
|
}
|
|
|
|
private func startDownloadProgressStream(_ eventSink: @escaping FlutterEventSink) {
|
|
stopDownloadProgressStream()
|
|
downloadProgressEventSink = eventSink
|
|
lastDownloadProgressPayload = nil
|
|
|
|
let timer = DispatchSource.makeTimerSource(queue: streamQueue)
|
|
timer.schedule(deadline: .now(), repeating: .milliseconds(800))
|
|
timer.setEventHandler { [weak self] in
|
|
guard let self else { return }
|
|
let payload = GobackendGetAllDownloadProgress() as String? ?? "{}"
|
|
if payload == self.lastDownloadProgressPayload {
|
|
return
|
|
}
|
|
self.lastDownloadProgressPayload = payload
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.downloadProgressEventSink?(payload)
|
|
}
|
|
}
|
|
downloadProgressTimer = timer
|
|
timer.resume()
|
|
}
|
|
|
|
private func stopDownloadProgressStream() {
|
|
downloadProgressTimer?.setEventHandler {}
|
|
downloadProgressTimer?.cancel()
|
|
downloadProgressTimer = nil
|
|
downloadProgressEventSink = nil
|
|
lastDownloadProgressPayload = nil
|
|
}
|
|
|
|
private func startLibraryScanProgressStream(_ eventSink: @escaping FlutterEventSink) {
|
|
stopLibraryScanProgressStream()
|
|
libraryScanProgressEventSink = eventSink
|
|
lastLibraryScanProgressPayload = nil
|
|
|
|
let timer = DispatchSource.makeTimerSource(queue: streamQueue)
|
|
timer.schedule(deadline: .now(), repeating: .milliseconds(800))
|
|
timer.setEventHandler { [weak self] in
|
|
guard let self else { return }
|
|
let payload = GobackendGetLibraryScanProgressJSON() as String? ?? "{}"
|
|
if payload == self.lastLibraryScanProgressPayload {
|
|
return
|
|
}
|
|
self.lastLibraryScanProgressPayload = payload
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.libraryScanProgressEventSink?(payload)
|
|
}
|
|
}
|
|
libraryScanProgressTimer = timer
|
|
timer.resume()
|
|
}
|
|
|
|
private func stopLibraryScanProgressStream() {
|
|
libraryScanProgressTimer?.setEventHandler {}
|
|
libraryScanProgressTimer?.cancel()
|
|
libraryScanProgressTimer = nil
|
|
libraryScanProgressEventSink = nil
|
|
lastLibraryScanProgressPayload = nil
|
|
}
|
|
|
|
private func handleMethodCall(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
do {
|
|
let response = try self.invokeGoMethod(call: call)
|
|
DispatchQueue.main.async {
|
|
result(response)
|
|
}
|
|
} catch {
|
|
DispatchQueue.main.async {
|
|
result(FlutterError(code: "ERROR", message: error.localizedDescription, details: nil))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func invokeGoMethod(call: FlutterMethodCall) throws -> Any? {
|
|
var error: NSError?
|
|
|
|
switch call.method {
|
|
case "parseSpotifyUrl":
|
|
let args = call.arguments as! [String: Any]
|
|
let url = args["url"] as! String
|
|
let response = GobackendParseSpotifyURL(url, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "checkAvailability":
|
|
let args = call.arguments as! [String: Any]
|
|
let spotifyId = args["spotify_id"] as! String
|
|
let isrc = args["isrc"] as! String
|
|
let response = GobackendCheckAvailability(spotifyId, isrc, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "downloadByStrategy":
|
|
let requestJson = call.arguments as! String
|
|
let response = GobackendDownloadByStrategy(requestJson, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "getDownloadProgress":
|
|
let response = GobackendGetDownloadProgress()
|
|
return response
|
|
|
|
case "getAllDownloadProgress":
|
|
let response = GobackendGetAllDownloadProgress()
|
|
return response
|
|
|
|
case "initItemProgress":
|
|
let args = call.arguments as! [String: Any]
|
|
let itemId = args["item_id"] as! String
|
|
GobackendInitItemProgress(itemId)
|
|
return nil
|
|
|
|
case "finishItemProgress":
|
|
let args = call.arguments as! [String: Any]
|
|
let itemId = args["item_id"] as! String
|
|
GobackendFinishItemProgress(itemId)
|
|
return nil
|
|
|
|
case "clearItemProgress":
|
|
let args = call.arguments as! [String: Any]
|
|
let itemId = args["item_id"] as! String
|
|
GobackendClearItemProgress(itemId)
|
|
return nil
|
|
|
|
case "cancelDownload":
|
|
let args = call.arguments as! [String: Any]
|
|
let itemId = args["item_id"] as! String
|
|
GobackendCancelDownload(itemId)
|
|
return nil
|
|
|
|
case "setDownloadDirectory":
|
|
let args = call.arguments as! [String: Any]
|
|
let path = args["path"] as! String
|
|
GobackendSetDownloadDirectory(path, &error)
|
|
if let error = error { throw error }
|
|
return nil
|
|
|
|
case "setNetworkCompatibilityOptions", "setSongLinkNetworkOptions":
|
|
let args = call.arguments as! [String: Any]
|
|
let allowHTTP = args["allow_http"] as? Bool ?? false
|
|
let insecureTLS = args["insecure_tls"] as? Bool ?? false
|
|
GobackendSetNetworkCompatibilityOptions(allowHTTP, insecureTLS)
|
|
return nil
|
|
|
|
case "checkDuplicate":
|
|
let args = call.arguments as! [String: Any]
|
|
let outputDir = args["output_dir"] as! String
|
|
let isrc = args["isrc"] as! String
|
|
let response = GobackendCheckDuplicate(outputDir, isrc, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "checkDuplicatesBatch":
|
|
let args = call.arguments as! [String: Any]
|
|
let outputDir = args["output_dir"] as! String
|
|
let tracksJson = args["tracks"] as? String ?? "[]"
|
|
let response = GobackendCheckDuplicatesBatch(outputDir, tracksJson, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "preBuildDuplicateIndex":
|
|
let args = call.arguments as! [String: Any]
|
|
let outputDir = args["output_dir"] as! String
|
|
GobackendPreBuildDuplicateIndex(outputDir, &error)
|
|
if let error = error { throw error }
|
|
return nil
|
|
|
|
case "invalidateDuplicateIndex":
|
|
let args = call.arguments as! [String: Any]
|
|
let outputDir = args["output_dir"] as! String
|
|
GobackendInvalidateDuplicateIndex(outputDir)
|
|
return nil
|
|
|
|
case "buildFilename":
|
|
let args = call.arguments as! [String: Any]
|
|
let template = args["template"] as! String
|
|
let metadata = args["metadata"] as! String
|
|
let response = GobackendBuildFilename(template, metadata, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "sanitizeFilename":
|
|
let args = call.arguments as! [String: Any]
|
|
let filename = args["filename"] as! String
|
|
let response = GobackendSanitizeFilename(filename)
|
|
return response
|
|
|
|
case "fetchLyrics":
|
|
let args = call.arguments as! [String: Any]
|
|
let spotifyId = args["spotify_id"] as! String
|
|
let trackName = args["track_name"] as! String
|
|
let artistName = args["artist_name"] as! String
|
|
let durationMs = args["duration_ms"] as? Int64 ?? 0
|
|
let response = GobackendFetchLyrics(spotifyId, trackName, artistName, durationMs, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "getLyricsLRC":
|
|
let args = call.arguments as! [String: Any]
|
|
let spotifyId = args["spotify_id"] as! String
|
|
let trackName = args["track_name"] as! String
|
|
let artistName = args["artist_name"] as! String
|
|
let filePath = args["file_path"] as? String ?? ""
|
|
let durationMs = args["duration_ms"] as? Int64 ?? 0
|
|
let response = GobackendGetLyricsLRC(spotifyId, trackName, artistName, filePath, durationMs, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "getLyricsLRCWithSource":
|
|
let args = call.arguments as! [String: Any]
|
|
let spotifyId = args["spotify_id"] as! String
|
|
let trackName = args["track_name"] as! String
|
|
let artistName = args["artist_name"] as! String
|
|
let filePath = args["file_path"] as? String ?? ""
|
|
let durationMs = args["duration_ms"] as? Int64 ?? 0
|
|
let response = GobackendGetLyricsLRCWithSource(spotifyId, trackName, artistName, filePath, durationMs, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "embedLyricsToFile":
|
|
let args = call.arguments as! [String: Any]
|
|
let filePath = args["file_path"] as! String
|
|
let lyrics = args["lyrics"] as! String
|
|
let response = GobackendEmbedLyricsToFile(filePath, lyrics, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "cleanupConnections":
|
|
GobackendCleanupConnections()
|
|
return nil
|
|
|
|
case "downloadCoverToFile":
|
|
let args = call.arguments as! [String: Any]
|
|
let coverURL = args["cover_url"] as! String
|
|
let outputPath = args["output_path"] as! String
|
|
let maxQuality = args["max_quality"] as? Bool ?? true
|
|
GobackendDownloadCoverToFile(coverURL, outputPath, maxQuality, &error)
|
|
if let error = error { throw error }
|
|
return "{\"success\":true}"
|
|
|
|
case "extractCoverToFile":
|
|
let args = call.arguments as! [String: Any]
|
|
let audioPath = args["audio_path"] as! String
|
|
let outputPath = args["output_path"] as! String
|
|
GobackendExtractCoverToFile(audioPath, outputPath, &error)
|
|
if let error = error { throw error }
|
|
return "{\"success\":true}"
|
|
|
|
case "fetchAndSaveLyrics":
|
|
let args = call.arguments as! [String: Any]
|
|
let trackName = args["track_name"] as! String
|
|
let artistName = args["artist_name"] as! String
|
|
let spotifyId = args["spotify_id"] as! String
|
|
let durationMs = args["duration_ms"] as? Int64 ?? 0
|
|
let outputPath = args["output_path"] as! String
|
|
GobackendFetchAndSaveLyrics(trackName, artistName, spotifyId, durationMs, outputPath, &error)
|
|
if let error = error { throw error }
|
|
return "{\"success\":true}"
|
|
|
|
case "reEnrichFile":
|
|
let args = call.arguments as! [String: Any]
|
|
let requestJson = args["request_json"] as? String ?? "{}"
|
|
let response = GobackendReEnrichFile(requestJson, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "readFileMetadata":
|
|
let args = call.arguments as! [String: Any]
|
|
let filePath = args["file_path"] as! String
|
|
let response = GobackendReadFileMetadata(filePath, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "editFileMetadata":
|
|
let args = call.arguments as! [String: Any]
|
|
let filePath = args["file_path"] as! String
|
|
let metadataJson = args["metadata_json"] as? String ?? "{}"
|
|
let response = GobackendEditFileMetadata(filePath, metadataJson, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "searchDeezerAll":
|
|
let args = call.arguments as! [String: Any]
|
|
let query = args["query"] as! String
|
|
let trackLimit = args["track_limit"] as? Int ?? 15
|
|
let artistLimit = args["artist_limit"] as? Int ?? 3
|
|
let filter = args["filter"] as? String ?? ""
|
|
let response = GobackendSearchDeezerAll(query, Int(trackLimit), Int(artistLimit), filter, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "getDeezerRelatedArtists":
|
|
let args = call.arguments as! [String: Any]
|
|
let artistId = args["artist_id"] as! String
|
|
let limit = args["limit"] as? Int ?? 12
|
|
let response = GobackendGetDeezerRelatedArtists(artistId, Int(limit), &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "getDeezerMetadata":
|
|
let args = call.arguments as! [String: Any]
|
|
let resourceType = args["resource_type"] as! String
|
|
let resourceId = args["resource_id"] as! String
|
|
let response = GobackendGetDeezerMetadata(resourceType, resourceId, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "parseDeezerUrl":
|
|
let args = call.arguments as! [String: Any]
|
|
let url = args["url"] as! String
|
|
let response = GobackendParseDeezerURLExport(url, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "parseTidalUrl":
|
|
let args = call.arguments as! [String: Any]
|
|
let url = args["url"] as! String
|
|
let response = GobackendParseTidalURLExport(url, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "convertTidalToSpotifyDeezer":
|
|
let args = call.arguments as! [String: Any]
|
|
let url = args["url"] as! String
|
|
let response = GobackendConvertTidalToSpotifyDeezer(url, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "searchDeezerByISRC":
|
|
let args = call.arguments as! [String: Any]
|
|
let isrc = args["isrc"] as! String
|
|
let response = GobackendSearchDeezerByISRC(isrc, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "getDeezerExtendedMetadata":
|
|
let args = call.arguments as! [String: Any]
|
|
let trackId = args["track_id"] as! String
|
|
let response = GobackendGetDeezerExtendedMetadata(trackId, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "convertSpotifyToDeezer":
|
|
let args = call.arguments as! [String: Any]
|
|
let resourceType = args["resource_type"] as! String
|
|
let spotifyId = args["spotify_id"] as! String
|
|
let response = GobackendConvertSpotifyToDeezer(resourceType, spotifyId, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "getSpotifyMetadataWithFallback":
|
|
let args = call.arguments as! [String: Any]
|
|
let url = args["url"] as! String
|
|
let response = GobackendGetSpotifyMetadataWithDeezerFallback(url, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "checkAvailabilityFromDeezerID":
|
|
let args = call.arguments as! [String: Any]
|
|
let deezerTrackId = args["deezer_track_id"] as! String
|
|
let response = GobackendCheckAvailabilityFromDeezerID(deezerTrackId, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "checkAvailabilityByPlatformID":
|
|
let args = call.arguments as! [String: Any]
|
|
let platform = args["platform"] as! String
|
|
let entityType = args["entity_type"] as! String
|
|
let entityId = args["entity_id"] as! String
|
|
let response = GobackendCheckAvailabilityByPlatformID(platform, entityType, entityId, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "getSpotifyIDFromDeezerTrack":
|
|
let args = call.arguments as! [String: Any]
|
|
let deezerTrackId = args["deezer_track_id"] as! String
|
|
let response = GobackendGetSpotifyIDFromDeezerTrack(deezerTrackId, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "getTidalURLFromDeezerTrack":
|
|
let args = call.arguments as! [String: Any]
|
|
let deezerTrackId = args["deezer_track_id"] as! String
|
|
let response = GobackendGetTidalURLFromDeezerTrack(deezerTrackId, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "preWarmTrackCache":
|
|
let args = call.arguments as! [String: Any]
|
|
let tracksJson = args["tracks"] as! String
|
|
let _ = GobackendPreWarmTrackCacheJSON(tracksJson, &error)
|
|
if let error = error { throw error }
|
|
return nil
|
|
|
|
case "getTrackCacheSize":
|
|
let response = GobackendGetTrackCacheSize()
|
|
return response
|
|
|
|
case "clearTrackCache":
|
|
GobackendClearTrackCache()
|
|
return nil
|
|
|
|
// Log methods
|
|
case "getLogs":
|
|
let response = GobackendGetLogs()
|
|
return response
|
|
|
|
case "getLogsSince":
|
|
let args = call.arguments as! [String: Any]
|
|
let index = args["index"] as? Int ?? 0
|
|
let response = GobackendGetLogsSince(Int(index))
|
|
return response
|
|
|
|
case "clearLogs":
|
|
GobackendClearLogs()
|
|
return nil
|
|
|
|
case "getLogCount":
|
|
let response = GobackendGetLogCount()
|
|
return response
|
|
|
|
case "setLoggingEnabled":
|
|
let args = call.arguments as! [String: Any]
|
|
let enabled = args["enabled"] as? Bool ?? false
|
|
GobackendSetLoggingEnabled(enabled)
|
|
return nil
|
|
|
|
// Extension System methods
|
|
case "initExtensionSystem":
|
|
let args = call.arguments as! [String: Any]
|
|
let extensionsDir = args["extensions_dir"] as! String
|
|
let dataDir = args["data_dir"] as! String
|
|
GobackendInitExtensionSystem(extensionsDir, dataDir, &error)
|
|
if let error = error { throw error }
|
|
return nil
|
|
|
|
case "loadExtensionsFromDir":
|
|
let args = call.arguments as! [String: Any]
|
|
let dirPath = args["dir_path"] as! String
|
|
let response = GobackendLoadExtensionsFromDir(dirPath, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "loadExtensionFromPath":
|
|
let args = call.arguments as! [String: Any]
|
|
let filePath = args["file_path"] as! String
|
|
let response = GobackendLoadExtensionFromPath(filePath, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "unloadExtension":
|
|
let args = call.arguments as! [String: Any]
|
|
let extensionId = args["extension_id"] as! String
|
|
GobackendUnloadExtensionByID(extensionId, &error)
|
|
if let error = error { throw error }
|
|
return nil
|
|
|
|
case "getInstalledExtensions":
|
|
let response = GobackendGetInstalledExtensions(&error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "setExtensionEnabled":
|
|
let args = call.arguments as! [String: Any]
|
|
let extensionId = args["extension_id"] as! String
|
|
let enabled = args["enabled"] as? Bool ?? false
|
|
GobackendSetExtensionEnabledByID(extensionId, enabled, &error)
|
|
if let error = error { throw error }
|
|
return nil
|
|
|
|
case "setProviderPriority":
|
|
let args = call.arguments as! [String: Any]
|
|
let priorityJson = args["priority"] as! String
|
|
GobackendSetProviderPriorityJSON(priorityJson, &error)
|
|
if let error = error { throw error }
|
|
return nil
|
|
|
|
case "getProviderPriority":
|
|
let response = GobackendGetProviderPriorityJSON(&error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "setMetadataProviderPriority":
|
|
let args = call.arguments as! [String: Any]
|
|
let priorityJson = args["priority"] as! String
|
|
GobackendSetMetadataProviderPriorityJSON(priorityJson, &error)
|
|
if let error = error { throw error }
|
|
return nil
|
|
|
|
case "getMetadataProviderPriority":
|
|
let response = GobackendGetMetadataProviderPriorityJSON(&error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "getExtensionSettings":
|
|
let args = call.arguments as! [String: Any]
|
|
let extensionId = args["extension_id"] as! String
|
|
let response = GobackendGetExtensionSettingsJSON(extensionId, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "setExtensionSettings":
|
|
let args = call.arguments as! [String: Any]
|
|
let extensionId = args["extension_id"] as! String
|
|
let settingsJson = args["settings"] as! String
|
|
GobackendSetExtensionSettingsJSON(extensionId, settingsJson, &error)
|
|
if let error = error { throw error }
|
|
return nil
|
|
|
|
case "invokeExtensionAction":
|
|
let args = call.arguments as! [String: Any]
|
|
let extensionId = args["extension_id"] as! String
|
|
let actionName = args["action"] as! String
|
|
let response = GobackendInvokeExtensionActionJSON(extensionId, actionName, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "searchTracksWithExtensions":
|
|
let args = call.arguments as! [String: Any]
|
|
let query = args["query"] as! String
|
|
let limit = args["limit"] as? Int ?? 20
|
|
let response = GobackendSearchTracksWithExtensionsJSON(query, Int(limit), &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "enrichTrackWithExtension":
|
|
let args = call.arguments as! [String: Any]
|
|
let extensionId = args["extension_id"] as! String
|
|
let trackJson = args["track"] as? String ?? "{}"
|
|
let response = GobackendEnrichTrackWithExtensionJSON(extensionId, trackJson, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "downloadWithExtensions":
|
|
let requestJson = call.arguments as! String
|
|
let response = GobackendDownloadWithExtensionsJSON(requestJson, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "removeExtension":
|
|
let args = call.arguments as! [String: Any]
|
|
let extensionId = args["extension_id"] as! String
|
|
GobackendRemoveExtensionByID(extensionId, &error)
|
|
if let error = error { throw error }
|
|
return nil
|
|
|
|
case "upgradeExtension":
|
|
let args = call.arguments as! [String: Any]
|
|
let filePath = args["file_path"] as! String
|
|
let response = GobackendUpgradeExtensionFromPath(filePath, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "checkExtensionUpgrade":
|
|
let args = call.arguments as! [String: Any]
|
|
let filePath = args["file_path"] as! String
|
|
let response = GobackendCheckExtensionUpgradeFromPath(filePath, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "cleanupExtensions":
|
|
GobackendCleanupExtensions()
|
|
return nil
|
|
|
|
// Extension Auth API
|
|
case "getExtensionPendingAuth":
|
|
let args = call.arguments as! [String: Any]
|
|
let extensionId = args["extension_id"] as! String
|
|
let response = GobackendGetExtensionPendingAuthJSON(extensionId, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "setExtensionAuthCode":
|
|
let args = call.arguments as! [String: Any]
|
|
let extensionId = args["extension_id"] as! String
|
|
let authCode = args["auth_code"] as! String
|
|
GobackendSetExtensionAuthCodeByID(extensionId, authCode)
|
|
return nil
|
|
|
|
case "setExtensionTokens":
|
|
let args = call.arguments as! [String: Any]
|
|
let extensionId = args["extension_id"] as! String
|
|
let accessToken = args["access_token"] as! String
|
|
let refreshToken = args["refresh_token"] as? String ?? ""
|
|
let expiresIn = args["expires_in"] as? Int ?? 0
|
|
GobackendSetExtensionTokensByID(extensionId, accessToken, refreshToken, Int(expiresIn))
|
|
return nil
|
|
|
|
case "clearExtensionPendingAuth":
|
|
let args = call.arguments as! [String: Any]
|
|
let extensionId = args["extension_id"] as! String
|
|
GobackendClearExtensionPendingAuthByID(extensionId)
|
|
return nil
|
|
|
|
case "isExtensionAuthenticated":
|
|
let args = call.arguments as! [String: Any]
|
|
let extensionId = args["extension_id"] as! String
|
|
let response = GobackendIsExtensionAuthenticatedByID(extensionId)
|
|
return response
|
|
|
|
case "getAllPendingAuthRequests":
|
|
let response = GobackendGetAllPendingAuthRequestsJSON(&error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
// Extension FFmpeg API
|
|
case "getPendingFFmpegCommand":
|
|
let args = call.arguments as! [String: Any]
|
|
let commandId = args["command_id"] as! String
|
|
let response = GobackendGetPendingFFmpegCommandJSON(commandId, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "setFFmpegCommandResult":
|
|
let args = call.arguments as! [String: Any]
|
|
let commandId = args["command_id"] as! String
|
|
let success = args["success"] as? Bool ?? false
|
|
let output = args["output"] as? String ?? ""
|
|
let errorMsg = args["error"] as? String ?? ""
|
|
GobackendSetFFmpegCommandResult(commandId, success, output, errorMsg)
|
|
return nil
|
|
|
|
case "getAllPendingFFmpegCommands":
|
|
let response = GobackendGetAllPendingFFmpegCommandsJSON(&error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
// Extension Custom Search API
|
|
case "customSearchWithExtension":
|
|
let args = call.arguments as! [String: Any]
|
|
let extensionId = args["extension_id"] as! String
|
|
let query = args["query"] as! String
|
|
let optionsJson = args["options"] as? String ?? ""
|
|
let response = GobackendCustomSearchWithExtensionJSON(extensionId, query, optionsJson, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "getSearchProviders":
|
|
let response = GobackendGetSearchProvidersJSON(&error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
// Extension URL Handler API
|
|
case "handleURLWithExtension":
|
|
let args = call.arguments as! [String: Any]
|
|
let url = args["url"] as! String
|
|
let response = GobackendHandleURLWithExtensionJSON(url, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "findURLHandler":
|
|
let args = call.arguments as! [String: Any]
|
|
let url = args["url"] as! String
|
|
let response = GobackendFindURLHandlerJSON(url)
|
|
return response
|
|
|
|
case "getURLHandlers":
|
|
let response = GobackendGetURLHandlersJSON(&error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "getAlbumWithExtension":
|
|
let args = call.arguments as! [String: Any]
|
|
let extensionId = args["extension_id"] as! String
|
|
let albumId = args["album_id"] as! String
|
|
let response = GobackendGetAlbumWithExtensionJSON(extensionId, albumId, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "getPlaylistWithExtension":
|
|
let args = call.arguments as! [String: Any]
|
|
let extensionId = args["extension_id"] as! String
|
|
let playlistId = args["playlist_id"] as! String
|
|
let response = GobackendGetPlaylistWithExtensionJSON(extensionId, playlistId, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "getArtistWithExtension":
|
|
let args = call.arguments as! [String: Any]
|
|
let extensionId = args["extension_id"] as! String
|
|
let artistId = args["artist_id"] as! String
|
|
let response = GobackendGetArtistWithExtensionJSON(extensionId, artistId, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
// Extension Post-Processing API
|
|
case "runPostProcessing":
|
|
let args = call.arguments as! [String: Any]
|
|
let filePath = args["file_path"] as! String
|
|
let metadataJson = args["metadata"] as? String ?? ""
|
|
let response = GobackendRunPostProcessingJSON(filePath, metadataJson, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "runPostProcessingV2":
|
|
let args = call.arguments as! [String: Any]
|
|
let inputJson = args["input"] as? String ?? ""
|
|
let metadataJson = args["metadata"] as? String ?? ""
|
|
let response = GobackendRunPostProcessingV2JSON(inputJson, metadataJson, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "getPostProcessingProviders":
|
|
let response = GobackendGetPostProcessingProvidersJSON(&error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
// Extension Store
|
|
case "initExtensionStore":
|
|
let args = call.arguments as! [String: Any]
|
|
let cacheDir = args["cache_dir"] as! String
|
|
GobackendInitExtensionStoreJSON(cacheDir, &error)
|
|
if let error = error { throw error }
|
|
return nil
|
|
|
|
case "setStoreRegistryUrl":
|
|
let args = call.arguments as! [String: Any]
|
|
let registryUrl = args["registry_url"] as? String ?? ""
|
|
GobackendSetStoreRegistryURLJSON(registryUrl, &error)
|
|
if let error = error { throw error }
|
|
return nil
|
|
|
|
case "getStoreRegistryUrl":
|
|
let response = GobackendGetStoreRegistryURLJSON(&error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "clearStoreRegistryUrl":
|
|
GobackendClearStoreRegistryURLJSON(&error)
|
|
if let error = error { throw error }
|
|
return nil
|
|
|
|
case "getStoreExtensions":
|
|
let args = call.arguments as! [String: Any]
|
|
let forceRefresh = args["force_refresh"] as? Bool ?? false
|
|
let response = GobackendGetStoreExtensionsJSON(forceRefresh, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "searchStoreExtensions":
|
|
let args = call.arguments as! [String: Any]
|
|
let query = args["query"] as? String ?? ""
|
|
let category = args["category"] as? String ?? ""
|
|
let response = GobackendSearchStoreExtensionsJSON(query, category, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "getStoreCategories":
|
|
let response = GobackendGetStoreCategoriesJSON(&error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "downloadStoreExtension":
|
|
let args = call.arguments as! [String: Any]
|
|
let extensionId = args["extension_id"] as! String
|
|
let destDir = args["dest_dir"] as! String
|
|
let response = GobackendDownloadStoreExtensionJSON(extensionId, destDir, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "clearStoreCache":
|
|
GobackendClearStoreCacheJSON(&error)
|
|
if let error = error { throw error }
|
|
return nil
|
|
|
|
// Extension Home Feed API
|
|
case "getExtensionHomeFeed":
|
|
let args = call.arguments as! [String: Any]
|
|
let extensionId = args["extension_id"] as! String
|
|
let response = GobackendGetExtensionHomeFeedJSON(extensionId, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "getExtensionBrowseCategories":
|
|
let args = call.arguments as! [String: Any]
|
|
let extensionId = args["extension_id"] as! String
|
|
let response = GobackendGetExtensionBrowseCategoriesJSON(extensionId, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
// Local Library Scanning
|
|
case "setLibraryCoverCacheDir":
|
|
let args = call.arguments as! [String: Any]
|
|
let cacheDir = args["cache_dir"] as! String
|
|
GobackendSetLibraryCoverCacheDirJSON(cacheDir)
|
|
return nil
|
|
|
|
case "scanLibraryFolder":
|
|
let args = call.arguments as! [String: Any]
|
|
let folderPath = args["folder_path"] as! String
|
|
let response = GobackendScanLibraryFolderJSON(folderPath, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "scanLibraryFolderIncremental":
|
|
let args = call.arguments as! [String: Any]
|
|
let folderPath = args["folder_path"] as! String
|
|
let existingFiles = args["existing_files"] as? String ?? "{}"
|
|
let response = GobackendScanLibraryFolderIncrementalJSON(folderPath, existingFiles, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "getLibraryScanProgress":
|
|
let response = GobackendGetLibraryScanProgressJSON()
|
|
return response
|
|
|
|
case "cancelLibraryScan":
|
|
GobackendCancelLibraryScanJSON()
|
|
return nil
|
|
|
|
case "readAudioMetadata":
|
|
let args = call.arguments as! [String: Any]
|
|
let filePath = args["file_path"] as! String
|
|
let response = GobackendReadAudioMetadataJSON(filePath, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
// iOS Security-Scoped Bookmark for Local Library
|
|
case "resolveIosBookmark":
|
|
let args = call.arguments as! [String: Any]
|
|
let bookmarkBase64 = args["bookmark"] as! String
|
|
return try resolveIosBookmark(bookmarkBase64)
|
|
|
|
case "startAccessingIosBookmark":
|
|
let args = call.arguments as! [String: Any]
|
|
let bookmarkBase64 = args["bookmark"] as! String
|
|
return try startAccessingIosBookmark(bookmarkBase64)
|
|
|
|
case "stopAccessingIosBookmark":
|
|
stopAccessingIosBookmark()
|
|
return nil
|
|
|
|
case "createIosBookmarkFromPath":
|
|
let args = call.arguments as! [String: Any]
|
|
let path = args["path"] as! String
|
|
return try createIosBookmarkFromPath(path)
|
|
|
|
// Lyrics Provider Settings
|
|
case "setLyricsProviders":
|
|
let args = call.arguments as! [String: Any]
|
|
let providersJson = args["providers_json"] as? String ?? "[]"
|
|
GobackendSetLyricsProvidersJSON(providersJson, &error)
|
|
if let error = error { throw error }
|
|
return "{\"success\":true}"
|
|
|
|
case "getLyricsProviders":
|
|
let response = GobackendGetLyricsProvidersJSON(&error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "getAvailableLyricsProviders":
|
|
let response = GobackendGetAvailableLyricsProvidersJSON(&error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "setLyricsFetchOptions":
|
|
let args = call.arguments as! [String: Any]
|
|
let optionsJson = args["options_json"] as? String ?? "{}"
|
|
GobackendSetLyricsFetchOptionsJSON(optionsJson, &error)
|
|
if let error = error { throw error }
|
|
return "{\"success\":true}"
|
|
|
|
case "getLyricsFetchOptions":
|
|
let response = GobackendGetLyricsFetchOptionsJSON(&error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
// CUE Sheet Parsing
|
|
case "parseCueSheet":
|
|
let args = call.arguments as! [String: Any]
|
|
let cuePath = args["cue_path"] as! String
|
|
let audioDir = args["audio_dir"] as? String ?? ""
|
|
let response = GobackendParseCueSheet(cuePath, audioDir, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
default:
|
|
throw NSError(
|
|
domain: "SpotiFLAC",
|
|
code: -1,
|
|
userInfo: [NSLocalizedDescriptionKey: "Method not implemented: \(call.method)"]
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - iOS Security-Scoped Bookmark Helpers
|
|
|
|
/// Create a security-scoped bookmark from a filesystem path (e.g. from FilePicker).
|
|
/// The path must currently be accessible (within the same picker session).
|
|
/// Returns base64-encoded bookmark data.
|
|
private func createIosBookmarkFromPath(_ path: String) throws -> String {
|
|
let url = URL(fileURLWithPath: path)
|
|
do {
|
|
let bookmarkData = try url.bookmarkData(
|
|
options: .minimalBookmark,
|
|
includingResourceValuesForKeys: nil,
|
|
relativeTo: nil
|
|
)
|
|
return bookmarkData.base64EncodedString()
|
|
} catch {
|
|
throw NSError(
|
|
domain: "SpotiFLAC",
|
|
code: -1,
|
|
userInfo: [NSLocalizedDescriptionKey: "Failed to create bookmark for path \(path): \(error.localizedDescription)"]
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Resolve a base64-encoded security-scoped bookmark and return the resolved path.
|
|
/// Does NOT start accessing the resource.
|
|
private func resolveIosBookmark(_ bookmarkBase64: String) throws -> String {
|
|
guard let bookmarkData = Data(base64Encoded: bookmarkBase64) else {
|
|
throw NSError(
|
|
domain: "SpotiFLAC",
|
|
code: -1,
|
|
userInfo: [NSLocalizedDescriptionKey: "Invalid base64 bookmark data"]
|
|
)
|
|
}
|
|
|
|
var isStale = false
|
|
let url: URL
|
|
do {
|
|
url = try URL(
|
|
resolvingBookmarkData: bookmarkData,
|
|
options: [],
|
|
relativeTo: nil,
|
|
bookmarkDataIsStale: &isStale
|
|
)
|
|
} catch {
|
|
throw NSError(
|
|
domain: "SpotiFLAC",
|
|
code: -1,
|
|
userInfo: [NSLocalizedDescriptionKey: "Failed to resolve bookmark: \(error.localizedDescription)"]
|
|
)
|
|
}
|
|
|
|
return url.path
|
|
}
|
|
|
|
/// Resolve a base64-encoded bookmark, start accessing the security-scoped resource,
|
|
/// and return the resolved filesystem path. The resource stays accessed until
|
|
/// `stopAccessingIosBookmark()` is called.
|
|
private func startAccessingIosBookmark(_ bookmarkBase64: String) throws -> String {
|
|
// Stop any previously accessed resource first
|
|
stopAccessingIosBookmark()
|
|
|
|
guard let bookmarkData = Data(base64Encoded: bookmarkBase64) else {
|
|
throw NSError(
|
|
domain: "SpotiFLAC",
|
|
code: -1,
|
|
userInfo: [NSLocalizedDescriptionKey: "Invalid base64 bookmark data"]
|
|
)
|
|
}
|
|
|
|
var isStale = false
|
|
let url: URL
|
|
do {
|
|
url = try URL(
|
|
resolvingBookmarkData: bookmarkData,
|
|
options: [],
|
|
relativeTo: nil,
|
|
bookmarkDataIsStale: &isStale
|
|
)
|
|
} catch {
|
|
throw NSError(
|
|
domain: "SpotiFLAC",
|
|
code: -1,
|
|
userInfo: [NSLocalizedDescriptionKey: "Failed to resolve bookmark: \(error.localizedDescription)"]
|
|
)
|
|
}
|
|
|
|
guard url.startAccessingSecurityScopedResource() else {
|
|
throw NSError(
|
|
domain: "SpotiFLAC",
|
|
code: -1,
|
|
userInfo: [NSLocalizedDescriptionKey: "Failed to start accessing security-scoped resource at \(url.path)"]
|
|
)
|
|
}
|
|
|
|
activeSecurityScopedURL = url
|
|
return url.path
|
|
}
|
|
|
|
/// Stop accessing the currently active security-scoped resource, if any.
|
|
private func stopAccessingIosBookmark() {
|
|
if let url = activeSecurityScopedURL {
|
|
url.stopAccessingSecurityScopedResource()
|
|
activeSecurityScopedURL = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class ClosureStreamHandler: NSObject, FlutterStreamHandler {
|
|
typealias ListenHandler = (_ arguments: Any?, _ events: @escaping FlutterEventSink) -> FlutterError?
|
|
typealias CancelHandler = (_ arguments: Any?) -> FlutterError?
|
|
|
|
private let onListenHandler: ListenHandler
|
|
private let onCancelHandler: CancelHandler
|
|
|
|
init(
|
|
onListen: @escaping ListenHandler,
|
|
onCancel: @escaping CancelHandler = { _ in nil }
|
|
) {
|
|
self.onListenHandler = onListen
|
|
self.onCancelHandler = onCancel
|
|
}
|
|
|
|
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
|
|
onListenHandler(arguments, events)
|
|
}
|
|
|
|
func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
|
onCancelHandler(arguments)
|
|
}
|
|
}
|