mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-06-04 13:48:01 +02:00
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:
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user