mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-02 19:05:57 +02:00
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:
+62
-12
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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':
|
||||
|
||||
Reference in New Issue
Block a user