From 63ae6f6d4b17747fe982fd2d5b14c487ecb062ac Mon Sep 17 00:00:00 2001 From: Deluan Date: Wed, 1 Jul 2026 22:01:24 -0400 Subject: [PATCH] fix(scanner): apply split exceptions when per-tag Split overrides participant tags Per-tag Tags..Split makes the generic ingestion path split the tag before participant mapping runs, bypassing the whitelist. Attach the exceptions to participant tag mappings (including sort variants) in clean(). --- model/metadata/metadata.go | 1 + model/tag_mappings.go | 27 +++++++++++++++++++++++++++ model/tag_mappings_test.go | 19 +++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/model/metadata/metadata.go b/model/metadata/metadata.go index 48928f989..729e83564 100644 --- a/model/metadata/metadata.go +++ b/model/metadata/metadata.go @@ -205,6 +205,7 @@ func clean(filePath string, tags model.RawTags) model.Tags { cleaned := make(model.Tags, len(mappings)) for name, mapping := range mappings { + mapping = mapping.WithParticipantExceptions(name) var values []string switch mapping.Type { case model.TagTypePair: diff --git a/model/tag_mappings.go b/model/tag_mappings.go index 7f293edbe..9f7bb915f 100644 --- a/model/tag_mappings.go +++ b/model/tag_mappings.go @@ -151,6 +151,33 @@ func ArtistSplitExceptionsRx() *regexp.Regexp { return c.rx } +// participantTagNames are the tags that hold artist names (or their sort +// values), where split exceptions apply. +var participantTagNames = sync.OnceValue(func() map[TagName]struct{} { + names := []TagName{ + TagTrackArtist, TagTrackArtists, TagTrackArtistSort, TagTrackArtistsSort, + TagAlbumArtist, TagAlbumArtists, TagAlbumArtistSort, TagAlbumArtistsSort, + } + set := make(map[TagName]struct{}, len(names)+2*len(AllRoles)) + for _, n := range names { + set[n] = struct{}{} + } + for role := range AllRoles { + set[TagName(role)] = struct{}{} + set[TagName(role+"sort")] = struct{}{} + } + return set +}) + +// WithParticipantExceptions returns the conf with the global artist split +// exceptions attached when name is a participant (artist/role) tag. +func (c TagConf) WithParticipantExceptions(name TagName) TagConf { + if _, ok := participantTagNames()[name]; ok { + c.ExceptionsRx = ArtistSplitExceptionsRx() + } + return c +} + type TagType string const ( diff --git a/model/tag_mappings_test.go b/model/tag_mappings_test.go index 2d66ba48d..c1eb4afba 100644 --- a/model/tag_mappings_test.go +++ b/model/tag_mappings_test.go @@ -172,4 +172,23 @@ var _ = Describe("TagConf", func() { Expect(second.MatchString("AC/DC")).To(BeTrue()) }) }) + + Describe("WithParticipantExceptions", func() { + BeforeEach(func() { + DeferCleanup(configtest.SetupConfig()) + conf.Server.Scanner.ArtistSplitExceptions = []string{"Iron and Wine"} + }) + + It("attaches the exceptions regex to participant tags", func() { + for _, tag := range []TagName{"artist", "albumartist", "artists", "artistsort", "composer", "lyricist", "composersort"} { + Expect(TagConf{}.WithParticipantExceptions(tag).ExceptionsRx).ToNot(BeNil(), string(tag)) + } + }) + + It("does not attach the exceptions regex to non-participant tags", func() { + for _, tag := range []TagName{"genre", "mood", "title", "releasetype"} { + Expect(TagConf{}.WithParticipantExceptions(tag).ExceptionsRx).To(BeNil(), string(tag)) + } + }) + }) })