mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-03-31 09:01:33 +02:00
- Add SearchAll() for Tidal and Qobuz in Go backend (tracks, artists, albums) - Add searchTidalAll/searchQobuzAll platform routing for Android and iOS - Add Tidal/Qobuz options to search provider dropdown in home tab - Show (Recommended) label and auto-select service in download picker
1151 lines
48 KiB
Swift
1151 lines
48 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 "searchTidalAll":
|
|
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 = GobackendSearchTidalAll(query, Int(trackLimit), Int(artistLimit), filter, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "searchQobuzAll":
|
|
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 = GobackendSearchQobuzAll(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 "getQobuzMetadata":
|
|
let args = call.arguments as! [String: Any]
|
|
let resourceType = args["resource_type"] as! String
|
|
let resourceId = args["resource_id"] as! String
|
|
let response = GobackendGetQobuzMetadata(resourceType, resourceId, &error)
|
|
if let error = error { throw error }
|
|
return response
|
|
|
|
case "getTidalMetadata":
|
|
let args = call.arguments as! [String: Any]
|
|
let resourceType = args["resource_type"] as! String
|
|
let resourceId = args["resource_id"] as! String
|
|
let response = GobackendGetTidalMetadata(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 "parseQobuzUrl":
|
|
let args = call.arguments as! [String: Any]
|
|
let url = args["url"] as! String
|
|
let response = GobackendParseQobuzURLExport(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 "searchTracksWithMetadataProviders":
|
|
let args = call.arguments as! [String: Any]
|
|
let query = args["query"] as! String
|
|
let limit = args["limit"] as? Int ?? 20
|
|
let includeExtensions = args["include_extensions"] as? Bool ?? true
|
|
let response = GobackendSearchTracksWithMetadataProvidersJSON(
|
|
query,
|
|
Int(limit),
|
|
includeExtensions,
|
|
&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)
|
|
}
|
|
}
|