From 7b9ed3ec8ed98bd1a18156f39c31969237233ab2 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 15 Mar 2026 18:52:41 +0700 Subject: [PATCH] feat: add Qobuz Afkar API provider and prefer request metadata for consistent album grouping --- go_backend/exports.go | 59 +++++++++++++++++--------- go_backend/exports_test.go | 86 ++++++++++++++++++++++++++++++++++++++ go_backend/qobuz.go | 29 ++++++++++--- go_backend/qobuz_test.go | 6 ++- go_backend/tidal.go | 16 +++++-- pubspec.lock | 32 ++++++-------- 6 files changed, 178 insertions(+), 50 deletions(-) create mode 100644 go_backend/exports_test.go diff --git a/go_backend/exports.go b/go_backend/exports.go index 5a8a7840..b8178d77 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -135,6 +135,36 @@ type DownloadResult struct { DecryptionKey string } +func preferredReleaseMetadata( + req DownloadRequest, + album string, + releaseDate string, + trackNumber int, + discNumber int, +) (string, string, int, int) { + preferredAlbum := strings.TrimSpace(req.AlbumName) + if preferredAlbum == "" { + preferredAlbum = album + } + + preferredReleaseDate := strings.TrimSpace(req.ReleaseDate) + if preferredReleaseDate == "" { + preferredReleaseDate = releaseDate + } + + preferredTrackNumber := req.TrackNumber + if preferredTrackNumber == 0 { + preferredTrackNumber = trackNumber + } + + preferredDiscNumber := req.DiscNumber + if preferredDiscNumber == 0 { + preferredDiscNumber = discNumber + } + + return preferredAlbum, preferredReleaseDate, preferredTrackNumber, preferredDiscNumber +} + func buildDownloadSuccessResponse( req DownloadRequest, result DownloadResult, @@ -153,25 +183,16 @@ func buildDownloadSuccessResponse( artist = req.ArtistName } - album := result.Album - if album == "" { - album = req.AlbumName - } - - releaseDate := result.ReleaseDate - if releaseDate == "" { - releaseDate = req.ReleaseDate - } - - trackNumber := result.TrackNumber - if trackNumber == 0 { - trackNumber = req.TrackNumber - } - - discNumber := result.DiscNumber - if discNumber == 0 { - discNumber = req.DiscNumber - } + // Preserve requested release metadata when available so mixed-provider + // fallback downloads from the same source album do not get split into + // different albums just because Tidal/Qobuz report variant titles/dates. + album, releaseDate, trackNumber, discNumber := preferredReleaseMetadata( + req, + result.Album, + result.ReleaseDate, + result.TrackNumber, + result.DiscNumber, + ) isrc := result.ISRC if isrc == "" { diff --git a/go_backend/exports_test.go b/go_backend/exports_test.go new file mode 100644 index 00000000..dbce4278 --- /dev/null +++ b/go_backend/exports_test.go @@ -0,0 +1,86 @@ +package gobackend + +import "testing" + +func TestBuildDownloadSuccessResponsePrefersRequestedAlbumMetadata(t *testing.T) { + req := DownloadRequest{ + TrackName: "Bonus Track", + ArtistName: "Artist", + AlbumName: "Album (Deluxe)", + AlbumArtist: "Artist", + ReleaseDate: "2024-01-01", + TrackNumber: 14, + DiscNumber: 1, + ISRC: "REQ123", + CoverURL: "https://example.com/cover.jpg", + Genre: "Pop", + Label: "Label", + Copyright: "Copyright", + } + + result := DownloadResult{ + Title: "Bonus Track", + Artist: "Artist", + Album: "Album", + ReleaseDate: "2023-12-01", + TrackNumber: 2, + DiscNumber: 9, + ISRC: "RES456", + } + + resp := buildDownloadSuccessResponse( + req, + result, + "tidal", + "ok", + "/tmp/test.flac", + false, + ) + + if resp.Album != req.AlbumName { + t.Fatalf("album = %q, want %q", resp.Album, req.AlbumName) + } + if resp.ReleaseDate != req.ReleaseDate { + t.Fatalf("release date = %q, want %q", resp.ReleaseDate, req.ReleaseDate) + } + if resp.TrackNumber != req.TrackNumber { + t.Fatalf("track number = %d, want %d", resp.TrackNumber, req.TrackNumber) + } + if resp.DiscNumber != req.DiscNumber { + t.Fatalf("disc number = %d, want %d", resp.DiscNumber, req.DiscNumber) + } + if resp.Artist != result.Artist { + t.Fatalf("artist = %q, want provider artist %q", resp.Artist, result.Artist) + } + if resp.ISRC != result.ISRC { + t.Fatalf("isrc = %q, want provider isrc %q", resp.ISRC, result.ISRC) + } +} + +func TestPreferredReleaseMetadataPrefersRequestValues(t *testing.T) { + album, releaseDate, trackNumber, discNumber := preferredReleaseMetadata( + DownloadRequest{ + AlbumName: "Album (Deluxe Edition)", + ReleaseDate: "2024-01-01", + TrackNumber: 13, + DiscNumber: 2, + }, + "Album", + "2023-01-01", + 3, + 1, + ) + + if album != "Album (Deluxe Edition)" { + t.Fatalf("album = %q", album) + } + if releaseDate != "2024-01-01" { + t.Fatalf("release date = %q", releaseDate) + } + if trackNumber != 13 { + t.Fatalf("track number = %d", trackNumber) + } + if discNumber != 2 { + t.Fatalf("disc number = %d", discNumber) + } +} diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go index 788f816f..ec0489ff 100644 --- a/go_backend/qobuz.go +++ b/go_backend/qobuz.go @@ -54,6 +54,7 @@ const ( qobuzDownloadAPIURL = "https://www.musicdl.me/api/qobuz/download" qobuzDabMusicAPIURL = "https://dabmusic.xyz/api/stream?trackId=" qobuzDeebAPIURL = "https://dab.yeet.su/api/stream?trackId=" + qobuzAfkarAPIURL = "https://qbz.afkarxyz.fun/api/track/" qobuzSquidAPIURL = "https://qobuz.squid.wtf/api/download-music?country=US&track_id=" qobuzDebugKeyXORMask = byte(0x5A) ) @@ -1019,6 +1020,10 @@ func (q *QobuzDownloader) GetArtistMetadata(resourceID string) (*ArtistResponseP func (q *QobuzDownloader) GetAvailableAPIs() []string { return []string{ qobuzDownloadAPIURL, + qobuzDabMusicAPIURL, + qobuzDeebAPIURL, + qobuzAfkarAPIURL, + qobuzSquidAPIURL, } } @@ -1039,6 +1044,8 @@ func (q *QobuzDownloader) GetAvailableProviders() []qobuzAPIProvider { {Name: "dabmusic", URL: qobuzDabMusicAPIURL, Kind: qobuzAPIKindStandard}, // "deeb" is mapped from the legacy reference fallback endpoint. {Name: "deeb", URL: qobuzDeebAPIURL, Kind: qobuzAPIKindStandard}, + // "qbz" comes from the desktop reference app and uses /api/track/{id}?quality=... + {Name: "qbz", URL: qobuzAfkarAPIURL, Kind: qobuzAPIKindStandard}, {Name: "squid", URL: qobuzSquidAPIURL, Kind: qobuzAPIKindStandard}, } } @@ -2156,6 +2163,10 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) { if req.AlbumName != "" { albumName = req.AlbumName } + releaseDate := track.Album.ReleaseDate + if req.ReleaseDate != "" { + releaseDate = req.ReleaseDate + } actualTrackNumber := req.TrackNumber if actualTrackNumber == 0 { @@ -2167,7 +2178,7 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) { Artist: track.Performer.Name, Album: albumName, AlbumArtist: req.AlbumArtist, - Date: track.Album.ReleaseDate, + Date: releaseDate, TrackNumber: actualTrackNumber, TotalTracks: req.TotalTracks, DiscNumber: req.DiscNumber, @@ -2231,16 +2242,24 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) { lyricsLRC = parallelResult.LyricsLRC } + resultAlbum, resultReleaseDate, resultTrackNumber, resultDiscNumber := preferredReleaseMetadata( + req, + track.Album.Title, + track.Album.ReleaseDate, + actualTrackNumber, + req.DiscNumber, + ) + return QobuzDownloadResult{ FilePath: outputPath, BitDepth: actualBitDepth, SampleRate: actualSampleRate, Title: track.Title, Artist: track.Performer.Name, - Album: track.Album.Title, - ReleaseDate: track.Album.ReleaseDate, - TrackNumber: actualTrackNumber, - DiscNumber: req.DiscNumber, + Album: resultAlbum, + ReleaseDate: resultReleaseDate, + TrackNumber: resultTrackNumber, + DiscNumber: resultDiscNumber, ISRC: track.ISRC, LyricsLRC: lyricsLRC, }, nil diff --git a/go_backend/qobuz_test.go b/go_backend/qobuz_test.go index d19629b6..9d15d318 100644 --- a/go_backend/qobuz_test.go +++ b/go_backend/qobuz_test.go @@ -213,14 +213,16 @@ func TestExtractQobuzAlbumIDsFromArtistHTML(t *testing.T) { func TestQobuzAvailableProviders(t *testing.T) { providers := NewQobuzDownloader().GetAvailableProviders() - if len(providers) != 3 { - t.Fatalf("expected 3 Qobuz providers, got %d", len(providers)) + if len(providers) != 5 { + t.Fatalf("expected 5 Qobuz providers, got %d", len(providers)) } want := map[string]string{ "musicdl": qobuzAPIKindMusicDL, "dabmusic": qobuzAPIKindStandard, "deeb": qobuzAPIKindStandard, + "qbz": qobuzAPIKindStandard, + "squid": qobuzAPIKindStandard, } for _, provider := range providers { diff --git a/go_backend/tidal.go b/go_backend/tidal.go index 8a68826d..a837380d 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -2189,16 +2189,24 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { lyricsLRC = parallelResult.LyricsLRC } + resultAlbum, resultReleaseDate, resultTrackNumber, resultDiscNumber := preferredReleaseMetadata( + req, + track.Album.Title, + track.Album.ReleaseDate, + actualTrackNumber, + actualDiscNumber, + ) + return TidalDownloadResult{ FilePath: actualOutputPath, BitDepth: bitDepth, SampleRate: sampleRate, Title: track.Title, Artist: track.Artist.Name, - Album: track.Album.Title, - ReleaseDate: track.Album.ReleaseDate, - TrackNumber: actualTrackNumber, - DiscNumber: actualDiscNumber, + Album: resultAlbum, + ReleaseDate: resultReleaseDate, + TrackNumber: resultTrackNumber, + DiscNumber: resultDiscNumber, ISRC: track.ISRC, LyricsLRC: lyricsLRC, }, nil diff --git a/pubspec.lock b/pubspec.lock index 4fc7941f..0370159a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -133,10 +133,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -557,14 +557,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - js: - dependency: transitive - description: - name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.dev" - source: hosted - version: "0.7.2" json_annotation: dependency: "direct main" description: @@ -633,18 +625,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -1166,26 +1158,26 @@ packages: dependency: transitive description: name: test - sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" url: "https://pub.dev" source: hosted - version: "1.26.3" + version: "1.30.0" test_api: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.10" test_core: dependency: transitive description: name: test_core - sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" url: "https://pub.dev" source: hosted - version: "0.6.12" + version: "0.6.16" timezone: dependency: transitive description: