fix(download): align native metadata preparation and ReplayGain support

This commit is contained in:
zarzet committed 2026-09-28 12:21:15 +07:00
1 parent c0997de19c
commit e61e54f93f
11 files changed
+288 -117

No files matched your search

@@ -3,6 +3,8 @@ package com.zarz.spotiflac
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import java.io.File
import org.junit.After
import org.junit.Before
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -12,6 +14,76 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class NativeDownloadContainerTest {
private lateinit var backend: CoreBackend
private lateinit var backendRoot: File
@Before
fun initializeBackend() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
backendRoot = File(context.filesDir, "finalizer-test-${System.nanoTime()}").apply { mkdirs() }
backend = createCoreBackend(context)
backend.invokeApplication("initExtensionSystem", mapOf(
"extensions_dir" to File(backendRoot, "sources").apply { mkdirs() }.path,
"data_dir" to File(backendRoot, "data").apply { mkdirs() }.path,
"master_key" to "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"allowed_directories" to listOf(context.cacheDir.path),
))
}
@After
fun closeBackend() {
backend.invokeApplication("cleanupExtensions", emptyMap<String, Any>())
backendRoot.deleteRecursively()
}
@Test
fun nativeFormatsPreserveTagsLyricsAndReplayGain() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val root = File(context.cacheDir, "finalizer-parity-${System.nanoTime()}").apply { mkdirs() }
try {
for ((extension, encoder) in listOf("mp3" to "libmp3lame", "opus" to "libopus", "flac" to "flac", "m4a" to "aac")) {
val input = File(root, "track.$extension")
val fixture = NativeDownloadFinalizer.runFFmpegArguments(arrayOf(
"-v", "error", "-f", "lavfi", "-i", "sine=frequency=997:sample_rate=48000",
"-t", "1.5", "-c:a", encoder, input.path,
))
assertTrue(fixture.second, fixture.first)
val request = JSONObject()
.put("contract_version", 1).put("item_id", "example-$extension")
.put("service", "example-provider").put("track_name", "Parity test")
.put("artist_name", "Example artist").put("album_name", "Example album")
.put("album_artist", "Album Artist").put("track_number", 2).put("total_tracks", 10)
.put("quality", "LOSSLESS").put("storage_mode", "app")
.put("output_ext", ".$extension").put("embed_metadata", true)
.put("embed_lyrics", true).put("lyrics_mode", "both")
.put("embed_replaygain", true).put("duration_ms", 1500)
val result = NativeDownloadFinalizer.finalize(
context, "example-$extension", request.toString(), "{}",
JSONObject().put("success", true).put("file_path", input.path)
.put("file_name", input.name).put("lyrics_lrc", "[00:00.00]Example line"),
"{\"save_download_history\":false}",
)
assertTrue(result.toString(), result.getBoolean("success"))
assertFalse(result.toString(), result.has("replaygain_warning"))
assertEquals(1.5, result.getJSONObject("replaygain").getDouble("duration_secs"), 0.001)
val output = File(result.getString("file_path"))
assertEquals(extension, output.extension)
assertEquals("[00:00.00]Example line", File(root, "track.lrc").readText())
File(root, "track.lrc").delete()
val probe = NativeDownloadFinalizer.runFFmpegArguments(arrayOf(
"-hide_banner", "-i", output.path, "-map", "0:a:0", "-f", "null", "-",
))
assertTrue(probe.second, probe.first)
val gainTag = if (extension == "opus") "r128_track_gain" else "replaygain_track_gain"
for (tag in listOf("Parity test", "Album Artist", "Example line", gainTag)) {
assertTrue("$extension: missing $tag\n${probe.second}", probe.second.contains(tag, ignoreCase = true))
}
}
} finally {
root.deleteRecursively()
}
}
@Test
fun misnamedMp4IsTaggedAndPublishedAsM4aWithoutOverwritingExistingAudio() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
@@ -1269,11 +1269,11 @@ object NativeDownloadFinalizer {
}
private fun replayGainDurationSeconds(input: FinalizeInput): Double {
val duration = input.request.optInt("duration_ms", 0).let {
if (it > 0) it else trackInt(input, "duration", 0)
}
if (duration <= 0) return 1.0
return if (duration > 10000) duration / 1000.0 else duration.toDouble()
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 {
@@ -11,6 +11,11 @@ import kotlin.math.roundToInt
* finalizer's I/O-heavy orchestration.
*/
internal object NativeFinalizationPolicy {
fun durationMilliseconds(requestMilliseconds: Long, trackSeconds: Long): Long {
if (requestMilliseconds > 0) return requestMilliseconds
return trackSeconds.coerceIn(0, Long.MAX_VALUE / 1000) * 1000
}
fun resolvedAlbumRelativeDirectory(
relativeDirectory: String,
albumFolderTemplate: String,
@@ -245,9 +245,7 @@ internal fun NativeDownloadFinalizer.resolveLyricsLrc(context: Context, input: N
internal fun NativeDownloadFinalizer.lyricsDurationMs(input: NativeDownloadFinalizer.FinalizeInput): Long {
val requestDuration = input.request.optLong("duration_ms", 0L)
val trackDuration = trackInt(input, "duration", 0).toLong()
val duration = if (requestDuration > 0L) requestDuration else trackDuration
if (duration <= 0L) return 0L
return if (duration > 10000L) duration else duration * 1000L
return NativeFinalizationPolicy.durationMilliseconds(requestDuration, trackDuration)
}
internal fun nativeMetadataEditHandled(response: String): Boolean {
@@ -7,6 +7,17 @@ import org.junit.Assert.assertTrue
import org.junit.Test
class NativeFinalizationPolicyTest {
@Test
fun durationUsesDeclaredUnitsForShortTracksAndLongPerformances() {
assertEquals(250L, NativeFinalizationPolicy.durationMilliseconds(250, 0))
assertEquals(8000L, NativeFinalizationPolicy.durationMilliseconds(8000, 8))
assertEquals(10000L, NativeFinalizationPolicy.durationMilliseconds(10000, 10))
assertEquals(180000L, NativeFinalizationPolicy.durationMilliseconds(180000, 0))
assertEquals(8000L, NativeFinalizationPolicy.durationMilliseconds(0, 8))
assertEquals(14400000L, NativeFinalizationPolicy.durationMilliseconds(0, 14400))
assertEquals(0L, NativeFinalizationPolicy.durationMilliseconds(0, -1))
}
@Test
fun lateAlbumMetadataResolvesOnlyThePendingFolderLeaf() {
assertEquals(