feat: persist codec format and bitrate in download history

Bump the history schema on both the Kotlin finalizer and the Dart database to v9, adding bitrate (kbps) and format (codec label) columns, and let the download flow fill them from backend/probe metadata so lossy downloads keep a 'AAC 256kbps' label instead of falling back to the stored placeholder. Library filtering and the track metadata screen now read format/bitrate directly from those columns, which also fixes mis-tagged quality badges after re-downloading a track at a different format.

Additional fixes bundled in: EditFileMetadata now routes ReplayGain writes through the M4A path whenever the file starts with ftyp (fixing .flac files that actually hold MP4 containers); GetM4AQuality falls back to the first trak/mdia/mdhd duration when mvhd is zero so EAC3 streams no longer report 0s; and both Kotlin and Dart reject bitrate values below 16 kbps to prevent probe noise from surfacing as '0 kbps' labels. New unit tests cover the EAC3 mdhd fallback and the mis-named M4A replaygain path.
This commit is contained in:
zarzet
2026-05-10 23:18:32 +07:00
parent d664d46ca4
commit 8e605cbd0f
11 changed files with 451 additions and 57 deletions
+128 -6
View File
@@ -38,7 +38,8 @@ final _multiUnderscoreRegex = RegExp(r'_+');
int? _readPositiveBitrateKbps(dynamic value) {
final parsed = readPositiveInt(value);
if (parsed == null) return null;
return parsed >= 10000 ? (parsed / 1000).round() : parsed;
final kbps = parsed >= 10000 ? (parsed / 1000).round() : parsed;
return kbps >= 16 ? kbps : null;
}
String? _audioFormatForPath(String? filePath, {String? fileName}) {
@@ -58,6 +59,14 @@ String? _nonPlaceholderQuality(String? quality) {
if (normalized == null || isPlaceholderQualityLabel(normalized)) {
return null;
}
final bitrateMatch = RegExp(
r'\b(\d+)\s*kbps\b',
caseSensitive: false,
).firstMatch(normalized);
if (bitrateMatch != null) {
final bitrate = int.tryParse(bitrateMatch.group(1) ?? '');
if (bitrate != null && bitrate < 16) return null;
}
final lower = normalized.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]+'), '_');
const requestedLosslessLabels = {
'hi_res_lossless',
@@ -70,6 +79,36 @@ String? _nonPlaceholderQuality(String? quality) {
return normalized;
}
String? _normalizeAudioFormatValue(String? value) {
final normalized = normalizeOptionalString(
value,
)?.toLowerCase().replaceAll('-', '_');
return switch (normalized) {
'flac' => 'flac',
'alac' => 'alac',
'aac' || 'mp4a' => 'aac',
'eac3' || 'ec_3' => 'eac3',
'ac3' || 'ac_3' => 'ac3',
'ac4' || 'ac_4' => 'ac4',
'mp3' => 'mp3',
'opus' || 'ogg' => 'opus',
'm4a' || 'mp4' => 'm4a',
_ => null,
};
}
bool _isLossyAudioFormat(String? value) {
return const {
'aac',
'eac3',
'ac3',
'ac4',
'mp3',
'opus',
'm4a',
}.contains(_normalizeAudioFormatValue(value));
}
String? _resolveDisplayQuality({
required String? filePath,
String? fileName,
@@ -151,6 +190,8 @@ class DownloadHistoryItem {
final String? quality;
final int? bitDepth;
final int? sampleRate;
final int? bitrate;
final String? format;
final String? genre;
final String? composer;
final String? label;
@@ -182,6 +223,8 @@ class DownloadHistoryItem {
this.quality,
this.bitDepth,
this.sampleRate,
this.bitrate,
this.format,
this.genre,
this.composer,
this.label,
@@ -214,6 +257,8 @@ class DownloadHistoryItem {
'quality': quality,
'bitDepth': bitDepth,
'sampleRate': sampleRate,
'bitrate': bitrate,
'format': format,
'genre': genre,
'composer': composer,
'label': label,
@@ -247,6 +292,8 @@ class DownloadHistoryItem {
quality: json['quality'] as String?,
bitDepth: json['bitDepth'] as int?,
sampleRate: json['sampleRate'] as int?,
bitrate: (json['bitrate'] as num?)?.toInt(),
format: json['format'] as String?,
genre: json['genre'] as String?,
composer: json['composer'] as String?,
label: json['label'] as String?,
@@ -276,6 +323,8 @@ class DownloadHistoryItem {
String? quality,
int? bitDepth,
int? sampleRate,
int? bitrate,
String? format,
String? genre,
String? composer,
String? label,
@@ -307,6 +356,8 @@ class DownloadHistoryItem {
quality: quality ?? this.quality,
bitDepth: bitDepth ?? this.bitDepth,
sampleRate: sampleRate ?? this.sampleRate,
bitrate: bitrate ?? this.bitrate,
format: format ?? this.format,
genre: genre ?? this.genre,
composer: composer ?? this.composer,
label: label ?? this.label,
@@ -703,6 +754,7 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
item.bitDepth! > 0 &&
item.sampleRate != null &&
item.sampleRate! > 0;
final needsFormatBackfill = normalizeOptionalString(item.format) == null;
final needsLosslessSpecProbe =
!hasResolvedSpecs &&
(trimmedPath.endsWith('.flac') ||
@@ -720,6 +772,7 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
final needsDiscNumberBackfill = item.discNumber == null;
final needsTotalDiscsBackfill = item.totalDiscs == null;
return needsComposerBackfill ||
needsFormatBackfill ||
needsDurationBackfill ||
needsTrackNumberBackfill ||
needsTotalTracksBackfill ||
@@ -735,6 +788,7 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
final needsDiscNumberBackfill = item.discNumber == null;
final needsTotalDiscsBackfill = item.totalDiscs == null;
return needsLosslessSpecProbe ||
needsFormatBackfill ||
isPlaceholderQualityLabel(item.quality) ||
normalizeOptionalString(item.quality) == null ||
needsComposerBackfill ||
@@ -761,11 +815,16 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
final bitDepth = readPositiveInt(result['bit_depth']);
final sampleRate = readPositiveInt(result['sample_rate']);
final bitrateKbps = _readPositiveBitrateKbps(result['bitrate']);
final detectedFormat = _normalizeAudioFormatValue(
result['audio_codec']?.toString() ?? result['format']?.toString(),
);
final rawBitrateKbps = _readPositiveBitrateKbps(result['bitrate']);
final bitrateKbps = _isLossyAudioFormat(detectedFormat)
? rawBitrateKbps
: null;
final quality = _resolveDisplayQuality(
filePath: filePath,
detectedFormat:
result['audio_codec']?.toString() ?? result['format']?.toString(),
detectedFormat: detectedFormat,
bitDepth: bitDepth,
sampleRate: sampleRate,
bitrateKbps: bitrateKbps,
@@ -782,6 +841,7 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
bitDepth == null &&
sampleRate == null &&
bitrateKbps == null &&
detectedFormat == null &&
composer == null &&
duration == null &&
trackNumber == null &&
@@ -795,6 +855,8 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
'quality': quality,
'bitDepth': bitDepth,
'sampleRate': sampleRate,
'bitrate': bitrateKbps,
'format': detectedFormat,
'bitrateKbps': bitrateKbps,
'composer': composer,
'duration': duration,
@@ -868,6 +930,10 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
);
final resolvedBitDepth = probed['bitDepth'] as int?;
final resolvedSampleRate = probed['sampleRate'] as int?;
final resolvedBitrate = probed['bitrate'] as int?;
final resolvedFormat = normalizeOptionalString(
probed['format'] as String?,
);
final resolvedComposer = normalizeOptionalString(
probed['composer'] as String?,
);
@@ -883,6 +949,10 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
resolvedBitDepth != null && resolvedBitDepth != item.bitDepth;
final sampleRateChanged =
resolvedSampleRate != null && resolvedSampleRate != item.sampleRate;
final bitrateChanged =
resolvedBitrate != null && resolvedBitrate != item.bitrate;
final formatChanged =
resolvedFormat != null && resolvedFormat != item.format;
final composerChanged =
resolvedComposer != null && resolvedComposer != item.composer;
final durationChanged =
@@ -901,6 +971,8 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
if (!qualityChanged &&
!bitDepthChanged &&
!sampleRateChanged &&
!bitrateChanged &&
!formatChanged &&
!composerChanged &&
!durationChanged &&
!trackNumberChanged &&
@@ -914,6 +986,8 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
quality: resolvedQuality,
bitDepth: resolvedBitDepth,
sampleRate: resolvedSampleRate,
bitrate: resolvedBitrate,
format: resolvedFormat,
composer: resolvedComposer,
duration: resolvedDuration,
trackNumber: resolvedTrackNumber,
@@ -1197,6 +1271,8 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
String? quality,
int? bitDepth,
int? sampleRate,
int? bitrate,
String? format,
int? trackNumber,
int? totalTracks,
int? discNumber,
@@ -1217,6 +1293,8 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
quality: quality,
bitDepth: bitDepth,
sampleRate: sampleRate,
bitrate: bitrate,
format: format,
trackNumber: trackNumber,
totalTracks: totalTracks,
discNumber: discNumber,
@@ -1228,6 +1306,8 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
if (updated.quality == current.quality &&
updated.bitDepth == current.bitDepth &&
updated.sampleRate == current.sampleRate &&
updated.bitrate == current.bitrate &&
updated.format == current.format &&
updated.trackNumber == current.trackNumber &&
updated.totalTracks == current.totalTracks &&
updated.discNumber == current.discNumber &&
@@ -5706,10 +5786,22 @@ 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 actualFormat =
_normalizeAudioFormatValue(
result['audio_codec']?.toString() ?? result['format']?.toString(),
) ??
_normalizeAudioFormatValue(_audioFormatForPath(filePath));
final actualBitrate = _isLossyAudioFormat(actualFormat)
? _readPositiveBitrateKbps(
result['bitrate'] ?? result['actual_bitrate'],
)
: null;
final resolvedQuality = _resolveDisplayQuality(
filePath: filePath,
detectedFormat: actualFormat,
bitDepth: actualBitDepth,
sampleRate: actualSampleRate,
bitrateKbps: actualBitrate,
storedQuality: actualQuality,
);
if (resolvedQuality != null) {
@@ -5818,7 +5910,13 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
final backendComposer = result['composer'] as String?;
final resultSafFileName = result['file_name'] as String?;
final lowerFilePath = filePath.toLowerCase();
final historyFormat =
_normalizeAudioFormatValue(
result['audio_codec']?.toString() ?? result['format']?.toString(),
) ??
_normalizeAudioFormatValue(_audioFormatForPath(filePath));
final isLossyOutput =
_isLossyAudioFormat(historyFormat) ||
lowerFilePath.endsWith('.mp3') ||
lowerFilePath.endsWith('.opus') ||
lowerFilePath.endsWith('.ogg');
@@ -5896,6 +5994,8 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
quality: actualQuality,
bitDepth: isLossyOutput ? null : actualBitDepth,
sampleRate: isLossyOutput ? null : actualSampleRate,
bitrate: isLossyOutput ? actualBitrate : null,
format: historyFormat,
genre: normalizeOptionalString(backendGenre),
composer: historyComposer,
label: normalizeOptionalString(backendLabel),
@@ -8192,6 +8292,12 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
final backendTotalDiscs = _parsePositiveInt(result['total_discs']);
final backendBitDepth = result['actual_bit_depth'] as int?;
final backendSampleRate = result['actual_sample_rate'] as int?;
final backendFormat =
_normalizeAudioFormatValue(
result['audio_codec']?.toString() ??
result['format']?.toString(),
) ??
_normalizeAudioFormatValue(_audioFormatForPath(filePath));
final backendBitrateKbps = _readPositiveBitrateKbps(
result['bitrate'] ?? result['actual_bitrate'],
);
@@ -8215,7 +8321,10 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
int? finalBitDepth = backendBitDepth;
int? finalSampleRate = backendSampleRate;
int? finalBitrateKbps = backendBitrateKbps;
String? finalFormat = backendFormat;
int? finalBitrateKbps = _isLossyAudioFormat(finalFormat)
? backendBitrateKbps
: null;
final lowerFilePath = filePath.toLowerCase();
final canProbeFinalMetadata =
filePath.startsWith('content://') ||
@@ -8244,16 +8353,25 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
if (probedSampleRate != null && probedSampleRate > 0) {
finalSampleRate = probedSampleRate;
}
final probedFormat = _normalizeAudioFormatValue(
metadata['audio_codec']?.toString() ??
metadata['format']?.toString(),
);
if (probedFormat != null) {
finalFormat = probedFormat;
}
final probedBitrateKbps = _readPositiveBitrateKbps(
metadata['bitrate'] ?? metadata['bit_rate'],
);
if (probedBitrateKbps != null && probedBitrateKbps > 0) {
if (probedBitrateKbps != null &&
_isLossyAudioFormat(finalFormat)) {
finalBitrateKbps = probedBitrateKbps;
}
final resolvedQuality = _resolveDisplayQuality(
filePath: filePath,
fileName: finalSafFileName,
detectedFormat: finalFormat,
bitDepth: finalBitDepth,
sampleRate: finalSampleRate,
bitrateKbps: finalBitrateKbps,
@@ -8275,11 +8393,13 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
);
final isLossyOutput =
_isLossyAudioFormat(finalFormat) ||
lowerFilePath.endsWith('.mp3') ||
lowerFilePath.endsWith('.opus') ||
lowerFilePath.endsWith('.ogg');
final historyBitDepth = isLossyOutput ? null : finalBitDepth;
final historySampleRate = isLossyOutput ? null : finalSampleRate;
final historyBitrate = isLossyOutput ? finalBitrateKbps : null;
final historyTotalTracks = _resolvePositiveMetadataInt(
trackToDownload.totalTracks,
backendTotalTracks,
@@ -8353,6 +8473,8 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
quality: actualQuality,
bitDepth: historyBitDepth,
sampleRate: historySampleRate,
bitrate: historyBitrate,
format: finalFormat,
genre: effectiveGenre,
composer: historyComposer,
label: effectiveLabel,
+4
View File
@@ -1491,6 +1491,10 @@ class _QueueTabState extends ConsumerState<QueueTab> {
if (localFormat != null) {
return localFormat.toLowerCase().replaceAll('-', '_');
}
final historyFormat = normalizeOptionalString(item.historyItem?.format);
if (historyFormat != null) {
return historyFormat.toLowerCase().replaceAll('-', '_');
}
return _fileExtLower(item.filePath);
}
+16 -5
View File
@@ -33,6 +33,21 @@ class UnifiedLibraryItem {
});
factory UnifiedLibraryItem.fromDownloadHistory(DownloadHistoryItem item) {
String? quality;
if (item.bitrate != null && item.bitrate! > 0) {
quality = buildDisplayAudioQuality(
bitrateKbps: item.bitrate,
format: item.format,
);
} else if (item.bitDepth != null &&
item.bitDepth! > 0 &&
item.sampleRate != null) {
quality = buildDisplayAudioQuality(
bitDepth: item.bitDepth,
sampleRate: item.sampleRate,
);
}
quality ??= item.quality;
return UnifiedLibraryItem(
id: 'dl_${item.id}',
trackName: item.trackName,
@@ -40,11 +55,7 @@ class UnifiedLibraryItem {
albumName: item.albumName,
coverUrl: item.coverUrl,
filePath: item.filePath,
quality: buildDisplayAudioQuality(
bitDepth: item.bitDepth,
sampleRate: item.sampleRate,
storedQuality: item.quality,
),
quality: quality,
addedAt: item.downloadedAt,
source: LibraryItemSource.downloaded,
historyItem: item,
+131 -17
View File
@@ -368,11 +368,21 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
final resolvedBitDepth = readPositiveInt(metadata['bit_depth']);
final resolvedSampleRate = readPositiveInt(metadata['sample_rate']);
final resolvedFormat = _normalizeAudioFormatValue(
metadata['audio_codec']?.toString() ?? metadata['format']?.toString(),
);
final resolvedBitrate = _isBitrateFormatValue(resolvedFormat)
? _readPlausibleBitrateKbps(
metadata['bitrate'] ?? metadata['bit_rate'],
)
: null;
final resolvedDuration = readPositiveInt(metadata['duration']);
final resolvedAlbum = metadata['album']?.toString();
final resolvedQuality = buildDisplayAudioQuality(
final resolvedQuality = _displayQualityForValues(
format: resolvedFormat ?? _storedAudioFormat,
bitDepth: resolvedBitDepth ?? bitDepth,
sampleRate: resolvedSampleRate ?? sampleRate,
bitrateKbps: resolvedBitrate ?? _audioBitrate,
storedQuality: _quality,
);
@@ -427,6 +437,8 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
!_isLocalItem &&
(resolvedBitDepth != null ||
resolvedSampleRate != null ||
resolvedBitrate != null ||
resolvedFormat != null ||
needsTrackNumber ||
needsTotalTracks ||
needsDiscNumber ||
@@ -476,6 +488,8 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
quality: resolvedQuality,
bitDepth: resolvedBitDepth,
sampleRate: resolvedSampleRate,
bitrate: resolvedBitrate,
format: resolvedFormat,
trackNumber: needsTrackNumber ? resolvedTrackNumber : null,
totalTracks: needsTotalTracks ? resolvedTotalTracks : null,
discNumber: needsDiscNumber ? resolvedDiscNumber : null,
@@ -483,6 +497,23 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
duration: needsDuration ? resolvedDuration : null,
composer: needsComposer ? resolvedComposer : null,
);
if (mounted && _downloadItem != null) {
setState(() {
_currentDownloadItem = _downloadItem!.copyWith(
quality: resolvedQuality,
bitDepth: resolvedBitDepth,
sampleRate: resolvedSampleRate,
bitrate: resolvedBitrate,
format: resolvedFormat,
trackNumber: needsTrackNumber ? resolvedTrackNumber : null,
totalTracks: needsTotalTracks ? resolvedTotalTracks : null,
discNumber: needsDiscNumber ? resolvedDiscNumber : null,
totalDiscs: needsTotalDiscs ? resolvedTotalDiscs : null,
duration: needsDuration ? resolvedDuration : null,
composer: needsComposer ? resolvedComposer : null,
);
});
}
} else if (_isLocalItem && needsDuration) {
await LibraryDatabase.instance.updateAudioMetadata(
_localLibraryItem!.id,
@@ -681,7 +712,10 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
(_isLocalItem
? _localLibraryItem!.sampleRate
: _downloadItem!.sampleRate);
int? get _localBitrate => _isLocalItem ? _localLibraryItem!.bitrate : null;
int? get _audioBitrate =>
_isLocalItem ? _localLibraryItem!.bitrate : _downloadItem?.bitrate;
String? get _storedAudioFormat =>
_isLocalItem ? _localLibraryItem?.format : _downloadItem?.format;
String get _filePath =>
_isLocalItem ? _localLibraryItem!.filePath : _downloadItem!.filePath;
@@ -707,6 +741,85 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
String? get _quality => _isLocalItem ? null : _downloadItem!.quality;
String? _normalizeAudioFormatValue(String? value) {
final normalized = normalizeOptionalString(
value,
)?.toLowerCase().replaceAll('-', '_');
return switch (normalized) {
'flac' => 'flac',
'alac' => 'alac',
'aac' || 'mp4a' => 'aac',
'eac3' || 'ec_3' => 'eac3',
'ac3' || 'ac_3' => 'ac3',
'ac4' || 'ac_4' => 'ac4',
'mp3' => 'mp3',
'opus' || 'ogg' => 'opus',
'm4a' || 'mp4' => 'm4a',
_ => null,
};
}
int? _readPlausibleBitrateKbps(dynamic value) {
final parsed = readPositiveInt(value);
if (parsed == null) return null;
final kbps = parsed >= 10000 ? (parsed / 1000).round() : parsed;
return kbps >= 16 ? kbps : null;
}
bool _isBitrateFormatValue(String? value) {
return const {
'aac',
'eac3',
'ac3',
'ac4',
'mp3',
'opus',
'm4a',
}.contains(_normalizeAudioFormatValue(value));
}
String? _usableStoredQuality(String? quality) {
final normalized = normalizeOptionalString(quality);
if (normalized == null || isPlaceholderQualityLabel(normalized)) {
return null;
}
final bitrateMatch = RegExp(
r'\b(\d+)\s*kbps\b',
caseSensitive: false,
).firstMatch(normalized);
if (bitrateMatch != null) {
final bitrate = int.tryParse(bitrateMatch.group(1) ?? '');
if (bitrate != null && bitrate < 16) return null;
}
return normalized;
}
String? _displayQualityForValues({
required String? format,
int? bitDepth,
int? sampleRate,
int? bitrateKbps,
String? storedQuality,
}) {
final normalizedFormat = _normalizeAudioFormatValue(format);
final formatLabel = normalizedFormat == null
? normalizeOptionalString(format)?.toUpperCase()
: _formatLabelForRaw(normalizedFormat);
if (_isBitrateFormatValue(normalizedFormat)) {
return buildDisplayAudioQuality(
bitrateKbps: bitrateKbps,
format: formatLabel,
) ??
_usableStoredQuality(storedQuality) ??
formatLabel;
}
return buildDisplayAudioQuality(
bitDepth: bitDepth,
sampleRate: sampleRate,
storedQuality: _usableStoredQuality(storedQuality),
);
}
String _displayServiceTrackId(String value) {
final raw = value.trim();
if (raw.isEmpty) return raw;
@@ -766,11 +879,11 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
? fileName.split('.').last.toUpperCase()
: null;
return buildDisplayAudioQuality(
return _displayQualityForValues(
format: _storedAudioFormat ?? fileExt,
bitDepth: bitDepth,
sampleRate: sampleRate,
bitrateKbps: _isLocalItem ? _localBitrate : null,
format: _isLocalItem ? (_localLibraryItem!.format ?? fileExt) : fileExt,
bitrateKbps: _audioBitrate,
storedQuality: _quality,
);
}
@@ -1611,13 +1724,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
return '$minutes:${secs.toString().padLeft(2, '0')}';
}
String _displayFormatLabelForFile(String fileName) {
final localFormat = _isLocalItem
? normalizeOptionalString(_localLibraryItem?.format)
: null;
final raw =
localFormat ??
(fileName.contains('.') ? fileName.split('.').last : 'Unknown');
String _formatLabelForRaw(String raw) {
final normalized = raw.toLowerCase().replaceAll('-', '_');
return switch (normalized) {
'flac' => 'FLAC',
@@ -1626,7 +1733,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
'ac3' || 'ac_3' => 'AC3',
'ac4' || 'ac_4' => 'AC4',
'aac' || 'mp4a' => 'AAC',
'm4a' => 'M4A',
'm4a' || 'mp4' => 'M4A',
'mp3' => 'MP3',
'opus' => 'Opus',
'ogg' => 'OGG',
@@ -1634,6 +1741,14 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
};
}
String _displayFormatLabelForFile(String fileName) {
final storedFormat = normalizeOptionalString(_storedAudioFormat);
final raw =
storedFormat ??
(fileName.contains('.') ? fileName.split('.').last : 'Unknown');
return _formatLabelForRaw(raw);
}
bool _isBitrateFormatLabel(String label) {
return const {
'MP3',
@@ -1748,9 +1863,8 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
),
),
)
else if (_isLocalItem &&
_localBitrate != null &&
_localBitrate! > 0 &&
else if (_audioBitrate != null &&
_audioBitrate! > 0 &&
_isBitrateFormatLabel(fileExtension))
Container(
padding: const EdgeInsets.symmetric(
@@ -1762,7 +1876,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
borderRadius: BorderRadius.circular(20),
),
child: Text(
'${_localBitrate}kbps',
'${_audioBitrate}kbps',
style: TextStyle(
color: colorScheme.onTertiaryContainer,
fontWeight: FontWeight.w600,
+11 -1
View File
@@ -84,7 +84,7 @@ class HistoryDatabase {
return await openDatabase(
path,
version: 8,
version: 9,
onConfigure: (db) async {
await db.rawQuery('PRAGMA journal_mode = WAL');
await db.execute('PRAGMA synchronous = NORMAL');
@@ -124,6 +124,8 @@ class HistoryDatabase {
quality TEXT,
bit_depth INTEGER,
sample_rate INTEGER,
bitrate INTEGER,
format TEXT,
genre TEXT,
composer TEXT,
label TEXT,
@@ -203,6 +205,10 @@ class HistoryDatabase {
await _backfillNormalizedColumns(db);
await _createNormalizedIndexes(db);
}
if (oldVersion < 9) {
await _addColumnIfMissing(db, 'history', 'bitrate', 'INTEGER');
await _addColumnIfMissing(db, 'history', 'format', 'TEXT');
}
}
static String normalizeLookupText(String? value) {
@@ -507,6 +513,8 @@ class HistoryDatabase {
'quality': json['quality'],
'bit_depth': json['bitDepth'],
'sample_rate': json['sampleRate'],
'bitrate': json['bitrate'],
'format': json['format'],
'genre': json['genre'],
'composer': json['composer'],
'label': json['label'],
@@ -550,6 +558,8 @@ class HistoryDatabase {
'quality': row['quality'],
'bitDepth': row['bit_depth'],
'sampleRate': row['sample_rate'],
'bitrate': row['bitrate'],
'format': row['format'],
'genre': row['genre'],
'composer': row['composer'],
'label': row['label'],
+5 -3
View File
@@ -1029,8 +1029,8 @@ class LibraryDatabase {
NULL AS cover_path,
NULL AS scanned_at,
NULL AS file_mod_time,
NULL AS bitrate,
NULL AS format,
h.bitrate,
h.format,
LOWER(h.track_name) AS sort_track,
LOWER(h.artist_name) AS sort_artist,
LOWER(h.album_name) AS sort_album,
@@ -1299,7 +1299,7 @@ class LibraryDatabase {
args,
request,
filePathExpr: 'h.file_path',
formatExpr: null,
formatExpr: 'h.format',
qualityExpr: 'h.quality',
bitDepthExpr: 'h.bit_depth',
artistExpr: 'h.artist_name',
@@ -1544,6 +1544,8 @@ class LibraryDatabase {
'quality': row['quality'],
'bitDepth': row['bit_depth'],
'sampleRate': row['sample_rate'],
'bitrate': row['bitrate'],
'format': row['format'],
'genre': row['genre'],
'composer': row['composer'],
'label': row['label'],