diff --git a/go_backend/tidal.go b/go_backend/tidal.go index a837380d..582e4ff6 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -1961,11 +1961,7 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { outputExt := strings.TrimSpace(req.OutputExt) if outputExt == "" { - if quality == "HIGH" { - outputExt = ".m4a" - } else { - outputExt = ".flac" - } + outputExt = ".flac" } else if !strings.HasPrefix(outputExt, ".") { outputExt = "." + outputExt } @@ -1979,7 +1975,7 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { } m4aPath = outputPath } else { - if outputExt == ".m4a" || quality == "HIGH" { + if outputExt == ".m4a" { filename = sanitizeFilename(filename) + ".m4a" outputPath = filepath.Join(req.OutputDir, filename) m4aPath = outputPath @@ -1992,10 +1988,8 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { if fileInfo, statErr := os.Stat(outputPath); statErr == nil && fileInfo.Size() > 0 { return TidalDownloadResult{FilePath: "EXISTS:" + outputPath}, nil } - if quality != "HIGH" { - if fileInfo, statErr := os.Stat(m4aPath); statErr == nil && fileInfo.Size() > 0 { - return TidalDownloadResult{FilePath: "EXISTS:" + m4aPath}, nil - } + if fileInfo, statErr := os.Stat(m4aPath); statErr == nil && fileInfo.Size() > 0 { + return TidalDownloadResult{FilePath: "EXISTS:" + m4aPath}, nil } } @@ -2151,27 +2145,7 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { fmt.Println("[Tidal] No lyrics available from parallel fetch") } } else if (isSafOutput && actualExt == ".m4a") || (!isSafOutput && strings.HasSuffix(actualOutputPath, ".m4a")) { - if quality == "HIGH" { - GoLog("[Tidal] HIGH quality M4A - skipping metadata embedding (file from server is already valid)\n") - - if req.EmbedMetadata && req.EmbedLyrics && parallelResult != nil && parallelResult.LyricsLRC != "" { - lyricsMode := req.LyricsMode - if lyricsMode == "" { - lyricsMode = "embed" - } - - if !isSafOutput && (lyricsMode == "external" || lyricsMode == "both") { - GoLog("[Tidal] Saving external LRC file for M4A (mode: %s)...\n", lyricsMode) - if lrcPath, lrcErr := SaveLRCFile(actualOutputPath, parallelResult.LyricsLRC); lrcErr != nil { - GoLog("[Tidal] Warning: failed to save LRC file: %v\n", lrcErr) - } else { - GoLog("[Tidal] LRC file saved: %s\n", lrcPath) - } - } - } - } else { - fmt.Println("[Tidal] Skipping metadata embedding for M4A file (will be handled after FFmpeg conversion)") - } + fmt.Println("[Tidal] Skipping metadata embedding for M4A file (will be handled after FFmpeg conversion)") } if !isSafOutput { @@ -2181,10 +2155,6 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { bitDepth := downloadInfo.BitDepth sampleRate := downloadInfo.SampleRate lyricsLRC := "" - if quality == "HIGH" { - bitDepth = 0 - sampleRate = 44100 - } if req.EmbedMetadata && req.EmbedLyrics && parallelResult != nil && parallelResult.LyricsLRC != "" { lyricsLRC = parallelResult.LyricsLRC } diff --git a/go_backend/youtube.go b/go_backend/youtube.go index 58e5d751..3bb65f5a 100644 --- a/go_backend/youtube.go +++ b/go_backend/youtube.go @@ -29,6 +29,7 @@ var ( type YouTubeQuality string const ( + YouTubeQualityOpus320 YouTubeQuality = "opus_320" YouTubeQualityOpus256 YouTubeQuality = "opus_256" YouTubeQualityOpus128 YouTubeQuality = "opus_128" YouTubeQualityMP3128 YouTubeQuality = "mp3_128" @@ -37,7 +38,7 @@ const ( ) var ( - youtubeOpusSupportedBitrates = []int{128, 256} + youtubeOpusSupportedBitrates = []int{128, 256, 320} youtubeMp3SupportedBitrates = []int{128, 256, 320} ) @@ -146,6 +147,8 @@ func parseYouTubeQualityInput(raw string) (format string, bitrate int, normalize switch normalizedRaw { case "opus_256", "opus256", "opus": return "opus", 256, YouTubeQualityOpus256 + case "opus_320", "opus320": + return "opus", 320, YouTubeQualityOpus320 case "opus_128", "opus128": return "opus", 128, YouTubeQualityOpus128 case "mp3_320", "mp3320", "mp3", "": diff --git a/go_backend/youtube_quality_test.go b/go_backend/youtube_quality_test.go index cb77e5bb..e0f2ebbf 100644 --- a/go_backend/youtube_quality_test.go +++ b/go_backend/youtube_quality_test.go @@ -30,8 +30,8 @@ func TestParseYouTubeQualityInput_Mp3NormalizesToSupportedBitrates(t *testing.T) func TestParseYouTubeQualityInput_PicksNearestSupportedBitrate(t *testing.T) { _, opusBitrate, _ := parseYouTubeQualityInput("opus_999") - if opusBitrate != 256 { - t.Fatalf("expected opus normalization to 256, got %d", opusBitrate) + if opusBitrate != 320 { + t.Fatalf("expected opus normalization to 320, got %d", opusBitrate) } _, mp3Bitrate, _ := parseYouTubeQualityInput("mp3_1") @@ -39,3 +39,16 @@ func TestParseYouTubeQualityInput_PicksNearestSupportedBitrate(t *testing.T) { t.Fatalf("expected mp3 normalization to 128, got %d", mp3Bitrate) } } + +func TestParseYouTubeQualityInput_Opus320(t *testing.T) { + format, bitrate, normalized := parseYouTubeQualityInput("opus_320") + if format != "opus" { + t.Fatalf("expected opus format, got %s", format) + } + if bitrate != 320 { + t.Fatalf("expected 320 bitrate, got %d", bitrate) + } + if normalized != YouTubeQualityOpus320 { + t.Fatalf("expected %s normalized, got %s", YouTubeQualityOpus320, normalized) + } +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index ce8174e2..ae9a4cc0 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3838,6 +3838,36 @@ abstract class AppLocalizations { /// **'FFmpeg metadata embed failed'** String get trackReEnrichFfmpegFailed; + /// Action/button label for queueing FLAC redownloads for local tracks + /// + /// In en, this message translates to: + /// **'Queue FLAC'** + String get queueFlacAction; + + /// Confirmation dialog body before queueing FLAC redownloads for local tracks + /// + /// In en, this message translates to: + /// **'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n{count} selected'** + String queueFlacConfirmMessage(int count); + + /// Snackbar while resolving remote matches for local FLAC redownloads + /// + /// In en, this message translates to: + /// **'Finding FLAC matches... ({current}/{total})'** + String queueFlacFindingProgress(int current, int total); + + /// Snackbar when no safe FLAC redownload matches were found + /// + /// In en, this message translates to: + /// **'No reliable online matches found for the selection'** + String get queueFlacNoReliableMatches; + + /// Snackbar when some selected local tracks were queued for FLAC redownload and some were skipped + /// + /// In en, this message translates to: + /// **'Added {addedCount} tracks to queue, skipped {skippedCount}'** + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount); + /// Snackbar when save operation fails /// /// In en, this message translates to: @@ -4602,18 +4632,6 @@ abstract class AppLocalizations { /// **'Select a built-in service to enable'** String get downloadSelectServiceToEnable; - /// Quality option label for Tidal lossy 320kbps - /// - /// In en, this message translates to: - /// **'Lossy 320kbps'** - String get downloadLossy320; - - /// Setting title to pick output format for Tidal lossy downloads - /// - /// In en, this message translates to: - /// **'Lossy Format'** - String get downloadLossyFormat; - /// Info hint when non-Tidal/Qobuz service is selected /// /// In en, this message translates to: @@ -4740,54 +4758,6 @@ abstract class AppLocalizations { /// **'Auto'** String get downloadMusixmatchAuto; - /// Title of the Tidal lossy format picker bottom sheet - /// - /// In en, this message translates to: - /// **'Lossy 320kbps Format'** - String get downloadLossy320Format; - - /// Description in the Tidal lossy format picker - /// - /// In en, this message translates to: - /// **'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'** - String get downloadLossy320FormatDesc; - - /// Tidal lossy format option - MP3 320kbps - /// - /// In en, this message translates to: - /// **'MP3 320kbps'** - String get downloadLossyMp3; - - /// Subtitle for MP3 320kbps option - /// - /// In en, this message translates to: - /// **'Best compatibility, ~10MB per track'** - String get downloadLossyMp3Subtitle; - - /// Tidal lossy format option - Opus 256kbps - /// - /// In en, this message translates to: - /// **'Opus 256kbps'** - String get downloadLossyOpus256; - - /// Subtitle for Opus 256kbps option - /// - /// In en, this message translates to: - /// **'Best quality Opus, ~8MB per track'** - String get downloadLossyOpus256Subtitle; - - /// Tidal lossy format option - Opus 128kbps - /// - /// In en, this message translates to: - /// **'Opus 128kbps'** - String get downloadLossyOpus128; - - /// Subtitle for Opus 128kbps option - /// - /// In en, this message translates to: - /// **'Smallest size, ~4MB per track'** - String get downloadLossyOpus128Subtitle; - /// Subtitle for 'Any' network mode option /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index b913d844..ce489e3d 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2172,6 +2172,28 @@ class AppLocalizationsDe extends AppLocalizations { String get trackReEnrichFfmpegFailed => 'FFmpeg Metadaten-Einbettung fehlgeschlagen'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Fehler: $error'; @@ -2666,12 +2688,6 @@ class AppLocalizationsDe extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2748,32 +2764,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 07e3ca2d..bd280e94 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2145,6 +2145,28 @@ class AppLocalizationsEn extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2638,12 +2660,6 @@ class AppLocalizationsEn extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2720,32 +2736,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 8a5a974b..81ce15ff 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2145,6 +2145,28 @@ class AppLocalizationsEs extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2638,12 +2660,6 @@ class AppLocalizationsEs extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2720,32 +2736,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 68d677e1..b35b22d0 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2147,6 +2147,28 @@ class AppLocalizationsFr extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2640,12 +2662,6 @@ class AppLocalizationsFr extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2722,32 +2738,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 5da28983..e36316e9 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -2145,6 +2145,28 @@ class AppLocalizationsHi extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2638,12 +2660,6 @@ class AppLocalizationsHi extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2720,32 +2736,6 @@ class AppLocalizationsHi extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 7fc8d1de..68bffbe5 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -2152,6 +2152,28 @@ class AppLocalizationsId extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Antrekan FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Cari kecocokan online untuk track yang dipilih lalu antrekan download FLAC.\n\nFile yang sudah ada tidak akan diubah atau dihapus.\n\nHanya kecocokan dengan keyakinan tinggi yang akan diantrikan otomatis.\n\n$count dipilih'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Mencari kecocokan FLAC... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'Tidak ada kecocokan online yang cukup meyakinkan untuk pilihan ini'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Menambahkan $addedCount track ke antrean, melewati $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2645,12 +2667,6 @@ class AppLocalizationsId extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2727,32 +2743,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 04a7a0c2..8145c794 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -2132,6 +2132,28 @@ class AppLocalizationsJa extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return '失敗: $error'; @@ -2625,12 +2647,6 @@ class AppLocalizationsJa extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2707,32 +2723,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 80e8a1df..66d21aec 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -2125,6 +2125,28 @@ class AppLocalizationsKo extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2618,12 +2640,6 @@ class AppLocalizationsKo extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2700,32 +2716,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index af603f60..e87906ac 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2145,6 +2145,28 @@ class AppLocalizationsNl extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2638,12 +2660,6 @@ class AppLocalizationsNl extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2720,32 +2736,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index fc40c186..466572ce 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2145,6 +2145,28 @@ class AppLocalizationsPt extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2638,12 +2660,6 @@ class AppLocalizationsPt extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2720,32 +2736,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 7a542295..dd460698 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2198,6 +2198,28 @@ class AppLocalizationsRu extends AppLocalizations { String get trackReEnrichFfmpegFailed => 'Ошибка встраивания метаданных FFmpeg'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Ошибка: $error'; @@ -2697,12 +2719,6 @@ class AppLocalizationsRu extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2779,32 +2795,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 1e5727bf..6e1a37f1 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -2157,6 +2157,28 @@ class AppLocalizationsTr extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2650,12 +2672,6 @@ class AppLocalizationsTr extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2732,32 +2748,6 @@ class AppLocalizationsTr extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 68381882..9248d997 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2145,6 +2145,28 @@ class AppLocalizationsZh extends AppLocalizations { @override String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + @override + String get queueFlacAction => 'Queue FLAC'; + + @override + String queueFlacConfirmMessage(int count) { + return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected'; + } + + @override + String queueFlacFindingProgress(int current, int total) { + return 'Finding FLAC matches... ($current/$total)'; + } + + @override + String get queueFlacNoReliableMatches => + 'No reliable online matches found for the selection'; + + @override + String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) { + return 'Added $addedCount tracks to queue, skipped $skippedCount'; + } + @override String trackSaveFailed(String error) { return 'Failed: $error'; @@ -2638,12 +2660,6 @@ class AppLocalizationsZh extends AppLocalizations { String get downloadSelectServiceToEnable => 'Select a built-in service to enable'; - @override - String get downloadLossy320 => 'Lossy 320kbps'; - - @override - String get downloadLossyFormat => 'Lossy Format'; - @override String get downloadSelectTidalQobuz => 'Select Tidal or Qobuz above to configure quality'; @@ -2720,32 +2736,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get downloadMusixmatchAuto => 'Auto'; - @override - String get downloadLossy320Format => 'Lossy 320kbps Format'; - - @override - String get downloadLossy320FormatDesc => - 'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.'; - - @override - String get downloadLossyMp3 => 'MP3 320kbps'; - - @override - String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; - - @override - String get downloadLossyOpus256 => 'Opus 256kbps'; - - @override - String get downloadLossyOpus256Subtitle => - 'Best quality Opus, ~8MB per track'; - - @override - String get downloadLossyOpus128 => 'Opus 128kbps'; - - @override - String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track'; - @override String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index bc3f2e13..3de4ec31 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2819,6 +2819,47 @@ "@trackReEnrichFfmpegFailed": { "description": "Snackbar when FFmpeg embed fails for MP3/Opus" }, + "queueFlacAction": "Queue FLAC", + "@queueFlacAction": { + "description": "Action/button label for queueing FLAC redownloads for local tracks" + }, + "queueFlacConfirmMessage": "Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n{count} selected", + "@queueFlacConfirmMessage": { + "description": "Confirmation dialog body before queueing FLAC redownloads for local tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "queueFlacFindingProgress": "Finding FLAC matches... ({current}/{total})", + "@queueFlacFindingProgress": { + "description": "Snackbar while resolving remote matches for local FLAC redownloads", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "queueFlacNoReliableMatches": "No reliable online matches found for the selection", + "@queueFlacNoReliableMatches": { + "description": "Snackbar when no safe FLAC redownload matches were found" + }, + "queueFlacQueuedWithSkipped": "Added {addedCount} tracks to queue, skipped {skippedCount}", + "@queueFlacQueuedWithSkipped": { + "description": "Snackbar when some selected local tracks were queued for FLAC redownload and some were skipped", + "placeholders": { + "addedCount": { + "type": "int" + }, + "skippedCount": { + "type": "int" + } + } + }, "trackSaveFailed": "Failed: {error}", "@trackSaveFailed": { "description": "Snackbar when save operation fails", @@ -3513,14 +3554,7 @@ "@downloadSelectServiceToEnable": { "description": "Hint shown instead of Ask-quality subtitle when no built-in service selected" }, - "downloadLossy320": "Lossy 320kbps", - "@downloadLossy320": { - "description": "Quality option label for Tidal lossy 320kbps" - }, - "downloadLossyFormat": "Lossy Format", - "@downloadLossyFormat": { - "description": "Setting title to pick output format for Tidal lossy downloads" - }, + "downloadSelectTidalQobuz": "Select Tidal or Qobuz above to configure quality", "@downloadSelectTidalQobuz": { "description": "Info hint when non-Tidal/Qobuz service is selected" @@ -3606,38 +3640,7 @@ "@downloadMusixmatchAuto": { "description": "Button to reset Musixmatch language to automatic" }, - "downloadLossy320Format": "Lossy 320kbps Format", - "@downloadLossy320Format": { - "description": "Title of the Tidal lossy format picker bottom sheet" - }, - "downloadLossy320FormatDesc": "Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.", - "@downloadLossy320FormatDesc": { - "description": "Description in the Tidal lossy format picker" - }, - "downloadLossyMp3": "MP3 320kbps", - "@downloadLossyMp3": { - "description": "Tidal lossy format option - MP3 320kbps" - }, - "downloadLossyMp3Subtitle": "Best compatibility, ~10MB per track", - "@downloadLossyMp3Subtitle": { - "description": "Subtitle for MP3 320kbps option" - }, - "downloadLossyOpus256": "Opus 256kbps", - "@downloadLossyOpus256": { - "description": "Tidal lossy format option - Opus 256kbps" - }, - "downloadLossyOpus256Subtitle": "Best quality Opus, ~8MB per track", - "@downloadLossyOpus256Subtitle": { - "description": "Subtitle for Opus 256kbps option" - }, - "downloadLossyOpus128": "Opus 128kbps", - "@downloadLossyOpus128": { - "description": "Tidal lossy format option - Opus 128kbps" - }, - "downloadLossyOpus128Subtitle": "Smallest size, ~4MB per track", - "@downloadLossyOpus128Subtitle": { - "description": "Subtitle for Opus 128kbps option" - }, + "downloadNetworkAnySubtitle": "WiFi + Mobile Data", "@downloadNetworkAnySubtitle": { "description": "Subtitle for 'Any' network mode option" diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 67e3e9ac..9bf2ee10 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -38,10 +38,8 @@ class AppSettings { final bool showExtensionStore; final String locale; final String lyricsMode; - final String - tidalHighFormat; // Format for Tidal HIGH quality: 'mp3_320', 'opus_256', or 'opus_128' final int - youtubeOpusBitrate; // YouTube Opus bitrate (supported: 128/256 kbps) + youtubeOpusBitrate; // YouTube Opus bitrate (supported: 128/256/320 kbps) final int youtubeMp3Bitrate; // YouTube MP3 bitrate (supported: 128/256/320 kbps) final bool @@ -114,7 +112,6 @@ class AppSettings { this.showExtensionStore = true, this.locale = 'system', this.lyricsMode = 'embed', - this.tidalHighFormat = 'mp3_320', this.youtubeOpusBitrate = 256, this.youtubeMp3Bitrate = 320, this.useAllFilesAccess = false, @@ -178,7 +175,6 @@ class AppSettings { bool? showExtensionStore, String? locale, String? lyricsMode, - String? tidalHighFormat, int? youtubeOpusBitrate, int? youtubeMp3Bitrate, bool? useAllFilesAccess, @@ -241,7 +237,6 @@ class AppSettings { showExtensionStore: showExtensionStore ?? this.showExtensionStore, locale: locale ?? this.locale, lyricsMode: lyricsMode ?? this.lyricsMode, - tidalHighFormat: tidalHighFormat ?? this.tidalHighFormat, youtubeOpusBitrate: youtubeOpusBitrate ?? this.youtubeOpusBitrate, youtubeMp3Bitrate: youtubeMp3Bitrate ?? this.youtubeMp3Bitrate, useAllFilesAccess: useAllFilesAccess ?? this.useAllFilesAccess, diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index c3eecb50..99a5114d 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -44,7 +44,6 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( showExtensionStore: json['showExtensionStore'] as bool? ?? true, locale: json['locale'] as String? ?? 'system', lyricsMode: json['lyricsMode'] as String? ?? 'embed', - tidalHighFormat: json['tidalHighFormat'] as String? ?? 'mp3_320', youtubeOpusBitrate: (json['youtubeOpusBitrate'] as num?)?.toInt() ?? 256, youtubeMp3Bitrate: (json['youtubeMp3Bitrate'] as num?)?.toInt() ?? 320, useAllFilesAccess: json['useAllFilesAccess'] as bool? ?? false, @@ -119,7 +118,6 @@ Map _$AppSettingsToJson( 'showExtensionStore': instance.showExtensionStore, 'locale': instance.locale, 'lyricsMode': instance.lyricsMode, - 'tidalHighFormat': instance.tidalHighFormat, 'youtubeOpusBitrate': instance.youtubeOpusBitrate, 'youtubeMp3Bitrate': instance.youtubeMp3Bitrate, 'useAllFilesAccess': instance.useAllFilesAccess, diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index df46e084..a01b8e88 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1840,7 +1840,7 @@ class DownloadQueueNotifier extends Notifier { return '.opus'; } if (service.toLowerCase() == 'tidal' && quality == 'HIGH') { - return '.m4a'; + return '.flac'; // HIGH quality no longer available; fallback to FLAC } return '.flac'; } @@ -2383,7 +2383,8 @@ class DownloadQueueNotifier extends Notifier { backendResult['album_artist'] as String?, ); - final hasOverrides = backendTrackNum != null || + final hasOverrides = + backendTrackNum != null || backendDiscNum != null || backendYear != null || backendAlbum != null || @@ -3612,6 +3613,11 @@ class DownloadQueueNotifier extends Notifier { 'Quality: $quality${item.qualityOverride != null ? ' (override)' : ''}', ); } + + if (!useSaf) { + await _ensureDirExists(outputDir, label: 'Output folder'); + } + _log.d('Output dir: $outputDir'); final normalizedTrackNumber = @@ -3903,7 +3909,6 @@ class DownloadQueueNotifier extends Notifier { isContentUriPath && effectiveSafMode && actualService == 'tidal' && - quality != 'HIGH' && filePath.endsWith('.flac') && (mimeType == null || mimeType.contains('flac')); @@ -3918,73 +3923,50 @@ class DownloadQueueNotifier extends Notifier { final currentFilePath = filePath; if (isContentUriPath && effectiveSafMode) { - if (quality == 'HIGH') { - final tidalHighFormat = settings.tidalHighFormat; - _log.i( - 'Tidal HIGH quality (SAF), converting M4A to $tidalHighFormat...', - ); - - final tempPath = await _copySafToTemp(currentFilePath); - if (tempPath != null) { - String? convertedPath; - try { + _log.d('M4A file detected (SAF), converting to FLAC...'); + final tempPath = await _copySafToTemp(currentFilePath); + if (tempPath != null) { + String? flacPath; + try { + final length = await File(tempPath).length(); + if (length < 1024) { + _log.w('Temp M4A is too small (<1KB), skipping conversion'); + } else { updateItemStatus( item.id, DownloadStatus.downloading, progress: 0.95, ); - - final format = tidalHighFormat.startsWith('opus') - ? 'opus' - : 'mp3'; - convertedPath = await FFmpegService.convertM4aToLossy( - tempPath, - format: format, - bitrate: tidalHighFormat, - deleteOriginal: false, - ); - - if (convertedPath != null) { - _log.i( - 'Successfully converted M4A to $format (temp): $convertedPath', - ); - _log.i('Embedding metadata to $format...'); - updateItemStatus( - item.id, - DownloadStatus.downloading, - progress: 0.99, + flacPath = await FFmpegService.convertM4aToFlac(tempPath); + if (flacPath != null) { + _log.d('Converted to FLAC (temp): $flacPath'); + _log.d('Embedding metadata and cover to converted FLAC...'); + final finalTrack = _buildTrackForMetadataEmbedding( + trackToDownload, + result, + resolvedAlbumArtist, ); final backendGenre = result['genre'] as String?; final backendLabel = result['label'] as String?; final backendCopyright = result['copyright'] as String?; - if (format == 'mp3') { - await _embedMetadataToMp3( - convertedPath, - trackToDownload, - genre: backendGenre ?? genre, - label: backendLabel ?? label, - copyright: backendCopyright, - ); - } else { - await _embedMetadataToOpus( - convertedPath, - trackToDownload, - genre: backendGenre ?? genre, - label: backendLabel ?? label, - copyright: backendCopyright, - ); - } + await _embedMetadataAndCover( + flacPath, + finalTrack, + genre: backendGenre ?? genre, + label: backendLabel ?? label, + copyright: backendCopyright, + writeExternalLrc: false, + ); - final newExt = format == 'opus' ? '.opus' : '.mp3'; - final newFileName = '${safBaseName ?? 'track'}$newExt'; + final newFileName = '${safBaseName ?? 'track'}.flac'; final newUri = await _writeTempToSaf( treeUri: settings.downloadTreeUri, relativeDir: effectiveOutputDir, fileName: newFileName, - mimeType: _mimeTypeForExt(newExt), - srcPath: convertedPath, + mimeType: _mimeTypeForExt('.flac'), + srcPath: flacPath, ); if (newUri != null) { @@ -3993,58 +3975,60 @@ class DownloadQueueNotifier extends Notifier { } filePath = newUri; finalSafFileName = newFileName; - final bitrateDisplay = tidalHighFormat.contains('_') - ? '${tidalHighFormat.split('_').last}kbps' - : '320kbps'; - actualQuality = '${format.toUpperCase()} $bitrateDisplay'; } else { - _log.w( - 'Failed to write converted $format to SAF, keeping M4A', - ); - actualQuality = 'AAC 320kbps'; + _log.w('Failed to write FLAC to SAF, keeping M4A'); } } else { - _log.w( - 'M4A to $format conversion failed, keeping M4A file', - ); - actualQuality = 'AAC 320kbps'; - } - } catch (e) { - _log.w('SAF M4A conversion failed: $e'); - actualQuality = 'AAC 320kbps'; - } finally { - // Clean up temp files - try { - await File(tempPath).delete(); - } catch (_) {} - if (convertedPath != null) { - try { - await File(convertedPath).delete(); - } catch (_) {} + _log.w('FFmpeg conversion returned null, keeping M4A file'); } } - } - } else { - _log.d('M4A file detected (SAF), converting to FLAC...'); - final tempPath = await _copySafToTemp(currentFilePath); - if (tempPath != null) { - String? flacPath; + } catch (e) { + _log.w('SAF M4A->FLAC conversion failed: $e'); + } finally { + // Clean up temp files try { - final length = await File(tempPath).length(); - if (length < 1024) { - _log.w('Temp M4A is too small (<1KB), skipping conversion'); - } else { - updateItemStatus( - item.id, - DownloadStatus.downloading, - progress: 0.95, - ); - flacPath = await FFmpegService.convertM4aToFlac(tempPath); - if (flacPath != null) { - _log.d('Converted to FLAC (temp): $flacPath'); - _log.d( - 'Embedding metadata and cover to converted FLAC...', - ); + await File(tempPath).delete(); + } catch (_) {} + if (flacPath != null) { + try { + await File(flacPath).delete(); + } catch (_) {} + } + } + } + } else { + _log.d( + 'M4A file detected (Hi-Res DASH stream), attempting conversion to FLAC...', + ); + + try { + final file = File(currentFilePath); + if (!await file.exists()) { + _log.e('File does not exist at path: $filePath'); + } else { + final length = await file.length(); + _log.i('File size before conversion: ${length / 1024} KB'); + + if (length < 1024) { + _log.w( + 'File is too small (<1KB), skipping conversion. Download might be corrupt.', + ); + } else { + updateItemStatus( + item.id, + DownloadStatus.downloading, + progress: 0.95, + ); + final flacPath = await FFmpegService.convertM4aToFlac( + currentFilePath, + ); + + if (flacPath != null) { + filePath = flacPath; + _log.d('Converted to FLAC: $flacPath'); + + _log.d('Embedding metadata and cover to converted FLAC...'); + try { final finalTrack = _buildTrackForMetadataEmbedding( trackToDownload, result, @@ -4055,201 +4039,32 @@ class DownloadQueueNotifier extends Notifier { final backendLabel = result['label'] as String?; final backendCopyright = result['copyright'] as String?; + if (backendGenre != null || + backendLabel != null || + backendCopyright != null) { + _log.d( + 'Extended metadata from backend - Genre: $backendGenre, Label: $backendLabel, Copyright: $backendCopyright', + ); + } + await _embedMetadataAndCover( flacPath, finalTrack, genre: backendGenre ?? genre, label: backendLabel ?? label, copyright: backendCopyright, - writeExternalLrc: false, - ); - - final newFileName = '${safBaseName ?? 'track'}.flac'; - final newUri = await _writeTempToSaf( - treeUri: settings.downloadTreeUri, - relativeDir: effectiveOutputDir, - fileName: newFileName, - mimeType: _mimeTypeForExt('.flac'), - srcPath: flacPath, - ); - - if (newUri != null) { - if (newUri != currentFilePath) { - await _deleteSafFile(currentFilePath); - } - filePath = newUri; - finalSafFileName = newFileName; - } else { - _log.w('Failed to write FLAC to SAF, keeping M4A'); - } - } else { - _log.w( - 'FFmpeg conversion returned null, keeping M4A file', ); + _log.d('Metadata and cover embedded successfully'); + } catch (e) { + _log.w('Warning: Failed to embed metadata/cover: $e'); } - } - } catch (e) { - _log.w('SAF M4A->FLAC conversion failed: $e'); - } finally { - // Clean up temp files - try { - await File(tempPath).delete(); - } catch (_) {} - if (flacPath != null) { - try { - await File(flacPath).delete(); - } catch (_) {} - } - } - } - } - } else { - if (quality == 'HIGH') { - final tidalHighFormat = settings.tidalHighFormat; - _log.i( - 'Tidal HIGH quality download, converting M4A to $tidalHighFormat...', - ); - - try { - updateItemStatus( - item.id, - DownloadStatus.downloading, - progress: 0.95, - ); - - final format = tidalHighFormat.startsWith('opus') - ? 'opus' - : 'mp3'; - 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 = '${format.toUpperCase()} $bitrateDisplay'; - _log.i( - 'Successfully converted M4A to $format: $convertedPath', - ); - - _log.i('Embedding metadata to $format...'); - updateItemStatus( - item.id, - DownloadStatus.downloading, - progress: 0.99, - ); - - final backendGenre = result['genre'] as String?; - final backendLabel = result['label'] as String?; - final backendCopyright = result['copyright'] as String?; - - if (format == 'mp3') { - await _embedMetadataToMp3( - convertedPath, - trackToDownload, - genre: backendGenre ?? genre, - label: backendLabel ?? label, - copyright: backendCopyright, - ); } else { - await _embedMetadataToOpus( - convertedPath, - trackToDownload, - genre: backendGenre ?? genre, - label: backendLabel ?? label, - copyright: backendCopyright, - ); - } - _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'; - } - } else { - _log.d( - 'M4A file detected (Hi-Res DASH stream), attempting conversion to FLAC...', - ); - - try { - final file = File(currentFilePath); - if (!await file.exists()) { - _log.e('File does not exist at path: $filePath'); - } else { - final length = await file.length(); - _log.i('File size before conversion: ${length / 1024} KB'); - - if (length < 1024) { - _log.w( - 'File is too small (<1KB), skipping conversion. Download might be corrupt.', - ); - } else { - updateItemStatus( - item.id, - DownloadStatus.downloading, - progress: 0.95, - ); - final flacPath = await FFmpegService.convertM4aToFlac( - currentFilePath, - ); - - if (flacPath != null) { - filePath = flacPath; - _log.d('Converted to FLAC: $flacPath'); - - _log.d( - 'Embedding metadata and cover to converted FLAC...', - ); - try { - final finalTrack = _buildTrackForMetadataEmbedding( - trackToDownload, - result, - resolvedAlbumArtist, - ); - - final backendGenre = result['genre'] as String?; - final backendLabel = result['label'] as String?; - final backendCopyright = result['copyright'] as String?; - - if (backendGenre != null || - backendLabel != null || - backendCopyright != null) { - _log.d( - 'Extended metadata from backend - Genre: $backendGenre, Label: $backendLabel, Copyright: $backendCopyright', - ); - } - - await _embedMetadataAndCover( - flacPath, - finalTrack, - genre: backendGenre ?? genre, - label: backendLabel ?? label, - copyright: backendCopyright, - ); - _log.d('Metadata and cover embedded successfully'); - } catch (e) { - _log.w('Warning: Failed to embed metadata/cover: $e'); - } - } else { - _log.w( - 'FFmpeg conversion returned null, keeping M4A file', - ); - } + _log.w('FFmpeg conversion returned null, keeping M4A file'); } } - } catch (e) { - _log.w( - 'FFmpeg conversion process failed: $e, keeping M4A file', - ); } + } catch (e) { + _log.w('FFmpeg conversion process failed: $e, keeping M4A file'); } } } else if (metadataEmbeddingEnabled && diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index ed44fea4..e98a70e0 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -1,20 +1,22 @@ import 'dart:convert'; +import 'dart:io'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:spotiflac_android/models/settings.dart'; import 'package:spotiflac_android/constants/app_info.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; +import 'package:spotiflac_android/utils/file_access.dart'; import 'package:spotiflac_android/utils/logger.dart'; const _settingsKey = 'app_settings'; const _migrationVersionKey = 'settings_migration_version'; -const _currentMigrationVersion = 5; +const _currentMigrationVersion = 6; const _spotifyClientSecretKey = 'spotify_client_secret'; final _log = AppLogger('SettingsProvider'); class SettingsNotifier extends Notifier { - static const List _youtubeOpusSupportedBitrates = [128, 256]; + static const List _youtubeOpusSupportedBitrates = [128, 256, 320]; static const List _youtubeMp3SupportedBitrates = [128, 256, 320]; static final RegExp _isoRegionPattern = RegExp(r'^[A-Z]{2}$'); @@ -37,6 +39,7 @@ class SettingsNotifier extends Notifier { state = AppSettings.fromJson(jsonDecode(json)); await _runMigrations(prefs); + await _normalizeIosDownloadDirectoryIfNeeded(); await _normalizeYouTubeBitratesIfNeeded(); await _normalizeSongLinkRegionIfNeeded(); } @@ -114,6 +117,10 @@ class SettingsNotifier extends Notifier { useCustomSpotifyCredentials: false, ); } + // Migration 6: Tidal HIGH quality removed — migrate to LOSSLESS + if (state.audioQuality == 'HIGH') { + state = state.copyWith(audioQuality: 'LOSSLESS'); + } state = state.copyWith(lastSeenVersion: AppInfo.version); await prefs.setInt(_migrationVersionKey, _currentMigrationVersion); await _saveSettings(); @@ -189,6 +196,20 @@ class SettingsNotifier extends Notifier { await _saveSettings(); } + Future _normalizeIosDownloadDirectoryIfNeeded() async { + if (!Platform.isIOS) return; + + final currentDir = state.downloadDirectory.trim(); + if (currentDir.isEmpty) return; + + final normalizedDir = await validateOrFixIosPath(currentDir); + if (normalizedDir == currentDir) return; + + _log.i('Normalized iOS download directory: $currentDir -> $normalizedDir'); + state = state.copyWith(downloadDirectory: normalizedDir); + await _saveSettings(); + } + String _normalizeSongLinkRegion(String region) { final normalized = region.trim().toUpperCase(); if (_isoRegionPattern.hasMatch(normalized)) return normalized; @@ -430,11 +451,6 @@ class SettingsNotifier extends Notifier { _saveSettings(); } - void setTidalHighFormat(String format) { - state = state.copyWith(tidalHighFormat: format); - _saveSettings(); - } - void setYoutubeOpusBitrate(int bitrate) { final normalized = _normalizeYouTubeOpusBitrate(bitrate); state = state.copyWith(youtubeOpusBitrate: normalized); diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index c502d142..b4a6d1a7 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -300,7 +300,6 @@ class _DownloadSettingsPageState extends ConsumerState { final topPadding = normalizedHeaderTopPadding(context); final isBuiltInService = _builtInServices.contains(settings.defaultService); - final isTidalService = settings.defaultService == 'tidal'; return PopScope( canPop: true, @@ -408,35 +407,8 @@ class _DownloadSettingsPageState extends ConsumerState { onTap: () => ref .read(settingsProvider.notifier) .setAudioQuality('HI_RES_LOSSLESS'), - showDivider: isTidalService, + showDivider: false, ), - // Lossy 320kbps option (Tidal only) - downloads M4A, converts to MP3/Opus - if (isTidalService) - _QualityOption( - title: context.l10n.downloadLossy320, - subtitle: _getTidalHighFormatLabel( - settings.tidalHighFormat, - ), - isSelected: settings.audioQuality == 'HIGH', - onTap: () => ref - .read(settingsProvider.notifier) - .setAudioQuality('HIGH'), - showDivider: false, - ), - if (isTidalService && settings.audioQuality == 'HIGH') - SettingsItem( - icon: Icons.tune, - title: context.l10n.downloadLossyFormat, - subtitle: _getTidalHighFormatLabel( - settings.tidalHighFormat, - ), - onTap: () => _showTidalHighFormatPicker( - context, - ref, - settings.tidalHighFormat, - ), - showDivider: false, - ), ], if (!isBuiltInService) ...[ Padding( @@ -464,12 +436,12 @@ class _DownloadSettingsPageState extends ConsumerState { ], SettingsItem( title: context.l10n.youtubeOpusBitrateTitle, - subtitle: '${settings.youtubeOpusBitrate}kbps (128/256)', + subtitle: '${settings.youtubeOpusBitrate}kbps (128/256/320)', onTap: () => _showYoutubeBitratePicker( context: context, title: context.l10n.youtubeOpusBitrateTitle, currentValue: settings.youtubeOpusBitrate, - options: const [128, 256], + options: const [128, 256, 320], onSave: (value) => ref .read(settingsProvider.notifier) .setYoutubeOpusBitrate(value), @@ -1691,104 +1663,6 @@ class _DownloadSettingsPageState extends ConsumerState { ); } - String _getTidalHighFormatLabel(String format) { - switch (format) { - case 'mp3_320': - return 'MP3 320kbps'; - case 'opus_256': - return 'Opus 256kbps'; - case 'opus_128': - return 'Opus 128kbps'; - default: - return 'MP3 320kbps'; - } - } - - void _showTidalHighFormatPicker( - BuildContext context, - WidgetRef ref, - String current, - ) { - final colorScheme = Theme.of(context).colorScheme; - showModalBottomSheet( - context: context, - useRootNavigator: true, - backgroundColor: colorScheme.surfaceContainerHigh, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(28)), - ), - 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.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 _showNetworkModePicker( BuildContext context, WidgetRef ref, diff --git a/lib/widgets/download_service_picker.dart b/lib/widgets/download_service_picker.dart index 41c8c330..977a31b6 100644 --- a/lib/widgets/download_service_picker.dart +++ b/lib/widgets/download_service_picker.dart @@ -23,7 +23,7 @@ class BuiltInService { } /// Default quality options for built-in services -/// Note: Tidal lossy (HIGH) removed - use YouTube for lossy downloads +/// Default quality options for each built-in service const _builtInServices = [ BuiltInService( id: 'tidal', @@ -83,9 +83,9 @@ const _builtInServices = [ label: 'YouTube', qualityOptions: [ QualityOption( - id: 'opus_256', - label: 'Opus 256kbps', - description: 'Best quality lossy (~8MB per track)', + id: 'opus_320', + label: 'Opus 320kbps', + description: 'Best quality lossy (~10MB per track)', ), QualityOption( id: 'mp3_320', @@ -146,7 +146,7 @@ class DownloadServicePicker extends ConsumerStatefulWidget { } class _DownloadServicePickerState extends ConsumerState { - static const List _youtubeOpusSupportedBitrates = [128, 256]; + static const List _youtubeOpusSupportedBitrates = [128, 256, 320]; static const List _youtubeMp3SupportedBitrates = [128, 256, 320]; late String _selectedService;