mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-02 11:05:38 +02:00
feat(go): verification early-abort in fallback + album metadata from tracks
- DownloadWithExtensionFallback now immediately surfaces verification_required when any provider needs verification (availability + download stages), instead of letting later providers mask it - classifyDownloadErrorType treats 'session is not authenticated' as verification_required (Go + Dart side) - parseExtensionAlbumValue.withTrackFallbacks() derives album artist, release date, and audio traits from tracks when album-level missing - albumAudioTraitsFromTracks detects dolby_atmos/hi_res_lossless/lossless from per-track audio_quality/audio_modes fields - parseBitDepthSampleRate parses '24bit/96kHz' style quality labels
This commit is contained in:
@@ -2523,6 +2523,8 @@ func classifyDownloadErrorType(msg string) string {
|
||||
strings.Contains(lowerMsg, "verification_required") ||
|
||||
strings.Contains(lowerMsg, "verification required") ||
|
||||
strings.Contains(lowerMsg, "needs verification") ||
|
||||
strings.Contains(lowerMsg, "session is not authenticated") ||
|
||||
strings.Contains(lowerMsg, "signed session is not authenticated") ||
|
||||
strings.Contains(lowerMsg, "unauthorized") ||
|
||||
strings.Contains(lowerMsg, "precondition required") ||
|
||||
messageHasHTTPStatusCode(lowerMsg, "401") ||
|
||||
|
||||
@@ -873,7 +873,139 @@ func parseExtensionAlbumValue(vm *goja.Runtime, value goja.Value) (ExtAlbumMetad
|
||||
AudioTraits: gojaObjectStringSlice(obj, "audio_traits", "audioTraits"),
|
||||
Tracks: tracks,
|
||||
ProviderID: gojaObjectString(obj, "provider_id", "providerId"),
|
||||
}, nil
|
||||
}.withTrackFallbacks(), nil
|
||||
}
|
||||
|
||||
// withTrackFallbacks fills the album-level artist and release date from the
|
||||
// album's tracks when the extension did not provide them at the album level.
|
||||
// This is a generic mechanism so any extension benefits, without per-extension
|
||||
// special-casing in the app.
|
||||
func (a ExtAlbumMetadata) withTrackFallbacks() ExtAlbumMetadata {
|
||||
if strings.TrimSpace(a.Artists) == "" {
|
||||
a.Artists = albumArtistFromTracks(a.Tracks)
|
||||
}
|
||||
if strings.TrimSpace(a.ReleaseDate) == "" {
|
||||
a.ReleaseDate = albumReleaseDateFromTracks(a.Tracks)
|
||||
}
|
||||
if len(a.AudioTraits) == 0 {
|
||||
a.AudioTraits = albumAudioTraitsFromTracks(a.Tracks)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// albumArtistFromTracks prefers an explicit per-track album artist, then falls
|
||||
// back to the most common track artist across the album.
|
||||
func albumArtistFromTracks(tracks []ExtTrackMetadata) string {
|
||||
for _, t := range tracks {
|
||||
if s := strings.TrimSpace(t.AlbumArtist); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
counts := map[string]int{}
|
||||
order := []string{}
|
||||
for _, t := range tracks {
|
||||
artist := strings.TrimSpace(t.Artists)
|
||||
if artist == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := counts[artist]; !ok {
|
||||
order = append(order, artist)
|
||||
}
|
||||
counts[artist]++
|
||||
}
|
||||
best := ""
|
||||
bestCount := 0
|
||||
for _, artist := range order {
|
||||
if counts[artist] > bestCount {
|
||||
best = artist
|
||||
bestCount = counts[artist]
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
// albumReleaseDateFromTracks returns the first non-empty track release date.
|
||||
func albumReleaseDateFromTracks(tracks []ExtTrackMetadata) string {
|
||||
for _, t := range tracks {
|
||||
if s := strings.TrimSpace(t.ReleaseDate); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// albumAudioTraitsFromTracks derives album-level audio badges (Dolby Atmos,
|
||||
// Hi-Res Lossless, Lossless) from the per-track audio quality/mode fields that
|
||||
// extensions like Tidal and Qobuz already provide. Tokens match what the album
|
||||
// header understands ("dolby_atmos", "hi_res_lossless", "lossless").
|
||||
func albumAudioTraitsFromTracks(tracks []ExtTrackMetadata) []string {
|
||||
atmos := false
|
||||
hiRes := false
|
||||
lossless := false
|
||||
|
||||
for _, t := range tracks {
|
||||
modes := strings.ToUpper(t.AudioModes)
|
||||
quality := strings.ToUpper(t.AudioQuality)
|
||||
if strings.Contains(modes, "ATMOS") || strings.Contains(quality, "ATMOS") {
|
||||
atmos = true
|
||||
}
|
||||
if strings.Contains(quality, "HI_RES") ||
|
||||
strings.Contains(quality, "HIRES") ||
|
||||
strings.Contains(quality, "MASTER") ||
|
||||
strings.Contains(quality, "MQA") {
|
||||
hiRes = true
|
||||
}
|
||||
if strings.Contains(quality, "LOSSLESS") ||
|
||||
strings.Contains(quality, "FLAC") {
|
||||
lossless = true
|
||||
}
|
||||
if bd, sr := parseBitDepthSampleRate(quality); bd > 0 {
|
||||
if bd > 16 || sr > 48 {
|
||||
hiRes = true
|
||||
} else {
|
||||
lossless = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traits := []string{}
|
||||
if atmos {
|
||||
traits = append(traits, "dolby_atmos")
|
||||
}
|
||||
if hiRes {
|
||||
traits = append(traits, "hi_res_lossless")
|
||||
} else if lossless {
|
||||
traits = append(traits, "lossless")
|
||||
}
|
||||
return traits
|
||||
}
|
||||
|
||||
// parseBitDepthSampleRate extracts a bit depth and sample rate (in kHz) from
|
||||
// labels such as "24bit/96kHz", "16bit/44.1kHz" or "24bit".
|
||||
func parseBitDepthSampleRate(quality string) (int, float64) {
|
||||
lower := strings.ToLower(quality)
|
||||
bitDepth := 0
|
||||
sampleRate := 0.0
|
||||
|
||||
if idx := strings.Index(lower, "bit"); idx > 0 {
|
||||
j := idx
|
||||
for j > 0 && lower[j-1] >= '0' && lower[j-1] <= '9' {
|
||||
j--
|
||||
}
|
||||
if n, err := strconv.Atoi(lower[j:idx]); err == nil {
|
||||
bitDepth = n
|
||||
}
|
||||
}
|
||||
if idx := strings.Index(lower, "khz"); idx > 0 {
|
||||
j := idx
|
||||
for j > 0 && ((lower[j-1] >= '0' && lower[j-1] <= '9') || lower[j-1] == '.') {
|
||||
j--
|
||||
}
|
||||
if f, err := strconv.ParseFloat(lower[j:idx], 64); err == nil {
|
||||
sampleRate = f
|
||||
}
|
||||
}
|
||||
return bitDepth, sampleRate
|
||||
}
|
||||
|
||||
func parseExtensionAlbumArray(vm *goja.Runtime, value goja.Value) ([]ExtAlbumMetadata, error) {
|
||||
@@ -2586,6 +2718,15 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
|
||||
GoLog("[DownloadWithExtensionFallback] %s: not available\n", providerID)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
if strings.EqualFold(classifyDownloadErrorType(err.Error()), "verification_required") {
|
||||
GoLog("[DownloadWithExtensionFallback] %s requires verification (availability); pausing fallback to open the challenge\n", providerID)
|
||||
return &DownloadResponse{
|
||||
Success: false,
|
||||
Error: "Download failed: " + err.Error(),
|
||||
ErrorType: "verification_required",
|
||||
Service: providerID,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
if terminalAvailability {
|
||||
GoLog("[DownloadWithExtensionFallback] %s requested skip_fallback after availability check\n", providerID)
|
||||
@@ -2707,6 +2848,24 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
|
||||
lastRetryAfterSeconds = result.RetryAfterSeconds
|
||||
}
|
||||
GoLog("[DownloadWithExtensionFallback] %s failed: %v\n", providerID, lastErr)
|
||||
|
||||
if lastErr != nil {
|
||||
effType := lastErrType
|
||||
if effType == "" {
|
||||
effType = classifyDownloadErrorType(lastErr.Error())
|
||||
}
|
||||
if strings.EqualFold(effType, "verification_required") {
|
||||
GoLog("[DownloadWithExtensionFallback] %s requires verification; pausing fallback to open the challenge\n", providerID)
|
||||
return &DownloadResponse{
|
||||
Success: false,
|
||||
Error: "Download failed: " + lastErr.Error(),
|
||||
ErrorType: "verification_required",
|
||||
RetryAfterSeconds: lastRetryAfterSeconds,
|
||||
Service: providerID,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
if terminalAvailability {
|
||||
GoLog("[DownloadWithExtensionFallback] %s requested skip_fallback after download failure\n", providerID)
|
||||
return buildExtensionFallbackStoppedResponse(providerID, availability, lastErr), nil
|
||||
|
||||
@@ -11,6 +11,7 @@ bool isExtensionVerificationRequired(Object error) {
|
||||
message.contains('verification required') ||
|
||||
message.contains('needsverification') ||
|
||||
message.contains('needs verification') ||
|
||||
message.contains('session is not authenticated') ||
|
||||
message.contains('unauthorized') ||
|
||||
message.contains('precondition required') ||
|
||||
_containsHttpStatusCode(message, '401') ||
|
||||
|
||||
Reference in New Issue
Block a user