Files
navidrome/model/metadata/persistent_ids_test.go
Tom Boucher 356b0716b6 fix(scanner): exclude Vorbis VERSION from albumversion tag mapping (#5194)
The Vorbis/FLAC VERSION field is for track-level disambiguation (e.g.
remix, live, 30s edit), not album versioning. Including it in the
albumversion aliases caused albums to split incorrectly when tracks
had different VERSION values and no MusicBrainz Album ID was set.

Remove 'version' from the albumversion aliases in mappings.yaml.
Users who want the old behavior can re-add it via Tags config.

Update the PID test to use 'albumversion' directly instead of
'version' as the raw PID attribute, with a realistic value.

Fixes #5082

Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2026-03-23 18:32:05 -04:00

291 lines
9.6 KiB
Go

package metadata
import (
"strings"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
"github.com/navidrome/navidrome/model"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("getPID", func() {
var (
md Metadata
mf model.MediaFile
sum hashFunc
getPID getPIDFunc
)
BeforeEach(func() {
sum = func(s ...string) string { return "(" + strings.Join(s, ",") + ")" }
getPID = createGetPID(sum)
})
Context("attributes are tags", func() {
spec := "musicbrainz_trackid|album,discnumber,tracknumber"
When("no attributes were present", func() {
It("should return empty pid", func() {
md.tags = map[model.TagName][]string{}
pid := getPID(mf, md, spec, false)
Expect(pid).To(Equal("()"))
})
})
When("all fields are present", func() {
It("should return the pid", func() {
md.tags = map[model.TagName][]string{
"musicbrainz_trackid": {"mbtrackid"},
"album": {"album name"},
"discnumber": {"1"},
"tracknumber": {"1"},
}
Expect(getPID(mf, md, spec, false)).To(Equal("(mbtrackid)"))
})
})
When("only first field is present", func() {
It("should return the pid", func() {
md.tags = map[model.TagName][]string{
"musicbrainz_trackid": {"mbtrackid"},
}
Expect(getPID(mf, md, spec, false)).To(Equal("(mbtrackid)"))
})
})
When("first is empty, but second field is present", func() {
It("should return the pid", func() {
md.tags = map[model.TagName][]string{
"album": {"album name"},
"discnumber": {"1"},
}
Expect(getPID(mf, md, spec, false)).To(Equal("(album name\\1\\)"))
})
})
})
Context("calculated attributes", func() {
BeforeEach(func() {
DeferCleanup(configtest.SetupConfig())
conf.Server.PID.Album = "musicbrainz_albumid|albumartistid,album,albumversion,releasedate"
})
When("field is title", func() {
It("should return the pid", func() {
spec := "title|folder"
md.tags = map[model.TagName][]string{"title": {"title"}}
md.filePath = "/path/to/file.mp3"
mf.Title = "Title"
Expect(getPID(mf, md, spec, false)).To(Equal("(Title)"))
})
})
When("field is folder", func() {
It("should return the pid", func() {
spec := "folder|title"
md.tags = map[model.TagName][]string{"title": {"title"}}
mf.Path = "/path/to/file.mp3"
Expect(getPID(mf, md, spec, false)).To(Equal("(/path/to)"))
})
})
When("field is albumid", func() {
It("should return the pid", func() {
spec := "albumid|title"
md.tags = map[model.TagName][]string{
"title": {"title"},
"album": {"album name"},
"albumversion": {"deluxe edition"},
"releasedate": {"2021-01-01"},
}
mf.AlbumArtist = "Album Artist"
Expect(getPID(mf, md, spec, false)).To(Equal("(((album artist)\\album name\\deluxe edition\\2021-01-01))"))
})
})
When("field is albumartistid", func() {
It("should return the pid", func() {
spec := "musicbrainz_albumartistid|albumartistid"
md.tags = map[model.TagName][]string{
"albumartist": {"Album Artist"},
}
mf.AlbumArtist = "Album Artist"
Expect(getPID(mf, md, spec, false)).To(Equal("((album artist))"))
})
})
When("field is album", func() {
It("should return the pid", func() {
spec := "album|title"
md.tags = map[model.TagName][]string{"album": {"Album Name"}}
Expect(getPID(mf, md, spec, false)).To(Equal("(album name)"))
})
})
When("albumid configuration refers to albumid recursively", func() {
It("should avoid infinite recursion", func() {
// Reproduce the issue from #4920
conf.Server.PID.Album = "albumid,album,albumversion,releasedate"
spec := conf.Server.PID.Album
md.tags = map[model.TagName][]string{
"album": {"Album Name"},
"albumversion": {"Version"},
"releasedate": {"2022"},
}
// Should not panic and return a valid PID ignoring the recursive "albumid"
Expect(func() {
pid := getPID(mf, md, spec, false)
Expect(pid).To(Equal("(\\album name\\Version\\2022)"))
}).To(Not(Panic()))
})
})
})
Context("edge cases", func() {
When("the spec has spaces between groups", func() {
It("should return the pid", func() {
spec := "albumartist| Album"
md.tags = map[model.TagName][]string{
"album": {"album name"},
}
Expect(getPID(mf, md, spec, false)).To(Equal("(album name)"))
})
})
When("the spec has spaces", func() {
It("should return the pid", func() {
spec := "albumartist, album"
md.tags = map[model.TagName][]string{
"albumartist": {"Album Artist"},
"album": {"album name"},
}
Expect(getPID(mf, md, spec, false)).To(Equal("(Album Artist\\album name)"))
})
})
When("the spec has mixed case fields", func() {
It("should return the pid", func() {
spec := "albumartist,Album"
md.tags = map[model.TagName][]string{
"albumartist": {"Album Artist"},
"album": {"album name"},
}
Expect(getPID(mf, md, spec, false)).To(Equal("(Album Artist\\album name)"))
})
})
})
Context("prependLibId functionality", func() {
BeforeEach(func() {
mf.LibraryID = 42
})
When("prependLibId is true", func() {
It("should prepend library ID to the hash input", func() {
spec := "album"
md.tags = map[model.TagName][]string{"album": {"Test Album"}}
pid := getPID(mf, md, spec, true)
// The hash function should receive "42\test album" as input
Expect(pid).To(Equal("(42\\test album)"))
})
})
When("prependLibId is false", func() {
It("should not prepend library ID to the hash input", func() {
spec := "album"
md.tags = map[model.TagName][]string{"album": {"Test Album"}}
pid := getPID(mf, md, spec, false)
// The hash function should receive "test album" as input
Expect(pid).To(Equal("(test album)"))
})
})
When("prependLibId is true with complex spec", func() {
It("should prepend library ID to the final hash input", func() {
spec := "musicbrainz_trackid|album,tracknumber"
md.tags = map[model.TagName][]string{
"album": {"Test Album"},
"tracknumber": {"1"},
}
pid := getPID(mf, md, spec, true)
// Should use the fallback field and prepend library ID
Expect(pid).To(Equal("(42\\test album\\1)"))
})
})
When("prependLibId is true with nested albumid", func() {
It("should handle nested albumid calls correctly", func() {
DeferCleanup(configtest.SetupConfig())
conf.Server.PID.Album = "album"
spec := "albumid"
md.tags = map[model.TagName][]string{"album": {"Test Album"}}
mf.AlbumArtist = "Test Artist"
pid := getPID(mf, md, spec, true)
// The albumid call should also use prependLibId=true
Expect(pid).To(Equal("(42\\(42\\test album))"))
})
})
})
Context("legacy specs", func() {
Context("track_legacy", func() {
When("library ID is default (1)", func() {
It("should not prepend library ID even when prependLibId is true", func() {
mf.Path = "/path/to/track.mp3"
mf.LibraryID = 1 // Default library ID
// With default library, both should be the same
pidTrue := getPID(mf, md, "track_legacy", true)
pidFalse := getPID(mf, md, "track_legacy", false)
Expect(pidTrue).To(Equal(pidFalse))
Expect(pidTrue).NotTo(BeEmpty())
})
})
When("library ID is non-default", func() {
It("should prepend library ID when prependLibId is true", func() {
mf.Path = "/path/to/track.mp3"
mf.LibraryID = 2 // Non-default library ID
pidTrue := getPID(mf, md, "track_legacy", true)
pidFalse := getPID(mf, md, "track_legacy", false)
Expect(pidTrue).NotTo(Equal(pidFalse))
Expect(pidTrue).NotTo(BeEmpty())
Expect(pidFalse).NotTo(BeEmpty())
})
})
When("library ID is non-default but prependLibId is false", func() {
It("should not prepend library ID", func() {
mf.Path = "/path/to/track.mp3"
mf.LibraryID = 3
mf2 := mf
mf2.LibraryID = 1 // Default library
pidNonDefault := getPID(mf, md, "track_legacy", false)
pidDefault := getPID(mf2, md, "track_legacy", false)
// Should be the same since prependLibId=false
Expect(pidNonDefault).To(Equal(pidDefault))
})
})
})
Context("album_legacy", func() {
When("library ID is default (1)", func() {
It("should not prepend library ID even when prependLibId is true", func() {
md.tags = map[model.TagName][]string{"album": {"Test Album"}}
mf.LibraryID = 1 // Default library ID
pidTrue := getPID(mf, md, "album_legacy", true)
pidFalse := getPID(mf, md, "album_legacy", false)
Expect(pidTrue).To(Equal(pidFalse))
Expect(pidTrue).NotTo(BeEmpty())
})
})
When("library ID is non-default", func() {
It("should prepend library ID when prependLibId is true", func() {
md.tags = map[model.TagName][]string{"album": {"Test Album"}}
mf.LibraryID = 2 // Non-default library ID
pidTrue := getPID(mf, md, "album_legacy", true)
pidFalse := getPID(mf, md, "album_legacy", false)
Expect(pidTrue).NotTo(Equal(pidFalse))
Expect(pidTrue).NotTo(BeEmpty())
Expect(pidFalse).NotTo(BeEmpty())
})
})
When("library ID is non-default but prependLibId is false", func() {
It("should not prepend library ID", func() {
md.tags = map[model.TagName][]string{"album": {"Test Album"}}
mf.LibraryID = 3
mf2 := mf
mf2.LibraryID = 1 // Default library
pidNonDefault := getPID(mf, md, "album_legacy", false)
pidDefault := getPID(mf2, md, "album_legacy", false)
// Should be the same since prependLibId=false
Expect(pidNonDefault).To(Equal(pidDefault))
})
})
})
})
})