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.
This commit is contained in:
zarzet committed 2026-10-19 00:21:34 +07:00
1 parent b6b609f334
commit 20b4c73b8d
2 files changed
+324 -315

No files matched your search

@@ -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<Any, Any>()
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<String> {
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")
}
}
@@ -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<String, String, List<Byte>>? = null
private var loggingEnabled = false
private var allowPrivateNetwork = false
private var allowHttpFallback = false
private var fallbackProviders: List<String>? = null
private var runtimeState: Pair<String, String>? = null
private val directoryScopes = mutableMapOf<String, AutoCloseable>()
internal var identity: Triple<String, String, List<Byte>>? = null
internal var loggingEnabled = false
internal var allowPrivateNetwork = false
internal var allowHttpFallback = false
internal var fallbackProviders: List<String>? = null
internal var runtimeState: Pair<String, String>? = null
internal val directoryScopes = mutableMapOf<String, AutoCloseable>()
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 <T> ExtensionManager.withDownloadState(block: (DownloadState) -> T): T =
internal fun <T> 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<ExtensionManager, RequestLease> =
internal fun acquireRequest(id: String): Pair<ExtensionManager, RequestLease> =
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 <T> withMediaFiles(paths: List<String>, block: (ExtensionManager) -> T): T =
internal fun <T> withMediaFiles(paths: List<String>, 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<Any, Any>()
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<String> {
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)