feat: improve Tidal metadata (copyright, album artist), remove Qobuz metadata search fallback, fix DATE/YAR tag sync

This commit is contained in:
zarzet
2026-04-14 18:18:15 +07:00
parent 62519d2d1c
commit 4af089f56c
4 changed files with 49 additions and 22 deletions
+1 -10
View File
@@ -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()
}
+7 -9
View File
@@ -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)
}
}
+35 -3
View File
@@ -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
}
+6
View File
@@ -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;
}