feat: add field selection dialog for bulk re-enrich metadata

Add a bottom sheet dialog that lets users choose which metadata field
groups to update during bulk re-enrich (cover, lyrics, album/album
artist, track/disc number, date/ISRC, genre/label/copyright).

Backend (Go):
- Filter FLAC Metadata struct and FFmpeg metadata map by selected
  update_fields so non-selected groups preserve existing file values
- Guard Deezer extended metadata fetch with shouldUpdateField(extra)
- Title/Artist are never overwritten by re-enrich (search keys only)
- enrichedMeta response only includes selected field groups

Frontend (Dart):
- New re_enrich_field_dialog.dart bottom sheet with checkboxes
- FFmpegService embed methods gain preserveMetadata param that uses
  -map_metadata 0 instead of -1 to preserve non-selected tags
- Hide selection overlay/bar before showing dialog, restore on cancel
- Fix setState-after-dispose guard in cancel branches

Cleanup:
- Remove dead code in library_tracks_folder_screen.dart
- Fix use_build_context_synchronously in main_shell.dart
- Suppress false-positive use_null_aware_elements lints
- Update l10n label from 'Title, Artist, Album' to 'Album, Album Artist'
This commit is contained in:
zarzet
2026-03-31 18:21:45 +07:00
parent 7dba938299
commit 3a60ea2f4e
24 changed files with 855 additions and 331 deletions
+12 -4
View File
@@ -967,6 +967,7 @@ class FFmpegService {
required String mp3Path,
String? coverPath,
Map<String, String>? metadata,
bool preserveMetadata = false,
}) async {
final tempDir = await getTemporaryDirectory();
final tempOutput = _nextTempEmbedPath(tempDir.path, '.mp3');
@@ -979,7 +980,9 @@ class FFmpegService {
}
cmdBuffer.write('-map 0:a ');
cmdBuffer.write('-map_metadata -1 ');
cmdBuffer.write(
preserveMetadata ? '-map_metadata 0 ' : '-map_metadata -1 ',
);
if (coverPath != null) {
cmdBuffer.write('-map 1:0 ');
@@ -1050,18 +1053,20 @@ class FFmpegService {
String? coverPath,
Map<String, String>? metadata,
String artistTagMode = artistTagModeJoined,
bool preserveMetadata = false,
}) async {
final tempDir = await getTemporaryDirectory();
final tempOutput = _nextTempEmbedPath(tempDir.path, '.opus');
final mapMetaValue = preserveMetadata ? '0' : '-1';
final arguments = <String>[
'-i',
opusPath,
'-map',
'0:a',
'-map_metadata',
'-1',
mapMetaValue,
'-map_metadata:s:a',
'-1',
mapMetaValue,
'-c:a',
'copy',
];
@@ -1140,6 +1145,7 @@ class FFmpegService {
required String m4aPath,
String? coverPath,
Map<String, String>? metadata,
bool preserveMetadata = false,
}) async {
final tempDir = await getTemporaryDirectory();
final tempOutput = _nextTempEmbedPath(tempDir.path, '.m4a');
@@ -1153,7 +1159,9 @@ class FFmpegService {
}
cmdBuffer.write('-map 0:a ');
cmdBuffer.write('-map_metadata -1 ');
cmdBuffer.write(
preserveMetadata ? '-map_metadata 0 ' : '-map_metadata -1 ',
);
// For M4A/MP4, cover art is mapped as a video stream and stored in the
// 'covr' atom automatically by FFmpeg. The '-disposition attached_pic'