diff --git a/go_backend/tidal.go b/go_backend/tidal.go index 09d9e745..7fecf2be 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -1502,6 +1502,11 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { GetTrackIDCache().SetTidal(req.ISRC, track.ID) } + quality := req.Quality + if quality == "" { + quality = "LOSSLESS" + } + filename := buildFilenameFromTemplate(req.FilenameFormat, map[string]interface{}{ "title": req.TrackName, "artist": req.ArtistName, @@ -1510,15 +1515,28 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { "year": extractYear(req.ReleaseDate), "disc": req.DiscNumber, }) - filename = sanitizeFilename(filename) + ".flac" - outputPath := filepath.Join(req.OutputDir, filename) + + // For HIGH quality (AAC 320kbps), use .m4a extension directly + var outputPath string + var m4aPath string + if quality == "HIGH" { + filename = sanitizeFilename(filename) + ".m4a" + outputPath = filepath.Join(req.OutputDir, filename) + m4aPath = outputPath // Same path for HIGH quality + } else { + filename = sanitizeFilename(filename) + ".flac" + outputPath = filepath.Join(req.OutputDir, filename) + m4aPath = strings.TrimSuffix(outputPath, ".flac") + ".m4a" + } if fileInfo, statErr := os.Stat(outputPath); statErr == nil && fileInfo.Size() > 0 { return TidalDownloadResult{FilePath: "EXISTS:" + outputPath}, nil } - m4aPath := strings.TrimSuffix(outputPath, ".flac") + ".m4a" - if fileInfo, statErr := os.Stat(m4aPath); statErr == nil && fileInfo.Size() > 0 { - return TidalDownloadResult{FilePath: "EXISTS:" + m4aPath}, nil + // For non-HIGH quality, also check for existing M4A (DASH downloads) + if quality != "HIGH" { + if fileInfo, statErr := os.Stat(m4aPath); statErr == nil && fileInfo.Size() > 0 { + return TidalDownloadResult{FilePath: "EXISTS:" + m4aPath}, nil + } } tmpPath := outputPath + ".m4a.tmp" @@ -1527,10 +1545,6 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { os.Remove(tmpPath) } - quality := req.Quality - if quality == "" { - quality = "LOSSLESS" - } GoLog("[Tidal] Using quality: %s\n", quality) downloadInfo, err := downloader.GetDownloadURL(track.ID, quality) @@ -1656,15 +1670,51 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { fmt.Println("[Tidal] No lyrics available from parallel fetch") } } else if strings.HasSuffix(actualOutputPath, ".m4a") { - fmt.Println("[Tidal] Skipping metadata embedding for M4A file (will be handled after FFmpeg conversion)") + // For HIGH quality (AAC 320kbps), embed metadata directly to M4A + if quality == "HIGH" { + GoLog("[Tidal] Embedding metadata to M4A file for HIGH quality...\n") + if err := EmbedM4AMetadata(actualOutputPath, metadata, coverData); err != nil { + GoLog("[Tidal] Warning: failed to embed M4A metadata: %v\n", err) + } else { + GoLog("[Tidal] M4A metadata embedded successfully\n") + } + + // Handle lyrics for M4A + if req.EmbedLyrics && parallelResult != nil && parallelResult.LyricsLRC != "" { + lyricsMode := req.LyricsMode + if lyricsMode == "" { + lyricsMode = "external" // Default to external for M4A since embedding is complex + } + + if lyricsMode == "external" || lyricsMode == "both" { + GoLog("[Tidal] Saving external LRC file for M4A...\n") + 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)") + } } AddToISRCIndex(req.OutputDir, req.ISRC, actualOutputPath) + // For HIGH quality (AAC), set appropriate values + bitDepth := downloadInfo.BitDepth + sampleRate := downloadInfo.SampleRate + if quality == "HIGH" { + // AAC 320kbps doesn't have traditional bit depth + bitDepth = 0 + sampleRate = 44100 + } + return TidalDownloadResult{ FilePath: actualOutputPath, - BitDepth: downloadInfo.BitDepth, - SampleRate: downloadInfo.SampleRate, + BitDepth: bitDepth, + SampleRate: sampleRate, Title: track.Title, Artist: track.Artist.Name, Album: track.Album.Title, diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index ebaaf56c..cfda0632 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1980,9 +1980,14 @@ class DownloadQueueNotifier extends Notifier { } if (filePath != null && filePath.endsWith('.m4a')) { - _log.d( - 'M4A file detected (Hi-Res DASH stream), attempting conversion to FLAC...', - ); + // For HIGH quality (native AAC 320kbps), skip M4A to FLAC conversion + if (quality == 'HIGH') { + _log.i('Native AAC 320kbps download (HIGH quality), keeping M4A file'); + actualQuality = 'AAC 320kbps'; + } else { + _log.d( + 'M4A file detected (Hi-Res DASH stream), attempting conversion to FLAC...', + ); try { final file = File(filePath); @@ -2084,6 +2089,7 @@ class DownloadQueueNotifier extends Notifier { } catch (e) { _log.w('FFmpeg conversion process failed: $e, keeping M4A file'); } + } // end else (not HIGH quality) } final itemAfterDownload = state.items.firstWhere( diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index 4cc2e501..8d14bf9f 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -94,6 +94,7 @@ class _DownloadSettingsPageState extends ConsumerState { final topPadding = MediaQuery.of(context).padding.top; final isBuiltInService = _builtInServices.contains(settings.defaultService); + final isTidalService = settings.defaultService == 'tidal'; return PopScope( canPop: true, @@ -215,8 +216,19 @@ class _DownloadSettingsPageState extends ConsumerState { onTap: () => ref .read(settingsProvider.notifier) .setAudioQuality('HI_RES_LOSSLESS'), - showDivider: settings.enableLossyOption, + showDivider: isTidalService || settings.enableLossyOption, ), + // Native AAC 320kbps option (Tidal only) + if (isTidalService) + _QualityOption( + title: 'AAC 320kbps', + subtitle: 'Native AAC (no conversion)', + isSelected: settings.audioQuality == 'HIGH', + onTap: () => ref + .read(settingsProvider.notifier) + .setAudioQuality('HIGH'), + showDivider: settings.enableLossyOption, + ), if (settings.enableLossyOption) _QualityOption( title: context.l10n.qualityLossy, diff --git a/lib/widgets/download_service_picker.dart b/lib/widgets/download_service_picker.dart index ec65bbde..c7e5b179 100644 --- a/lib/widgets/download_service_picker.dart +++ b/lib/widgets/download_service_picker.dart @@ -27,6 +27,7 @@ const _builtInServices = [ QualityOption(id: 'LOSSLESS', label: 'FLAC Lossless', description: '16-bit / 44.1kHz'), QualityOption(id: 'HI_RES', label: 'Hi-Res FLAC', description: '24-bit / up to 96kHz'), QualityOption(id: 'HI_RES_LOSSLESS', label: 'Hi-Res FLAC Max', description: '24-bit / up to 192kHz'), + QualityOption(id: 'HIGH', label: 'AAC 320kbps', description: 'Native AAC (no conversion)'), ], ), BuiltInService( @@ -257,6 +258,8 @@ class _DownloadServicePickerState extends ConsumerState { return Icons.high_quality; case 'LOSSLESS': return Icons.music_note; + case 'HIGH': + return Icons.aod; case 'MP3_320': case 'MP3': case 'LOSSY':