navidrome/core/agents/interfaces_test.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

27 lines
724 B
Go
Raw Permalink Normal View History

feat(matcher): match similar/top songs by multiple artists (#5668) * feat(matcher): add Song.Artists (agents.Artist) and field-wise song dedup * refactor(matcher): make song equality an agents.Song.Equals method via hashstructure Move the sameSong free function from core/matcher into an Equals method on agents.Song, following the model.MediaFile/Album.Equals convention. Uses strict hashstructure hashing (nil opts, no IgnoreZeroValue) to preserve the original whole-value equality contract. Tests moved to core/agents. * feat(matcher): match by multiple artists with overlap ranking and artist-ID fast-path * refactor(matcher): rank artist overlap and specificity above the preferred-track flag Identity signals (specificityLevel, artistOverlap) now outrank the taste signal (preferredMatch) in betterThan. A starred/4-star track that is a worse identity match no longer beats a more specific or higher-overlap track. PreferStarred still breaks ties when specificity and overlap are equal. * fix(matcher): score artist-MBID specificity against all credited artists, not just the last sanitizedTrack.artistMBID (string) replaced with artistMBIDs (map[string]struct{}) so bucketTracks collects all credited owned MBIDs per query instead of last-write-wins. computeSpecificityLevel tests set membership, letting each of a collaboration's MBID-bearing artists reach the proper specificity level (4/5) independently. * feat(plugins): carry multiple artists (with IDs) through SongRef conversions * feat(plugins): regenerate schemas and PDK wrappers for multi-artist SongRef * refactor(matcher): tidy bucketTracks accumulator and artist resolution Replace bucketTracks' two parallel per-track maps (overlapByQuery/mbidsByQuery) with a named queryAccum struct (F2). Collapse resolveArtists' four hand-mutated parallel maps into a pendingArtist slice with derived nameToQueries/mbidToQueries maps (F1). Replace the own() method on resolvedArtists with a package-level addToSet helper that drops the method/receiver indirection (F3). * docs(matcher): trim comments that restate the code * fix(plugins): use Vec::is_empty for slice fields in generated Rust PDK * fix(matcher): treat a resolved artist ID as an identity match for specificity * docs(matcher): reflect artist-ID identity in the specificity ladder
2026-06-26 14:46:09 -04:00
package agents
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Song.Equals", func() {
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>
2026-06-26 17:06:14 -04:00
base := Song{ID: "1", Name: "S", Artists: []Artist{{ID: "x", Name: "A"}}}
feat(matcher): match similar/top songs by multiple artists (#5668) * feat(matcher): add Song.Artists (agents.Artist) and field-wise song dedup * refactor(matcher): make song equality an agents.Song.Equals method via hashstructure Move the sameSong free function from core/matcher into an Equals method on agents.Song, following the model.MediaFile/Album.Equals convention. Uses strict hashstructure hashing (nil opts, no IgnoreZeroValue) to preserve the original whole-value equality contract. Tests moved to core/agents. * feat(matcher): match by multiple artists with overlap ranking and artist-ID fast-path * refactor(matcher): rank artist overlap and specificity above the preferred-track flag Identity signals (specificityLevel, artistOverlap) now outrank the taste signal (preferredMatch) in betterThan. A starred/4-star track that is a worse identity match no longer beats a more specific or higher-overlap track. PreferStarred still breaks ties when specificity and overlap are equal. * fix(matcher): score artist-MBID specificity against all credited artists, not just the last sanitizedTrack.artistMBID (string) replaced with artistMBIDs (map[string]struct{}) so bucketTracks collects all credited owned MBIDs per query instead of last-write-wins. computeSpecificityLevel tests set membership, letting each of a collaboration's MBID-bearing artists reach the proper specificity level (4/5) independently. * feat(plugins): carry multiple artists (with IDs) through SongRef conversions * feat(plugins): regenerate schemas and PDK wrappers for multi-artist SongRef * refactor(matcher): tidy bucketTracks accumulator and artist resolution Replace bucketTracks' two parallel per-track maps (overlapByQuery/mbidsByQuery) with a named queryAccum struct (F2). Collapse resolveArtists' four hand-mutated parallel maps into a pendingArtist slice with derived nameToQueries/mbidToQueries maps (F1). Replace the own() method on resolvedArtists with a package-level addToSet helper that drops the method/receiver indirection (F3). * docs(matcher): trim comments that restate the code * fix(plugins): use Vec::is_empty for slice fields in generated Rust PDK * fix(matcher): treat a resolved artist ID as an identity match for specificity * docs(matcher): reflect artist-ID identity in the specificity ladder
2026-06-26 14:46:09 -04:00
It("true for identical songs incl Artists", func() {
Expect(base.Equals(base)).To(BeTrue())
})
It("false when Artists differ", func() {
other := base
other.Artists = []Artist{{ID: "y", Name: "B"}}
Expect(base.Equals(other)).To(BeFalse())
})
It("false when a scalar differs", func() {
other := base
other.Name = "T"
Expect(base.Equals(other)).To(BeFalse())
})
It("true when both have empty Artists and equal scalars", func() {
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>
2026-06-26 17:06:14 -04:00
a := Song{ID: "1", Name: "S"}
feat(matcher): match similar/top songs by multiple artists (#5668) * feat(matcher): add Song.Artists (agents.Artist) and field-wise song dedup * refactor(matcher): make song equality an agents.Song.Equals method via hashstructure Move the sameSong free function from core/matcher into an Equals method on agents.Song, following the model.MediaFile/Album.Equals convention. Uses strict hashstructure hashing (nil opts, no IgnoreZeroValue) to preserve the original whole-value equality contract. Tests moved to core/agents. * feat(matcher): match by multiple artists with overlap ranking and artist-ID fast-path * refactor(matcher): rank artist overlap and specificity above the preferred-track flag Identity signals (specificityLevel, artistOverlap) now outrank the taste signal (preferredMatch) in betterThan. A starred/4-star track that is a worse identity match no longer beats a more specific or higher-overlap track. PreferStarred still breaks ties when specificity and overlap are equal. * fix(matcher): score artist-MBID specificity against all credited artists, not just the last sanitizedTrack.artistMBID (string) replaced with artistMBIDs (map[string]struct{}) so bucketTracks collects all credited owned MBIDs per query instead of last-write-wins. computeSpecificityLevel tests set membership, letting each of a collaboration's MBID-bearing artists reach the proper specificity level (4/5) independently. * feat(plugins): carry multiple artists (with IDs) through SongRef conversions * feat(plugins): regenerate schemas and PDK wrappers for multi-artist SongRef * refactor(matcher): tidy bucketTracks accumulator and artist resolution Replace bucketTracks' two parallel per-track maps (overlapByQuery/mbidsByQuery) with a named queryAccum struct (F2). Collapse resolveArtists' four hand-mutated parallel maps into a pendingArtist slice with derived nameToQueries/mbidToQueries maps (F1). Replace the own() method on resolvedArtists with a package-level addToSet helper that drops the method/receiver indirection (F3). * docs(matcher): trim comments that restate the code * fix(plugins): use Vec::is_empty for slice fields in generated Rust PDK * fix(matcher): treat a resolved artist ID as an identity match for specificity * docs(matcher): reflect artist-ID identity in the specificity ladder
2026-06-26 14:46:09 -04:00
Expect(a.Equals(a)).To(BeTrue())
})
})