mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-05 18:48:38 +02:00
fix(library): persist lossless bitrate metadata
This commit is contained in:
@@ -275,6 +275,18 @@ extension _HistoryStartupMaintenance on DownloadHistoryNotifier {
|
||||
}
|
||||
|
||||
bool _shouldBackfillAudioMetadata(DownloadHistoryItem item) {
|
||||
return _needsAverageBitrateBackfill(item) ||
|
||||
_shouldBackfillAudioMetadataIgnoringBitrate(item);
|
||||
}
|
||||
|
||||
bool _needsAverageBitrateBackfill(DownloadHistoryItem item) {
|
||||
return _supportsAudioMetadataProbe(item.filePath) &&
|
||||
(item.bitrate == null || item.bitrate! <= 0) &&
|
||||
item.duration != null &&
|
||||
item.duration! > 0;
|
||||
}
|
||||
|
||||
bool _shouldBackfillAudioMetadataIgnoringBitrate(DownloadHistoryItem item) {
|
||||
if (!_supportsAudioMetadataProbe(item.filePath)) {
|
||||
return false;
|
||||
}
|
||||
@@ -396,10 +408,9 @@ extension _HistoryStartupMaintenance on DownloadHistoryNotifier {
|
||||
final detectedFormat = normalizeAudioFormatValue(
|
||||
result['audio_codec']?.toString() ?? result['format']?.toString(),
|
||||
);
|
||||
final rawBitrateKbps = readPositiveBitrateKbps(result['bitrate']);
|
||||
final bitrateKbps = isLossyAudioFormat(detectedFormat)
|
||||
? rawBitrateKbps
|
||||
: null;
|
||||
final bitrateKbps = readPositiveBitrateKbps(
|
||||
result['bitrate'] ?? result['bit_rate'],
|
||||
);
|
||||
final quality = resolveDisplayQuality(
|
||||
filePath: filePath,
|
||||
detectedFormat: detectedFormat,
|
||||
@@ -496,10 +507,24 @@ extension _HistoryStartupMaintenance on DownloadHistoryNotifier {
|
||||
for (final index in selectedIndexes) {
|
||||
final item = items[index];
|
||||
|
||||
final probed = await _probeAudioMetadata(
|
||||
item.filePath,
|
||||
fallbackQuality: item.quality,
|
||||
);
|
||||
Map<String, dynamic>? probed;
|
||||
if (_shouldBackfillAudioMetadataIgnoringBitrate(item)) {
|
||||
probed = await _probeAudioMetadata(
|
||||
item.filePath,
|
||||
fallbackQuality: item.quality,
|
||||
);
|
||||
} else if (_needsAverageBitrateBackfill(item)) {
|
||||
// A stat is enough for average bitrate and avoids copying an entire
|
||||
// SAF file to cache merely to update the Library badge.
|
||||
final stat = await fileStat(item.filePath);
|
||||
final bitrate = estimateAverageBitrateKbps(
|
||||
fileSizeBytes: stat?.size,
|
||||
durationSeconds: item.duration,
|
||||
);
|
||||
if (bitrate != null) {
|
||||
probed = {'bitrate': bitrate};
|
||||
}
|
||||
}
|
||||
if (probed == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -382,7 +382,7 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
|
||||
if (bitDepth != null) result['actual_bit_depth'] = bitDepth;
|
||||
if (sampleRate != null) result['actual_sample_rate'] = sampleRate;
|
||||
if (detectedFormat != null) result['audio_codec'] = detectedFormat;
|
||||
if (bitrateKbps != null && isLossyAudioFormat(detectedFormat)) {
|
||||
if (bitrateKbps != null) {
|
||||
result['bitrate'] = bitrateKbps;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,9 +50,9 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
: readPositiveInt(
|
||||
result['actual_sample_rate'] ?? result['sample_rate'],
|
||||
);
|
||||
final bitrate = isLossy
|
||||
? readPositiveBitrateKbps(result['actual_bitrate'] ?? result['bitrate'])
|
||||
: null;
|
||||
final bitrate = readPositiveBitrateKbps(
|
||||
result['actual_bitrate'] ?? result['bitrate'],
|
||||
);
|
||||
final storedQuality =
|
||||
result['quality']?.toString().trim().isNotEmpty == true
|
||||
? result['quality'].toString()
|
||||
@@ -1023,9 +1023,9 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
result['audio_codec']?.toString() ?? result['format']?.toString(),
|
||||
) ??
|
||||
normalizeAudioFormatValue(audioFormatForPath(filePath));
|
||||
var actualBitrate = isLossyAudioFormat(actualFormat)
|
||||
? readPositiveBitrateKbps(result['bitrate'] ?? result['actual_bitrate'])
|
||||
: null;
|
||||
var actualBitrate = readPositiveBitrateKbps(
|
||||
result['bitrate'] ?? result['actual_bitrate'],
|
||||
);
|
||||
final resolvedQuality = resolveDisplayQuality(
|
||||
filePath: filePath,
|
||||
detectedFormat: actualFormat,
|
||||
@@ -1119,11 +1119,9 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
actualFormat =
|
||||
normalizeAudioFormatValue(result['audio_codec']?.toString()) ??
|
||||
normalizeAudioFormatValue(audioFormatForPath(filePath));
|
||||
actualBitrate = isLossyAudioFormat(actualFormat)
|
||||
? readPositiveBitrateKbps(
|
||||
result['bitrate'] ?? result['actual_bitrate'],
|
||||
)
|
||||
: null;
|
||||
actualBitrate = readPositiveBitrateKbps(
|
||||
result['bitrate'] ?? result['actual_bitrate'],
|
||||
);
|
||||
final finalQuality = resolveDisplayQuality(
|
||||
filePath: filePath,
|
||||
fileName: variantOutcome.fileName,
|
||||
@@ -1199,7 +1197,7 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
: context.safFileName,
|
||||
bitDepth: isLossyOutput ? null : actualBitDepth,
|
||||
sampleRate: isLossyOutput ? null : actualSampleRate,
|
||||
bitrate: isLossyOutput ? actualBitrate : null,
|
||||
bitrate: actualBitrate,
|
||||
format: historyFormat,
|
||||
genre: normalizeOptionalString(result['genre'] as String?),
|
||||
label: normalizeOptionalString(result['label'] as String?),
|
||||
|
||||
@@ -1481,9 +1481,7 @@ class _DownloadRun {
|
||||
int? finalBitDepth = backendBitDepth;
|
||||
int? finalSampleRate = backendSampleRate;
|
||||
String? finalFormat = backendFormat;
|
||||
int? finalBitrateKbps = isLossyAudioFormat(finalFormat)
|
||||
? backendBitrateKbps
|
||||
: null;
|
||||
int? finalBitrateKbps = backendBitrateKbps;
|
||||
final lowerFilePath = path.toLowerCase();
|
||||
final canProbeFinalMetadata =
|
||||
path.startsWith('content://') ||
|
||||
@@ -1525,7 +1523,7 @@ class _DownloadRun {
|
||||
final probedBitrateKbps = readPositiveBitrateKbps(
|
||||
metadata['bitrate'] ?? metadata['bit_rate'],
|
||||
);
|
||||
if (probedBitrateKbps != null && isLossyAudioFormat(finalFormat)) {
|
||||
if (probedBitrateKbps != null) {
|
||||
finalBitrateKbps = probedBitrateKbps;
|
||||
}
|
||||
|
||||
@@ -1556,7 +1554,7 @@ class _DownloadRun {
|
||||
lowerFilePath.endsWith('.ogg');
|
||||
final historyBitDepth = isLossyOutput ? null : finalBitDepth;
|
||||
final historySampleRate = isLossyOutput ? null : finalSampleRate;
|
||||
final historyBitrate = isLossyOutput ? finalBitrateKbps : null;
|
||||
final historyBitrate = finalBitrateKbps;
|
||||
|
||||
await persistBeforePublishingDownloadCompletion(
|
||||
persist: () async {
|
||||
|
||||
@@ -21,7 +21,7 @@ class LocalLibraryItem {
|
||||
final String? releaseDate;
|
||||
final int? bitDepth;
|
||||
final int? sampleRate;
|
||||
final int? bitrate; // kbps, for lossy formats (mp3, opus, ogg)
|
||||
final int? bitrate; // average kbps for both lossless and lossy audio
|
||||
final String? genre;
|
||||
final String? composer;
|
||||
final String? label;
|
||||
|
||||
@@ -10,6 +10,24 @@ int? readPositiveBitrateKbps(dynamic value) {
|
||||
return kbps >= 16 ? kbps : null;
|
||||
}
|
||||
|
||||
/// Estimates average stream bitrate without decoding audio. Older SAF-backed
|
||||
/// Library rows can therefore be updated with a cheap size query instead of
|
||||
/// copying the complete audio file into app cache.
|
||||
int? estimateAverageBitrateKbps({
|
||||
required int? fileSizeBytes,
|
||||
required int? durationSeconds,
|
||||
}) {
|
||||
if (fileSizeBytes == null ||
|
||||
fileSizeBytes <= 0 ||
|
||||
durationSeconds == null ||
|
||||
durationSeconds <= 0) {
|
||||
return null;
|
||||
}
|
||||
return readPositiveBitrateKbps(
|
||||
(fileSizeBytes * 8 / durationSeconds / 1000).round(),
|
||||
);
|
||||
}
|
||||
|
||||
String? audioFormatForPath(String? filePath, {String? fileName}) {
|
||||
final candidates = <String>[?filePath, ?fileName];
|
||||
for (final candidate in candidates) {
|
||||
|
||||
Reference in New Issue
Block a user