fix(metadata): resolve audio format and local artwork

This commit is contained in:
zarzet
2026-07-22 15:55:30 +07:00
parent b2726db84a
commit 87376ee73b
5 changed files with 72 additions and 43 deletions
+6 -1
View File
@@ -184,6 +184,11 @@ extension _TrackMetadataFileActions on _TrackMetadataScreenState {
return;
}
try {
final playbackCover =
normalizeOptionalString(_localCoverPath) ??
normalizeOptionalString(_embeddedCoverPreviewPath) ??
normalizeOptionalString(_coverUrl) ??
'';
await ref
.read(playbackProvider.notifier)
.playLocalPath(
@@ -191,7 +196,7 @@ extension _TrackMetadataFileActions on _TrackMetadataScreenState {
title: trackName,
artist: artistName,
album: albumName,
coverUrl: _coverUrl ?? '',
coverUrl: playbackCover,
);
} catch (e) {
if (context.mounted) {
+31 -25
View File
@@ -21,6 +21,7 @@ import 'package:spotiflac_android/services/ffmpeg_service.dart';
import 'package:spotiflac_android/services/replaygain_service.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/utils/audio_conversion_utils.dart';
import 'package:spotiflac_android/utils/audio_format_utils.dart';
import 'package:spotiflac_android/utils/cover_art_utils.dart';
import 'package:spotiflac_android/utils/logger.dart';
import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart';
@@ -124,6 +125,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen>
late LocalLibraryItem? _currentLocalLibraryItem;
late int? _currentNavigationIndex;
Map<String, dynamic>? _editedMetadata;
String? _resolvedAudioFormat;
String? _embeddedCoverPreviewPath;
static final RegExp _lrcTimestampPattern = RegExp(
r'^\[\d{2}:\d{2}\.\d{2,3}\]',
@@ -324,6 +326,19 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen>
filePath == cleanFilePath &&
_hasPath(cachedPath)) {
setState(() => _embeddedCoverPreviewPath = cachedPath);
} else if (mounted &&
generation == _metadataLoadGeneration &&
filePath == cleanFilePath) {
final localCoverExists =
_hasPath(_localCoverPath) && await fileExists(_localCoverPath!);
if (!mounted ||
generation != _metadataLoadGeneration ||
filePath != cleanFilePath) {
return;
}
if (!localCoverExists && !_hasPath(_coverUrl)) {
unawaited(_refreshEmbeddedCoverPreview());
}
}
}
}
@@ -376,9 +391,13 @@ 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 resolvedFormat = detectedAudioFormatFromMetadata(metadata);
final storedFormat = _isLocalItem
? _localLibraryItem?.format
: _downloadItem?.format;
final formatChanged =
resolvedFormat != null &&
resolvedFormat != normalizeAudioFormatValue(storedFormat);
final resolvedBitrate = _isBitrateFormatValue(resolvedFormat)
? _readPlausibleBitrateKbps(
metadata['bitrate'] ?? metadata['bit_rate'],
@@ -475,6 +494,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen>
if ((resolvedBitDepth != null ||
resolvedSampleRate != null ||
resolvedFormat != null ||
fileHasTitle ||
fileHasArtist ||
fileHasAlbumArtist ||
@@ -493,6 +513,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen>
isPlaceholderQualityLabel(_quality)) &&
mounted) {
setState(() {
_resolvedAudioFormat = resolvedFormat;
_editedMetadata = {
...?_editedMetadata,
// ignore: use_null_aware_elements
@@ -552,10 +573,11 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen>
);
});
}
} else if (_isLocalItem && needsDuration) {
} else if (_isLocalItem && (needsDuration || formatChanged)) {
await LibraryDatabase.instance.updateAudioMetadata(
_localLibraryItem!.id,
duration: resolvedDuration,
format: formatChanged ? resolvedFormat : null,
);
await ref.read(localLibraryProvider.notifier).reloadFromStorage();
}
@@ -753,7 +775,8 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen>
int? get _audioBitrate =>
_isLocalItem ? _localLibraryItem!.bitrate : _downloadItem?.bitrate;
String? get _storedAudioFormat =>
_isLocalItem ? _localLibraryItem?.format : _downloadItem?.format;
_resolvedAudioFormat ??
(_isLocalItem ? _localLibraryItem?.format : _downloadItem?.format);
String get _filePath =>
_isLocalItem ? _localLibraryItem!.filePath : _downloadItem!.filePath;
@@ -782,24 +805,6 @@ 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;
@@ -816,7 +821,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen>
'mp3',
'opus',
'm4a',
}.contains(_normalizeAudioFormatValue(value));
}.contains(normalizeAudioFormatValue(value));
}
String? _usableStoredQuality(String? quality) {
@@ -842,7 +847,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen>
int? bitrateKbps,
String? storedQuality,
}) {
final normalizedFormat = _normalizeAudioFormatValue(format);
final normalizedFormat = normalizeAudioFormatValue(format);
final formatLabel = normalizedFormat == null
? normalizeOptionalString(format)?.toUpperCase()
: _formatLabelForRaw(normalizedFormat);
@@ -1082,6 +1087,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen>
_embeddedLyricsChecked = false;
_hasLoadedResolvedAudioMetadata = false;
_editedMetadata = null;
_resolvedAudioFormat = null;
_embeddedCoverPreviewPath = null;
});
+7 -17
View File
@@ -4,6 +4,7 @@ import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:spotiflac_android/utils/logger.dart';
import 'package:spotiflac_android/utils/audio_format_utils.dart';
import 'package:spotiflac_android/utils/file_access.dart';
import 'package:spotiflac_android/services/history_database.dart';
import 'package:spotiflac_android/services/sqlite_helpers.dart' as sqlite;
@@ -1844,6 +1845,7 @@ class LibraryDatabase {
int? bitDepth,
int? sampleRate,
int? bitrate,
String? format,
}) async {
final values = <String, dynamic>{};
if (duration != null && duration > 0) {
@@ -1858,6 +1860,10 @@ class LibraryDatabase {
if (bitrate != null && bitrate > 0) {
values['bitrate'] = bitrate;
}
final normalizedFormat = normalizeAudioFormatValue(format);
if (normalizedFormat != null) {
values['format'] = normalizedFormat;
}
if (values.isEmpty) return;
final db = await database;
@@ -2049,23 +2055,7 @@ class LibraryDatabase {
}
String _normalizeConvertedFormat(String targetFormat) {
switch (targetFormat.trim().toLowerCase()) {
case 'alac':
return 'm4a';
case 'aac':
return 'aac';
case 'flac':
return 'flac';
case 'opus':
return 'opus';
case 'wav':
return 'wav';
case 'aiff':
case 'aif':
return 'aiff';
default:
return 'mp3';
}
return normalizeAudioFormatValue(targetFormat) ?? 'mp3';
}
int? _convertedBitrate({
+13
View File
@@ -54,6 +54,8 @@ String? normalizeAudioFormatValue(String? value) {
return switch (normalized) {
'flac' => 'flac',
'alac' => 'alac',
'wav' || 'wave' => 'wav',
'aiff' || 'aif' || 'aifc' => 'aiff',
'aac' || 'mp4a' => 'aac',
'eac3' || 'ec_3' => 'eac3',
'ac3' || 'ac_3' => 'ac3',
@@ -65,6 +67,17 @@ String? normalizeAudioFormatValue(String? value) {
};
}
/// Resolves the actual audio codec reported by native metadata probing, while
/// falling back to the container format when the codec is absent or generic.
///
/// This distinction matters for MP4/M4A files: the same container may hold
/// AAC, ALAC, Dolby, or other codecs.
String? detectedAudioFormatFromMetadata(Map<String, dynamic> metadata) {
final codec = normalizeAudioFormatValue(metadata['audio_codec']?.toString());
if (codec != null) return codec;
return normalizeAudioFormatValue(metadata['format']?.toString());
}
bool isLossyAudioFormat(String? value) {
return const {
'aac',
+15
View File
@@ -586,6 +586,21 @@ void main() {
});
group('audio conversion utils', () {
test('distinguishes an ALAC codec from its M4A container', () {
expect(normalizeAudioFormatValue('ALAC'), 'alac');
expect(
detectedAudioFormatFromMetadata({
'audio_codec': 'alac',
'format': 'm4a',
}),
'alac',
);
expect(
detectedAudioFormatFromMetadata({'audio_codec': '', 'format': 'alac'}),
'alac',
);
});
test(
'detects Dolby formats from stored scan format before file extension',
() {