From 4adaed8da08ee9361ce21f31290850f2e1235051 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 02:39:11 +0700 Subject: [PATCH] fix: filter batch convert target formats based on source formats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exclude same-format and lossy-to-lossless targets from the batch convert sheet so users cannot pick pointless conversions like FLAC→FLAC. Also clean up redundant inline comments. --- lib/screens/downloaded_album_screen.dart | 42 ++++++++++++++++-- lib/screens/local_album_screen.dart | 54 ++++++++++++++++++++++-- lib/screens/queue_tab.dart | 48 +++++++++++++++++++-- lib/services/ffmpeg_service.dart | 19 ++------- 4 files changed, 135 insertions(+), 28 deletions(-) diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index f320aa40..ecbdbbe7 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -910,9 +910,44 @@ class _DownloadedAlbumScreenState extends ConsumerState { BuildContext context, List allTracks, ) { - String selectedFormat = 'MP3'; - String selectedBitrate = '320k'; - bool isLosslessTarget = false; + final tracksById = {for (final t in allTracks) t.id: t}; + final sourceFormats = {}; + for (final id in _selectedIds) { + final item = tracksById[id]; + if (item == null) continue; + final nameToCheck = + (item.safFileName != null && item.safFileName!.isNotEmpty) + ? item.safFileName!.toLowerCase() + : item.filePath.toLowerCase(); + final ext = nameToCheck.endsWith('.flac') + ? 'FLAC' + : nameToCheck.endsWith('.m4a') + ? 'M4A' + : nameToCheck.endsWith('.mp3') + ? 'MP3' + : (nameToCheck.endsWith('.opus') || nameToCheck.endsWith('.ogg')) + ? 'Opus' + : null; + if (ext != null) sourceFormats.add(ext); + } + + final formats = ['ALAC', 'FLAC', 'MP3', 'Opus'].where((target) { + return sourceFormats.any((src) { + if (src == target) return false; + final isLosslessTarget = target == 'ALAC' || target == 'FLAC'; + final isLosslessSource = src == 'FLAC' || src == 'M4A'; + if (isLosslessTarget && !isLosslessSource) return false; + return true; + }); + }).toList(); + + if (formats.isEmpty) return; + + String selectedFormat = formats.first; + bool isLosslessTarget = + selectedFormat == 'ALAC' || selectedFormat == 'FLAC'; + String selectedBitrate = + isLosslessTarget ? '320k' : (selectedFormat == 'Opus' ? '128k' : '320k'); showModalBottomSheet( context: context, @@ -924,7 +959,6 @@ class _DownloadedAlbumScreenState extends ConsumerState { return StatefulBuilder( builder: (context, setSheetState) { final colorScheme = Theme.of(context).colorScheme; - final formats = ['ALAC', 'FLAC', 'MP3', 'Opus']; final bitrates = ['128k', '192k', '256k', '320k']; return SafeArea( diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index 1b662324..42818a12 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -1129,9 +1129,56 @@ class _LocalAlbumScreenState extends ConsumerState { BuildContext context, List allTracks, ) { - String selectedFormat = 'MP3'; - String selectedBitrate = '320k'; - bool isLosslessTarget = false; + final tracksById = {for (final t in allTracks) t.id: t}; + final sourceFormats = {}; + for (final id in _selectedIds) { + final item = tracksById[id]; + if (item == null) continue; + String? ext; + if (item.format != null && item.format!.isNotEmpty) { + final fmt = item.format!.toLowerCase(); + if (fmt == 'flac') { + ext = 'FLAC'; + } else if (fmt == 'm4a') { + ext = 'M4A'; + } else if (fmt == 'mp3') { + ext = 'MP3'; + } else if (fmt == 'opus' || fmt == 'ogg') { + ext = 'Opus'; + } + } + if (ext == null) { + final lower = item.filePath.toLowerCase(); + if (lower.endsWith('.flac')) { + ext = 'FLAC'; + } else if (lower.endsWith('.m4a')) { + ext = 'M4A'; + } else if (lower.endsWith('.mp3')) { + ext = 'MP3'; + } else if (lower.endsWith('.opus') || lower.endsWith('.ogg')) { + ext = 'Opus'; + } + } + if (ext != null) sourceFormats.add(ext); + } + + final formats = ['ALAC', 'FLAC', 'MP3', 'Opus'].where((target) { + return sourceFormats.any((src) { + if (src == target) return false; + final isLosslessTarget = target == 'ALAC' || target == 'FLAC'; + final isLosslessSource = src == 'FLAC' || src == 'M4A'; + if (isLosslessTarget && !isLosslessSource) return false; + return true; + }); + }).toList(); + + if (formats.isEmpty) return; + + String selectedFormat = formats.first; + bool isLosslessTarget = + selectedFormat == 'ALAC' || selectedFormat == 'FLAC'; + String selectedBitrate = + isLosslessTarget ? '320k' : (selectedFormat == 'Opus' ? '128k' : '320k'); showModalBottomSheet( context: context, @@ -1143,7 +1190,6 @@ class _LocalAlbumScreenState extends ConsumerState { return StatefulBuilder( builder: (context, setSheetState) { final colorScheme = Theme.of(context).colorScheme; - final formats = ['ALAC', 'FLAC', 'MP3', 'Opus']; final bitrates = ['128k', '192k', '256k', '320k']; return SafeArea( diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 64f9c0e3..69b1aa44 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -4755,9 +4755,50 @@ class _QueueTabState extends ConsumerState { BuildContext context, List allItems, ) async { - String selectedFormat = 'MP3'; - String selectedBitrate = '320k'; - bool isLosslessTarget = false; + final itemsById = {for (final item in allItems) item.id: item}; + final sourceFormats = {}; + for (final id in _selectedIds) { + final item = itemsById[id]; + if (item == null) continue; + String nameToCheck; + if (item.historyItem?.safFileName != null && + item.historyItem!.safFileName!.isNotEmpty) { + nameToCheck = item.historyItem!.safFileName!.toLowerCase(); + } else if (item.localItem?.format != null && + item.localItem!.format!.isNotEmpty) { + nameToCheck = '.${item.localItem!.format!.toLowerCase()}'; + } else { + nameToCheck = item.filePath.toLowerCase(); + } + final ext = nameToCheck.endsWith('.flac') + ? 'FLAC' + : nameToCheck.endsWith('.m4a') + ? 'M4A' + : nameToCheck.endsWith('.mp3') + ? 'MP3' + : (nameToCheck.endsWith('.opus') || nameToCheck.endsWith('.ogg')) + ? 'Opus' + : null; + if (ext != null) sourceFormats.add(ext); + } + + final formats = ['ALAC', 'FLAC', 'MP3', 'Opus'].where((target) { + return sourceFormats.any((src) { + if (src == target) return false; + final isLosslessTarget = target == 'ALAC' || target == 'FLAC'; + final isLosslessSource = src == 'FLAC' || src == 'M4A'; + if (isLosslessTarget && !isLosslessSource) return false; + return true; + }); + }).toList(); + + if (formats.isEmpty) return; + + String selectedFormat = formats.first; + bool isLosslessTarget = + selectedFormat == 'ALAC' || selectedFormat == 'FLAC'; + String selectedBitrate = + isLosslessTarget ? '320k' : (selectedFormat == 'Opus' ? '128k' : '320k'); var didStartConversion = false; _hideSelectionOverlay(); @@ -4773,7 +4814,6 @@ class _QueueTabState extends ConsumerState { return StatefulBuilder( builder: (context, setSheetState) { final colorScheme = Theme.of(context).colorScheme; - final formats = ['ALAC', 'FLAC', 'MP3', 'Opus']; final bitrates = ['128k', '192k', '256k', '320k']; return SafeArea( diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index dfb88847..f5608c9f 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -1377,12 +1377,7 @@ class FFmpegService { return outputPath; } - /// Convert any audio format to FLAC. - /// Source metadata is preserved via -map_metadata 0 (FFmpeg auto-remaps - /// tag names between container formats), then explicit Vorbis comment - /// overrides are applied from the [metadata] map. - /// Cover art is embedded via a second input stream (same approach as - /// [embedMetadata] and [_convertToAlac]). + /// Convert any audio format to FLAC with metadata and cover art preservation. static Future _convertToFlac({ required String inputPath, required Map metadata, @@ -1394,7 +1389,6 @@ class FFmpegService { final cmdBuffer = StringBuffer(); cmdBuffer.write('-i "$inputPath" '); - // Cover art as second input for attached picture final hasCover = coverPath != null && coverPath.trim().isNotEmpty && await File(coverPath).exists(); @@ -1409,12 +1403,8 @@ class FFmpegService { cmdBuffer.write('-metadata:s:v comment="Cover (front)" '); } cmdBuffer.write('-c:a flac -compression_level 8 '); - - // Copy source metadata as base (FFmpeg auto-remaps M4A/ID3 tags to - // Vorbis comment names), then override with our explicit values. cmdBuffer.write('-map_metadata 0 '); - // Apply normalized Vorbis comment overrides final vorbisComments = _normalizeToVorbisComments(metadata); for (final entry in vorbisComments.entries) { final sanitized = entry.value.replaceAll('"', '\\"'); @@ -1447,8 +1437,8 @@ class FFmpegService { return outputPath; } - /// Normalize metadata keys to standard Vorbis comment names and filter out - /// technical/non-tag fields (bit_depth, sample_rate, duration, etc.). + /// Normalize metadata keys to standard Vorbis comment names, filtering out + /// technical fields (bit_depth, sample_rate, duration, etc.). static Map _normalizeToVorbisComments( Map metadata, ) { @@ -1508,12 +1498,9 @@ class FFmpegService { break; case 'LYRICS': case 'UNSYNCEDLYRICS': - // Write both keys for compatibility with different FLAC readers vorbis['LYRICS'] = value; vorbis['UNSYNCEDLYRICS'] = value; break; - // Technical fields (BIT_DEPTH, SAMPLE_RATE, DURATION, etc.) are - // intentionally dropped — they are not Vorbis comment tags. } }