refactor(android): move finalizer metadata into NativeFinalizerMetadata.kt

Move the metadata embedding stage, ReplayGain tagging and verification,
the album key and duration helpers, and the final audio quality probe
out of NativeDownloadFinalizer into a sibling file of extension
functions. Bodies are verbatim. The moved code reads three JSON helpers
that stay on the object, so parseObject, optPositiveInt and
optPositiveBitrateKbps widen from private to internal.
This commit is contained in:
zarzet committed 2026-10-19 00:14:47 +07:00
1 parent ead04c00b4
commit 0b4725c2c0
2 files changed
+264 -252

No files matched your search

@@ -2,19 +2,15 @@ package com.zarz.spotiflac
import android.content.ContentValues
import android.content.Context
import android.net.Uri
import android.util.Log
import com.antonkarpenko.ffmpegkit.FFmpegKit
import com.antonkarpenko.ffmpegkit.FFmpegSessionCompleteCallback
import com.zarz.spotiflac.SafDownloadHandler.mimeTypeForExt
import com.zarz.spotiflac.SafDownloadHandler.normalizeExt
import com.zarz.spotiflac.NativeFinalizationPolicy.displayAudioQuality
import com.zarz.spotiflac.NativeFinalizationPolicy.normalizeAudioCodec
import org.json.JSONObject
import java.io.File
import java.util.Locale
import java.util.concurrent.CancellationException
import kotlin.math.abs
import kotlin.math.pow
object NativeDownloadFinalizer {
@@ -368,7 +364,7 @@ object NativeDownloadFinalizer {
.toString()
}
private fun parseObject(raw: String): JSONObject {
internal fun parseObject(raw: String): JSONObject {
val trimmed = raw.trim()
if (trimmed.isEmpty()) return JSONObject()
return try {
@@ -428,235 +424,6 @@ object NativeDownloadFinalizer {
}
}
private fun finalizeMetadata(context: Context, input: FinalizeInput, state: FinalizeState) {
if (!input.request.optBoolean("embed_metadata", false)) return
if (!state.filePath.startsWith("content://")) {
embedBasicMetadata(context, state.filePath, input, formatForPath(state.filePath))
return
}
val tempPath = SafDownloadHandler.copyContentUriToTemp(context, state.filePath)
?: throw IllegalStateException("failed to copy SAF file for metadata")
try {
embedBasicMetadata(context, tempPath, input, formatForPath(state.fileName.ifBlank { tempPath }))
val tempFile = File(tempPath)
val finalName = desiredFileName(input, state, normalizeExt(state.fileName.substringAfterLast('.', "")))
val newUri = SafDownloadHandler.writeFileToSaf(
context = context,
treeUriStr = input.request.optString("saf_tree_uri", ""),
relativeDir = input.request.optString("saf_relative_dir", ""),
fileName = finalName,
mimeType = mimeTypeForExt(finalName.substringAfterLast('.', "")),
srcPath = tempFile.absolutePath,
) ?: throw IllegalStateException("failed to write metadata-updated SAF file")
if (newUri != state.filePath) SafDownloadHandler.deleteContentUri(context, state.filePath)
state.filePath = newUri
state.fileName = finalName
} finally {
File(tempPath).delete()
}
}
private fun writeReplayGain(
context: Context,
input: FinalizeInput,
state: FinalizeState,
shouldCancel: () -> Boolean,
): JSONObject? {
if (!input.request.optBoolean("embed_replaygain", false)) return null
val ext = normalizeExt(File(state.filePath).extension)
val fileExt = if (state.filePath.startsWith("content://")) {
normalizeExt(state.fileName.substringAfterLast('.', ""))
} else {
ext
}
if (
fileExt != ".flac" &&
fileExt != ".m4a" &&
fileExt != ".mp4" &&
fileExt != ".mp3" &&
fileExt != ".opus" &&
fileExt != ".ogg"
) return null
val scanPath = if (state.filePath.startsWith("content://")) {
SafDownloadHandler.copyContentUriToTemp(context, state.filePath)
?: throw IllegalStateException("failed to copy SAF file for ReplayGain")
} else {
state.filePath
}
val deleteScanPath = scanPath != state.filePath
val scan = try {
scanReplayGain(scanPath, shouldCancel)
?: throw IllegalStateException("ReplayGain analysis produced no valid measurement")
} finally {
if (deleteScanPath) File(scanPath).delete()
}
checkCancelled(shouldCancel)
val fields = JSONObject()
.put("replaygain_track_gain", scan.trackGain)
.put("replaygain_track_peak", scan.trackPeak)
writeReplayGainFields(context, state.filePath, fields)
return JSONObject()
.put("album_key", albumKey(input))
.put("file_path", state.filePath)
.put("file_name", state.fileName)
.put("track_id", trackString(input, "id", input.request.optString("spotify_id", input.itemId)))
.put("integrated_lufs", scan.integratedLufs)
.put("true_peak_linear", scan.truePeakLinear)
.put("duration_secs", replayGainDurationSeconds(input))
.put("track_gain", scan.trackGain)
.put("track_peak", scan.trackPeak)
}
private fun writeReplayGainFields(context: Context, path: String, fields: JSONObject) {
if (!path.startsWith("content://")) {
writeLocalReplayGainFields(context, path, fields)
return
}
val tempPath = SafDownloadHandler.copyContentUriToTemp(context, path)
?: throw IllegalStateException("failed to copy SAF file for ReplayGain write")
try {
writeLocalReplayGainFields(context, tempPath, fields)
val uri = Uri.parse(path)
context.contentResolver.openOutputStream(uri, "wt")?.use { output ->
File(tempPath).inputStream().use { input -> input.copyTo(output) }
SafDownloadHandler.syncOutputStream(output)
} ?: throw IllegalStateException("failed to write ReplayGain back to SAF")
} finally {
File(tempPath).delete()
}
}
private fun writeLocalReplayGainFields(context: Context, path: String, fields: JSONObject) {
val backend = createCoreBackend(context)
val result = parseObject(backend.editFileMetadata(path, fields.toString()))
val method = result.optString("method", "")
check(
result.optBoolean("success", false) &&
!result.has("error") &&
(method == "native" || method.startsWith("native_")),
) { "ReplayGain native write did not complete: $result" }
val metadata = parseObject(backend.readFileMetadata(path, ""))
check(!metadata.has("error")) { "ReplayGain verification failed: $metadata" }
val isOpus = metadata.optString("audio_codec", "") == "opus"
for (key in fields.keys()) {
// Opus stores only R128 gain, exposed by the Rust reader as dB.
if (isOpus && key.endsWith("_peak")) continue
val expected = fields.optString(key, "").trim().removeSuffix("dB").trim().toDoubleOrNull()
val actual = metadata.optString(key, "").trim().removeSuffix("dB").trim().toDoubleOrNull()
val tolerance = if (key.endsWith("_gain")) 0.01 else 0.000001
check(expected != null && actual != null && abs(actual - expected) <= tolerance) {
"ReplayGain verification failed for $key"
}
}
}
private fun refreshFinalAudioQualityMetadata(context: Context, result: JSONObject, state: FinalizeState) {
if (!supportsAudioMetadataProbe(state.filePath, state.fileName)) return
val probePath = if (state.filePath.startsWith("content://")) {
SafDownloadHandler.copyContentUriToTemp(context, state.filePath) ?: return
} else {
state.filePath
}
val deleteProbePath = probePath != state.filePath
try {
val metadata = parseObject(createCoreBackend(context).readFileMetadata(probePath, state.fileName))
if (metadata.has("error")) return
if (!metadata.optBoolean("metadataFromFilename", false) &&
(metadata.has("audio_codec") || metadata.has("format") ||
metadata.has("replaygain_track_gain") || metadata.has("replaygain_album_gain"))
) {
state.replayGainMetadataScanned = true
state.hasReplayGain = listOf("replaygain_track_gain", "replaygain_album_gain").any { key ->
val value = metadata.optString(key, "").trim()
val gain = (if (value.endsWith("dB", ignoreCase = true)) value.dropLast(2) else value)
.trim().toDoubleOrNull()
gain != null && gain.isFinite()
}
}
if (metadata.has("lyrics") || metadata.has("hasLyrics")) {
state.lyricsMetadataScanned = true
state.hasEmbeddedLyrics =
metadata.optBoolean("hasLyrics", false) ||
NativeFinalizationPolicy.hasUsableLyricsContent(
metadata.optString("lyrics", ""),
)
}
val bitDepth = optPositiveInt(metadata, "bit_depth")
val sampleRate = optPositiveInt(metadata, "sample_rate")
val probedCodec = normalizeAudioCodec(
metadata.optString("audio_codec", "").ifBlank {
metadata.optString("codec", "").ifBlank {
metadata.optString("format", "")
}
}
)
if (probedCodec != null) {
state.audioCodec = probedCodec
result.put("audio_codec", probedCodec)
}
if (bitDepth != null) {
state.bitDepth = bitDepth
result.put("actual_bit_depth", bitDepth)
}
if (sampleRate != null) {
state.sampleRate = sampleRate
result.put("actual_sample_rate", sampleRate)
}
val bitrateKbps = optPositiveBitrateKbps(metadata, "bitrate")
?: optPositiveBitrateKbps(metadata, "bit_rate")
if (bitrateKbps != null) {
state.bitrateKbps = bitrateKbps
result.put("bitrate", bitrateKbps)
}
val displayQuality = displayAudioQuality(
filePath = state.filePath,
fileName = state.fileName,
bitDepth = state.bitDepth,
sampleRate = state.sampleRate,
bitrateKbps = state.bitrateKbps,
audioCodec = state.audioCodec,
storedQuality = state.quality,
)
if (displayQuality != null) {
state.quality = displayQuality
}
} catch (_: Exception) {
} finally {
if (deleteProbePath) File(probePath).delete()
}
}
private fun supportsAudioMetadataProbe(filePath: String, fileName: String): Boolean {
val lowerPath = filePath.trim().lowercase(Locale.ROOT)
val lowerName = fileName.trim().lowercase(Locale.ROOT)
if (lowerPath.startsWith("content://")) return true
return lowerPath.endsWith(".flac") ||
lowerPath.endsWith(".m4a") ||
lowerPath.endsWith(".mp4") ||
lowerPath.endsWith(".aac") ||
lowerPath.endsWith(".mp3") ||
lowerPath.endsWith(".opus") ||
lowerPath.endsWith(".ogg") ||
lowerName.endsWith(".flac") ||
lowerName.endsWith(".m4a") ||
lowerName.endsWith(".mp4") ||
lowerName.endsWith(".aac") ||
lowerName.endsWith(".mp3") ||
lowerName.endsWith(".opus") ||
lowerName.endsWith(".ogg")
}
private fun runPostProcessing(
context: Context,
input: FinalizeInput,
@@ -796,22 +563,6 @@ object NativeDownloadFinalizer {
}
}
private fun albumKey(input: FinalizeInput): String {
val albumId = trackString(input, "albumId", "")
if (albumId.isNotBlank()) return "id:$albumId"
val albumName = trackString(input, "albumName", input.request.optString("album_name", ""))
val albumArtist = resolvedAlbumArtist(input)
return "name:$albumName|$albumArtist"
}
private fun replayGainDurationSeconds(input: FinalizeInput): Double {
val duration = NativeFinalizationPolicy.durationMilliseconds(
input.request.optLong("duration_ms", 0L),
trackInt(input, "duration", 0).toLong(),
)
return if (duration > 0L) duration / 1000.0 else 1.0
}
private fun buildHistoryRow(input: FinalizeInput, state: FinalizeState): ContentValues {
val result = input.result
val values = ContentValues()
@@ -941,12 +692,12 @@ object NativeDownloadFinalizer {
return if (value > 0) value else fallback
}
private fun optPositiveInt(obj: JSONObject, key: String): Int? {
internal fun optPositiveInt(obj: JSONObject, key: String): Int? {
val value = obj.optInt(key, 0)
return if (value > 0) value else null
}
private fun optPositiveBitrateKbps(obj: JSONObject, key: String): Int? {
internal fun optPositiveBitrateKbps(obj: JSONObject, key: String): Int? {
val value = optPositiveInt(obj, key) ?: return null
val kbps = if (value >= 10000) {
Math.round(value / 1000.0).toInt()
@@ -0,0 +1,261 @@
package com.zarz.spotiflac
import android.content.Context
import android.net.Uri
import com.zarz.spotiflac.SafDownloadHandler.mimeTypeForExt
import com.zarz.spotiflac.SafDownloadHandler.normalizeExt
import com.zarz.spotiflac.NativeFinalizationPolicy.displayAudioQuality
import com.zarz.spotiflac.NativeFinalizationPolicy.normalizeAudioCodec
import org.json.JSONObject
import java.io.File
import java.util.Locale
import kotlin.math.abs
// Metadata stage, ReplayGain tagging, and final audio quality probing for
// NativeDownloadFinalizer.
internal fun NativeDownloadFinalizer.finalizeMetadata(context: Context, input: NativeDownloadFinalizer.FinalizeInput, state: NativeDownloadFinalizer.FinalizeState) {
if (!input.request.optBoolean("embed_metadata", false)) return
if (!state.filePath.startsWith("content://")) {
embedBasicMetadata(context, state.filePath, input, formatForPath(state.filePath))
return
}
val tempPath = SafDownloadHandler.copyContentUriToTemp(context, state.filePath)
?: throw IllegalStateException("failed to copy SAF file for metadata")
try {
embedBasicMetadata(context, tempPath, input, formatForPath(state.fileName.ifBlank { tempPath }))
val tempFile = File(tempPath)
val finalName = desiredFileName(input, state, normalizeExt(state.fileName.substringAfterLast('.', "")))
val newUri = SafDownloadHandler.writeFileToSaf(
context = context,
treeUriStr = input.request.optString("saf_tree_uri", ""),
relativeDir = input.request.optString("saf_relative_dir", ""),
fileName = finalName,
mimeType = mimeTypeForExt(finalName.substringAfterLast('.', "")),
srcPath = tempFile.absolutePath,
) ?: throw IllegalStateException("failed to write metadata-updated SAF file")
if (newUri != state.filePath) SafDownloadHandler.deleteContentUri(context, state.filePath)
state.filePath = newUri
state.fileName = finalName
} finally {
File(tempPath).delete()
}
}
internal fun NativeDownloadFinalizer.writeReplayGain(
context: Context,
input: NativeDownloadFinalizer.FinalizeInput,
state: NativeDownloadFinalizer.FinalizeState,
shouldCancel: () -> Boolean,
): JSONObject? {
if (!input.request.optBoolean("embed_replaygain", false)) return null
val ext = normalizeExt(File(state.filePath).extension)
val fileExt = if (state.filePath.startsWith("content://")) {
normalizeExt(state.fileName.substringAfterLast('.', ""))
} else {
ext
}
if (
fileExt != ".flac" &&
fileExt != ".m4a" &&
fileExt != ".mp4" &&
fileExt != ".mp3" &&
fileExt != ".opus" &&
fileExt != ".ogg"
) return null
val scanPath = if (state.filePath.startsWith("content://")) {
SafDownloadHandler.copyContentUriToTemp(context, state.filePath)
?: throw IllegalStateException("failed to copy SAF file for ReplayGain")
} else {
state.filePath
}
val deleteScanPath = scanPath != state.filePath
val scan = try {
scanReplayGain(scanPath, shouldCancel)
?: throw IllegalStateException("ReplayGain analysis produced no valid measurement")
} finally {
if (deleteScanPath) File(scanPath).delete()
}
checkCancelled(shouldCancel)
val fields = JSONObject()
.put("replaygain_track_gain", scan.trackGain)
.put("replaygain_track_peak", scan.trackPeak)
writeReplayGainFields(context, state.filePath, fields)
return JSONObject()
.put("album_key", albumKey(input))
.put("file_path", state.filePath)
.put("file_name", state.fileName)
.put("track_id", trackString(input, "id", input.request.optString("spotify_id", input.itemId)))
.put("integrated_lufs", scan.integratedLufs)
.put("true_peak_linear", scan.truePeakLinear)
.put("duration_secs", replayGainDurationSeconds(input))
.put("track_gain", scan.trackGain)
.put("track_peak", scan.trackPeak)
}
internal fun NativeDownloadFinalizer.writeReplayGainFields(context: Context, path: String, fields: JSONObject) {
if (!path.startsWith("content://")) {
writeLocalReplayGainFields(context, path, fields)
return
}
val tempPath = SafDownloadHandler.copyContentUriToTemp(context, path)
?: throw IllegalStateException("failed to copy SAF file for ReplayGain write")
try {
writeLocalReplayGainFields(context, tempPath, fields)
val uri = Uri.parse(path)
context.contentResolver.openOutputStream(uri, "wt")?.use { output ->
File(tempPath).inputStream().use { input -> input.copyTo(output) }
SafDownloadHandler.syncOutputStream(output)
} ?: throw IllegalStateException("failed to write ReplayGain back to SAF")
} finally {
File(tempPath).delete()
}
}
private fun NativeDownloadFinalizer.writeLocalReplayGainFields(context: Context, path: String, fields: JSONObject) {
val backend = createCoreBackend(context)
val result = parseObject(backend.editFileMetadata(path, fields.toString()))
val method = result.optString("method", "")
check(
result.optBoolean("success", false) &&
!result.has("error") &&
(method == "native" || method.startsWith("native_")),
) { "ReplayGain native write did not complete: $result" }
val metadata = parseObject(backend.readFileMetadata(path, ""))
check(!metadata.has("error")) { "ReplayGain verification failed: $metadata" }
val isOpus = metadata.optString("audio_codec", "") == "opus"
for (key in fields.keys()) {
// Opus stores only R128 gain, exposed by the Rust reader as dB.
if (isOpus && key.endsWith("_peak")) continue
val expected = fields.optString(key, "").trim().removeSuffix("dB").trim().toDoubleOrNull()
val actual = metadata.optString(key, "").trim().removeSuffix("dB").trim().toDoubleOrNull()
val tolerance = if (key.endsWith("_gain")) 0.01 else 0.000001
check(expected != null && actual != null && abs(actual - expected) <= tolerance) {
"ReplayGain verification failed for $key"
}
}
}
internal fun NativeDownloadFinalizer.refreshFinalAudioQualityMetadata(context: Context, result: JSONObject, state: NativeDownloadFinalizer.FinalizeState) {
if (!supportsAudioMetadataProbe(state.filePath, state.fileName)) return
val probePath = if (state.filePath.startsWith("content://")) {
SafDownloadHandler.copyContentUriToTemp(context, state.filePath) ?: return
} else {
state.filePath
}
val deleteProbePath = probePath != state.filePath
try {
val metadata = parseObject(createCoreBackend(context).readFileMetadata(probePath, state.fileName))
if (metadata.has("error")) return
if (!metadata.optBoolean("metadataFromFilename", false) &&
(metadata.has("audio_codec") || metadata.has("format") ||
metadata.has("replaygain_track_gain") || metadata.has("replaygain_album_gain"))
) {
state.replayGainMetadataScanned = true
state.hasReplayGain = listOf("replaygain_track_gain", "replaygain_album_gain").any { key ->
val value = metadata.optString(key, "").trim()
val gain = (if (value.endsWith("dB", ignoreCase = true)) value.dropLast(2) else value)
.trim().toDoubleOrNull()
gain != null && gain.isFinite()
}
}
if (metadata.has("lyrics") || metadata.has("hasLyrics")) {
state.lyricsMetadataScanned = true
state.hasEmbeddedLyrics =
metadata.optBoolean("hasLyrics", false) ||
NativeFinalizationPolicy.hasUsableLyricsContent(
metadata.optString("lyrics", ""),
)
}
val bitDepth = optPositiveInt(metadata, "bit_depth")
val sampleRate = optPositiveInt(metadata, "sample_rate")
val probedCodec = normalizeAudioCodec(
metadata.optString("audio_codec", "").ifBlank {
metadata.optString("codec", "").ifBlank {
metadata.optString("format", "")
}
}
)
if (probedCodec != null) {
state.audioCodec = probedCodec
result.put("audio_codec", probedCodec)
}
if (bitDepth != null) {
state.bitDepth = bitDepth
result.put("actual_bit_depth", bitDepth)
}
if (sampleRate != null) {
state.sampleRate = sampleRate
result.put("actual_sample_rate", sampleRate)
}
val bitrateKbps = optPositiveBitrateKbps(metadata, "bitrate")
?: optPositiveBitrateKbps(metadata, "bit_rate")
if (bitrateKbps != null) {
state.bitrateKbps = bitrateKbps
result.put("bitrate", bitrateKbps)
}
val displayQuality = displayAudioQuality(
filePath = state.filePath,
fileName = state.fileName,
bitDepth = state.bitDepth,
sampleRate = state.sampleRate,
bitrateKbps = state.bitrateKbps,
audioCodec = state.audioCodec,
storedQuality = state.quality,
)
if (displayQuality != null) {
state.quality = displayQuality
}
} catch (_: Exception) {
} finally {
if (deleteProbePath) File(probePath).delete()
}
}
private fun NativeDownloadFinalizer.supportsAudioMetadataProbe(filePath: String, fileName: String): Boolean {
val lowerPath = filePath.trim().lowercase(Locale.ROOT)
val lowerName = fileName.trim().lowercase(Locale.ROOT)
if (lowerPath.startsWith("content://")) return true
return lowerPath.endsWith(".flac") ||
lowerPath.endsWith(".m4a") ||
lowerPath.endsWith(".mp4") ||
lowerPath.endsWith(".aac") ||
lowerPath.endsWith(".mp3") ||
lowerPath.endsWith(".opus") ||
lowerPath.endsWith(".ogg") ||
lowerName.endsWith(".flac") ||
lowerName.endsWith(".m4a") ||
lowerName.endsWith(".mp4") ||
lowerName.endsWith(".aac") ||
lowerName.endsWith(".mp3") ||
lowerName.endsWith(".opus") ||
lowerName.endsWith(".ogg")
}
internal fun NativeDownloadFinalizer.albumKey(input: NativeDownloadFinalizer.FinalizeInput): String {
val albumId = trackString(input, "albumId", "")
if (albumId.isNotBlank()) return "id:$albumId"
val albumName = trackString(input, "albumName", input.request.optString("album_name", ""))
val albumArtist = resolvedAlbumArtist(input)
return "name:$albumName|$albumArtist"
}
private fun NativeDownloadFinalizer.replayGainDurationSeconds(input: NativeDownloadFinalizer.FinalizeInput): Double {
val duration = NativeFinalizationPolicy.durationMilliseconds(
input.request.optLong("duration_ms", 0L),
trackInt(input, "duration", 0).toLong(),
)
return if (duration > 0L) duration / 1000.0 else 1.0
}