mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-15 05:10:28 +02:00
feat: expose audio codec in download result and skip lossy-to-lossless conversion
Go backend: - Add AudioCodec field to DownloadResult and DownloadResponse - Extension download results can now include audio_codec/audioCodec - ffmpegGetInfo and probeAudioQuality now return codec field - Add trackItemBytes option to file.download() for custom progress handling Flutter: - Check audio_codec before container conversion - Skip FLAC conversion if source codec is lossy (AAC, MP3, Opus, etc.) - Prevents fake upscale from lossy to lossless containers
This commit is contained in:
@@ -313,6 +313,7 @@ type DownloadResponse struct {
|
||||
AlreadyExists bool `json:"already_exists,omitempty"`
|
||||
ActualBitDepth int `json:"actual_bit_depth,omitempty"`
|
||||
ActualSampleRate int `json:"actual_sample_rate,omitempty"`
|
||||
AudioCodec string `json:"audio_codec,omitempty"`
|
||||
ActualExtension string `json:"actual_extension,omitempty"`
|
||||
ActualContainer string `json:"actual_container,omitempty"`
|
||||
RequiresContainerConversion bool `json:"requires_container_conversion,omitempty"`
|
||||
@@ -342,6 +343,7 @@ type DownloadResult struct {
|
||||
FilePath string
|
||||
BitDepth int
|
||||
SampleRate int
|
||||
AudioCodec string
|
||||
Title string
|
||||
Artist string
|
||||
Album string
|
||||
@@ -863,6 +865,7 @@ func buildDownloadSuccessResponse(
|
||||
AlreadyExists: alreadyExists,
|
||||
ActualBitDepth: result.BitDepth,
|
||||
ActualSampleRate: result.SampleRate,
|
||||
AudioCodec: result.AudioCodec,
|
||||
ActualExtension: result.ActualExtension,
|
||||
ActualContainer: result.ActualContainer,
|
||||
RequiresContainerConversion: result.RequiresContainerConversion,
|
||||
@@ -920,7 +923,12 @@ func enrichResultQualityFromFile(result *DownloadResult) {
|
||||
if qErr == nil {
|
||||
result.BitDepth = quality.BitDepth
|
||||
result.SampleRate = quality.SampleRate
|
||||
GoLog("[Download] Actual quality from file: %d-bit/%dHz\n", quality.BitDepth, quality.SampleRate)
|
||||
result.AudioCodec = quality.Codec
|
||||
if quality.Codec != "" {
|
||||
GoLog("[Download] Actual quality from file: %s %d-bit/%dHz\n", quality.Codec, quality.BitDepth, quality.SampleRate)
|
||||
} else {
|
||||
GoLog("[Download] Actual quality from file: %d-bit/%dHz\n", quality.BitDepth, quality.SampleRate)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -236,6 +236,7 @@ func normalizeExtensionDownloadResult(result *ExtDownloadResult) (DownloadResult
|
||||
FilePath: strings.TrimSpace(result.FilePath),
|
||||
BitDepth: result.BitDepth,
|
||||
SampleRate: result.SampleRate,
|
||||
AudioCodec: strings.TrimSpace(result.AudioCodec),
|
||||
Title: result.Title,
|
||||
Artist: result.Artist,
|
||||
Album: result.Album,
|
||||
@@ -420,6 +421,7 @@ type ExtDownloadResult struct {
|
||||
AlreadyExists bool `json:"already_exists,omitempty"`
|
||||
BitDepth int `json:"bit_depth,omitempty"`
|
||||
SampleRate int `json:"sample_rate,omitempty"`
|
||||
AudioCodec string `json:"audio_codec,omitempty"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
ErrorType string `json:"error_type,omitempty"`
|
||||
|
||||
@@ -873,6 +875,7 @@ func parseExtensionDownloadResultValue(vm *goja.Runtime, value goja.Value) ExtDo
|
||||
AlreadyExists: gojaObjectBool(obj, "already_exists", "alreadyExists"),
|
||||
BitDepth: gojaObjectInt(obj, "bit_depth", "bitDepth"),
|
||||
SampleRate: gojaObjectInt(obj, "sample_rate", "sampleRate"),
|
||||
AudioCodec: gojaObjectString(obj, "audio_codec", "audioCodec", "codec"),
|
||||
ErrorMessage: gojaObjectString(obj, "error_message", "errorMessage", "error"),
|
||||
ErrorType: gojaObjectString(obj, "error_type", "errorType"),
|
||||
Title: gojaObjectString(obj, "title"),
|
||||
|
||||
@@ -131,6 +131,7 @@ func (r *extensionRuntime) ffmpegGetInfo(call goja.FunctionCall) goja.Value {
|
||||
"sample_rate": quality.SampleRate,
|
||||
"total_samples": quality.TotalSamples,
|
||||
"duration": float64(quality.TotalSamples) / float64(quality.SampleRate),
|
||||
"codec": quality.Codec,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@ func (r *extensionRuntime) fileDownload(call goja.FunctionCall) goja.Value {
|
||||
var onProgress goja.Callable
|
||||
var headers map[string]string
|
||||
var chunkedDownload bool
|
||||
trackItemBytes := true
|
||||
var chunkSize int64
|
||||
if len(call.Arguments) > 2 && !goja.IsUndefined(call.Arguments[2]) && !goja.IsNull(call.Arguments[2]) {
|
||||
optionsObj := call.Arguments[2].Export()
|
||||
@@ -151,6 +152,15 @@ func (r *extensionRuntime) fileDownload(call goja.FunctionCall) goja.Value {
|
||||
onProgress = callable
|
||||
}
|
||||
}
|
||||
if trackBytes, ok := opts["trackItemBytes"]; ok {
|
||||
if v, ok := trackBytes.(bool); ok {
|
||||
trackItemBytes = v
|
||||
}
|
||||
} else if trackBytes, ok := opts["track_item_bytes"]; ok {
|
||||
if v, ok := trackBytes.(bool); ok {
|
||||
trackItemBytes = v
|
||||
}
|
||||
}
|
||||
if chunked, ok := opts["chunked"]; ok {
|
||||
switch v := chunked.(type) {
|
||||
case bool:
|
||||
@@ -194,7 +204,7 @@ func (r *extensionRuntime) fileDownload(call goja.FunctionCall) goja.Value {
|
||||
}
|
||||
|
||||
if chunkedDownload {
|
||||
return r.fileDownloadChunked(client, urlStr, fullPath, headers, ua, chunkSize, onProgress)
|
||||
return r.fileDownloadChunked(client, urlStr, fullPath, headers, ua, chunkSize, onProgress, trackItemBytes)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", urlStr, nil)
|
||||
@@ -244,7 +254,7 @@ func (r *extensionRuntime) fileDownload(call goja.FunctionCall) goja.Value {
|
||||
}
|
||||
|
||||
contentLength := resp.ContentLength
|
||||
shouldTrackItemBytes := activeItemID != ""
|
||||
shouldTrackItemBytes := activeItemID != "" && trackItemBytes
|
||||
if shouldTrackItemBytes && contentLength > 0 {
|
||||
SetItemBytesTotal(activeItemID, contentLength)
|
||||
}
|
||||
@@ -321,7 +331,7 @@ func (r *extensionRuntime) fileDownload(call goja.FunctionCall) goja.Value {
|
||||
// fileDownloadChunked downloads a URL using sequential Range requests.
|
||||
// This is needed for servers (like YouTube's googlevideo CDN) that reject
|
||||
// non-ranged or large-range requests with 403 and require small chunk downloads.
|
||||
func (r *extensionRuntime) fileDownloadChunked(client *http.Client, urlStr, fullPath string, headers map[string]string, ua string, chunkSize int64, onProgress goja.Callable) goja.Value {
|
||||
func (r *extensionRuntime) fileDownloadChunked(client *http.Client, urlStr, fullPath string, headers map[string]string, ua string, chunkSize int64, onProgress goja.Callable, trackItemBytes bool) goja.Value {
|
||||
// First, get the total content length with a small probe request
|
||||
probeReq, err := http.NewRequest("GET", urlStr, nil)
|
||||
if err != nil {
|
||||
@@ -391,7 +401,7 @@ func (r *extensionRuntime) fileDownloadChunked(client *http.Client, urlStr, full
|
||||
SetItemDownloading(activeItemID)
|
||||
}
|
||||
|
||||
shouldTrackItemBytes := activeItemID != ""
|
||||
shouldTrackItemBytes := activeItemID != "" && trackItemBytes
|
||||
if shouldTrackItemBytes && totalSize > 0 {
|
||||
SetItemBytesTotal(activeItemID, totalSize)
|
||||
}
|
||||
|
||||
@@ -415,6 +415,7 @@ func (r *extensionRuntime) RegisterGoBackendAPIs(vm *goja.Runtime) {
|
||||
"sampleRate": quality.SampleRate,
|
||||
"totalSamples": quality.TotalSamples,
|
||||
"duration": quality.Duration,
|
||||
"codec": quality.Codec,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -6246,6 +6246,16 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
if (context.quality == 'HIGH' || context.outputExt != '.flac') {
|
||||
return filePath;
|
||||
}
|
||||
final resultAudioFormat = _normalizeAudioFormatValue(
|
||||
result['audio_codec']?.toString() ??
|
||||
result['actual_audio_codec']?.toString(),
|
||||
);
|
||||
if (_isLossyAudioFormat(resultAudioFormat)) {
|
||||
_log.d(
|
||||
'Native-worker output is $resultAudioFormat; preserving native container.',
|
||||
);
|
||||
return filePath;
|
||||
}
|
||||
final requiresContainerConversion =
|
||||
result['requires_container_conversion'] == true ||
|
||||
result['requiresContainerConversion'] == true;
|
||||
@@ -7411,10 +7421,16 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
result,
|
||||
filePath: filePath,
|
||||
);
|
||||
final resultAudioFormat = _normalizeAudioFormatValue(
|
||||
result['audio_codec']?.toString() ??
|
||||
result['actual_audio_codec']?.toString(),
|
||||
);
|
||||
final resultIsLossyAudio = _isLossyAudioFormat(resultAudioFormat);
|
||||
final requiresContainerConversion =
|
||||
result['requires_container_conversion'] == true ||
|
||||
result['requiresContainerConversion'] == true ||
|
||||
_shouldRequestContainerConversion(actualService, safOutputExt);
|
||||
(!resultIsLossyAudio &&
|
||||
_shouldRequestContainerConversion(actualService, safOutputExt));
|
||||
final preferredOutputExt = _extensionPreferredOutputExt(actualService);
|
||||
final shouldPreserveNativeM4a =
|
||||
!requiresContainerConversion &&
|
||||
|
||||
Reference in New Issue
Block a user