From 20b4c73b8d00dba4fd01238e14fd7bbdfcc05b18 Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:14:32 +0700 Subject: [PATCH] refactor(android): move RustCoreBackend.invokeApplication into its own file NT-16 Kotlin part. The application method dispatch (about 290 lines) moves verbatim into RustCoreBackendApplication.kt as an internal extension `RustCoreBackend.dispatchApplication`. The `invokeApplication` override stays in SelectedCoreBackend.kt as a one-line delegate. `synchronized(this)` still locks the RustCoreBackend object. The object state and helpers the dispatch uses are widened from private to internal; method names, argument keys, results and error strings are unchanged. --- .../spotiflac/RustCoreBackendApplication.kt | 303 ++++++++++++++++ .../com/zarz/spotiflac/SelectedCoreBackend.kt | 336 ++---------------- 2 files changed, 324 insertions(+), 315 deletions(-) create mode 100644 android/app/src/rust/kotlin/com/zarz/spotiflac/RustCoreBackendApplication.kt diff --git a/android/app/src/rust/kotlin/com/zarz/spotiflac/RustCoreBackendApplication.kt b/android/app/src/rust/kotlin/com/zarz/spotiflac/RustCoreBackendApplication.kt new file mode 100644 index 00000000..01f31a41 --- /dev/null +++ b/android/app/src/rust/kotlin/com/zarz/spotiflac/RustCoreBackendApplication.kt @@ -0,0 +1,303 @@ +package com.zarz.spotiflac + +import com.spotiflac.backend.LyricsRequest +import java.io.File +import java.util.UUID +import org.json.JSONArray +import org.json.JSONObject + +// Application method dispatch behind RustCoreBackend.invokeApplication. + +internal fun RustCoreBackend.dispatchApplication(method: String, arguments: Any?): Any? { + val args = arguments as? Map<*, *> ?: emptyMap() + fun string(key: String, default: String = "") = args[key] as? String ?: default + fun lyricsRequest(fileKey: String) = LyricsRequest( + string("spotify_id"), + string("track_name"), + string("artist_name"), + mediaPath(string(fileKey)), + (args["duration_ms"] as? Number)?.toLong() ?: 0L, + ) + fun ids(raw: String): List { + val values = JSONArray(raw) + return (0 until values.length()).map { values.getString(it) } + } + when (method) { + "cancelExtensionRequest" -> synchronized(this) { + requestRegistry().cancel(string("request_id")) + return null + } + "customSearchWithExtension", "getExtensionHomeFeed" -> { + val (current, lease) = acquireRequest(string("request_id")) + return lease.use { + try { + if (method == "customSearchWithExtension") { + current.customSearchJson(string("extension_id"), string("query"), string("options"), it) + } else { + current.getExtensionHomeFeedJson(string("extension_id"), it) + } + } finally { + it.release() + } + } + } + "prepareRuntimeState" -> synchronized(this) { + val directory = File(string("data_dir")) + require(directory.isAbsolute) { "Extension data directory must be absolute" } + val data = directory.canonicalPath + check(manager == null || identity?.second == data) { + "Runtime state directory does not match the initialized owner" + } + val raw = string("runtime_state") + manager?.environment()?.use { it.setRuntimeState(raw) } + runtimeState = data to raw + return null + } + "initExtensionSystem" -> { + initializeOwner(args) + return null + } + "setDownloadDirectory" -> { + setDownloadDirectory(string("path")) + return null + } + "acquireDownloadDirectory" -> synchronized(this) { + val scope = openDownloadDirectory(string("path")) + val token = UUID.randomUUID().toString() + directoryScopes[token] = scope + return token + } + "releaseDownloadDirectory" -> synchronized(this) { + directoryScopes.remove(string("token"))?.close() + return null + } + "cleanupExtensions" -> { + shutdownOwner() + return null + } + "buildFilename" -> return buildFilename(string("template"), string("metadata", "{}")) + "sanitizeFilename" -> return sanitizeFilename(string("filename")) + "getLogsSince" -> return synchronized(this) { + manager?.environment()?.use { environment -> + environment.logBuffer().use { it.since((args["index"] as? Number)?.toLong() ?: 0L) } + } ?: """{"logs":[],"next_index":0}""" + } + "clearLogs" -> return synchronized(this) { + manager?.environment()?.use { environment -> environment.logBuffer().use { it.clear() } } + null + } + "setLoggingEnabled", "setAllowPrivateNetwork", "setDownloadFallbackExtensionIds", + "setNetworkCompatibilityOptions", + "setLyricsProviders", "setLyricsFetchOptions" -> synchronized(this) { + when (method) { + "setLoggingEnabled" -> { + val enabled = args["enabled"] as? Boolean ?: false + manager?.environment()?.use { environment -> + environment.logBuffer().use { it.setEnabled(enabled) } + } + loggingEnabled = enabled + } + "setAllowPrivateNetwork" -> { + val allowed = args["allowed"] as? Boolean ?: false + manager?.environment()?.use { it.setAllowPrivateNetwork(allowed) } + allowPrivateNetwork = allowed + } + "setNetworkCompatibilityOptions" -> { + val allowed = args["allow_http"] as? Boolean ?: false + val insecureTls = args["insecure_tls"] as? Boolean ?: false + manager?.environment()?.use { it.setNetworkCompatibilityOptions(allowed, insecureTls) } + allowHttpFallback = allowed + } + "setDownloadFallbackExtensionIds" -> { + val raw = string("extension_ids") + val value = if (raw.isBlank() || raw.trim() == "null") null else ids(raw) + manager?.setFallbackProviders(value) + fallbackProviders = value + } + "setLyricsProviders" -> { + val raw = string("providers_json", "[]") + if (manager != null) manager!!.setLyricsProvidersJson(raw) else ids(raw) + return """{"success":true}""" + } + "setLyricsFetchOptions" -> { + val raw = string("options_json", "{}") + if (manager != null) manager!!.setLyricsFetchOptionsJson(raw) else JSONObject(raw) + return """{"success":true}""" + } + } + return null + } + "initExtensionRepo" -> { + initializeRepository(string("cache_dir")) + return null + } + "setRepoRegistryUrl" -> { + repositoryOwner().setRegistryUrl(string("registry_url")) + return null + } + "getRepoRegistryUrl" -> return repositoryOwner().registryUrl() + "clearRepoRegistryUrl" -> { + repositoryOwner().clearRegistryUrl() + return null + } + "getRepoExtensions" -> return repositoryOwner().extensions( + args["force_refresh"] as? Boolean ?: false, + ) + "downloadRepoExtension" -> return repositoryOwner().download( + string("extension_id"), + string("dest_dir"), + ) + } + val current = owner() + return when (method) { + "loadExtensionsFromDir" -> { + synchronized(this) { + check(File(string("dir_path")).canonicalPath == identity?.first) { "Extension source directory does not match the initialized owner" } + } + current.loadAll() + } + "loadExtensionFromPath" -> current.install(string("file_path")) + "upgradeExtension" -> current.upgrade(string("file_path")) + "checkExtensionUpgrade" -> current.checkUpgrade(string("file_path")) + "getInstalledExtensions" -> current.installed() + "setExtensionEnabled" -> { + current.setEnabled(string("extension_id"), args["enabled"] as? Boolean ?: false) + null + } + "removeExtension" -> { + current.remove(string("extension_id")) + null + } + "getExtensionSettings" -> current.environment().use { it.settings(string("extension_id")) } + "setExtensionSettings" -> { + current.updateSettings(string("extension_id"), string("settings", "{}")) + null + } + "invokeExtensionAction" -> if (args.containsKey("arguments_json")) { + // Transient form input bypasses persistent extension settings. + current.call(string("extension_id"), string("action"), string("arguments_json", "[]"), null, 120_000uL) + } else current.invokeAction(string("extension_id"), string("action")) + "checkExtensionHealth" -> current.checkExtensionHealthJson(string("extension_id")) + "setProviderPriority", "setMetadataProviderPriority" -> { + current.setProviderPriority(if (method == "setProviderPriority") "download" else "metadata", ids(string("priority", "[]"))) + null + } + "getProviderPriority", "getMetadataProviderPriority" -> { + JSONObject(current.providerPriorities()).optJSONArray(if (method == "getProviderPriority") "download" else "metadata")?.toString() ?: "[]" + } + "searchTracksWithMetadataProviders" -> current.searchMetadataProviders( + string("query"), + (args["limit"] as? Number)?.toLong() ?: 20L, + args["include_extensions"] as? Boolean ?: true, + "", + 30_000uL, + ) + "searchTracksWithMetadataProvider" -> current.searchMetadataProvider( + string("extension_id"), + string("query"), + (args["limit"] as? Number)?.toLong() ?: 20L, + 30_000uL, + ) + "getProviderMetadata" -> current.getProviderMetadataJson( + string("provider_id"), + string("resource_type"), + string("resource_id"), + null, + ) + "searchDeezerByISRC" -> current.searchDeezerByIsrcForItemId( + string("isrc"), + string("item_id"), + null, + ) + "getDeezerExtendedMetadata" -> current.getDeezerExtendedMetadata( + string("track_id"), + null, + ) + "convertSpotifyToDeezer" -> current.convertSpotifyToDeezer( + string("resource_type"), + string("spotify_id"), + null, + ) + "getTrackPlatformLinks" -> current.getTrackPlatformLinksJson( + string("spotify_id"), + string("isrc"), + null, + ) + "fetchMusicBrainzTags" -> { + val genre = try { + current.fetchMusicBrainzGenreByIsrc(string("isrc"), null) + } catch (_: Exception) { + "" + } + val albumArtist = try { + current.fetchMusicBrainzAlbumArtistByIsrc( + string("isrc"), + string("album_name"), + null, + ) + } catch (_: Exception) { + "" + } + JSONObject() + .put("genre", genre) + .put("album_artist", albumArtist) + .toString() + } + "getTrackCacheSize" -> current.getTrackCacheSize().toInt() + "clearTrackCache" -> { + current.clearTrackIdCache() + null + } + "setMetadataLanguage" -> { + current.setMetadataLanguage(string("tag")) + null + } + "findURLHandler" -> current.findUrlHandler(string("url")) ?: "" + "handleURLWithExtension" -> current.handleUrlJson(string("url")) + "getExtensionPendingAuth" -> current.getExtensionPendingAuthJson(string("extension_id")).ifEmpty { null } + "completeExtensionSessionGrant" -> { + completeSessionGrant(current, string("extension_id"), string("grant")) + true + } + "getAllDownloadProgress" -> current.withDownloadState { state -> state.allProgress() } + "cleanupConnections" -> current.environment().use { it.cleanupConnections(); null } + "resetDownloadCancels", "cancelDownloads" -> { + val values = args["item_ids"] as? List<*> ?: error("Expected item_ids list") + val itemIds = values.map { it as? String ?: error("Expected string item ID") } + current.withDownloadState { state -> + if (method == "cancelDownloads") state.cancelDownloads(itemIds) + else state.resetDownloadCancels(itemIds) + } + null + } + "clearItemProgress", "cancelDownload", "resetDownloadCancel" -> current.withDownloadState { state -> + when (method) { + "clearItemProgress" -> state.clearItemProgress(string("item_id")) + "cancelDownload" -> state.cancelDownload(string("item_id")) + else -> state.resetDownloadCancel(string("item_id")) + } + null + } + "getLyricsLRC", "getLyricsLRCWithSource" -> withMediaFiles(listOf(string("file_path"))) { + val request = lyricsRequest("file_path") + if (method == "getLyricsLRC") it.getLyricsLrc(request, null) + else it.getLyricsLrcWithSource(request, null) + } + "embedLyricsToFile" -> withMediaFiles(listOf(string("file_path"))) { + it.embedLyricsToFile(mediaPath(string("file_path")), string("lyrics"), null) + } + "fetchAndSaveLyrics" -> try { + withMediaFiles(listOf(string("audio_file_path"), string("output_path"))) { + it.fetchAndSaveLyrics(lyricsRequest("audio_file_path"), mediaPath(string("output_path")), null) + """{"success":true}""" + } + } catch (error: Exception) { + JSONObject().put("success", false).put("error", error.message ?: "Lyrics operation failed").toString() + } + "findCollectionAcrossExtensions" -> current.findCollectionAcrossExtensionsJson( + arguments as? String ?: "{}", + null, + ) + else -> error("Rust application method is not connected yet: $method") + } +} diff --git a/android/app/src/rust/kotlin/com/zarz/spotiflac/SelectedCoreBackend.kt b/android/app/src/rust/kotlin/com/zarz/spotiflac/SelectedCoreBackend.kt index 3855a863..8811e68b 100644 --- a/android/app/src/rust/kotlin/com/zarz/spotiflac/SelectedCoreBackend.kt +++ b/android/app/src/rust/kotlin/com/zarz/spotiflac/SelectedCoreBackend.kt @@ -15,8 +15,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File import java.security.MessageDigest -import java.util.UUID -import org.json.JSONArray import org.json.JSONObject internal fun createCoreBackend(context: Context): CoreBackend = RustCoreBackend.initialize(context) @@ -63,16 +61,16 @@ internal suspend fun MainActivity.dispatchBackendApplication(call: MethodCall, r internal object RustCoreBackend : CoreBackend { override val implementation = "rust" private lateinit var workspace: NativeMediaWorkspace - private var manager: ExtensionManager? = null + internal var manager: ExtensionManager? = null private var repository: ExtensionRepository? = null private var requests: CancellationRegistry? = null - private var identity: Triple>? = null - private var loggingEnabled = false - private var allowPrivateNetwork = false - private var allowHttpFallback = false - private var fallbackProviders: List? = null - private var runtimeState: Pair? = null - private val directoryScopes = mutableMapOf() + internal var identity: Triple>? = null + internal var loggingEnabled = false + internal var allowPrivateNetwork = false + internal var allowHttpFallback = false + internal var fallbackProviders: List? = null + internal var runtimeState: Pair? = null + internal val directoryScopes = mutableMapOf() private var libraryCoverScope: AutoCloseable? = null @Volatile private var libraryCoverDirectory: File? = null @@ -85,11 +83,11 @@ internal object RustCoreBackend : CoreBackend { } @Synchronized - private fun owner(): ExtensionManager { + internal fun owner(): ExtensionManager { return checkNotNull(manager) { "Rust backend is not initialized" } } - private fun ExtensionManager.withDownloadState(block: (DownloadState) -> T): T = + internal fun ExtensionManager.withDownloadState(block: (DownloadState) -> T): T = environment().use { environment -> environment.downloadState().use(block) } @@ -123,7 +121,7 @@ internal object RustCoreBackend : CoreBackend { } @Synchronized - private fun setDownloadDirectory(path: String) { + internal fun setDownloadDirectory(path: String) { val current = owner() val storage = checkNotNull(identity) val files = workspace.ensureDirectory() @@ -133,12 +131,12 @@ internal object RustCoreBackend : CoreBackend { } @Synchronized - private fun requestRegistry(): CancellationRegistry = requests ?: CancellationRegistry( + internal fun requestRegistry(): CancellationRegistry = requests ?: CancellationRegistry( CancellationDomain.EXTENSION_REQUEST, ).also { requests = it } @Synchronized - private fun acquireRequest(id: String): Pair = + internal fun acquireRequest(id: String): Pair = owner() to requestRegistry().acquire(id) internal fun runDataJob(request: JSONObject, bytes: ByteArray, lease: RequestLease): String { @@ -160,11 +158,11 @@ internal object RustCoreBackend : CoreBackend { } @Synchronized - private fun repositoryOwner(): ExtensionRepository = + internal fun repositoryOwner(): ExtensionRepository = checkNotNull(repository) { "Extension repository is not initialized" } @Synchronized - private fun initializeRepository(cachePath: String) { + internal fun initializeRepository(cachePath: String) { val current = owner() if (repository != null) return val cache = File(cachePath) @@ -173,7 +171,7 @@ internal object RustCoreBackend : CoreBackend { } @Synchronized - private fun initializeOwner(arguments: Map<*, *>) { + internal fun initializeOwner(arguments: Map<*, *>) { val sourcePath = File(arguments["extensions_dir"] as String) val dataPath = File(arguments["data_dir"] as String) require(sourcePath.isAbsolute && dataPath.isAbsolute) { "Extension storage directories must be absolute" } @@ -230,7 +228,7 @@ internal object RustCoreBackend : CoreBackend { } @Synchronized - private fun shutdownOwner() { + internal fun shutdownOwner() { requests?.let { it.shutdown() it.close() @@ -305,7 +303,7 @@ internal object RustCoreBackend : CoreBackend { // Native callers have already selected/resolved these files through app or // SAF access. Retain only their parent directories for this operation. - private fun withMediaFiles(paths: List, block: (ExtensionManager) -> T): T = + internal fun withMediaFiles(paths: List, block: (ExtensionManager) -> T): T = withLibraryDirectories(paths.filter { it.isNotBlank() }.map { path -> require(File(path).isAbsolute) { "Media paths must be absolute" } File(path).absoluteFile.parent!! @@ -415,7 +413,7 @@ internal object RustCoreBackend : CoreBackend { } } - private fun mediaPath(path: String): String = if (path.isEmpty()) "" else File(path).canonicalPath + internal fun mediaPath(path: String): String = if (path.isEmpty()) "" else File(path).canonicalPath override fun reEnrichFile(requestJson: String): String { val request = JSONObject(requestJson) @@ -566,304 +564,12 @@ internal object RustCoreBackend : CoreBackend { } } - private fun completeSessionGrant(current: ExtensionManager, id: String, grant: String) { + internal fun completeSessionGrant(current: ExtensionManager, id: String, grant: String) { current.environment().use { it.setSessionGrant(id, grant) } requireSuccessfulExtensionAction(id, "completeGrant", current.invokeAction(id, "completeGrant")) } - override fun invokeApplication(method: String, arguments: Any?): Any? { - val args = arguments as? Map<*, *> ?: emptyMap() - fun string(key: String, default: String = "") = args[key] as? String ?: default - fun lyricsRequest(fileKey: String) = LyricsRequest( - string("spotify_id"), - string("track_name"), - string("artist_name"), - mediaPath(string(fileKey)), - (args["duration_ms"] as? Number)?.toLong() ?: 0L, - ) - fun ids(raw: String): List { - val values = JSONArray(raw) - return (0 until values.length()).map { values.getString(it) } - } - when (method) { - "cancelExtensionRequest" -> synchronized(this) { - requestRegistry().cancel(string("request_id")) - return null - } - "customSearchWithExtension", "getExtensionHomeFeed" -> { - val (current, lease) = acquireRequest(string("request_id")) - return lease.use { - try { - if (method == "customSearchWithExtension") { - current.customSearchJson(string("extension_id"), string("query"), string("options"), it) - } else { - current.getExtensionHomeFeedJson(string("extension_id"), it) - } - } finally { - it.release() - } - } - } - "prepareRuntimeState" -> synchronized(this) { - val directory = File(string("data_dir")) - require(directory.isAbsolute) { "Extension data directory must be absolute" } - val data = directory.canonicalPath - check(manager == null || identity?.second == data) { - "Runtime state directory does not match the initialized owner" - } - val raw = string("runtime_state") - manager?.environment()?.use { it.setRuntimeState(raw) } - runtimeState = data to raw - return null - } - "initExtensionSystem" -> { - initializeOwner(args) - return null - } - "setDownloadDirectory" -> { - setDownloadDirectory(string("path")) - return null - } - "acquireDownloadDirectory" -> synchronized(this) { - val scope = openDownloadDirectory(string("path")) - val token = UUID.randomUUID().toString() - directoryScopes[token] = scope - return token - } - "releaseDownloadDirectory" -> synchronized(this) { - directoryScopes.remove(string("token"))?.close() - return null - } - "cleanupExtensions" -> { - shutdownOwner() - return null - } - "buildFilename" -> return buildFilename(string("template"), string("metadata", "{}")) - "sanitizeFilename" -> return sanitizeFilename(string("filename")) - "getLogsSince" -> return synchronized(this) { - manager?.environment()?.use { environment -> - environment.logBuffer().use { it.since((args["index"] as? Number)?.toLong() ?: 0L) } - } ?: """{"logs":[],"next_index":0}""" - } - "clearLogs" -> return synchronized(this) { - manager?.environment()?.use { environment -> environment.logBuffer().use { it.clear() } } - null - } - "setLoggingEnabled", "setAllowPrivateNetwork", "setDownloadFallbackExtensionIds", - "setNetworkCompatibilityOptions", - "setLyricsProviders", "setLyricsFetchOptions" -> synchronized(this) { - when (method) { - "setLoggingEnabled" -> { - val enabled = args["enabled"] as? Boolean ?: false - manager?.environment()?.use { environment -> - environment.logBuffer().use { it.setEnabled(enabled) } - } - loggingEnabled = enabled - } - "setAllowPrivateNetwork" -> { - val allowed = args["allowed"] as? Boolean ?: false - manager?.environment()?.use { it.setAllowPrivateNetwork(allowed) } - allowPrivateNetwork = allowed - } - "setNetworkCompatibilityOptions" -> { - val allowed = args["allow_http"] as? Boolean ?: false - val insecureTls = args["insecure_tls"] as? Boolean ?: false - manager?.environment()?.use { it.setNetworkCompatibilityOptions(allowed, insecureTls) } - allowHttpFallback = allowed - } - "setDownloadFallbackExtensionIds" -> { - val raw = string("extension_ids") - val value = if (raw.isBlank() || raw.trim() == "null") null else ids(raw) - manager?.setFallbackProviders(value) - fallbackProviders = value - } - "setLyricsProviders" -> { - val raw = string("providers_json", "[]") - if (manager != null) manager!!.setLyricsProvidersJson(raw) else ids(raw) - return """{"success":true}""" - } - "setLyricsFetchOptions" -> { - val raw = string("options_json", "{}") - if (manager != null) manager!!.setLyricsFetchOptionsJson(raw) else JSONObject(raw) - return """{"success":true}""" - } - } - return null - } - "initExtensionRepo" -> { - initializeRepository(string("cache_dir")) - return null - } - "setRepoRegistryUrl" -> { - repositoryOwner().setRegistryUrl(string("registry_url")) - return null - } - "getRepoRegistryUrl" -> return repositoryOwner().registryUrl() - "clearRepoRegistryUrl" -> { - repositoryOwner().clearRegistryUrl() - return null - } - "getRepoExtensions" -> return repositoryOwner().extensions( - args["force_refresh"] as? Boolean ?: false, - ) - "downloadRepoExtension" -> return repositoryOwner().download( - string("extension_id"), - string("dest_dir"), - ) - } - val current = owner() - return when (method) { - "loadExtensionsFromDir" -> { - synchronized(this) { - check(File(string("dir_path")).canonicalPath == identity?.first) { "Extension source directory does not match the initialized owner" } - } - current.loadAll() - } - "loadExtensionFromPath" -> current.install(string("file_path")) - "upgradeExtension" -> current.upgrade(string("file_path")) - "checkExtensionUpgrade" -> current.checkUpgrade(string("file_path")) - "getInstalledExtensions" -> current.installed() - "setExtensionEnabled" -> { - current.setEnabled(string("extension_id"), args["enabled"] as? Boolean ?: false) - null - } - "removeExtension" -> { - current.remove(string("extension_id")) - null - } - "getExtensionSettings" -> current.environment().use { it.settings(string("extension_id")) } - "setExtensionSettings" -> { - current.updateSettings(string("extension_id"), string("settings", "{}")) - null - } - "invokeExtensionAction" -> if (args.containsKey("arguments_json")) { - // Transient form input bypasses persistent extension settings. - current.call(string("extension_id"), string("action"), string("arguments_json", "[]"), null, 120_000uL) - } else current.invokeAction(string("extension_id"), string("action")) - "checkExtensionHealth" -> current.checkExtensionHealthJson(string("extension_id")) - "setProviderPriority", "setMetadataProviderPriority" -> { - current.setProviderPriority(if (method == "setProviderPriority") "download" else "metadata", ids(string("priority", "[]"))) - null - } - "getProviderPriority", "getMetadataProviderPriority" -> { - JSONObject(current.providerPriorities()).optJSONArray(if (method == "getProviderPriority") "download" else "metadata")?.toString() ?: "[]" - } - "searchTracksWithMetadataProviders" -> current.searchMetadataProviders( - string("query"), - (args["limit"] as? Number)?.toLong() ?: 20L, - args["include_extensions"] as? Boolean ?: true, - "", - 30_000uL, - ) - "searchTracksWithMetadataProvider" -> current.searchMetadataProvider( - string("extension_id"), - string("query"), - (args["limit"] as? Number)?.toLong() ?: 20L, - 30_000uL, - ) - "getProviderMetadata" -> current.getProviderMetadataJson( - string("provider_id"), - string("resource_type"), - string("resource_id"), - null, - ) - "searchDeezerByISRC" -> current.searchDeezerByIsrcForItemId( - string("isrc"), - string("item_id"), - null, - ) - "getDeezerExtendedMetadata" -> current.getDeezerExtendedMetadata( - string("track_id"), - null, - ) - "convertSpotifyToDeezer" -> current.convertSpotifyToDeezer( - string("resource_type"), - string("spotify_id"), - null, - ) - "getTrackPlatformLinks" -> current.getTrackPlatformLinksJson( - string("spotify_id"), - string("isrc"), - null, - ) - "fetchMusicBrainzTags" -> { - val genre = try { - current.fetchMusicBrainzGenreByIsrc(string("isrc"), null) - } catch (_: Exception) { - "" - } - val albumArtist = try { - current.fetchMusicBrainzAlbumArtistByIsrc( - string("isrc"), - string("album_name"), - null, - ) - } catch (_: Exception) { - "" - } - JSONObject() - .put("genre", genre) - .put("album_artist", albumArtist) - .toString() - } - "getTrackCacheSize" -> current.getTrackCacheSize().toInt() - "clearTrackCache" -> { - current.clearTrackIdCache() - null - } - "setMetadataLanguage" -> { - current.setMetadataLanguage(string("tag")) - null - } - "findURLHandler" -> current.findUrlHandler(string("url")) ?: "" - "handleURLWithExtension" -> current.handleUrlJson(string("url")) - "getExtensionPendingAuth" -> current.getExtensionPendingAuthJson(string("extension_id")).ifEmpty { null } - "completeExtensionSessionGrant" -> { - completeSessionGrant(current, string("extension_id"), string("grant")) - true - } - "getAllDownloadProgress" -> current.withDownloadState { state -> state.allProgress() } - "cleanupConnections" -> current.environment().use { it.cleanupConnections(); null } - "resetDownloadCancels", "cancelDownloads" -> { - val values = args["item_ids"] as? List<*> ?: error("Expected item_ids list") - val itemIds = values.map { it as? String ?: error("Expected string item ID") } - current.withDownloadState { state -> - if (method == "cancelDownloads") state.cancelDownloads(itemIds) - else state.resetDownloadCancels(itemIds) - } - null - } - "clearItemProgress", "cancelDownload", "resetDownloadCancel" -> current.withDownloadState { state -> - when (method) { - "clearItemProgress" -> state.clearItemProgress(string("item_id")) - "cancelDownload" -> state.cancelDownload(string("item_id")) - else -> state.resetDownloadCancel(string("item_id")) - } - null - } - "getLyricsLRC", "getLyricsLRCWithSource" -> withMediaFiles(listOf(string("file_path"))) { - val request = lyricsRequest("file_path") - if (method == "getLyricsLRC") it.getLyricsLrc(request, null) - else it.getLyricsLrcWithSource(request, null) - } - "embedLyricsToFile" -> withMediaFiles(listOf(string("file_path"))) { - it.embedLyricsToFile(mediaPath(string("file_path")), string("lyrics"), null) - } - "fetchAndSaveLyrics" -> try { - withMediaFiles(listOf(string("audio_file_path"), string("output_path"))) { - it.fetchAndSaveLyrics(lyricsRequest("audio_file_path"), mediaPath(string("output_path")), null) - """{"success":true}""" - } - } catch (error: Exception) { - JSONObject().put("success", false).put("error", error.message ?: "Lyrics operation failed").toString() - } - "findCollectionAcrossExtensions" -> current.findCollectionAcrossExtensionsJson( - arguments as? String ?: "{}", - null, - ) - else -> error("Rust application method is not connected yet: $method") - } - } + override fun invokeApplication(method: String, arguments: Any?): Any? = dispatchApplication(method, arguments) override fun buildFilename(template: String, metadataJson: String): String = com.spotiflac.backend.buildFilename(template, metadataJson)