mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
refactor(scanner): consolidate split-exception wiring and drop hot-path lock
ArtistSplitExceptionsRx is called per tag mapping per scanned file across concurrent goroutines; replace the mutex+joined-key cache with an atomic pointer compared via slices.Equal. Route all participant call sites through WithParticipantExceptions and a shared splitParticipantValues helper.
This commit is contained in:
parent
b3a3d3b2f1
commit
3b41bab94c
@ -94,15 +94,11 @@ func (md Metadata) processPerformers(participants model.Participants, rolesMbzId
|
||||
roleIdx[role] = 0
|
||||
}
|
||||
|
||||
conf := model.TagRolesConf()
|
||||
conf.ExceptionsRx = model.ArtistSplitExceptionsRx()
|
||||
conf := model.TagRolesConf().WithParticipantExceptions(model.TagPerformer)
|
||||
titleCaser := cases.Title(language.Und)
|
||||
for _, performer := range md.Pairs(model.TagPerformer) {
|
||||
subRole := titleCaser.String(performer.Key())
|
||||
names := []string{performer.Value()}
|
||||
if len(conf.Split) > 0 {
|
||||
names = filterDuplicatedOrEmptyValues(conf.SplitTagValue(names))
|
||||
}
|
||||
names := splitParticipantValues(conf, []string{performer.Value()})
|
||||
for _, name := range names {
|
||||
artist := model.Artist{
|
||||
ID: md.artistID(name),
|
||||
@ -177,6 +173,16 @@ func (md Metadata) buildArtists(names, sorts, mbids []string) []model.Artist {
|
||||
return artists
|
||||
}
|
||||
|
||||
// splitParticipantValues splits values by the conf separators, dropping
|
||||
// duplicated or empty entries. Values are returned unchanged when the conf
|
||||
// has no separators.
|
||||
func splitParticipantValues(conf model.TagConf, values []string) []string {
|
||||
if len(conf.Split) == 0 {
|
||||
return values
|
||||
}
|
||||
return filterDuplicatedOrEmptyValues(conf.SplitTagValue(values))
|
||||
}
|
||||
|
||||
// getRoleValues returns the values of a role tag, splitting them if necessary
|
||||
func (md Metadata) getRoleValues(role model.TagName) []string {
|
||||
values := md.Strings(role)
|
||||
@ -187,12 +193,8 @@ func (md Metadata) getRoleValues(role model.TagName) []string {
|
||||
if conf.Split == nil {
|
||||
conf = model.TagRolesConf()
|
||||
}
|
||||
if len(conf.Split) > 0 {
|
||||
conf.ExceptionsRx = model.ArtistSplitExceptionsRx()
|
||||
values = conf.SplitTagValue(values)
|
||||
return filterDuplicatedOrEmptyValues(values)
|
||||
}
|
||||
return values
|
||||
conf = conf.WithParticipantExceptions(role)
|
||||
return splitParticipantValues(conf, values)
|
||||
}
|
||||
|
||||
// getArtistValues returns the values of a single or multi artist tag, splitting them if necessary
|
||||
@ -209,12 +211,8 @@ func (md Metadata) getArtistValues(single, multi model.TagName) []string {
|
||||
if conf.Split == nil {
|
||||
conf = model.TagArtistsConf()
|
||||
}
|
||||
if len(conf.Split) > 0 {
|
||||
conf.ExceptionsRx = model.ArtistSplitExceptionsRx()
|
||||
vSingle = conf.SplitTagValue(vSingle)
|
||||
return filterDuplicatedOrEmptyValues(vSingle)
|
||||
}
|
||||
return vSingle
|
||||
conf = conf.WithParticipantExceptions(single)
|
||||
return splitParticipantValues(conf, vSingle)
|
||||
}
|
||||
|
||||
func (md Metadata) mapDisplayName(singularTagName, pluralTagName model.TagName) string {
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
@ -129,25 +130,25 @@ func compileExceptionsRegex(exceptions []string) *regexp.Regexp {
|
||||
return rx
|
||||
}
|
||||
|
||||
var artistSplitExceptions struct {
|
||||
sync.Mutex
|
||||
key string
|
||||
rx *regexp.Regexp
|
||||
type artistSplitExceptionsCache struct {
|
||||
names []string
|
||||
rx *regexp.Regexp
|
||||
}
|
||||
|
||||
var artistSplitExceptions atomic.Pointer[artistSplitExceptionsCache]
|
||||
|
||||
// ArtistSplitExceptionsRx returns the regex for Scanner.ArtistSplitExceptions,
|
||||
// or nil if none are configured. Compiled lazily (config hooks only run once
|
||||
// per process, before tests can override the option) and cached until the
|
||||
// configured list changes.
|
||||
// configured list changes. Lock-free on the cache-hit path, as this is called
|
||||
// per tag mapping per scanned file, across concurrent scanner goroutines.
|
||||
func ArtistSplitExceptionsRx() *regexp.Regexp {
|
||||
c := &artistSplitExceptions
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
key := strings.Join(conf.Server.Scanner.ArtistSplitExceptions, "\x00")
|
||||
if c.key != key {
|
||||
c.key = key
|
||||
c.rx = compileExceptionsRegex(conf.Server.Scanner.ArtistSplitExceptions)
|
||||
names := conf.Server.Scanner.ArtistSplitExceptions
|
||||
if c := artistSplitExceptions.Load(); c != nil && slices.Equal(c.names, names) {
|
||||
return c.rx
|
||||
}
|
||||
c := &artistSplitExceptionsCache{names: slices.Clone(names), rx: compileExceptionsRegex(names)}
|
||||
artistSplitExceptions.Store(c)
|
||||
return c.rx
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user