mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-21 15:02:21 +02:00
123 lines
4.5 KiB
Dart
123 lines
4.5 KiB
Dart
import 'package:spotiflac_android/models/download_result.dart';
|
|
import 'package:spotiflac_android/services/download_saf_file_replacer.dart';
|
|
import 'package:spotiflac_android/utils/audio_format_utils.dart';
|
|
|
|
/// Finalizes a requested lossless container using the measured audio stream.
|
|
/// Native FLAC bytes can be renamed; FLAC-in-MP4 still needs remuxing.
|
|
class DownloadContainerFinalizer {
|
|
const DownloadContainerFinalizer({
|
|
required this.probeCodec,
|
|
required this.isNativeFlac,
|
|
required this.ensureFlacExtension,
|
|
required this.convertToFlac,
|
|
required this.replaceSafFile,
|
|
required this.debug,
|
|
});
|
|
|
|
final Future<String?> Function(String) probeCodec;
|
|
final Future<bool> Function(String) isNativeFlac;
|
|
final Future<String> Function(String) ensureFlacExtension;
|
|
final Future<String?> Function(String) convertToFlac;
|
|
final ReplaceSafDownloadFile replaceSafFile;
|
|
final void Function(String) debug;
|
|
|
|
Future<String?> finalize({
|
|
required DownloadResult result,
|
|
required String filePath,
|
|
required String requestedExtension,
|
|
required bool forceConversion,
|
|
required bool useSaf,
|
|
String? treeUri,
|
|
String relativeDir = '',
|
|
String? fallbackFileName,
|
|
Future<void> Function(String path)? embedMetadata,
|
|
}) async {
|
|
if (requestedExtension != '.flac') return filePath;
|
|
forceConversion = forceConversion || result.requiresContainerConversion;
|
|
final reportedCodec = normalizeAudioFormatValue(result.audioCodec);
|
|
if (!forceConversion && isLossyAudioFormat(reportedCodec)) {
|
|
debug(
|
|
'Native-worker output is $reportedCodec; preserving native container.',
|
|
);
|
|
return filePath;
|
|
}
|
|
final outputExtension = result.outputExtension(path: filePath);
|
|
final lowerPath = filePath.toLowerCase();
|
|
final isSafSource = filePath.startsWith('content://');
|
|
final pathLooksLikeM4a =
|
|
lowerPath.endsWith('.m4a') ||
|
|
lowerPath.endsWith('.mp4') ||
|
|
outputExtension == '.m4a' ||
|
|
outputExtension == '.mp4';
|
|
if (!forceConversion && !pathLooksLikeM4a && !isSafSource) return filePath;
|
|
final decryptionExtension = result.decryption?.normalizedOutputExtension;
|
|
if (!forceConversion &&
|
|
decryptionExtension != null &&
|
|
decryptionExtension != '.flac') {
|
|
debug(
|
|
'Native-worker decrypted output requested $decryptionExtension; preserving native container.',
|
|
);
|
|
return filePath;
|
|
}
|
|
bool shouldConvert(String? codec) {
|
|
if (shouldAttemptLosslessContainerConversion(
|
|
forceConversion: forceConversion,
|
|
probedCodec: codec,
|
|
)) {
|
|
return true;
|
|
}
|
|
debug(
|
|
'Preserving native container; audio codec is ${codec ?? 'unknown'}, no FLAC container conversion needed.',
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if (useSaf && isSafSource) {
|
|
if (treeUri == null || treeUri.isEmpty) return null;
|
|
var preserve = false;
|
|
final name = result.fileName ?? fallbackFileName ?? 'track';
|
|
final flacName = '${name.replaceFirst(RegExp(r'\.[^.]+$'), '')}.flac';
|
|
final uri = await replaceSafFile(
|
|
uri: filePath,
|
|
treeUri: treeUri,
|
|
relativeDir: relativeDir,
|
|
op: (tempPath, addCleanup) async {
|
|
final codec = await probeCodec(tempPath);
|
|
final nativeFlac = codec == 'flac' && await isNativeFlac(tempPath);
|
|
if (!shouldConvert(codec)) {
|
|
preserve = true;
|
|
return null;
|
|
}
|
|
if (nativeFlac) {
|
|
debug(
|
|
'Native FLAC payload detected in temporary container; publishing as FLAC and embedding metadata.',
|
|
);
|
|
await embedMetadata?.call(tempPath);
|
|
return (tempPath, flacName);
|
|
}
|
|
final output = await convertToFlac(tempPath);
|
|
if (output == null) return null;
|
|
addCleanup(output);
|
|
await embedMetadata?.call(output);
|
|
return (output, flacName);
|
|
},
|
|
);
|
|
if (preserve) return filePath;
|
|
if (uri == null) return null;
|
|
result.fileName = flacName;
|
|
result.markFlacContainer();
|
|
return uri;
|
|
}
|
|
final codec = await probeCodec(filePath);
|
|
final nativeFlac = codec == 'flac' && await isNativeFlac(filePath);
|
|
if (!shouldConvert(codec)) return filePath;
|
|
final output = nativeFlac
|
|
? await ensureFlacExtension(filePath)
|
|
: await convertToFlac(filePath);
|
|
if (output == null) return null;
|
|
await embedMetadata?.call(output);
|
|
result.markFlacContainer();
|
|
return output;
|
|
}
|
|
}
|