mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-07 21:28:05 +02:00
feat: audio analysis rescan and AAC conversion support
Audio Analysis: - Add rescan capability by bumping cache version - Display channel layout (stereo, 5.1, etc.) and bitrate - Use astats filter for more accurate peak/RMS measurements - Support more formats: mp4, ac3, eac3, mka, wv, ape, tta, aif - Only report bit depth for codecs that store it (FLAC, ALAC, WAV) - Validate cache for SAF content:// URIs Conversion: - Add AAC as conversion target format - Recognize ALAC as lossless source - Prevent accidental deletion when source and target URI match - Store format and bitrate in database after conversion Utilities: - Add audio_conversion_utils.dart for centralized conversion logic - Add isSameContentUri() helper for safe URI comparison
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
const List<String> audioConversionTargetFormats = [
|
||||
'ALAC',
|
||||
'FLAC',
|
||||
'AAC',
|
||||
'MP3',
|
||||
'Opus',
|
||||
];
|
||||
|
||||
bool isLosslessConversionTarget(String targetFormat) {
|
||||
final normalized = targetFormat.trim().toLowerCase();
|
||||
return normalized == 'alac' || normalized == 'flac';
|
||||
}
|
||||
|
||||
bool isLosslessConversionSource(String sourceFormat) {
|
||||
switch (sourceFormat.trim().toUpperCase()) {
|
||||
case 'FLAC':
|
||||
case 'ALAC':
|
||||
case 'M4A':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool canConvertAudioFormat({
|
||||
required String sourceFormat,
|
||||
required String targetFormat,
|
||||
}) {
|
||||
if (sourceFormat.trim().toUpperCase() == targetFormat.trim().toUpperCase()) {
|
||||
return false;
|
||||
}
|
||||
if (isLosslessConversionTarget(targetFormat) &&
|
||||
!isLosslessConversionSource(sourceFormat)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
String? convertibleAudioSourceFormat({
|
||||
String? storedFormat,
|
||||
String? filePath,
|
||||
String? fileName,
|
||||
}) {
|
||||
final fromStored = _convertibleAudioFormatLabel(storedFormat);
|
||||
if (fromStored != null) return fromStored;
|
||||
|
||||
final name = (fileName != null && fileName.trim().isNotEmpty)
|
||||
? fileName
|
||||
: filePath;
|
||||
if (name == null || name.trim().isEmpty) return null;
|
||||
|
||||
final normalizedName = name.trim().toLowerCase();
|
||||
final dotIndex = normalizedName.lastIndexOf('.');
|
||||
if (dotIndex < 0 || dotIndex == normalizedName.length - 1) {
|
||||
return null;
|
||||
}
|
||||
return _convertibleAudioFormatLabel(normalizedName.substring(dotIndex + 1));
|
||||
}
|
||||
|
||||
String? _convertibleAudioFormatLabel(String? rawFormat) {
|
||||
final format = rawFormat?.trim().toLowerCase();
|
||||
if (format == null || format.isEmpty) return null;
|
||||
|
||||
switch (format) {
|
||||
case 'flac':
|
||||
return 'FLAC';
|
||||
case 'alac':
|
||||
return 'ALAC';
|
||||
case 'm4a':
|
||||
case 'mp4':
|
||||
return 'M4A';
|
||||
case 'aac':
|
||||
case 'mp4a':
|
||||
return 'AAC';
|
||||
case 'mp3':
|
||||
return 'MP3';
|
||||
case 'opus':
|
||||
case 'ogg':
|
||||
return 'Opus';
|
||||
case 'eac3':
|
||||
case 'ec-3':
|
||||
return 'EAC3';
|
||||
case 'ac3':
|
||||
case 'ac-3':
|
||||
return 'AC3';
|
||||
case 'ac4':
|
||||
case 'ac-4':
|
||||
return 'AC4';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String normalizedConvertedAudioFormat(String targetFormat) {
|
||||
return targetFormat.trim().toLowerCase();
|
||||
}
|
||||
|
||||
int? convertedAudioBitrateKbps({
|
||||
required String targetFormat,
|
||||
required String bitrate,
|
||||
}) {
|
||||
if (isLosslessConversionTarget(targetFormat)) return null;
|
||||
final match = RegExp(r'(\d+)').firstMatch(bitrate);
|
||||
return match != null ? int.tryParse(match.group(1)!) : null;
|
||||
}
|
||||
@@ -229,6 +229,22 @@ bool isContentUri(String? path) {
|
||||
return path != null && path.startsWith('content://');
|
||||
}
|
||||
|
||||
bool isSameContentUri(String? first, String? second) {
|
||||
if (first == null || second == null) return false;
|
||||
if (first == second) return true;
|
||||
if (!isContentUri(first) || !isContentUri(second)) return false;
|
||||
|
||||
String decode(String value) {
|
||||
try {
|
||||
return Uri.decodeFull(value);
|
||||
} catch (_) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return decode(first) == decode(second);
|
||||
}
|
||||
|
||||
/// Pattern matching CUE virtual path suffixes like #track01, #track12, etc.
|
||||
final _cueTrackSuffix = RegExp(r'#track\d+$');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user