feat(tidal): add native AAC 320kbps quality option

- Add HIGH quality option (AAC 320kbps) for Tidal downloads
- Download directly as M4A without FLAC conversion
- Embed metadata to M4A using EmbedM4AMetadata()
- Skip M4A to FLAC conversion in download provider for HIGH quality
- Add AAC 320kbps option in settings page (Tidal only)
- Add HIGH quality option in download service picker
This commit is contained in:
zarzet
2026-02-01 17:26:25 +07:00
parent 9d479b61d6
commit 2073516666
4 changed files with 87 additions and 16 deletions
+62 -12
View File
@@ -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,
+9 -3
View File
@@ -1980,9 +1980,14 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
}
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<DownloadQueueState> {
} catch (e) {
_log.w('FFmpeg conversion process failed: $e, keeping M4A file');
}
} // end else (not HIGH quality)
}
final itemAfterDownload = state.items.firstWhere(
@@ -94,6 +94,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
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<DownloadSettingsPage> {
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,
+3
View File
@@ -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<DownloadServicePicker> {
return Icons.high_quality;
case 'LOSSLESS':
return Icons.music_note;
case 'HIGH':
return Icons.aod;
case 'MP3_320':
case 'MP3':
case 'LOSSY':