fix: show lossy audio bitrate in quality labels

This commit is contained in:
zarzet
2026-05-06 21:10:04 +07:00
parent 83d7106e35
commit aad72226c5
3 changed files with 174 additions and 5 deletions
@@ -110,6 +110,7 @@ object NativeDownloadFinalizer {
var quality: String,
var bitDepth: Int?,
var sampleRate: Int?,
var bitrateKbps: Int? = null,
var pendingExternalLrc: String? = null,
var pendingExternalLrcFileName: String? = null,
)
@@ -170,6 +171,8 @@ object NativeDownloadFinalizer {
quality = requestQuality(effectiveInput),
bitDepth = optPositiveInt(result, "actual_bit_depth"),
sampleRate = optPositiveInt(result, "actual_sample_rate"),
bitrateKbps = optPositiveBitrateKbps(result, "bitrate")
?: optPositiveBitrateKbps(result, "actual_bitrate"),
)
try {
@@ -637,10 +640,19 @@ object NativeDownloadFinalizer {
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,
storedQuality = state.quality,
)
if (displayQuality != null) {
@@ -672,14 +684,64 @@ object NativeDownloadFinalizer {
lowerName.endsWith(".ogg")
}
private fun displayAudioQuality(bitDepth: Int?, sampleRate: Int?, storedQuality: String?): String? {
private fun displayAudioQuality(
filePath: String,
fileName: String,
bitDepth: Int?,
sampleRate: Int?,
bitrateKbps: Int?,
storedQuality: String?,
): String? {
val format = audioFormatForPath(filePath, fileName)
if (format == "OPUS" ||
format == "MP3" ||
format == "AAC" ||
(format == "M4A" && (bitDepth == null || bitDepth <= 0))
) {
return if (bitrateKbps != null && bitrateKbps > 0) {
"$format ${bitrateKbps}kbps"
} else {
nonPlaceholderQuality(storedQuality) ?: format
}
}
if (bitDepth != null && bitDepth > 0 && sampleRate != null && sampleRate > 0) {
val khz = sampleRate / 1000.0
val precision = if (sampleRate % 1000 == 0) 0 else 1
val sampleRateLabel = "%.${precision}f".format(Locale.US, khz)
return "$bitDepth-bit/${sampleRateLabel}kHz"
}
return normalizeOptional(storedQuality)
return nonPlaceholderQuality(storedQuality) ?: normalizeOptional(storedQuality)
}
private fun audioFormatForPath(filePath: String, fileName: String): String? {
for (candidate in listOf(filePath, fileName)) {
val lower = candidate.trim().lowercase(Locale.ROOT)
when {
lower.endsWith(".opus") || lower.endsWith(".ogg") -> return "OPUS"
lower.endsWith(".mp3") -> return "MP3"
lower.endsWith(".aac") -> return "AAC"
lower.endsWith(".m4a") || lower.endsWith(".mp4") -> return "M4A"
}
}
return null
}
private fun nonPlaceholderQuality(quality: String?): String? {
val normalized = normalizeOptional(quality) ?: return null
val key = normalized.lowercase(Locale.ROOT).replace(Regex("[^a-z0-9]+"), "_").trim('_')
val placeholders = setOf(
"best",
"lossless",
"hi_res",
"hires",
"hi_res_lossless",
"hires_lossless",
"high",
"cd",
"flac_best_available",
)
return if (placeholders.contains(key)) null else normalized
}
private fun writeExternalLrc(context: Context, input: FinalizeInput, state: FinalizeState) {
@@ -1898,6 +1960,15 @@ object NativeDownloadFinalizer {
return if (value > 0) value else null
}
private fun optPositiveBitrateKbps(obj: JSONObject, key: String): Int? {
val value = optPositiveInt(obj, key) ?: return null
return if (value >= 10000) {
Math.round(value / 1000.0).toInt()
} else {
value
}
}
private fun positiveOrNull(primary: Int, fallback: Int): Int? {
val value = if (primary > 0) primary else fallback
return if (value > 0) value else null
+9
View File
@@ -1157,6 +1157,9 @@ func ReadFileMetadata(filePath string) (string, error) {
if qualityErr == nil {
result["sample_rate"] = quality.SampleRate
result["duration"] = quality.Duration
if quality.Bitrate > 0 {
result["bitrate"] = quality.Bitrate / 1000
}
}
} else {
return "", fmt.Errorf("failed to read metadata: %w", err)
@@ -1257,6 +1260,9 @@ func ReadFileMetadata(filePath string) (string, error) {
result["bit_depth"] = quality.BitDepth
result["sample_rate"] = quality.SampleRate
result["duration"] = quality.Duration
if quality.Bitrate > 0 {
result["bitrate"] = quality.Bitrate / 1000
}
}
} else if isOgg {
meta, err := ReadOggVorbisComments(filePath)
@@ -1289,6 +1295,9 @@ func ReadFileMetadata(filePath string) (string, error) {
if qualityErr == nil {
result["sample_rate"] = quality.SampleRate
result["duration"] = quality.Duration
if quality.Bitrate > 0 {
result["bitrate"] = quality.Bitrate / 1000
}
}
} else if isApe || isWv || isMpc {
// APE, WavPack, Musepack: read APEv2 tags
+92 -3
View File
@@ -34,6 +34,76 @@ final _trimUnderscoresAndSpacesRegex = RegExp(r'^[_ ]+|[_ ]+$');
final _multiWhitespaceRegex = RegExp(r'\s+');
final _multiUnderscoreRegex = RegExp(r'_+');
int? _readPositiveIntValue(dynamic value) {
if (value == null) return null;
if (value is num) {
final asInt = value.toInt();
return asInt > 0 ? asInt : null;
}
final parsed = int.tryParse(value.toString());
if (parsed == null || parsed <= 0) return null;
return parsed;
}
int? _readPositiveBitrateKbps(dynamic value) {
final parsed = _readPositiveIntValue(value);
if (parsed == null) return null;
return parsed >= 10000 ? (parsed / 1000).round() : parsed;
}
String? _audioFormatForPath(String? filePath, {String? fileName}) {
final candidates = <String>[?filePath, ?fileName];
for (final candidate in candidates) {
final lower = candidate.trim().toLowerCase();
if (lower.endsWith('.opus') || lower.endsWith('.ogg')) return 'OPUS';
if (lower.endsWith('.mp3')) return 'MP3';
if (lower.endsWith('.aac')) return 'AAC';
if (lower.endsWith('.m4a') || lower.endsWith('.mp4')) return 'M4A';
}
return null;
}
String? _nonPlaceholderQuality(String? quality) {
final normalized = normalizeOptionalString(quality);
if (normalized == null || isPlaceholderQualityLabel(normalized)) {
return null;
}
final lower = normalized.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]+'), '_');
const requestedLosslessLabels = {
'hi_res_lossless',
'hires_lossless',
'hi_res',
'hires',
'flac_best_available',
};
if (requestedLosslessLabels.contains(lower)) return null;
return normalized;
}
String? _resolveDisplayQuality({
required String? filePath,
String? fileName,
int? bitDepth,
int? sampleRate,
int? bitrateKbps,
String? storedQuality,
}) {
final format = _audioFormatForPath(filePath, fileName: fileName);
if (format == 'OPUS' ||
format == 'MP3' ||
format == 'AAC' ||
(format == 'M4A' && (bitDepth == null || bitDepth <= 0))) {
return buildDisplayAudioQuality(bitrateKbps: bitrateKbps, format: format) ??
_nonPlaceholderQuality(storedQuality) ??
format;
}
return buildDisplayAudioQuality(
bitDepth: bitDepth,
sampleRate: sampleRate,
storedQuality: _nonPlaceholderQuality(storedQuality) ?? storedQuality,
);
}
/// log10 helper using dart:math's natural log.
double _log10(num x) => log(x) / ln10;
final _yearRegex = RegExp(r'^(\d{4})');
@@ -689,9 +759,12 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
final bitDepth = _readPositiveInt(result['bit_depth']);
final sampleRate = _readPositiveInt(result['sample_rate']);
final quality = buildDisplayAudioQuality(
final bitrateKbps = _readPositiveBitrateKbps(result['bitrate']);
final quality = _resolveDisplayQuality(
filePath: filePath,
bitDepth: bitDepth,
sampleRate: sampleRate,
bitrateKbps: bitrateKbps,
storedQuality: fallbackQuality,
);
final composer = normalizeOptionalString(result['composer']?.toString());
@@ -704,6 +777,7 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
if (quality == null &&
bitDepth == null &&
sampleRate == null &&
bitrateKbps == null &&
composer == null &&
duration == null &&
trackNumber == null &&
@@ -717,6 +791,7 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
'quality': quality,
'bitDepth': bitDepth,
'sampleRate': sampleRate,
'bitrateKbps': bitrateKbps,
'composer': composer,
'duration': duration,
'trackNumber': trackNumber,
@@ -5532,7 +5607,8 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
var actualQuality = context.quality;
final actualBitDepth = result['actual_bit_depth'] as int?;
final actualSampleRate = result['actual_sample_rate'] as int?;
final resolvedQuality = buildDisplayAudioQuality(
final resolvedQuality = _resolveDisplayQuality(
filePath: filePath,
bitDepth: actualBitDepth,
sampleRate: actualSampleRate,
storedQuality: actualQuality,
@@ -7931,6 +8007,9 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
final backendTotalDiscs = result['total_discs'] as int?;
final backendBitDepth = result['actual_bit_depth'] as int?;
final backendSampleRate = result['actual_sample_rate'] as int?;
final backendBitrateKbps = _readPositiveBitrateKbps(
result['bitrate'] ?? result['actual_bitrate'],
);
final backendISRC = result['isrc'] as String?;
final backendGenre = result['genre'] as String?;
final backendLabel = result['label'] as String?;
@@ -7951,6 +8030,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
int? finalBitDepth = backendBitDepth;
int? finalSampleRate = backendSampleRate;
int? finalBitrateKbps = backendBitrateKbps;
final lowerFilePath = filePath.toLowerCase();
final canProbeFinalMetadata =
filePath.startsWith('content://') ||
@@ -7979,10 +8059,19 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
if (probedSampleRate != null && probedSampleRate > 0) {
finalSampleRate = probedSampleRate;
}
final probedBitrateKbps = _readPositiveBitrateKbps(
metadata['bitrate'] ?? metadata['bit_rate'],
);
if (probedBitrateKbps != null && probedBitrateKbps > 0) {
finalBitrateKbps = probedBitrateKbps;
}
final resolvedQuality = buildDisplayAudioQuality(
final resolvedQuality = _resolveDisplayQuality(
filePath: filePath,
fileName: finalSafFileName,
bitDepth: finalBitDepth,
sampleRate: finalSampleRate,
bitrateKbps: finalBitrateKbps,
storedQuality: actualQuality,
);
if (resolvedQuality != null) {