fix: filter batch convert target formats based on source formats

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.
This commit is contained in:
zarzet
2026-03-16 02:39:11 +07:00
parent 554fe08fcd
commit 4adaed8da0
4 changed files with 135 additions and 28 deletions
+38 -4
View File
@@ -910,9 +910,44 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
BuildContext context,
List<DownloadHistoryItem> allTracks,
) {
String selectedFormat = 'MP3';
String selectedBitrate = '320k';
bool isLosslessTarget = false;
final tracksById = {for (final t in allTracks) t.id: t};
final sourceFormats = <String>{};
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<DownloadedAlbumScreen> {
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(
+50 -4
View File
@@ -1129,9 +1129,56 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
BuildContext context,
List<LocalLibraryItem> allTracks,
) {
String selectedFormat = 'MP3';
String selectedBitrate = '320k';
bool isLosslessTarget = false;
final tracksById = {for (final t in allTracks) t.id: t};
final sourceFormats = <String>{};
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<LocalAlbumScreen> {
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(
+44 -4
View File
@@ -4755,9 +4755,50 @@ class _QueueTabState extends ConsumerState<QueueTab> {
BuildContext context,
List<UnifiedLibraryItem> allItems,
) async {
String selectedFormat = 'MP3';
String selectedBitrate = '320k';
bool isLosslessTarget = false;
final itemsById = {for (final item in allItems) item.id: item};
final sourceFormats = <String>{};
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<QueueTab> {
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(
+3 -16
View File
@@ -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<String?> _convertToFlac({
required String inputPath,
required Map<String, String> 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<String, String> _normalizeToVorbisComments(
Map<String, String> 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.
}
}