From 81ce6771493006c760618b8d829c09b966385ce6 Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Mon, 7 Sep 2026 01:20:28 +0700 Subject: [PATCH] fix(download): remove legacy HIGH quality conversion --- .../zarz/spotiflac/NativeDownloadFinalizer.kt | 82 +-------- go_backend/exports_download.go | 1 - lib/models/settings.dart | 5 - lib/models/settings.g.dart | 2 - lib/providers/download_queue_provider.dart | 1 - .../download_queue_provider_finalization.dart | 110 +----------- ...download_queue_provider_native_worker.dart | 64 ++++--- .../download_queue_provider_single_item.dart | 159 +----------------- lib/providers/settings_provider.dart | 5 - .../settings/download_settings_page.dart | 145 ---------------- lib/services/download_request_payload.dart | 4 - lib/services/ffmpeg_service.dart | 54 ------ lib/utils/audio_format_utils.dart | 26 --- test/audio_conversion_utils_test.dart | 4 - test/models_and_utils_test.dart | 26 ++- 15 files changed, 73 insertions(+), 615 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 1c9cf04c..f940dcbf 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt @@ -228,8 +228,6 @@ object NativeDownloadFinalizer { currentStatus("finalizing") finalizeDecryption(context, effectiveInput, state, shouldCancel) checkCancelled(shouldCancel) - finalizeHighConversion(context, effectiveInput, state, shouldCancel) - checkCancelled(shouldCancel) finalizeContainerConversion(context, effectiveInput, state, shouldCancel) checkCancelled(shouldCancel) finalizeMetadata(context, effectiveInput, state) @@ -491,12 +489,7 @@ object NativeDownloadFinalizer { private fun outputExt(input: FinalizeInput): String { val safExt = input.request.optString("saf_output_ext", "") val ext = safExt.ifBlank { input.request.optString("output_ext", "") } - return normalizeExt(ext.ifBlank { - when (requestQuality(input)) { - "HIGH" -> ".mp3" - else -> ".flac" - } - }) + return normalizeExt(ext.ifBlank { ".flac" }) } private fun finalizeDecryption( @@ -619,77 +612,6 @@ object NativeDownloadFinalizer { return target.absolutePath } - private fun finalizeHighConversion( - context: Context, - input: FinalizeInput, - state: FinalizeState, - shouldCancel: () -> Boolean, - ) { - if (requestQuality(input) != "HIGH") return - if (!looksLikeM4a(state.filePath, state.fileName)) return - - val autoTarget = NativeFinalizationPolicy.autoConversionTarget( - enabled = input.request.optBoolean("auto_convert_downloads", false), - format = input.request.optString("auto_convert_format", ""), - bitrate = input.request.optString("auto_convert_bitrate", ""), - ) - val tidalHighFormat = input.request.optString("tidal_high_format", "").ifBlank { "mp3_320" } - val format = autoTarget?.codec ?: when { - tidalHighFormat.startsWith("opus") -> "opus" - tidalHighFormat.startsWith("aac") || tidalHighFormat.startsWith("m4a") -> "aac" - else -> "mp3" - } - val metadataFormat = if (format == "aac") "m4a" else format - val displayFormat = if (format == "aac") "AAC" else format.uppercase(Locale.ROOT) - val bitrate = if (autoTarget != null) { - "${autoTarget.bitrateKbps}k" - } else if (tidalHighFormat.contains("_")) { - "${tidalHighFormat.substringAfterLast("_")}k" - } else { - if (format == "opus") "128k" else "320k" - } - val ext = when (format) { - "opus" -> ".opus" - "aac" -> ".m4a" - else -> ".mp3" - } - val localInput = materializeForFFmpeg(context, input, state) - val deleteLocalInput = state.filePath.startsWith("content://") - val output = buildOutputPath(localInput, ext) - val stagedOutput = stagedConversionPath(output) - var adoptedOutput = false - try { - val command = if (format == "opus") { - "-v error -hide_banner -i ${q(localInput)} -codec:a libopus -b:a $bitrate -vbr on -compression_level 10 -map 0:a ${q(stagedOutput)} -y" - } else if (format == "aac") { - "-v error -hide_banner -i ${q(localInput)} -codec:a aac -b:a $bitrate -map 0:a -f mp4 ${q(stagedOutput)} -y" - } else { - "-v error -hide_banner -i ${q(localInput)} -codec:a libmp3lame -b:a $bitrate -map 0:a -id3v2_version 3 ${q(stagedOutput)} -y" - } - val result = runFFmpeg(command, shouldCancel) - if (!result.first || !File(stagedOutput).exists()) { - throw IllegalStateException("HIGH conversion failed: ${result.second}") - } - if (!promoteStagedConversion(stagedOutput, output)) { - throw IllegalStateException("failed to publish HIGH conversion output") - } - embedBasicMetadata(context, output, input, metadataFormat) - replaceStatePath(context, input, state, output, deleteOld = true) - adoptedOutput = true - } finally { - if (!adoptedOutput) { - File(stagedOutput).delete() - File(output).delete() - } - if (deleteLocalInput) File(localInput).delete() - } - state.quality = "$displayFormat ${bitrate.removeSuffix("k")}kbps" - state.bitDepth = null - state.sampleRate = null - state.bitrateKbps = bitrate.removeSuffix("k").toIntOrNull() - state.audioCodec = format - } - private fun finalizeAutoConversion( context: Context, input: FinalizeInput, @@ -799,7 +721,7 @@ object NativeDownloadFinalizer { state: FinalizeState, shouldCancel: () -> Boolean, ) { - if (requestQuality(input) == "HIGH" || outputExt(input) != ".flac") return + if (outputExt(input) != ".flac") return val requestedDecryptionExt = requestedDecryptionOutputExt(input) val forceContainerConversion = shouldForceContainerConversion(input, state) if (!forceContainerConversion && requestedDecryptionExt.isNotBlank() && requestedDecryptionExt != ".flac") return diff --git a/go_backend/exports_download.go b/go_backend/exports_download.go index ae58dec2..d80efd9b 100644 --- a/go_backend/exports_download.go +++ b/go_backend/exports_download.go @@ -31,7 +31,6 @@ type DownloadRequest struct { EmbedLyrics bool `json:"embed_lyrics"` EmbedReplayGain bool `json:"embed_replaygain,omitempty"` PostProcessingEnabled bool `json:"post_processing_enabled,omitempty"` - TidalHighFormat string `json:"tidal_high_format,omitempty"` TrackNumber int `json:"track_number"` PlaylistPosition int `json:"playlist_position,omitempty"` DiscNumber int `json:"disc_number"` diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 0459e8a5..df0e1caa 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -74,8 +74,6 @@ class AppSettings { extensionVerificationBrowserMode; // 'external_first' or 'in_app_first' final String locale; final String lyricsMode; - final String - tidalHighFormat; // Legacy key for 320kbps lossy output format: 'mp3_320', 'aac_320', 'opus_256', or 'opus_128' final bool autoConvertDownloads; final String autoConvertFormat; // 'mp3', 'aac' (M4A), or 'opus' final String autoConvertBitrate; // '128k', '192k', '256k', or '320k' @@ -169,7 +167,6 @@ class AppSettings { this.extensionVerificationBrowserMode = 'in_app_first', this.locale = 'system', this.lyricsMode = 'embed', - this.tidalHighFormat = 'mp3_320', this.autoConvertDownloads = false, this.autoConvertFormat = 'mp3', this.autoConvertBitrate = '320k', @@ -247,7 +244,6 @@ class AppSettings { String? extensionVerificationBrowserMode, String? locale, String? lyricsMode, - String? tidalHighFormat, bool? autoConvertDownloads, String? autoConvertFormat, String? autoConvertBitrate, @@ -339,7 +335,6 @@ class AppSettings { this.extensionVerificationBrowserMode, locale: locale ?? this.locale, lyricsMode: lyricsMode ?? this.lyricsMode, - tidalHighFormat: tidalHighFormat ?? this.tidalHighFormat, autoConvertDownloads: autoConvertDownloads ?? this.autoConvertDownloads, autoConvertFormat: autoConvertFormat ?? this.autoConvertFormat, autoConvertBitrate: autoConvertBitrate ?? this.autoConvertBitrate, diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index 16872407..aff5b88d 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -60,7 +60,6 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( json['extensionVerificationBrowserMode'] as String? ?? 'in_app_first', locale: json['locale'] as String? ?? 'system', lyricsMode: json['lyricsMode'] as String? ?? 'embed', - tidalHighFormat: json['tidalHighFormat'] as String? ?? 'mp3_320', autoConvertDownloads: json['autoConvertDownloads'] as bool? ?? false, autoConvertFormat: json['autoConvertFormat'] as String? ?? 'mp3', autoConvertBitrate: json['autoConvertBitrate'] as String? ?? '320k', @@ -148,7 +147,6 @@ Map _$AppSettingsToJson( 'extensionVerificationBrowserMode': instance.extensionVerificationBrowserMode, 'locale': instance.locale, 'lyricsMode': instance.lyricsMode, - 'tidalHighFormat': instance.tidalHighFormat, 'autoConvertDownloads': instance.autoConvertDownloads, 'autoConvertFormat': instance.autoConvertFormat, 'autoConvertBitrate': instance.autoConvertBitrate, diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 45b885f0..ff1f8bcf 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -873,7 +873,6 @@ class DownloadQueueNotifier extends Notifier { !_shouldSkipLyrics(extensionState, track.source, item.service), embedReplayGain: settings.embedReplayGain, postProcessingEnabled: postProcessingEnabled, - tidalHighFormat: settings.tidalHighFormat, autoConvertDownloads: settings.autoConvertDownloads, autoConvertFormat: normalizeAutoConvertFormat(settings.autoConvertFormat), autoConvertBitrate: normalizeAutoConvertBitrate( diff --git a/lib/providers/download_queue_provider_finalization.dart b/lib/providers/download_queue_provider_finalization.dart index fda710e8..8921ba24 100644 --- a/lib/providers/download_queue_provider_finalization.dart +++ b/lib/providers/download_queue_provider_finalization.dart @@ -992,114 +992,6 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier { ); } - Future _finalizeNativeWorkerHighConversion({ - required _NativeWorkerRequestContext context, - required Map result, - required AppSettings settings, - required Track track, - required String filePath, - }) async { - if (context.quality != 'HIGH') { - return filePath; - } - - final lowerPath = filePath.toLowerCase(); - final resultFileName = (result['file_name'] as String?)?.toLowerCase(); - final looksLikeM4a = - lowerPath.endsWith('.m4a') || - lowerPath.endsWith('.mp4') || - (resultFileName != null && - (resultFileName.endsWith('.m4a') || - resultFileName.endsWith('.mp4'))); - if (!looksLikeM4a) { - return filePath; - } - - final tidalHighFormat = settings.autoConvertDownloads - ? autoConvertLossySetting( - format: settings.autoConvertFormat, - bitrate: settings.autoConvertBitrate, - ) - : settings.tidalHighFormat; - final format = lossyFormatForSetting(tidalHighFormat); - final newExt = lossyExtensionForFormat(format); - final displayFormat = displayFormatForLossyFormat(format); - final bitrateDisplay = tidalHighFormat.contains('_') - ? '${tidalHighFormat.split('_').last}kbps' - : '320kbps'; - - Future embedConvertedMetadata(String convertedPath) async { - if (!settings.embedMetadata) return; - await _embedMetadataToFile( - convertedPath, - track, - format: metadataFormatForLossyFormat(format), - genre: result['genre'] as String?, - label: result['label'] as String?, - copyright: result['copyright'] as String?, - comment: result['comment'] as String?, - downloadService: context.item.service, - ); - } - - if (context.storageMode == 'saf' && isContentUri(filePath)) { - final treeUri = context.downloadTreeUri; - if (treeUri == null || treeUri.isEmpty) { - return null; - } - final rawFileName = - (result['file_name'] as String?) ?? context.safFileName ?? 'track'; - final baseName = rawFileName.replaceFirst(RegExp(r'\.[^.]+$'), ''); - final newFileName = '$baseName$newExt'; - final newUri = await _replaceSafFileVia( - uri: filePath, - treeUri: treeUri, - relativeDir: context.safRelativeDir ?? '', - op: (tempPath, addCleanup) async { - final convertedPath = await FFmpegService.convertM4aToLossy( - tempPath, - format: format, - bitrate: tidalHighFormat, - deleteOriginal: false, - ); - if (convertedPath == null) return null; - addCleanup(convertedPath); - await embedConvertedMetadata(convertedPath); - return (convertedPath, newFileName); - }, - ); - if (newUri == null) { - return null; - } - result['file_name'] = newFileName; - result['_native_actual_quality'] = '$displayFormat $bitrateDisplay'; - result['audio_codec'] = format; - result['format'] = format; - result['bitrate'] = int.tryParse(tidalHighFormat.split('_').last); - result.remove('actual_bit_depth'); - result.remove('actual_sample_rate'); - return newUri; - } - - final convertedPath = await FFmpegService.convertM4aToLossy( - filePath, - format: format, - bitrate: tidalHighFormat, - deleteOriginal: true, - ); - if (convertedPath == null) { - return null; - } - await embedConvertedMetadata(convertedPath); - result['_native_actual_quality'] = '$displayFormat $bitrateDisplay'; - result['audio_codec'] = format; - result['format'] = format; - result['bitrate'] = int.tryParse(tidalHighFormat.split('_').last); - result.remove('actual_bit_depth'); - result.remove('actual_sample_rate'); - return convertedPath; - } - Future _finalizeNativeWorkerContainerConversion({ required _NativeWorkerRequestContext context, required Map result, @@ -1107,7 +999,7 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier { required Track track, required String filePath, }) async { - if (context.quality == 'HIGH' || context.outputExt != '.flac') { + if (context.outputExt != '.flac') { return filePath; } final resultAudioFormat = normalizeAudioFormatValue( diff --git a/lib/providers/download_queue_provider_native_worker.dart b/lib/providers/download_queue_provider_native_worker.dart index 21dbe7a2..08dc0932 100644 --- a/lib/providers/download_queue_provider_native_worker.dart +++ b/lib/providers/download_queue_provider_native_worker.dart @@ -1341,28 +1341,6 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier { result, resolvedAlbumArtist, ); - final convertedHighPath = await _finalizeNativeWorkerHighConversion( - context: context, - result: result, - settings: settings, - track: trackToDownload, - filePath: filePath, - ); - if (convertedHighPath == null) { - updateItemStatus( - item.id, - DownloadStatus.failed, - error: 'Failed to convert HIGH quality download', - errorType: DownloadErrorType.unknown, - ); - _failedInSession++; - return; - } - filePath = convertedHighPath; - final nativeActualQuality = result['_native_actual_quality'] as String?; - if (nativeActualQuality != null && nativeActualQuality.isNotEmpty) { - actualQuality = nativeActualQuality; - } final convertedContainerPath = await _finalizeNativeWorkerContainerConversion( context: context, @@ -1413,6 +1391,46 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier { actualSampleRate = null; actualFormat = normalizeAutoConvertFormat(settings.autoConvertFormat); actualBitrate = autoConvertBitrateKbps(settings.autoConvertBitrate); + } else if (settings.embedMetadata && + _isMp4Container(result['file_name']?.toString() ?? filePath)) { + // Native lossy containers still need tags and lyrics even though their + // audio is no longer passed through the retired quality conversion. + Future embedNativeMetadata(String path) async { + final lyrics = await _embedMetadataToFile( + path, + trackToDownload, + format: 'm4a', + genre: result['genre'] as String?, + label: result['label'] as String?, + copyright: result['copyright'] as String?, + comment: result['comment'] as String?, + downloadService: context.item.service, + writeExternalLrc: context.storageMode != 'saf', + ); + if (lyrics != null && lyrics.isNotEmpty) result['lyrics_lrc'] = lyrics; + } + + if (context.storageMode == 'saf' && isContentUri(filePath)) { + final treeUri = context.downloadTreeUri; + if (treeUri != null && treeUri.isNotEmpty) { + final fileName = + result['file_name']?.toString() ?? context.safFileName; + if (fileName != null && fileName.isNotEmpty) { + final updatedUri = await _replaceSafFileVia( + uri: filePath, + treeUri: treeUri, + relativeDir: context.safRelativeDir ?? '', + op: (tempPath, _) async { + await embedNativeMetadata(tempPath); + return (tempPath, fileName); + }, + ); + if (updatedUri != null) filePath = updatedUri; + } + } + } else { + await embedNativeMetadata(filePath); + } } await _writeNativeWorkerReplayGain( settings: settings, @@ -1479,7 +1497,7 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier { final resultSafFileName = result['file_name'] as String?; final lowerFilePath = filePath.toLowerCase(); - // Recompute from the FINAL file path/result: the HIGH and container + // Recompute from the FINAL file path/result: automatic and container // conversions above may have changed the format since actualFormat was // derived from the pre-conversion output. final historyFormat = diff --git a/lib/providers/download_queue_provider_single_item.dart b/lib/providers/download_queue_provider_single_item.dart index d3471749..9df34862 100644 --- a/lib/providers/download_queue_provider_single_item.dart +++ b/lib/providers/download_queue_provider_single_item.dart @@ -933,16 +933,12 @@ class _DownloadRun { if (isContentUriPath && effectiveSafMode) { if (shouldPreserveNativeM4a) { await _preserveSafNativeM4a(path); - } else if (quality == 'HIGH') { - await _convertSafM4aToLossy(path); } else { await _convertSafM4aToFlac(path); } } else { if (shouldPreserveNativeM4a) { await _preserveLocalNativeM4a(path); - } else if (quality == 'HIGH') { - await _convertLocalM4aToLossy(path); } else { await _convertLocalM4aToFlac(path); } @@ -1110,91 +1106,6 @@ class _DownloadRun { return true; } - Future _convertSafM4aToLossy(String currentFilePath) async { - final tidalHighFormat = settings.autoConvertDownloads - ? autoConvertLossySetting( - format: settings.autoConvertFormat, - bitrate: settings.autoConvertBitrate, - ) - : settings.tidalHighFormat; - _log.i( - 'Lossy 320kbps quality (SAF), converting M4A to $tidalHighFormat...', - ); - - final format = lossyFormatForSetting(tidalHighFormat); - final displayFormat = displayFormatForLossyFormat(format); - final newExt = lossyExtensionForFormat(format); - final newFileName = '${safBaseName ?? 'track'}$newExt'; - var opStarted = false; - var convertFailed = false; - try { - final newUri = await n._replaceSafFileVia( - uri: currentFilePath, - treeUri: settings.downloadTreeUri, - relativeDir: effectiveOutputDir, - op: (tempPath, addCleanup) async { - opStarted = true; - n.updateItemStatus( - item.id, - DownloadStatus.finalizing, - progress: 0.95, - ); - final convertedPath = await FFmpegService.convertM4aToLossy( - tempPath, - format: format, - bitrate: tidalHighFormat, - deleteOriginal: false, - ); - if (convertedPath == null) { - convertFailed = true; - return null; - } - addCleanup(convertedPath); - _log.i( - 'Successfully converted M4A to $format (temp): $convertedPath', - ); - _log.i('Embedding metadata to $format...'); - n.updateItemStatus( - item.id, - DownloadStatus.finalizing, - progress: 0.99, - ); - - await _embedFinalMetadata( - convertedPath, - format: metadataFormatForLossyFormat(format), - rebuildTrack: false, - ); - - return (convertedPath, newFileName); - }, - ); - - if (newUri != null) { - filePath = newUri; - finalSafFileName = newFileName; - final bitrateDisplay = tidalHighFormat.contains('_') - ? '${tidalHighFormat.split('_').last}kbps' - : '320kbps'; - actualQuality = '$displayFormat $bitrateDisplay'; - result['audio_codec'] = format; - result['format'] = format; - result['bitrate'] = int.tryParse(tidalHighFormat.split('_').last); - result.remove('actual_bit_depth'); - result.remove('actual_sample_rate'); - } else if (convertFailed) { - _log.w('M4A to $format conversion failed, keeping M4A file'); - actualQuality = 'AAC 320kbps'; - } else if (opStarted) { - _log.w('Failed to write converted $format to SAF, keeping M4A'); - actualQuality = 'AAC 320kbps'; - } - } catch (e) { - _log.w('SAF M4A conversion failed: $e'); - actualQuality = 'AAC 320kbps'; - } - } - Future _preserveSafNativeM4a(String currentFilePath) async { // Decrypted streams are already in their final format. // Converting e.g. eac3 M4A to FLAC would produce fake upscaled output. @@ -1328,61 +1239,6 @@ class _DownloadRun { } } - Future _convertLocalM4aToLossy(String currentFilePath) async { - final tidalHighFormat = settings.autoConvertDownloads - ? autoConvertLossySetting( - format: settings.autoConvertFormat, - bitrate: settings.autoConvertBitrate, - ) - : settings.tidalHighFormat; - _log.i( - 'Lossy 320kbps quality download, converting M4A to $tidalHighFormat...', - ); - - try { - n.updateItemStatus(item.id, DownloadStatus.finalizing, progress: 0.95); - - final format = lossyFormatForSetting(tidalHighFormat); - final displayFormat = displayFormatForLossyFormat(format); - final convertedPath = await FFmpegService.convertM4aToLossy( - currentFilePath, - format: format, - bitrate: tidalHighFormat, - deleteOriginal: true, - ); - - if (convertedPath != null) { - filePath = convertedPath; - final bitrateDisplay = tidalHighFormat.contains('_') - ? '${tidalHighFormat.split('_').last}kbps' - : '320kbps'; - actualQuality = '$displayFormat $bitrateDisplay'; - result['audio_codec'] = format; - result['format'] = format; - result['bitrate'] = int.tryParse(tidalHighFormat.split('_').last); - result.remove('actual_bit_depth'); - result.remove('actual_sample_rate'); - _log.i('Successfully converted M4A to $format: $convertedPath'); - - _log.i('Embedding metadata to $format...'); - n.updateItemStatus(item.id, DownloadStatus.finalizing, progress: 0.99); - - await _embedFinalMetadata( - convertedPath, - format: metadataFormatForLossyFormat(format), - rebuildTrack: false, - ); - _log.d('Metadata embedded successfully'); - } else { - _log.w('M4A to $format conversion failed, keeping M4A file'); - actualQuality = 'AAC 320kbps'; - } - } catch (e) { - _log.w('M4A conversion process failed: $e, keeping M4A file'); - actualQuality = 'AAC 320kbps'; - } - } - Future _preserveLocalNativeM4a(String currentFilePath) async { _log.d('M4A/MP4 file detected, preserving native container...'); @@ -1608,21 +1464,16 @@ class _DownloadRun { /// Final metadata embed shared by every publish branch. Backend-provided /// genre/label/copyright win over the Deezer extended-metadata lookup. - /// [rebuildTrack] is false only for the lossy-HIGH branches, which embed - /// the track as-is instead of re-merging the download result into it. Future _embedFinalMetadata( String path, { required String format, bool writeExternalLrc = true, - bool rebuildTrack = true, }) async { - final track = rebuildTrack - ? buildTrackForMetadataEmbedding( - trackToDownload, - result, - resolvedAlbumArtist, - ) - : trackToDownload; + final track = buildTrackForMetadataEmbedding( + trackToDownload, + result, + resolvedAlbumArtist, + ); final lrcContent = await n._embedMetadataToFile( path, track, diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 1b6e373a..cf840212 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -768,11 +768,6 @@ class SettingsNotifier extends Notifier { _saveSettings(); } - void setTidalHighFormat(String format) { - state = state.copyWith(tidalHighFormat: format); - _saveSettings(); - } - void setAutoConvertDownloads(bool enabled) { state = state.copyWith(autoConvertDownloads: enabled); _saveSettings(); diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index f244f345..042453cf 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -42,14 +42,6 @@ class _DownloadSettingsPageState extends ConsumerState { final qualityOptions = selectedDownloadExtension?.qualityOptions ?? const []; final canSelectQuality = qualityOptions.isNotEmpty; - final usesTidalCompatibilityOptions = selectedDownloadService.isNotEmpty - ? ref - .read(extensionProvider.notifier) - .downloadProviderReplacesLegacyProvider( - selectedDownloadService, - 'tidal', - ) - : false; final nativeWorkerAvailable = Platform.isAndroid && hasDownloadExtensions; return PopScope( @@ -113,22 +105,6 @@ class _DownloadSettingsPageState extends ConsumerState { .setAudioQuality(quality.id), showDivider: true, ), - if (usesTidalCompatibilityOptions && - settings.audioQuality == 'HIGH') - SettingsItem( - icon: Icons.tune, - title: context.l10n.downloadLossyFormat, - subtitle: _getLossyCompatibilityFormatLabel( - context, - settings.tidalHighFormat, - ), - onTap: () => _showLossyCompatibilityFormatPicker( - context, - ref, - settings.tidalHighFormat, - ), - showDivider: true, - ), ], SettingsSwitchItem( icon: Icons.auto_fix_high_outlined, @@ -391,8 +367,6 @@ class _DownloadSettingsPageState extends ConsumerState { return context.l10n.qualityHiResFlac; case 'HI_RES_LOSSLESS': return context.l10n.qualityHiResFlacMax; - case 'HIGH': - return context.l10n.downloadLossy320; default: return quality.label; } @@ -409,130 +383,11 @@ class _DownloadSettingsPageState extends ConsumerState { return context.l10n.qualityHiResFlacSubtitle; case 'HI_RES_LOSSLESS': return context.l10n.qualityHiResFlacMaxSubtitle; - case 'HIGH': - return _getLossyCompatibilityFormatLabel( - context, - ref.read(settingsProvider).tidalHighFormat, - ); default: return quality.description ?? ''; } } - String _getLossyCompatibilityFormatLabel( - BuildContext context, - String format, - ) { - switch (format) { - case 'mp3_320': - return context.l10n.downloadLossyMp3; - case 'aac_320': - return context.l10n.downloadLossyAac; - case 'opus_256': - return context.l10n.downloadLossyOpus256; - case 'opus_128': - return context.l10n.downloadLossyOpus128; - default: - return context.l10n.downloadLossyMp3; - } - } - - void _showLossyCompatibilityFormatPicker( - BuildContext context, - WidgetRef ref, - String current, - ) { - final colorScheme = Theme.of(context).colorScheme; - showModalBottomSheet( - context: context, - useRootNavigator: true, - backgroundColor: colorScheme.surfaceContainerHigh, - builder: (context) => SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), - child: Text( - context.l10n.downloadLossy320Format, - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), - child: Text( - context.l10n.downloadLossy320FormatDesc, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ), - ListTile( - leading: const Icon(Icons.audiotrack), - title: Text(context.l10n.downloadLossyMp3), - subtitle: Text(context.l10n.downloadLossyMp3Subtitle), - trailing: current == 'mp3_320' - ? Icon(Icons.check, color: colorScheme.primary) - : null, - onTap: () { - ref - .read(settingsProvider.notifier) - .setTidalHighFormat('mp3_320'); - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.album_outlined), - title: Text(context.l10n.downloadLossyAac), - subtitle: Text(context.l10n.downloadLossyAacSubtitle), - trailing: current == 'aac_320' - ? Icon(Icons.check, color: colorScheme.primary) - : null, - onTap: () { - ref - .read(settingsProvider.notifier) - .setTidalHighFormat('aac_320'); - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.graphic_eq), - title: Text(context.l10n.downloadLossyOpus256), - subtitle: Text(context.l10n.downloadLossyOpus256Subtitle), - trailing: current == 'opus_256' - ? Icon(Icons.check, color: colorScheme.primary) - : null, - onTap: () { - ref - .read(settingsProvider.notifier) - .setTidalHighFormat('opus_256'); - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.graphic_eq), - title: Text(context.l10n.downloadLossyOpus128), - subtitle: Text(context.l10n.downloadLossyOpus128Subtitle), - trailing: current == 'opus_128' - ? Icon(Icons.check, color: colorScheme.primary) - : null, - onTap: () { - ref - .read(settingsProvider.notifier) - .setTidalHighFormat('opus_128'); - Navigator.pop(context); - }, - ), - const SizedBox(height: 16), - ], - ), - ), - ); - } - void _showAutoConvertFormatPicker( BuildContext context, WidgetRef ref, diff --git a/lib/services/download_request_payload.dart b/lib/services/download_request_payload.dart index aa7c2d62..18391b87 100644 --- a/lib/services/download_request_payload.dart +++ b/lib/services/download_request_payload.dart @@ -21,7 +21,6 @@ class DownloadRequestPayload { final bool embedLyrics; final bool embedReplayGain; final bool postProcessingEnabled; - final String tidalHighFormat; final bool autoConvertDownloads; final String autoConvertFormat; final String autoConvertBitrate; @@ -84,7 +83,6 @@ class DownloadRequestPayload { this.embedLyrics = true, this.embedReplayGain = false, this.postProcessingEnabled = false, - this.tidalHighFormat = 'mp3_320', this.autoConvertDownloads = false, this.autoConvertFormat = 'mp3', this.autoConvertBitrate = '320k', @@ -149,7 +147,6 @@ class DownloadRequestPayload { 'embed_lyrics': embedLyrics, 'embed_replaygain': embedReplayGain, 'post_processing_enabled': postProcessingEnabled, - 'tidal_high_format': tidalHighFormat, 'auto_convert_downloads': autoConvertDownloads, 'auto_convert_format': autoConvertFormat, 'auto_convert_bitrate': autoConvertBitrate, @@ -218,7 +215,6 @@ class DownloadRequestPayload { embedLyrics: embedLyrics, embedReplayGain: embedReplayGain, postProcessingEnabled: postProcessingEnabled, - tidalHighFormat: tidalHighFormat, autoConvertDownloads: autoConvertDownloads, autoConvertFormat: autoConvertFormat, autoConvertBitrate: autoConvertBitrate, diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index f7ff0e0e..1281f735 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -717,60 +717,6 @@ class FFmpegService { } } - static Future convertM4aToLossy( - String inputPath, { - required String format, - String? bitrate, - bool deleteOriginal = true, - }) async { - final normalizedFormat = format.toLowerCase(); - String bitrateValue = normalizedFormat == 'opus' ? '128k' : '320k'; - if (bitrate != null && bitrate.contains('_')) { - final parts = bitrate.split('_'); - if (parts.length == 2) { - bitrateValue = '${parts[1]}k'; - } - } - - final extension = switch (normalizedFormat) { - 'opus' => '.opus', - 'aac' || 'm4a' => '.m4a', - _ => '.mp3', - }; - final outputPlan = await _conversionOutputPlan( - inputPath, - extension, - deleteOriginal: deleteOriginal, - ); - final outputPath = outputPlan.workingPath; - - String command; - if (normalizedFormat == 'opus') { - command = - '-v error -hide_banner -i "$inputPath" -codec:a libopus -b:a $bitrateValue -vbr on -compression_level 10 -map 0:a "$outputPath" -y'; - } else if (normalizedFormat == 'aac' || normalizedFormat == 'm4a') { - command = - '-v error -hide_banner -i "$inputPath" -codec:a aac -b:a $bitrateValue -map 0:a -f mp4 "$outputPath" -y'; - } else { - command = - '-v error -hide_banner -i "$inputPath" -codec:a libmp3lame -b:a $bitrateValue -map 0:a -id3v2_version 3 "$outputPath" -y'; - } - - final result = await _execute(command); - - if (result.success) { - return _finalizeConversionOutput( - plan: outputPlan, - inputPath: inputPath, - deleteOriginal: deleteOriginal, - ); - } - - _log.e('M4A to $normalizedFormat conversion failed: ${result.output}'); - await _cleanupConversionOutput(outputPlan); - return null; - } - static Future decryptWithDescriptor({ required String inputPath, required DownloadDecryptionDescriptor descriptor, diff --git a/lib/utils/audio_format_utils.dart b/lib/utils/audio_format_utils.dart index f8b14848..79a10dad 100644 --- a/lib/utils/audio_format_utils.dart +++ b/lib/utils/audio_format_utils.dart @@ -304,23 +304,6 @@ String resolveQualityVariantFilename({ ); } -String lossyFormatForSetting(String value) { - final normalized = value.trim().toLowerCase(); - if (normalized.startsWith('opus')) return 'opus'; - if (normalized.startsWith('aac') || normalized.startsWith('m4a')) { - return 'aac'; - } - return 'mp3'; -} - -String lossyExtensionForFormat(String format) { - return switch (format) { - 'opus' => '.opus', - 'aac' => '.m4a', - _ => '.mp3', - }; -} - String metadataFormatForLossyFormat(String format) { return format == 'aac' ? 'm4a' : format; } @@ -348,15 +331,6 @@ int autoConvertBitrateKbps(String value) { return int.parse(normalizeAutoConvertBitrate(value).replaceAll('k', '')); } -String autoConvertLossySetting({ - required String format, - required String bitrate, -}) { - final normalizedFormat = normalizeAutoConvertFormat(format); - final normalizedBitrate = autoConvertBitrateKbps(bitrate); - return '${normalizedFormat}_$normalizedBitrate'; -} - String autoConvertFormatLabel(String format) { return switch (normalizeAutoConvertFormat(format)) { 'aac' => 'M4A (AAC)', diff --git a/test/audio_conversion_utils_test.dart b/test/audio_conversion_utils_test.dart index e6418602..68d0ea90 100644 --- a/test/audio_conversion_utils_test.dart +++ b/test/audio_conversion_utils_test.dart @@ -93,10 +93,6 @@ void main() { expect(normalizeAutoConvertFormat('unexpected'), 'mp3'); expect(normalizeAutoConvertBitrate('256 kbps'), '256k'); expect(normalizeAutoConvertBitrate('999k'), '320k'); - expect( - autoConvertLossySetting(format: 'opus', bitrate: '192k'), - 'opus_192', - ); }); test('skips only an output that already matches format and bitrate', () { diff --git a/test/models_and_utils_test.dart b/test/models_and_utils_test.dart index 92aabcd8..66086047 100644 --- a/test/models_and_utils_test.dart +++ b/test/models_and_utils_test.dart @@ -23,6 +23,30 @@ import 'package:spotiflac_android/utils/path_match_keys.dart'; import 'package:spotiflac_android/utils/string_utils.dart'; void main() { + test( + 'retired quality conversion settings do not enable automatic conversion', + () { + for (final format in ['mp3_320', 'aac_320', 'opus_256', 'opus_128']) { + final settings = AppSettings.fromJson({ + 'audioQuality': 'HIGH', + 'tidalHighFormat': format, + }); + expect(settings.audioQuality, 'HIGH'); + expect(settings.autoConvertDownloads, isFalse); + expect(settings.toJson().containsKey('tidalHighFormat'), isFalse); + } + final settings = AppSettings.fromJson({ + 'tidalHighFormat': 'mp3_320', + 'autoConvertDownloads': true, + 'autoConvertFormat': 'opus', + 'autoConvertBitrate': '192k', + }); + expect(settings.autoConvertDownloads, isTrue); + expect(settings.autoConvertFormat, 'opus'); + expect(settings.autoConvertBitrate, '192k'); + }, + ); + group('Finalized SAF audio names', () { for (final extension in ['mp3', 'opus', 'flac', 'ogg', 'm4a', 'mp4']) { test( @@ -1112,7 +1136,6 @@ void main() { embedLyrics: false, embedReplayGain: true, postProcessingEnabled: true, - tidalHighFormat: 'opus_256', autoConvertDownloads: true, autoConvertFormat: 'opus', autoConvertBitrate: '192k', @@ -1173,7 +1196,6 @@ void main() { 'embed_lyrics': false, 'embed_replaygain': true, 'post_processing_enabled': true, - 'tidal_high_format': 'opus_256', 'auto_convert_downloads': true, 'auto_convert_format': 'opus', 'auto_convert_bitrate': '192k',