From 631b9aa5e84f618589f4ccbb6ccef6ef3fcfa84e Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:03:42 +0700 Subject: [PATCH] refactor(android): move finalizer post-processing into NativeFinalizerPostProcess.kt Move the extension post-processing hook stage out of NativeDownloadFinalizer into a sibling file as an extension function. The body is verbatim. The shared materializeForFFmpeg and replaceStatePath helpers stay on the object because every stage uses them, and no object member needed wider visibility. --- .../zarz/spotiflac/NativeDownloadFinalizer.kt | 85 ----------------- .../spotiflac/NativeFinalizerPostProcess.kt | 94 +++++++++++++++++++ 2 files changed, 94 insertions(+), 85 deletions(-) create mode 100644 android/app/src/main/kotlin/com/zarz/spotiflac/NativeFinalizerPostProcess.kt diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt index e1be1648..f3ab43d4 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt @@ -424,91 +424,6 @@ object NativeDownloadFinalizer { } } - private fun runPostProcessing( - context: Context, - input: FinalizeInput, - state: FinalizeState, - shouldCancel: () -> Boolean, - ) { - if (!input.request.optBoolean("post_processing_enabled", false)) return - val metadata = JSONObject() - .put("title", trackString(input, "name", input.request.optString("track_name", ""))) - .put("artist", trackString(input, "artistName", input.request.optString("artist_name", ""))) - .put("album", trackString(input, "albumName", input.request.optString("album_name", ""))) - .put("album_artist", resolvedAlbumArtist(input)) - .put("track_number", trackInt(input, "trackNumber", input.request.optInt("track_number", 0))) - .put("disc_number", trackInt(input, "discNumber", input.request.optInt("disc_number", 0))) - .put("isrc", trackString(input, "isrc", input.request.optString("isrc", ""))) - .put("release_date", trackString(input, "releaseDate", input.request.optString("release_date", ""))) - .put("duration_ms", trackInt(input, "duration", 0) * 1000) - .put("cover_url", metadataCoverUrl(input)) - - if (state.filePath.startsWith("content://")) { - val uri = state.filePath - val tempInput = SafDownloadHandler.copyContentUriToTemp(context, uri) - ?: throw IllegalStateException("failed to copy SAF file for post-processing") - try { - val inputObj = JSONObject() - .put("item_id", input.itemId) - .put("path", tempInput) - .put("uri", uri) - .put("name", state.fileName) - .put("mime_type", mimeTypeForExt(state.fileName.substringAfterLast('.', ""))) - .put("size", File(tempInput).length()) - .put("is_saf", true) - checkCancelled(shouldCancel) - val response = JSONObject(createCoreBackend(context).runPostProcessing(inputObj.toString(), metadata.toString())) - checkCancelled(shouldCancel) - if (!response.optBoolean("success", false)) return - val newPath = response.optString("new_file_path", "") - val outputPath = newPath.ifBlank { tempInput } - val outputFile = File(outputPath) - if (!outputFile.exists()) return - val outputName = if (newPath.isBlank()) state.fileName else outputFile.name - val newUri = SafDownloadHandler.writeFileToSaf( - context = context, - treeUriStr = input.request.optString("saf_tree_uri", ""), - relativeDir = input.request.optString("saf_relative_dir", ""), - fileName = outputName, - mimeType = mimeTypeForExt(outputFile.extension), - srcPath = outputFile.absolutePath, - ) ?: return - if (newUri != uri) SafDownloadHandler.deleteContentUri(context, uri) - state.filePath = newUri - state.fileName = outputName - if (outputPath != tempInput) outputFile.delete() - } finally { - File(tempInput).delete() - } - return - } - - val inputObj = JSONObject() - .put("item_id", input.itemId) - .put("path", state.filePath) - .put("name", state.fileName) - .put("is_saf", false) - checkCancelled(shouldCancel) - val response = JSONObject(createCoreBackend(context).runPostProcessing(inputObj.toString(), metadata.toString())) - checkCancelled(shouldCancel) - if (response.optBoolean("success", false)) { - val newPath = response.optString("new_file_path", "") - if (newPath.isNotBlank() && newPath != state.filePath) { - if (isDeferredSafPublish(input)) { - val output = File(newPath) - check(output.isFile && output.length() > 0L) { - "post-processing output missing or empty" - } - // This input is an owned staging file; publication later - // removes the replacement, so retire the old stage now. - File(state.filePath).delete() - } - state.filePath = newPath - state.fileName = File(newPath).name - } - } - } - internal fun materializeForFFmpeg(context: Context, input: FinalizeInput, state: FinalizeState): String { if (!state.filePath.startsWith("content://")) return state.filePath return SafDownloadHandler.copyContentUriToTemp(context, state.filePath) diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/NativeFinalizerPostProcess.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/NativeFinalizerPostProcess.kt new file mode 100644 index 00000000..96d6fc89 --- /dev/null +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/NativeFinalizerPostProcess.kt @@ -0,0 +1,94 @@ +package com.zarz.spotiflac + +import android.content.Context +import com.zarz.spotiflac.SafDownloadHandler.mimeTypeForExt +import org.json.JSONObject +import java.io.File + + +// Extension post-processing hook stage for NativeDownloadFinalizer. + +internal fun NativeDownloadFinalizer.runPostProcessing( + context: Context, + input: NativeDownloadFinalizer.FinalizeInput, + state: NativeDownloadFinalizer.FinalizeState, + shouldCancel: () -> Boolean, +) { + if (!input.request.optBoolean("post_processing_enabled", false)) return + val metadata = JSONObject() + .put("title", trackString(input, "name", input.request.optString("track_name", ""))) + .put("artist", trackString(input, "artistName", input.request.optString("artist_name", ""))) + .put("album", trackString(input, "albumName", input.request.optString("album_name", ""))) + .put("album_artist", resolvedAlbumArtist(input)) + .put("track_number", trackInt(input, "trackNumber", input.request.optInt("track_number", 0))) + .put("disc_number", trackInt(input, "discNumber", input.request.optInt("disc_number", 0))) + .put("isrc", trackString(input, "isrc", input.request.optString("isrc", ""))) + .put("release_date", trackString(input, "releaseDate", input.request.optString("release_date", ""))) + .put("duration_ms", trackInt(input, "duration", 0) * 1000) + .put("cover_url", metadataCoverUrl(input)) + + if (state.filePath.startsWith("content://")) { + val uri = state.filePath + val tempInput = SafDownloadHandler.copyContentUriToTemp(context, uri) + ?: throw IllegalStateException("failed to copy SAF file for post-processing") + try { + val inputObj = JSONObject() + .put("item_id", input.itemId) + .put("path", tempInput) + .put("uri", uri) + .put("name", state.fileName) + .put("mime_type", mimeTypeForExt(state.fileName.substringAfterLast('.', ""))) + .put("size", File(tempInput).length()) + .put("is_saf", true) + checkCancelled(shouldCancel) + val response = JSONObject(createCoreBackend(context).runPostProcessing(inputObj.toString(), metadata.toString())) + checkCancelled(shouldCancel) + if (!response.optBoolean("success", false)) return + val newPath = response.optString("new_file_path", "") + val outputPath = newPath.ifBlank { tempInput } + val outputFile = File(outputPath) + if (!outputFile.exists()) return + val outputName = if (newPath.isBlank()) state.fileName else outputFile.name + val newUri = SafDownloadHandler.writeFileToSaf( + context = context, + treeUriStr = input.request.optString("saf_tree_uri", ""), + relativeDir = input.request.optString("saf_relative_dir", ""), + fileName = outputName, + mimeType = mimeTypeForExt(outputFile.extension), + srcPath = outputFile.absolutePath, + ) ?: return + if (newUri != uri) SafDownloadHandler.deleteContentUri(context, uri) + state.filePath = newUri + state.fileName = outputName + if (outputPath != tempInput) outputFile.delete() + } finally { + File(tempInput).delete() + } + return + } + + val inputObj = JSONObject() + .put("item_id", input.itemId) + .put("path", state.filePath) + .put("name", state.fileName) + .put("is_saf", false) + checkCancelled(shouldCancel) + val response = JSONObject(createCoreBackend(context).runPostProcessing(inputObj.toString(), metadata.toString())) + checkCancelled(shouldCancel) + if (response.optBoolean("success", false)) { + val newPath = response.optString("new_file_path", "") + if (newPath.isNotBlank() && newPath != state.filePath) { + if (isDeferredSafPublish(input)) { + val output = File(newPath) + check(output.isFile && output.length() > 0L) { + "post-processing output missing or empty" + } + // This input is an owned staging file; publication later + // removes the replacement, so retire the old stage now. + File(state.filePath).delete() + } + state.filePath = newPath + state.fileName = File(newPath).name + } + } +}