refactor(android): move finalizer history into NativeFinalizerHistory.kt

Move worker request contract validation and the download history row
and JSON mapping out of NativeDownloadFinalizer into a sibling file of
extension functions. Bodies are verbatim. NATIVE_WORKER_CONTRACT_VERSION
stays on the object with its value, so its contract test is unchanged.
The history column contract test now reads buildHistoryRow from the new
file. normalizeOptional moves with the row builder, so
cleanMetadataString widens from private to internal.
This commit is contained in:
zarzet committed 2026-10-19 00:14:47 +07:00
1 parent 631b9aa5e8
commit b5cf97881f
3 files changed
+144 -131

No files matched your search

@@ -1,6 +1,5 @@
package com.zarz.spotiflac
import android.content.ContentValues
import android.content.Context
import android.util.Log
import com.antonkarpenko.ffmpegkit.FFmpegKit
@@ -461,128 +460,6 @@ object NativeDownloadFinalizer {
if (deleteOld && oldPath != localOutput) File(oldPath).delete()
}
private fun validateRequestContract(request: JSONObject) {
val version = request.optInt("contract_version", -1)
if (version != NATIVE_WORKER_CONTRACT_VERSION) {
throw IllegalArgumentException(
"unsupported native worker request contract v$version"
)
}
val required = listOf("item_id", "service", "track_name", "quality", "storage_mode")
val missing = required.filter { request.optString(it, "").trim().isEmpty() }
if (missing.isNotEmpty()) {
throw IllegalArgumentException(
"native worker request missing fields: ${missing.joinToString()}"
)
}
}
private fun buildHistoryRow(input: FinalizeInput, state: FinalizeState): ContentValues {
val result = input.result
val values = ContentValues()
values.put("id", input.itemId)
values.put("track_name", result.optString("title", "").ifBlank { trackString(input, "name", input.request.optString("track_name", "")) })
values.put("artist_name", result.optString("artist", "").ifBlank { trackString(input, "artistName", input.request.optString("artist_name", "")) })
values.put("album_name", result.optString("album", "").ifBlank { trackString(input, "albumName", input.request.optString("album_name", "")) })
values.put("album_artist", normalizeOptional(resolvedAlbumArtist(input)))
values.put("cover_url", normalizeOptional(metadataCoverUrl(input).ifBlank { resultString(input, "cover_url") }))
values.put("file_path", state.filePath)
values.put("storage_mode", input.request.optString("storage_mode", "app"))
values.put("download_tree_uri", normalizeOptional(input.request.optString("saf_tree_uri", "")))
values.put("saf_relative_dir", normalizeOptional(input.request.optString("saf_relative_dir", "")))
values.put("saf_file_name", if (state.filePath.startsWith("content://")) state.fileName else null)
values.put("saf_repaired", 0)
values.put("service", result.optString("service", "").ifBlank { input.item.optString("service", "") })
values.put("downloaded_at", java.time.Instant.now().toString())
values.put("isrc", normalizeOptional(result.optString("isrc", "").ifBlank { trackString(input, "isrc", input.request.optString("isrc", "")) }))
values.put("spotify_id", normalizeOptional(trackString(input, "id", input.request.optString("spotify_id", ""))))
values.put("track_number", positiveOrNull(result.optInt("track_number", 0), trackInt(input, "trackNumber", input.request.optInt("track_number", 0))))
values.put("total_tracks", positiveOrNull(result.optInt("total_tracks", 0), trackInt(input, "totalTracks", input.request.optInt("total_tracks", 0))))
values.put("disc_number", positiveOrNull(result.optInt("disc_number", 0), trackInt(input, "discNumber", input.request.optInt("disc_number", 0))))
values.put("total_discs", positiveOrNull(result.optInt("total_discs", 0), trackInt(input, "totalDiscs", input.request.optInt("total_discs", 0))))
values.put("duration", trackInt(input, "duration", input.request.optInt("duration_ms", 0) / 1000))
values.put("release_date", normalizeOptional(resultString(input, "release_date").ifBlank { resultString(input, "date").ifBlank { trackString(input, "releaseDate", requestString(input, "release_date")) } }))
values.put("quality", state.quality)
state.bitDepth?.let { values.put("bit_depth", it) }
state.sampleRate?.let { values.put("sample_rate", it) }
state.bitrateKbps?.takeIf { it >= 16 }?.let {
values.put("bitrate", it)
}
normalizeAudioCodec(state.audioCodec)?.let { values.put("format", it) }
values.put("genre", normalizeOptional(result.optString("genre", "").ifBlank { input.request.optString("genre", "") }))
values.put("composer", normalizeOptional(resultString(input, "composer").ifBlank { trackString(input, "composer", requestString(input, "composer")) }))
values.put("label", normalizeOptional(result.optString("label", "").ifBlank { input.request.optString("label", "") }))
values.put("copyright", normalizeOptional(result.optString("copyright", "").ifBlank { input.request.optString("copyright", "") }))
values.put(
"explicit",
if (
result.optBoolean("explicit", false) ||
input.track.optBoolean("explicit", false) ||
input.request.optBoolean("explicit", false)
) 1 else 0,
)
values.put(
"has_lyrics",
if (state.hasEmbeddedLyrics || state.externalLrcWritten) 1 else 0,
)
values.put(
"lyrics_metadata_scan_version",
if (
state.lyricsMetadataScanned ||
state.hasEmbeddedLyrics ||
state.externalLrcWritten
) 1 else 0,
)
values.put("has_replaygain", if (state.hasReplayGain) 1 else 0)
values.put("replaygain_metadata_scan_version", if (state.replayGainMetadataScanned) 1 else 0)
return values
}
private fun historyToJson(values: ContentValues): JSONObject {
val json = JSONObject()
fun putCamel(column: String, key: String) {
if (values.containsKey(column)) json.put(key, values.get(column))
}
putCamel("id", "id")
putCamel("track_name", "trackName")
putCamel("artist_name", "artistName")
putCamel("album_name", "albumName")
putCamel("album_artist", "albumArtist")
putCamel("cover_url", "coverUrl")
putCamel("file_path", "filePath")
putCamel("storage_mode", "storageMode")
putCamel("download_tree_uri", "downloadTreeUri")
putCamel("saf_relative_dir", "safRelativeDir")
putCamel("saf_file_name", "safFileName")
json.put("safRepaired", values.getAsInteger("saf_repaired") == 1)
putCamel("service", "service")
putCamel("downloaded_at", "downloadedAt")
putCamel("isrc", "isrc")
putCamel("spotify_id", "spotifyId")
putCamel("track_number", "trackNumber")
putCamel("total_tracks", "totalTracks")
putCamel("disc_number", "discNumber")
putCamel("total_discs", "totalDiscs")
putCamel("duration", "duration")
putCamel("release_date", "releaseDate")
putCamel("quality", "quality")
putCamel("bit_depth", "bitDepth")
putCamel("sample_rate", "sampleRate")
putCamel("bitrate", "bitrate")
putCamel("format", "format")
putCamel("genre", "genre")
putCamel("composer", "composer")
putCamel("label", "label")
putCamel("copyright", "copyright")
json.put("explicit", values.getAsInteger("explicit") == 1)
json.put("hasLyrics", values.getAsInteger("has_lyrics") == 1)
json.put("lyricsMetadataScanVersion", values.getAsInteger("lyrics_metadata_scan_version") ?: 0)
json.put("hasReplayGain", values.getAsInteger("has_replaygain") == 1)
json.put("replayGainMetadataScanVersion", values.getAsInteger("replaygain_metadata_scan_version") ?: 0)
return json
}
internal fun trackString(input: FinalizeInput, key: String, fallback: String): String =
cleanMetadataString(input.track.optString(key, "")).ifBlank { cleanMetadataString(fallback) }
@@ -627,12 +504,7 @@ object NativeDownloadFinalizer {
return if (value > 0) value else null
}
private fun normalizeOptional(value: String?): String? {
val trimmed = cleanMetadataString(value)
return trimmed.ifBlank { null }
}
private fun cleanMetadataString(value: String?): String {
internal fun cleanMetadataString(value: String?): String {
val trimmed = value?.trim().orEmpty()
return if (trimmed.equals("null", ignoreCase = true)) "" else trimmed
}
@@ -0,0 +1,136 @@
package com.zarz.spotiflac
import android.content.ContentValues
import com.zarz.spotiflac.NativeFinalizationPolicy.normalizeAudioCodec
import org.json.JSONObject
// Worker request contract validation and download history row helpers for
// NativeDownloadFinalizer.
internal fun NativeDownloadFinalizer.validateRequestContract(request: JSONObject) {
val version = request.optInt("contract_version", -1)
if (version != NATIVE_WORKER_CONTRACT_VERSION) {
throw IllegalArgumentException(
"unsupported native worker request contract v$version"
)
}
val required = listOf("item_id", "service", "track_name", "quality", "storage_mode")
val missing = required.filter { request.optString(it, "").trim().isEmpty() }
if (missing.isNotEmpty()) {
throw IllegalArgumentException(
"native worker request missing fields: ${missing.joinToString()}"
)
}
}
internal fun NativeDownloadFinalizer.buildHistoryRow(input: NativeDownloadFinalizer.FinalizeInput, state: NativeDownloadFinalizer.FinalizeState): ContentValues {
val result = input.result
val values = ContentValues()
values.put("id", input.itemId)
values.put("track_name", result.optString("title", "").ifBlank { trackString(input, "name", input.request.optString("track_name", "")) })
values.put("artist_name", result.optString("artist", "").ifBlank { trackString(input, "artistName", input.request.optString("artist_name", "")) })
values.put("album_name", result.optString("album", "").ifBlank { trackString(input, "albumName", input.request.optString("album_name", "")) })
values.put("album_artist", normalizeOptional(resolvedAlbumArtist(input)))
values.put("cover_url", normalizeOptional(metadataCoverUrl(input).ifBlank { resultString(input, "cover_url") }))
values.put("file_path", state.filePath)
values.put("storage_mode", input.request.optString("storage_mode", "app"))
values.put("download_tree_uri", normalizeOptional(input.request.optString("saf_tree_uri", "")))
values.put("saf_relative_dir", normalizeOptional(input.request.optString("saf_relative_dir", "")))
values.put("saf_file_name", if (state.filePath.startsWith("content://")) state.fileName else null)
values.put("saf_repaired", 0)
values.put("service", result.optString("service", "").ifBlank { input.item.optString("service", "") })
values.put("downloaded_at", java.time.Instant.now().toString())
values.put("isrc", normalizeOptional(result.optString("isrc", "").ifBlank { trackString(input, "isrc", input.request.optString("isrc", "")) }))
values.put("spotify_id", normalizeOptional(trackString(input, "id", input.request.optString("spotify_id", ""))))
values.put("track_number", positiveOrNull(result.optInt("track_number", 0), trackInt(input, "trackNumber", input.request.optInt("track_number", 0))))
values.put("total_tracks", positiveOrNull(result.optInt("total_tracks", 0), trackInt(input, "totalTracks", input.request.optInt("total_tracks", 0))))
values.put("disc_number", positiveOrNull(result.optInt("disc_number", 0), trackInt(input, "discNumber", input.request.optInt("disc_number", 0))))
values.put("total_discs", positiveOrNull(result.optInt("total_discs", 0), trackInt(input, "totalDiscs", input.request.optInt("total_discs", 0))))
values.put("duration", trackInt(input, "duration", input.request.optInt("duration_ms", 0) / 1000))
values.put("release_date", normalizeOptional(resultString(input, "release_date").ifBlank { resultString(input, "date").ifBlank { trackString(input, "releaseDate", requestString(input, "release_date")) } }))
values.put("quality", state.quality)
state.bitDepth?.let { values.put("bit_depth", it) }
state.sampleRate?.let { values.put("sample_rate", it) }
state.bitrateKbps?.takeIf { it >= 16 }?.let {
values.put("bitrate", it)
}
normalizeAudioCodec(state.audioCodec)?.let { values.put("format", it) }
values.put("genre", normalizeOptional(result.optString("genre", "").ifBlank { input.request.optString("genre", "") }))
values.put("composer", normalizeOptional(resultString(input, "composer").ifBlank { trackString(input, "composer", requestString(input, "composer")) }))
values.put("label", normalizeOptional(result.optString("label", "").ifBlank { input.request.optString("label", "") }))
values.put("copyright", normalizeOptional(result.optString("copyright", "").ifBlank { input.request.optString("copyright", "") }))
values.put(
"explicit",
if (
result.optBoolean("explicit", false) ||
input.track.optBoolean("explicit", false) ||
input.request.optBoolean("explicit", false)
) 1 else 0,
)
values.put(
"has_lyrics",
if (state.hasEmbeddedLyrics || state.externalLrcWritten) 1 else 0,
)
values.put(
"lyrics_metadata_scan_version",
if (
state.lyricsMetadataScanned ||
state.hasEmbeddedLyrics ||
state.externalLrcWritten
) 1 else 0,
)
values.put("has_replaygain", if (state.hasReplayGain) 1 else 0)
values.put("replaygain_metadata_scan_version", if (state.replayGainMetadataScanned) 1 else 0)
return values
}
internal fun NativeDownloadFinalizer.historyToJson(values: ContentValues): JSONObject {
val json = JSONObject()
fun putCamel(column: String, key: String) {
if (values.containsKey(column)) json.put(key, values.get(column))
}
putCamel("id", "id")
putCamel("track_name", "trackName")
putCamel("artist_name", "artistName")
putCamel("album_name", "albumName")
putCamel("album_artist", "albumArtist")
putCamel("cover_url", "coverUrl")
putCamel("file_path", "filePath")
putCamel("storage_mode", "storageMode")
putCamel("download_tree_uri", "downloadTreeUri")
putCamel("saf_relative_dir", "safRelativeDir")
putCamel("saf_file_name", "safFileName")
json.put("safRepaired", values.getAsInteger("saf_repaired") == 1)
putCamel("service", "service")
putCamel("downloaded_at", "downloadedAt")
putCamel("isrc", "isrc")
putCamel("spotify_id", "spotifyId")
putCamel("track_number", "trackNumber")
putCamel("total_tracks", "totalTracks")
putCamel("disc_number", "discNumber")
putCamel("total_discs", "totalDiscs")
putCamel("duration", "duration")
putCamel("release_date", "releaseDate")
putCamel("quality", "quality")
putCamel("bit_depth", "bitDepth")
putCamel("sample_rate", "sampleRate")
putCamel("bitrate", "bitrate")
putCamel("format", "format")
putCamel("genre", "genre")
putCamel("composer", "composer")
putCamel("label", "label")
putCamel("copyright", "copyright")
json.put("explicit", values.getAsInteger("explicit") == 1)
json.put("hasLyrics", values.getAsInteger("has_lyrics") == 1)
json.put("lyricsMetadataScanVersion", values.getAsInteger("lyrics_metadata_scan_version") ?: 0)
json.put("hasReplayGain", values.getAsInteger("has_replaygain") == 1)
json.put("replayGainMetadataScanVersion", values.getAsInteger("replaygain_metadata_scan_version") ?: 0)
return json
}
private fun NativeDownloadFinalizer.normalizeOptional(value: String?): String? {
val trimmed = cleanMetadataString(value)
return trimmed.ifBlank { null }
}
+7 -2
View File
@@ -89,6 +89,10 @@ void main() {
'android/app/src/main/kotlin/com/zarz/spotiflac/'
'NativeDownloadFinalizer.kt',
).readAsStringSync();
final finalizerHistorySource = File(
'android/app/src/main/kotlin/com/zarz/spotiflac/'
'NativeFinalizerHistory.kt',
).readAsStringSync();
final historyStoreSource = File(
'android/app/src/main/kotlin/com/zarz/spotiflac/'
'NativeHistoryStore.kt',
@@ -197,8 +201,9 @@ void main() {
expect(nativeDefinitions, dartDefinitions);
final buildHistoryRow = RegExp(
r'private fun buildHistoryRow\([\s\S]*?return values',
).firstMatch(finalizerSource);
r'internal fun NativeDownloadFinalizer\.buildHistoryRow\('
r'[\s\S]*?return values',
).firstMatch(finalizerHistorySource);
expect(buildHistoryRow, isNotNull);
final nativeWrittenColumns = RegExp(
r'values\.put\("([a-z0-9_]+)"',