mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-15 05:47:13 +02:00
feat(listenbrainz): match collaboration top-songs via all credited artist MBIDs (#5670)
* refactor(agents): drop single Artist fields from Song, keep only Artists Song now represents credited artists solely via the Artists slice; the single Artist/ArtistMBID fields and the ArtistList passthrough are removed. Equals continues to hash the whole value. * refactor(lastfm,listenbrainz): build Song.Artists in built-in agents Last.fm and ListenBrainz now populate the Artists slice directly. For ListenBrainz top songs, all credited artist MBIDs are mapped (the combined display name on the first credit plus MBID-only collaborators) instead of keeping only the first MBID, feeding the matcher's per-MBID specificity. * refactor(external): set Song.Artists in top-songs enrichment getMatchingTopSongs now seeds an Artists entry from the known artist when a song carries none, replacing the single Artist/ArtistMBID writes. * refactor(plugins): fold single-artist SongRef into Song.Artists SongRef keeps its single Artist/ArtistMBID fields as part of the plugin wire contract; songRefToAgentSong now folds them into a one-element Artists list when a plugin sends no artists array. * refactor(matcher): read Song.Artists directly groupQueries consumes s.Artists now that ArtistList is gone; test inputs build the Artists slice. * fix(matcher): keep MBID-only artists as identity signals Review follow-up: an artist credited only by MBID (empty ID and name) was dropped before resolution in four places, defeating the multi-MBID matching path this PR adds. - matcher.groupQueries: treat a non-empty MBID as a usable artist signal - external.getMatchingTopSongs: backfill the primary credit's name/MBID when the agent left them empty - plugins.songRefToAgentSong: fold a single-artist SongRef when only ArtistMBID is set (not just when Artist name is set) - listenbrainz.topSongArtists: return nil instead of an empty-name placeholder when neither name nor MBIDs are present * fix(external): only backfill top-song artist onto an unnamed credit Review follow-up (codex P2): the previous backfill stamped the queried artist's MBID onto Artists[0] whenever it was empty, even when that credit already named a different (e.g. featured) artist — producing a mismatched name+MBID pair that could mis-rank matches. Now only an unnamed first credit is filled (it is, by construction, the queried artist); an already-named credit is left untouched. Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
7b7721f002
commit
bd9fa1c602
16 changed files with 305 additions and 224 deletions
|
|
@ -227,26 +227,29 @@ func (a *MetadataAgent) GetSimilarSongsByArtist(ctx context.Context, id, name, m
|
|||
return callSimilarSongsPluginFunction[capabilities.SimilarSongsByArtistRequest](ctx, a.plugin, FuncGetSimilarSongsByArtist, capabilities.SimilarSongsByArtistRequest{ID: id, Name: name, MBID: mbid, Count: int32(count)})
|
||||
}
|
||||
|
||||
// songRefToAgentSong converts a single SongRef to agents.Song
|
||||
// songRefToAgentSong converts a single SongRef to agents.Song. SongRef keeps the single
|
||||
// Artist/ArtistMBID fields as part of the plugin wire contract; when a plugin sends those instead
|
||||
// of the artists array, they are folded into a one-element Artists list here.
|
||||
func songRefToAgentSong(s capabilities.SongRef) agents.Song {
|
||||
var artists []agents.Artist
|
||||
if len(s.Artists) > 0 {
|
||||
switch {
|
||||
case len(s.Artists) > 0:
|
||||
artists = make([]agents.Artist, len(s.Artists))
|
||||
for i, a := range s.Artists {
|
||||
artists[i] = agents.Artist{ID: a.ID, Name: a.Name, MBID: a.MBID}
|
||||
}
|
||||
case s.Artist != "" || s.ArtistMBID != "":
|
||||
artists = []agents.Artist{{Name: s.Artist, MBID: s.ArtistMBID}}
|
||||
}
|
||||
return agents.Song{
|
||||
ID: s.ID,
|
||||
Name: s.Name,
|
||||
MBID: s.MBID,
|
||||
ISRC: s.ISRC,
|
||||
Artist: s.Artist,
|
||||
ArtistMBID: s.ArtistMBID,
|
||||
Artists: artists,
|
||||
Album: s.Album,
|
||||
AlbumMBID: s.AlbumMBID,
|
||||
Duration: uint32(s.Duration * 1000),
|
||||
ID: s.ID,
|
||||
Name: s.Name,
|
||||
MBID: s.MBID,
|
||||
ISRC: s.ISRC,
|
||||
Artists: artists,
|
||||
Album: s.Album,
|
||||
AlbumMBID: s.AlbumMBID,
|
||||
Duration: uint32(s.Duration * 1000),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -119,7 +119,8 @@ var _ = Describe("MetadataAgent", Ordered, func() {
|
|||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(songs).To(HaveLen(3))
|
||||
Expect(songs[0].Name).To(Equal("Similar to Yesterday #1"))
|
||||
Expect(songs[0].Artist).To(Equal("The Beatles"))
|
||||
Expect(songs[0].Artists).To(HaveLen(1))
|
||||
Expect(songs[0].Artists[0].Name).To(Equal("The Beatles"))
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -343,11 +344,18 @@ var _ = Describe("songRefToAgentSong multi-artist", func() {
|
|||
{Name: "Future", MBID: "m-future"},
|
||||
}))
|
||||
})
|
||||
It("leaves Artists nil and keeps the single Artist when no Artists provided", func() {
|
||||
It("folds the single Artist/ArtistMBID into a one-element Artists when no Artists provided", func() {
|
||||
ref := capabilities.SongRef{Name: "Solo", Artist: "Drake", ArtistMBID: "m-drake"}
|
||||
got := songRefToAgentSong(ref)
|
||||
Expect(got.Artists).To(Equal([]agents.Artist{{Name: "Drake", MBID: "m-drake"}}))
|
||||
})
|
||||
It("folds an MBID-only single artist (empty name) so the MBID is not dropped", func() {
|
||||
ref := capabilities.SongRef{Name: "Solo", ArtistMBID: "m-drake"}
|
||||
got := songRefToAgentSong(ref)
|
||||
Expect(got.Artists).To(Equal([]agents.Artist{{MBID: "m-drake"}}))
|
||||
})
|
||||
It("leaves Artists nil when neither Artists nor the single Artist/ArtistMBID are provided", func() {
|
||||
got := songRefToAgentSong(capabilities.SongRef{Name: "Anon"})
|
||||
Expect(got.Artists).To(BeNil())
|
||||
Expect(got.Artist).To(Equal("Drake"))
|
||||
Expect(got.ArtistMBID).To(Equal("m-drake"))
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -43,7 +43,8 @@ var _ = Describe("SonicSimilarityPlugin", Ordered, func() {
|
|||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(results).To(HaveLen(3))
|
||||
Expect(results[0].Song.Name).To(Equal("Similar to Yesterday #1"))
|
||||
Expect(results[0].Song.Artist).To(Equal("The Beatles"))
|
||||
Expect(results[0].Song.Artists).To(HaveLen(1))
|
||||
Expect(results[0].Song.Artists[0].Name).To(Equal("The Beatles"))
|
||||
Expect(results[0].Similarity).To(Equal(1.0))
|
||||
Expect(results[1].Similarity).To(Equal(0.9))
|
||||
Expect(results[2].Similarity).To(Equal(0.8))
|
||||
|
|
@ -68,7 +69,8 @@ var _ = Describe("SonicSimilarityPlugin", Ordered, func() {
|
|||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(results).To(HaveLen(3))
|
||||
Expect(results[0].Song.Name).To(Equal("Path Yesterday to Tomorrow Never Knows #1"))
|
||||
Expect(results[0].Song.Artist).To(Equal("The Beatles"))
|
||||
Expect(results[0].Song.Artists).To(HaveLen(1))
|
||||
Expect(results[0].Song.Artists[0].Name).To(Equal("The Beatles"))
|
||||
Expect(results[0].Similarity).To(Equal(1.0))
|
||||
Expect(results[1].Similarity).To(Equal(0.95))
|
||||
Expect(results[2].Similarity).To(Equal(0.9))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue