From 4af089f56c15c8b20ddf6941ce9630717b79f0f8 Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 14 Apr 2026 18:18:15 +0700 Subject: [PATCH] feat: improve Tidal metadata (copyright, album artist), remove Qobuz metadata search fallback, fix DATE/YAR tag sync --- go_backend/qobuz.go | 11 +-------- go_backend/qobuz_test.go | 16 ++++++-------- go_backend/tidal.go | 38 +++++++++++++++++++++++++++++--- lib/services/ffmpeg_service.dart | 6 +++++ 4 files changed, 49 insertions(+), 22 deletions(-) diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go index a1330fed..4d50e109 100644 --- a/go_backend/qobuz.go +++ b/go_backend/qobuz.go @@ -2655,17 +2655,8 @@ func resolveQobuzTrackForRequest(req DownloadRequest, downloader *QobuzDownloade } } - // Strategy 5: Metadata search with strict matching (duration tolerance: 10 seconds) if track == nil { - GoLog("[%s] Trying metadata search: '%s' by '%s'\n", logPrefix, req.TrackName, req.ArtistName) - track, err = qobuzSearchTrackByMetadataWithDurationFunc(downloader, req.TrackName, req.ArtistName, expectedDurationSec) - if track != nil && !qobuzTrackMatchesRequest(req, track, logPrefix, "metadata search", false) { - track = nil - } - } - - if track == nil { - errMsg := "could not find matching track on Qobuz (artist/duration mismatch)" + errMsg := "could not find matching track on Qobuz without identifier match" if err != nil { errMsg = err.Error() } diff --git a/go_backend/qobuz_test.go b/go_backend/qobuz_test.go index cf7106a6..78d0b121 100644 --- a/go_backend/qobuz_test.go +++ b/go_backend/qobuz_test.go @@ -429,11 +429,9 @@ func TestResolveQobuzTrackForRequestRejectsOdesliMismatch(t *testing.T) { t.Fatal("ISRC fallback should not run without an ISRC") return nil, nil } - qobuzSearchTrackByMetadataWithDurationFunc = func(_ *QobuzDownloader, trackName, artistName string, expectedDurationSec int) (*QobuzTrack, error) { - if trackName != "Taste Back" || artistName != "Harry Styles" || expectedDurationSec != 181 { - t.Fatalf("unexpected metadata fallback arguments: %q / %q / %d", trackName, artistName, expectedDurationSec) - } - return testQobuzTrack(444, "Taste Back", "Harry Styles", 181), nil + qobuzSearchTrackByMetadataWithDurationFunc = func(_ *QobuzDownloader, _, _ string, _ int) (*QobuzTrack, error) { + t.Fatal("metadata fallback should not run") + return nil, nil } songLinkCheckTrackAvailabilityFunc = func(_ *SongLinkClient, _, _ string) (*TrackAvailability, error) { t.Fatal("SongLink should not run when Odesli QobuzID is provided") @@ -448,11 +446,11 @@ func TestResolveQobuzTrackForRequestRejectsOdesliMismatch(t *testing.T) { } track, err := resolveQobuzTrackForRequest(req, &QobuzDownloader{}, "Test") - if err != nil { - t.Fatalf("expected no error, got %v", err) + if err == nil { + t.Fatalf("expected error, got track %+v", track) } - if track == nil || track.ID != 444 || track.Title != "Taste Back" { - t.Fatalf("unexpected resolved track: %+v", track) + if track != nil { + t.Fatalf("expected nil track, got %+v", track) } } diff --git a/go_backend/tidal.go b/go_backend/tidal.go index 2e201904..7d964d1d 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -51,6 +51,7 @@ type TidalTrack struct { ID int64 `json:"id"` Title string `json:"title"` ISRC string `json:"isrc"` + Copyright string `json:"copyright"` AudioQuality string `json:"audioQuality"` TrackNumber int `json:"trackNumber"` VolumeNumber int `json:"volumeNumber"` @@ -135,6 +136,7 @@ type tidalPublicAlbum struct { Type string `json:"type"` Cover string `json:"cover"` ReleaseDate string `json:"releaseDate"` + Copyright string `json:"copyright"` URL string `json:"url"` NumberOfTracks int `json:"numberOfTracks"` Explicit bool `json:"explicit"` @@ -306,6 +308,29 @@ func tidalTrackArtistsDisplay(track *TidalTrack) string { return strings.TrimSpace(track.Artist.Name) } +func tidalTrackAlbumArtistDisplay(track *TidalTrack) string { + if track == nil { + return "" + } + + if len(track.Artists) > 0 { + names := make([]string, 0, len(track.Artists)) + for _, artist := range track.Artists { + if strings.ToUpper(strings.TrimSpace(artist.Type)) != "MAIN" { + continue + } + if trimmed := strings.TrimSpace(artist.Name); trimmed != "" { + names = append(names, trimmed) + } + } + if len(names) > 0 { + return strings.Join(names, ", ") + } + } + + return strings.TrimSpace(track.Artist.Name) +} + func tidalAlbumArtistsDisplay(album *tidalPublicAlbum) string { if album == nil { return "" @@ -354,7 +379,7 @@ func tidalTrackToTrackMetadata(track *TidalTrack) TrackMetadata { Artists: tidalTrackArtistsDisplay(track), Name: strings.TrimSpace(track.Title), AlbumName: strings.TrimSpace(track.Album.Title), - AlbumArtist: strings.TrimSpace(track.Artist.Name), + AlbumArtist: tidalTrackAlbumArtistDisplay(track), DurationMS: track.Duration * 1000, Images: tidalImageURL(track.Album.Cover, "1280x1280"), ReleaseDate: strings.TrimSpace(track.Album.ReleaseDate), @@ -377,7 +402,7 @@ func tidalTrackToAlbumTrackMetadata(track *TidalTrack) AlbumTrackMetadata { Artists: tidalTrackArtistsDisplay(track), Name: strings.TrimSpace(track.Title), AlbumName: strings.TrimSpace(track.Album.Title), - AlbumArtist: strings.TrimSpace(track.Artist.Name), + AlbumArtist: tidalTrackAlbumArtistDisplay(track), DurationMS: track.Duration * 1000, Images: tidalImageURL(track.Album.Cover, "1280x1280"), ReleaseDate: strings.TrimSpace(track.Album.ReleaseDate), @@ -407,6 +432,7 @@ func tidalAlbumToAlbumInfo(album *tidalPublicAlbum) AlbumInfoMetadata { Artists: tidalAlbumArtistsDisplay(album), ArtistId: artistID, Images: tidalImageURL(album.Cover, "1280x1280"), + Copyright: strings.TrimSpace(album.Copyright), } } @@ -1740,6 +1766,7 @@ type TidalDownloadResult struct { TrackNumber int DiscNumber int ISRC string + Copyright string LyricsLRC string // LRC content for embedding in converted files } @@ -2348,6 +2375,10 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { if actualDiscNumber == 0 { actualDiscNumber = track.VolumeNumber } + copyright := strings.TrimSpace(req.Copyright) + if copyright == "" { + copyright = strings.TrimSpace(track.Copyright) + } metadata := Metadata{ Title: req.TrackName, @@ -2363,7 +2394,7 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { ISRC: track.ISRC, Genre: req.Genre, Label: req.Label, - Copyright: req.Copyright, + Copyright: copyright, Composer: req.Composer, } @@ -2474,6 +2505,7 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { TrackNumber: resultTrackNumber, DiscNumber: resultDiscNumber, ISRC: track.ISRC, + Copyright: copyright, LyricsLRC: lyricsLRC, }, nil } diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index 69a66b55..aa0a9eb9 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -1978,8 +1978,14 @@ class FFmpegService { break; case 'DATE': vorbis['DATE'] = value; + final yearMatch = RegExp(r'^(\d{4})').firstMatch(value); + if (yearMatch != null && + (!vorbis.containsKey('YEAR') || vorbis['YEAR']!.isEmpty)) { + vorbis['YEAR'] = yearMatch.group(1)!; + } break; case 'YEAR': + vorbis['YEAR'] = value; if (!vorbis.containsKey('DATE') || vorbis['DATE']!.isEmpty) { vorbis['DATE'] = value; }