mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-20 22:41:38 +02:00
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:
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
|
||||
}
|
||||
Reference in new issue
Block a user