From 20ac6b2cd4132b26fa1d0fa9ae9eab8181b5baa1 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 9 May 2026 01:18:17 +0700 Subject: [PATCH] fix(native-worker): preserve requested output container in finalizer When the native worker result advertises a requested non-FLAC output extension (for example '.m4a'), skip the m4a-to-flac container conversion in both the Dart and Kotlin finalizers so the native output container is preserved end-to-end. - ffmpeg_service: propagate the top-level 'output_extension' hint into the download-result descriptor for both the map-backed and legacy paths; expose a normalized getter for consistent comparisons. - download_queue_provider: short-circuit the native-worker container-conversion step when the descriptor's requested extension is not '.flac', with a debug log describing the skip. - NativeDownloadFinalizer: mirror the guard on the Kotlin side so the finalizer does not force a container conversion that would clobber the requested native output. --- .../com/zarz/spotiflac/NativeDownloadFinalizer.kt | 10 ++++++++++ lib/providers/download_queue_provider.dart | 10 ++++++++++ lib/services/ffmpeg_service.dart | 13 ++++++++++--- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt index 84104d6d..24c5d6c6 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt @@ -501,6 +501,8 @@ object NativeDownloadFinalizer { shouldCancel: () -> Boolean, ) { if (requestQuality(input) == "HIGH" || outputExt(input) != ".flac") return + val requestedDecryptionExt = requestedDecryptionOutputExt(input) + if (requestedDecryptionExt.isNotBlank() && requestedDecryptionExt != ".flac") return if (!looksLikeM4a(state.filePath, state.fileName) && !shouldForceContainerConversion(input, state)) return val localInput = materializeForFFmpeg(context, input, state) @@ -1330,6 +1332,14 @@ object NativeDownloadFinalizer { return container == "m4a" || container == "mp4" || container == "mov" || container == "aac" } + private fun requestedDecryptionOutputExt(input: FinalizeInput): String { + val descriptor = input.result.optJSONObject("decryption") + return normalizeExt( + descriptor?.optString("output_extension", "") + ?.ifBlank { input.result.optString("output_extension", "") } + ) + } + private fun validateRequestContract(request: JSONObject) { val version = request.optInt("contract_version", -1) if (version != NATIVE_WORKER_CONTRACT_VERSION) { diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 3990951f..ed85b11c 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -6049,6 +6049,16 @@ class DownloadQueueNotifier extends Notifier { if (context.quality == 'HIGH' || context.outputExt != '.flac') { return filePath; } + final requestedDecryptionExt = + DownloadDecryptionDescriptor.fromDownloadResult( + result, + )?.normalizedOutputExtension; + if (requestedDecryptionExt != null && requestedDecryptionExt != '.flac') { + _log.d( + 'Native-worker decrypted output requested $requestedDecryptionExt; preserving native container.', + ); + return filePath; + } final lowerPath = filePath.toLowerCase(); final resultFileName = (result['file_name'] as String?)?.toLowerCase(); final looksLikeM4a = diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index 69491c3f..4cc5080a 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -66,9 +66,9 @@ class DownloadDecryptionDescriptor { ) { final rawDecryption = result['decryption']; if (rawDecryption is Map) { - final descriptor = DownloadDecryptionDescriptor.fromJson( - Map.from(rawDecryption), - ); + final descriptorJson = Map.from(rawDecryption); + descriptorJson['output_extension'] ??= result['output_extension']; + final descriptor = DownloadDecryptionDescriptor.fromJson(descriptorJson); if (descriptor.normalizedStrategy == 'ffmpeg.mov_key' && descriptor.key.isNotEmpty) { return descriptor; @@ -84,6 +84,7 @@ class DownloadDecryptionDescriptor { strategy: 'ffmpeg.mov_key', key: legacyKey, inputFormat: 'mov', + outputExtension: (result['output_extension'] as String?)?.trim(), ); } @@ -100,6 +101,12 @@ class DownloadDecryptionDescriptor { return strategy.trim(); } } + + String? get normalizedOutputExtension { + final trimmed = (outputExtension ?? '').trim().toLowerCase(); + if (trimmed.isEmpty) return null; + return trimmed.startsWith('.') ? trimmed : '.$trimmed'; + } } class FFmpegService {