mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-21 06:52:18 +02:00
refactor(android): move the metadata and tag backend calls into MainActivityMetadataCalls.kt
MainActivity.kt should be the dispatcher only. The metadata, tag, cover, re-enrich, post-processing and CUE cases keep their labels and order in the backend `when` and now call MainActivity.handle<Name> functions in MainActivityMetadataCalls.kt. Bodies move verbatim apart from the receiver label for this@MainActivity and re-indenting the FLAC copy-back lines of editFileMetadata, which sat at the wrong depth.
This commit is contained in:
1 parent
01c51bf751
commit
5d0f274414
2 files changed
+397
-357
No files matched your search
@@ -4,13 +4,11 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.ParcelFileDescriptor
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||
import io.flutter.embedding.android.FlutterActivityLaunchConfigs.BackgroundMode
|
||||
import io.flutter.embedding.android.FlutterFragment
|
||||
@@ -549,270 +547,18 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
"safCreateCollisionAwareFromPath" -> handleSafCreateCollisionAwareFromPath(call, result)
|
||||
"openContentUri" -> handleOpenContentUri(call, result)
|
||||
"shareContentUri" -> handleShareContentUri(call, result)
|
||||
"rewriteSplitArtistTags" -> {
|
||||
val filePath = call.argument<String>("file_path") ?: ""
|
||||
val artist = call.argument<String>("artist") ?: ""
|
||||
val albumArtist = call.argument<String>("album_artist") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
if (filePath.startsWith("content://")) {
|
||||
val uri = Uri.parse(filePath)
|
||||
val tempPath = copyUriToTemp(uri, ".flac")
|
||||
?: return@withContext errorJson("Failed to copy SAF file to temp")
|
||||
try {
|
||||
val raw = coreBackend.rewriteSplitArtistTags(tempPath, artist, albumArtist)
|
||||
val obj = JSONObject(raw)
|
||||
if (!obj.optBoolean("success", false)) {
|
||||
return@withContext raw
|
||||
}
|
||||
|
||||
if (!writeUriFromPath(uri, tempPath)) {
|
||||
return@withContext errorJson("Failed to write rewritten tags back to SAF file")
|
||||
}
|
||||
|
||||
obj.put("file_path", filePath)
|
||||
obj.toString()
|
||||
} catch (e: Exception) {
|
||||
errorJson("Failed to rewrite split artist tags in SAF file: ${e.message}")
|
||||
} finally {
|
||||
try {
|
||||
File(tempPath).delete()
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
} else {
|
||||
coreBackend.rewriteSplitArtistTags(filePath, artist, albumArtist)
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"readFileMetadata" -> {
|
||||
val filePath = call.argument<String>("file_path") ?: ""
|
||||
val displayName = call.argument<String>("display_name") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (filePath.startsWith("content://")) {
|
||||
readCompleteMetadataFromUri(Uri.parse(filePath), displayName)
|
||||
?.toString() ?: errorJson("Failed to read SAF metadata")
|
||||
} else {
|
||||
coreBackend.readFileMetadata(filePath, displayName)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errorJson(e.message ?: "Failed to read metadata")
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"checkHiResAuthenticity" -> {
|
||||
val filePath = call.argument<String>("file_path") ?: ""
|
||||
val optionsJson = call.argument<String>("options_json") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (filePath.startsWith("content://")) {
|
||||
checkHiResAuthenticityFromUri(Uri.parse(filePath), optionsJson)
|
||||
?.toString()
|
||||
?: errorJson("Failed to read SAF audio file")
|
||||
} else {
|
||||
coreBackend.checkHiResAuthenticity(filePath, optionsJson)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errorJson(e.message ?: "Hi-Res check failed")
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"editFileMetadata" -> {
|
||||
val filePath = call.argument<String>("file_path") ?: ""
|
||||
val metadataJson = call.argument<String>("metadata_json") ?: "{}"
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (filePath.startsWith("content://")) {
|
||||
val uri = Uri.parse(filePath)
|
||||
val tempPath = copyUriToTemp(uri)
|
||||
?: return@withContext """{"error":"Failed to copy SAF file to temp"}"""
|
||||
try {
|
||||
val raw = coreBackend.editFileMetadata(tempPath, metadataJson)
|
||||
val obj = JSONObject(raw)
|
||||
val method = obj.optString("method", "")
|
||||
if (method == "ffmpeg") {
|
||||
// MP3/Opus: Dart needs to FFmpeg the temp file, then call writeTempToSaf
|
||||
obj.put("temp_path", tempPath)
|
||||
obj.put("saf_uri", filePath)
|
||||
return@withContext obj.toString()
|
||||
// Note: temp file NOT deleted here - Dart will clean up after FFmpeg + writeTempToSaf
|
||||
}
|
||||
// FLAC: the backend wrote to temp, copy back now
|
||||
if (!writeUriFromPath(uri, tempPath)) {
|
||||
try { File(tempPath).delete() } catch (_: Exception) {}
|
||||
return@withContext """{"error":"Failed to write metadata back to SAF file"}"""
|
||||
}
|
||||
try { File(tempPath).delete() } catch (_: Exception) {}
|
||||
raw
|
||||
} catch (e: Exception) {
|
||||
try { File(tempPath).delete() } catch (_: Exception) {}
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
coreBackend.editFileMetadata(filePath, metadataJson)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("SpotiFLAC", "editFileMetadata failed: ${e.message}", e)
|
||||
"""{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}"""
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"writeM4AFreeformTags" -> {
|
||||
val filePath = call.argument<String>("file_path") ?: ""
|
||||
val metadataJson = call.argument<String>("metadata_json") ?: "{}"
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
coreBackend.writeM4aFreeformTags(filePath, metadataJson)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("SpotiFLAC", "writeM4AFreeformTags failed: ${e.message}", e)
|
||||
"""{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}"""
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"ensureAC4Config" -> {
|
||||
val filePath = call.argument<String>("file_path") ?: ""
|
||||
val sourcePath = call.argument<String>("source_path") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
coreBackend.ensureAc4Config(filePath, sourcePath)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("SpotiFLAC", "ensureAC4Config failed: ${e.message}", e)
|
||||
"""{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}"""
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"writeAC4Metadata" -> {
|
||||
val filePath = call.argument<String>("file_path") ?: ""
|
||||
val metadataJson = call.argument<String>("metadata_json") ?: "{}"
|
||||
val coverPath = call.argument<String>("cover_path") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
coreBackend.writeAc4Metadata(filePath, metadataJson, coverPath)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("SpotiFLAC", "writeAC4Metadata failed: ${e.message}", e)
|
||||
"""{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}"""
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"rewriteSplitArtistTags" -> handleRewriteSplitArtistTags(call, result)
|
||||
"readFileMetadata" -> handleReadFileMetadata(call, result)
|
||||
"checkHiResAuthenticity" -> handleCheckHiResAuthenticity(call, result)
|
||||
"editFileMetadata" -> handleEditFileMetadata(call, result)
|
||||
"writeM4AFreeformTags" -> handleWriteM4AFreeformTags(call, result)
|
||||
"ensureAC4Config" -> handleEnsureAC4Config(call, result)
|
||||
"writeAC4Metadata" -> handleWriteAC4Metadata(call, result)
|
||||
"writeTempToSaf" -> handleWriteTempToSaf(call, result)
|
||||
"writeSafSidecarLrc" -> handleWriteSafSidecarLrc(call, result)
|
||||
"downloadCoverToFile" -> {
|
||||
val coverUrl = call.argument<String>("cover_url") ?: ""
|
||||
val outputPath = call.argument<String>("output_path") ?: ""
|
||||
val maxDimension = call.argument<Number>("max_dimension")
|
||||
?.toLong()
|
||||
?.coerceAtLeast(0L)
|
||||
?: 0L
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
var temporaryCover: File? = null
|
||||
try {
|
||||
val destination = if (outputPath.isBlank()) {
|
||||
coreBackend.createTemporaryMediaFile(
|
||||
this@MainActivity,
|
||||
"cover_",
|
||||
".jpg",
|
||||
).also { temporaryCover = it }.absolutePath
|
||||
} else {
|
||||
outputPath
|
||||
}
|
||||
coreBackend.downloadCoverToFileSized(
|
||||
coverUrl,
|
||||
destination,
|
||||
maxDimension
|
||||
)
|
||||
JSONObject()
|
||||
.put("success", true)
|
||||
.put("file_path", destination)
|
||||
.toString()
|
||||
} catch (e: Exception) {
|
||||
temporaryCover?.delete()
|
||||
"""{"success":false,"error":"${e.message?.replace("\"", "'")}"}"""
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"extractCoverToFile" -> {
|
||||
val audioPath = call.argument<String>("audio_path") ?: ""
|
||||
val outputPath = call.argument<String>("output_path") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (audioPath.startsWith("content://")) {
|
||||
extractCoverFromUri(Uri.parse(audioPath), outputPath)
|
||||
"""{"success":true}"""
|
||||
} else {
|
||||
coreBackend.extractCoverToFile(audioPath, outputPath)
|
||||
"""{"success":true}"""
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
"""{"success":false,"error":"${e.message?.replace("\"", "'")}"}"""
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"reEnrichFile" -> {
|
||||
val requestJson = call.argument<String>("request_json") ?: "{}"
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val reqObj = JSONObject(requestJson)
|
||||
val filePath = reqObj.optString("file_path", "")
|
||||
|
||||
// Preview only resolves online metadata; it does not need
|
||||
// a full SAF document copy and never writes the source.
|
||||
if (filePath.startsWith("content://") && !reqObj.optBoolean("preview_only", false)) {
|
||||
val uri = Uri.parse(filePath)
|
||||
val tempPath = copyUriToTemp(uri)
|
||||
?: return@withContext """{"error":"Failed to copy SAF file to temp"}"""
|
||||
var retainedForFfmpeg = false
|
||||
try {
|
||||
reqObj.put("file_path", tempPath)
|
||||
val raw = coreBackend.reEnrichFile(reqObj.toString())
|
||||
val obj = JSONObject(raw)
|
||||
|
||||
if (obj.has("error")) {
|
||||
return@withContext raw
|
||||
}
|
||||
|
||||
val method = obj.optString("method", "")
|
||||
if (method == "ffmpeg") {
|
||||
// MP3/Opus: Dart handles FFmpeg on temp file, then writes back
|
||||
obj.put("temp_path", tempPath)
|
||||
obj.put("saf_uri", filePath)
|
||||
retainedForFfmpeg = true
|
||||
return@withContext obj.toString()
|
||||
// Dart cleans up after FFmpeg + writeTempToSaf.
|
||||
}
|
||||
|
||||
// FLAC: the selected backend wrote to temp; copy back now.
|
||||
if (!writeUriFromPath(uri, tempPath)) {
|
||||
return@withContext """{"error":"Failed to write enriched metadata back to SAF file"}"""
|
||||
}
|
||||
if (obj.optBoolean("write_external_lrc", false) &&
|
||||
!writeSafSidecarLrc(uri, obj.optString("lyrics", ""))) {
|
||||
return@withContext errorJson(
|
||||
"Metadata was saved, but the LRC lyrics file could not be updated"
|
||||
)
|
||||
}
|
||||
raw
|
||||
} finally {
|
||||
if (!retainedForFfmpeg) {
|
||||
try { File(tempPath).delete() } catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
coreBackend.reEnrichFile(requestJson)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
"""{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}"""
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"downloadCoverToFile" -> handleDownloadCoverToFile(call, result)
|
||||
"extractCoverToFile" -> handleExtractCoverToFile(call, result)
|
||||
"reEnrichFile" -> handleReEnrichFile(call, result)
|
||||
"startDownloadService" -> {
|
||||
val trackName = call.argument<String>("track_name") ?: ""
|
||||
val artistName = call.argument<String>("artist_name") ?: ""
|
||||
@@ -950,31 +696,7 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
android.util.Log.d("SpotiFLAC", "Backend memory pressure release completed")
|
||||
result.success(null)
|
||||
}
|
||||
"runPostProcessingV2" -> {
|
||||
val inputJson = call.argument<String>("input") ?: ""
|
||||
val metadataJson = call.argument<String>("metadata") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
val inputObj = if (inputJson.isNotBlank()) JSONObject(inputJson) else JSONObject()
|
||||
val uriStr = inputObj.optString("uri", "")
|
||||
val pathStr = inputObj.optString("path", "")
|
||||
val effectiveUri = when {
|
||||
uriStr.startsWith("content://") -> uriStr
|
||||
pathStr.startsWith("content://") -> pathStr
|
||||
else -> ""
|
||||
}
|
||||
|
||||
if (effectiveUri.isNotBlank()) {
|
||||
runPostProcessingSafV2(effectiveUri, metadataJson, inputObj.optString("item_id", ""))
|
||||
} else {
|
||||
if (pathStr.isNotBlank()) {
|
||||
inputObj.put("name", File(pathStr).name)
|
||||
inputObj.put("is_saf", false)
|
||||
}
|
||||
coreBackend.runPostProcessing(inputObj.toString(), metadataJson)
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"runPostProcessingV2" -> handleRunPostProcessingV2(call, result)
|
||||
"setLibraryCoverCacheDir" -> {
|
||||
val cacheDir = call.argument<String>("cache_dir") ?: ""
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -1060,74 +782,8 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
"readAudioMetadata" -> {
|
||||
val filePath = call.argument<String>("file_path") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (filePath.startsWith("content://")) {
|
||||
val uri = Uri.parse(filePath)
|
||||
val metadata = readAudioMetadataFromUri(uri)
|
||||
?: return@withContext """{"error":"Failed to read SAF audio metadata"}"""
|
||||
metadata.put("filePath", filePath)
|
||||
metadata.toString()
|
||||
} else {
|
||||
coreBackend.readAudioMetadata(filePath, "", "")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
"""{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}"""
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"parseCueSheet" -> {
|
||||
val cuePath = call.argument<String>("cue_path") ?: ""
|
||||
val audioDir = call.argument<String>("audio_dir") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (cuePath.startsWith("content://")) {
|
||||
val uri = Uri.parse(cuePath)
|
||||
val tempCuePath = copyUriToTemp(uri, ".cue")
|
||||
?: return@withContext """{"error":"Failed to copy CUE file to temp"}"""
|
||||
try {
|
||||
val audioFileName = extractCueAudioFileName(tempCuePath)
|
||||
|
||||
val parentDir = safParentDir(uri)
|
||||
val audioDoc = if (parentDir != null) {
|
||||
val cueName = try {
|
||||
DocumentFile.fromSingleUri(this@MainActivity, uri)?.name ?: ""
|
||||
} catch (_: Exception) { "" }
|
||||
findSafCueAudioSibling(this@MainActivity, parentDir, cueName, audioFileName)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (audioDoc == null || !audioDoc.isFile) {
|
||||
return@withContext JSONObject()
|
||||
.put("error", "Audio file not found for CUE sheet")
|
||||
.toString()
|
||||
}
|
||||
|
||||
// Preview parses the CUE text only; the selected audio
|
||||
// stays in SAF instead of copying a whole album to cache.
|
||||
val resultJson = coreBackend.parseCueSheetWithResolvedAudio(
|
||||
tempCuePath,
|
||||
audioDoc.uri.toString(),
|
||||
)
|
||||
JSONObject(resultJson)
|
||||
.put("cue_path", cuePath)
|
||||
.toString()
|
||||
} finally {
|
||||
try { File(tempCuePath).delete() } catch (_: Exception) {}
|
||||
}
|
||||
} else {
|
||||
coreBackend.parseCueSheet(cuePath, audioDir)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
"""{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}"""
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"readAudioMetadata" -> handleReadAudioMetadata(call, result)
|
||||
"parseCueSheet" -> handleParseCueSheet(call, result)
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
package com.zarz.spotiflac
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
|
||||
// Metadata and tag backend calls: reading and editing tags, M4A and AC-4
|
||||
// writes, cover download and extraction, re-enrichment, post-processing and
|
||||
// CUE sheets. SAF documents go through a temp copy where the backend needs
|
||||
// a file path. MainActivity's backend `when` dispatches each case here, in
|
||||
// the order it lists them.
|
||||
|
||||
internal suspend fun MainActivity.handleRewriteSplitArtistTags(call: MethodCall, result: MethodChannel.Result) {
|
||||
val filePath = call.argument<String>("file_path") ?: ""
|
||||
val artist = call.argument<String>("artist") ?: ""
|
||||
val albumArtist = call.argument<String>("album_artist") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
if (filePath.startsWith("content://")) {
|
||||
val uri = Uri.parse(filePath)
|
||||
val tempPath = copyUriToTemp(uri, ".flac")
|
||||
?: return@withContext errorJson("Failed to copy SAF file to temp")
|
||||
try {
|
||||
val raw = coreBackend.rewriteSplitArtistTags(tempPath, artist, albumArtist)
|
||||
val obj = JSONObject(raw)
|
||||
if (!obj.optBoolean("success", false)) {
|
||||
return@withContext raw
|
||||
}
|
||||
|
||||
if (!writeUriFromPath(uri, tempPath)) {
|
||||
return@withContext errorJson("Failed to write rewritten tags back to SAF file")
|
||||
}
|
||||
|
||||
obj.put("file_path", filePath)
|
||||
obj.toString()
|
||||
} catch (e: Exception) {
|
||||
errorJson("Failed to rewrite split artist tags in SAF file: ${e.message}")
|
||||
} finally {
|
||||
try {
|
||||
File(tempPath).delete()
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
} else {
|
||||
coreBackend.rewriteSplitArtistTags(filePath, artist, albumArtist)
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
|
||||
internal suspend fun MainActivity.handleReadFileMetadata(call: MethodCall, result: MethodChannel.Result) {
|
||||
val filePath = call.argument<String>("file_path") ?: ""
|
||||
val displayName = call.argument<String>("display_name") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (filePath.startsWith("content://")) {
|
||||
readCompleteMetadataFromUri(Uri.parse(filePath), displayName)
|
||||
?.toString() ?: errorJson("Failed to read SAF metadata")
|
||||
} else {
|
||||
coreBackend.readFileMetadata(filePath, displayName)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errorJson(e.message ?: "Failed to read metadata")
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
|
||||
internal suspend fun MainActivity.handleCheckHiResAuthenticity(call: MethodCall, result: MethodChannel.Result) {
|
||||
val filePath = call.argument<String>("file_path") ?: ""
|
||||
val optionsJson = call.argument<String>("options_json") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (filePath.startsWith("content://")) {
|
||||
checkHiResAuthenticityFromUri(Uri.parse(filePath), optionsJson)
|
||||
?.toString()
|
||||
?: errorJson("Failed to read SAF audio file")
|
||||
} else {
|
||||
coreBackend.checkHiResAuthenticity(filePath, optionsJson)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errorJson(e.message ?: "Hi-Res check failed")
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
|
||||
internal suspend fun MainActivity.handleEditFileMetadata(call: MethodCall, result: MethodChannel.Result) {
|
||||
val filePath = call.argument<String>("file_path") ?: ""
|
||||
val metadataJson = call.argument<String>("metadata_json") ?: "{}"
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (filePath.startsWith("content://")) {
|
||||
val uri = Uri.parse(filePath)
|
||||
val tempPath = copyUriToTemp(uri)
|
||||
?: return@withContext """{"error":"Failed to copy SAF file to temp"}"""
|
||||
try {
|
||||
val raw = coreBackend.editFileMetadata(tempPath, metadataJson)
|
||||
val obj = JSONObject(raw)
|
||||
val method = obj.optString("method", "")
|
||||
if (method == "ffmpeg") {
|
||||
// MP3/Opus: Dart needs to FFmpeg the temp file, then call writeTempToSaf
|
||||
obj.put("temp_path", tempPath)
|
||||
obj.put("saf_uri", filePath)
|
||||
return@withContext obj.toString()
|
||||
// Note: temp file NOT deleted here - Dart will clean up after FFmpeg + writeTempToSaf
|
||||
}
|
||||
// FLAC: the backend wrote to temp, copy back now
|
||||
if (!writeUriFromPath(uri, tempPath)) {
|
||||
try { File(tempPath).delete() } catch (_: Exception) {}
|
||||
return@withContext """{"error":"Failed to write metadata back to SAF file"}"""
|
||||
}
|
||||
try { File(tempPath).delete() } catch (_: Exception) {}
|
||||
raw
|
||||
} catch (e: Exception) {
|
||||
try { File(tempPath).delete() } catch (_: Exception) {}
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
coreBackend.editFileMetadata(filePath, metadataJson)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("SpotiFLAC", "editFileMetadata failed: ${e.message}", e)
|
||||
"""{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}"""
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
|
||||
internal suspend fun MainActivity.handleWriteM4AFreeformTags(call: MethodCall, result: MethodChannel.Result) {
|
||||
val filePath = call.argument<String>("file_path") ?: ""
|
||||
val metadataJson = call.argument<String>("metadata_json") ?: "{}"
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
coreBackend.writeM4aFreeformTags(filePath, metadataJson)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("SpotiFLAC", "writeM4AFreeformTags failed: ${e.message}", e)
|
||||
"""{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}"""
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
|
||||
internal suspend fun MainActivity.handleEnsureAC4Config(call: MethodCall, result: MethodChannel.Result) {
|
||||
val filePath = call.argument<String>("file_path") ?: ""
|
||||
val sourcePath = call.argument<String>("source_path") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
coreBackend.ensureAc4Config(filePath, sourcePath)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("SpotiFLAC", "ensureAC4Config failed: ${e.message}", e)
|
||||
"""{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}"""
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
|
||||
internal suspend fun MainActivity.handleWriteAC4Metadata(call: MethodCall, result: MethodChannel.Result) {
|
||||
val filePath = call.argument<String>("file_path") ?: ""
|
||||
val metadataJson = call.argument<String>("metadata_json") ?: "{}"
|
||||
val coverPath = call.argument<String>("cover_path") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
coreBackend.writeAc4Metadata(filePath, metadataJson, coverPath)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("SpotiFLAC", "writeAC4Metadata failed: ${e.message}", e)
|
||||
"""{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}"""
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
|
||||
internal suspend fun MainActivity.handleDownloadCoverToFile(call: MethodCall, result: MethodChannel.Result) {
|
||||
val coverUrl = call.argument<String>("cover_url") ?: ""
|
||||
val outputPath = call.argument<String>("output_path") ?: ""
|
||||
val maxDimension = call.argument<Number>("max_dimension")
|
||||
?.toLong()
|
||||
?.coerceAtLeast(0L)
|
||||
?: 0L
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
var temporaryCover: File? = null
|
||||
try {
|
||||
val destination = if (outputPath.isBlank()) {
|
||||
coreBackend.createTemporaryMediaFile(
|
||||
this@handleDownloadCoverToFile,
|
||||
"cover_",
|
||||
".jpg",
|
||||
).also { temporaryCover = it }.absolutePath
|
||||
} else {
|
||||
outputPath
|
||||
}
|
||||
coreBackend.downloadCoverToFileSized(
|
||||
coverUrl,
|
||||
destination,
|
||||
maxDimension
|
||||
)
|
||||
JSONObject()
|
||||
.put("success", true)
|
||||
.put("file_path", destination)
|
||||
.toString()
|
||||
} catch (e: Exception) {
|
||||
temporaryCover?.delete()
|
||||
"""{"success":false,"error":"${e.message?.replace("\"", "'")}"}"""
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
|
||||
internal suspend fun MainActivity.handleExtractCoverToFile(call: MethodCall, result: MethodChannel.Result) {
|
||||
val audioPath = call.argument<String>("audio_path") ?: ""
|
||||
val outputPath = call.argument<String>("output_path") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (audioPath.startsWith("content://")) {
|
||||
extractCoverFromUri(Uri.parse(audioPath), outputPath)
|
||||
"""{"success":true}"""
|
||||
} else {
|
||||
coreBackend.extractCoverToFile(audioPath, outputPath)
|
||||
"""{"success":true}"""
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
"""{"success":false,"error":"${e.message?.replace("\"", "'")}"}"""
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
|
||||
internal suspend fun MainActivity.handleReEnrichFile(call: MethodCall, result: MethodChannel.Result) {
|
||||
val requestJson = call.argument<String>("request_json") ?: "{}"
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val reqObj = JSONObject(requestJson)
|
||||
val filePath = reqObj.optString("file_path", "")
|
||||
|
||||
// Preview only resolves online metadata; it does not need
|
||||
// a full SAF document copy and never writes the source.
|
||||
if (filePath.startsWith("content://") && !reqObj.optBoolean("preview_only", false)) {
|
||||
val uri = Uri.parse(filePath)
|
||||
val tempPath = copyUriToTemp(uri)
|
||||
?: return@withContext """{"error":"Failed to copy SAF file to temp"}"""
|
||||
var retainedForFfmpeg = false
|
||||
try {
|
||||
reqObj.put("file_path", tempPath)
|
||||
val raw = coreBackend.reEnrichFile(reqObj.toString())
|
||||
val obj = JSONObject(raw)
|
||||
|
||||
if (obj.has("error")) {
|
||||
return@withContext raw
|
||||
}
|
||||
|
||||
val method = obj.optString("method", "")
|
||||
if (method == "ffmpeg") {
|
||||
// MP3/Opus: Dart handles FFmpeg on temp file, then writes back
|
||||
obj.put("temp_path", tempPath)
|
||||
obj.put("saf_uri", filePath)
|
||||
retainedForFfmpeg = true
|
||||
return@withContext obj.toString()
|
||||
// Dart cleans up after FFmpeg + writeTempToSaf.
|
||||
}
|
||||
|
||||
// FLAC: the selected backend wrote to temp; copy back now.
|
||||
if (!writeUriFromPath(uri, tempPath)) {
|
||||
return@withContext """{"error":"Failed to write enriched metadata back to SAF file"}"""
|
||||
}
|
||||
if (obj.optBoolean("write_external_lrc", false) &&
|
||||
!writeSafSidecarLrc(uri, obj.optString("lyrics", ""))) {
|
||||
return@withContext errorJson(
|
||||
"Metadata was saved, but the LRC lyrics file could not be updated"
|
||||
)
|
||||
}
|
||||
raw
|
||||
} finally {
|
||||
if (!retainedForFfmpeg) {
|
||||
try { File(tempPath).delete() } catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
coreBackend.reEnrichFile(requestJson)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
"""{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}"""
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
|
||||
internal suspend fun MainActivity.handleRunPostProcessingV2(call: MethodCall, result: MethodChannel.Result) {
|
||||
val inputJson = call.argument<String>("input") ?: ""
|
||||
val metadataJson = call.argument<String>("metadata") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
val inputObj = if (inputJson.isNotBlank()) JSONObject(inputJson) else JSONObject()
|
||||
val uriStr = inputObj.optString("uri", "")
|
||||
val pathStr = inputObj.optString("path", "")
|
||||
val effectiveUri = when {
|
||||
uriStr.startsWith("content://") -> uriStr
|
||||
pathStr.startsWith("content://") -> pathStr
|
||||
else -> ""
|
||||
}
|
||||
|
||||
if (effectiveUri.isNotBlank()) {
|
||||
runPostProcessingSafV2(effectiveUri, metadataJson, inputObj.optString("item_id", ""))
|
||||
} else {
|
||||
if (pathStr.isNotBlank()) {
|
||||
inputObj.put("name", File(pathStr).name)
|
||||
inputObj.put("is_saf", false)
|
||||
}
|
||||
coreBackend.runPostProcessing(inputObj.toString(), metadataJson)
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
|
||||
internal suspend fun MainActivity.handleReadAudioMetadata(call: MethodCall, result: MethodChannel.Result) {
|
||||
val filePath = call.argument<String>("file_path") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (filePath.startsWith("content://")) {
|
||||
val uri = Uri.parse(filePath)
|
||||
val metadata = readAudioMetadataFromUri(uri)
|
||||
?: return@withContext """{"error":"Failed to read SAF audio metadata"}"""
|
||||
metadata.put("filePath", filePath)
|
||||
metadata.toString()
|
||||
} else {
|
||||
coreBackend.readAudioMetadata(filePath, "", "")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
"""{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}"""
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
|
||||
internal suspend fun MainActivity.handleParseCueSheet(call: MethodCall, result: MethodChannel.Result) {
|
||||
val cuePath = call.argument<String>("cue_path") ?: ""
|
||||
val audioDir = call.argument<String>("audio_dir") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (cuePath.startsWith("content://")) {
|
||||
val uri = Uri.parse(cuePath)
|
||||
val tempCuePath = copyUriToTemp(uri, ".cue")
|
||||
?: return@withContext """{"error":"Failed to copy CUE file to temp"}"""
|
||||
try {
|
||||
val audioFileName = extractCueAudioFileName(tempCuePath)
|
||||
|
||||
val parentDir = safParentDir(uri)
|
||||
val audioDoc = if (parentDir != null) {
|
||||
val cueName = try {
|
||||
DocumentFile.fromSingleUri(this@handleParseCueSheet, uri)?.name ?: ""
|
||||
} catch (_: Exception) { "" }
|
||||
findSafCueAudioSibling(this@handleParseCueSheet, parentDir, cueName, audioFileName)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (audioDoc == null || !audioDoc.isFile) {
|
||||
return@withContext JSONObject()
|
||||
.put("error", "Audio file not found for CUE sheet")
|
||||
.toString()
|
||||
}
|
||||
|
||||
// Preview parses the CUE text only; the selected audio
|
||||
// stays in SAF instead of copying a whole album to cache.
|
||||
val resultJson = coreBackend.parseCueSheetWithResolvedAudio(
|
||||
tempCuePath,
|
||||
audioDoc.uri.toString(),
|
||||
)
|
||||
JSONObject(resultJson)
|
||||
.put("cue_path", cuePath)
|
||||
.toString()
|
||||
} finally {
|
||||
try { File(tempCuePath).delete() } catch (_: Exception) {}
|
||||
}
|
||||
} else {
|
||||
coreBackend.parseCueSheet(cuePath, audioDir)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
"""{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}"""
|
||||
}
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
Reference in new issue
Block a user