feat(scanner): add Scanner.ArtistSplitExceptions config option

This commit is contained in:
Deluan 2026-07-01 21:39:27 -04:00
parent 10de3b9bc5
commit 2b0fc5762a
3 changed files with 66 additions and 11 deletions

View File

@ -153,17 +153,18 @@ type configOptions struct {
}
type scannerOptions struct {
Enabled bool
Schedule string
WatcherWait time.Duration
ScanOnStartup bool
Extractor string
ArtistJoiner string
GenreSeparators string // Deprecated: Use Tags.genre.Split instead
GroupAlbumReleases bool // Deprecated: Use PID.Album instead
FollowSymlinks bool // Whether to follow symlinks when scanning directories
IgnoreDotFolders bool // Whether to ignore folders whose name starts with a dot when scanning
PurgeMissing string // Values: "never", "always", "full"
Enabled bool
Schedule string
WatcherWait time.Duration
ScanOnStartup bool
Extractor string
ArtistJoiner string
ArtistSplitExceptions []string // Artist names never split by tag separators
GenreSeparators string // Deprecated: Use Tags.genre.Split instead
GroupAlbumReleases bool // Deprecated: Use PID.Album instead
FollowSymlinks bool // Whether to follow symlinks when scanning directories
IgnoreDotFolders bool // Whether to ignore folders whose name starts with a dot when scanning
PurgeMissing string // Values: "never", "always", "full"
}
type transcodingOptions struct {
@ -819,6 +820,7 @@ func setViperDefaults() {
viper.SetDefault("scanner.watcherwait", consts.DefaultWatcherWait)
viper.SetDefault("scanner.scanonstartup", true)
viper.SetDefault("scanner.artistjoiner", consts.ArtistJoiner)
viper.SetDefault("scanner.artistsplitexceptions", []string{})
viper.SetDefault("scanner.genreseparators", "")
viper.SetDefault("scanner.groupalbumreleases", false)
viper.SetDefault("scanner.followsymlinks", true)

View File

@ -129,6 +129,28 @@ func compileExceptionsRegex(exceptions []string) *regexp.Regexp {
return rx
}
var artistSplitExceptions struct {
sync.Mutex
key string
rx *regexp.Regexp
}
// 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.
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)
}
return c.rx
}
type TagType string
const (

View File

@ -1,6 +1,8 @@
package model
import (
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@ -141,4 +143,33 @@ var _ = Describe("TagConf", func() {
Expect(rx.MatchString("IRON AND WINE")).To(BeTrue())
})
})
Describe("ArtistSplitExceptionsRx", func() {
BeforeEach(func() {
DeferCleanup(configtest.SetupConfig())
})
It("returns nil when no exceptions are configured", func() {
conf.Server.Scanner.ArtistSplitExceptions = nil
Expect(ArtistSplitExceptionsRx()).To(BeNil())
})
It("compiles the configured exceptions", func() {
conf.Server.Scanner.ArtistSplitExceptions = []string{"Iron and Wine"}
rx := ArtistSplitExceptionsRx()
Expect(rx).ToNot(BeNil())
Expect(rx.MatchString("iron and wine")).To(BeTrue())
})
It("caches the compiled regex until the configuration changes", func() {
conf.Server.Scanner.ArtistSplitExceptions = []string{"Iron and Wine"}
first := ArtistSplitExceptionsRx()
Expect(ArtistSplitExceptionsRx()).To(BeIdenticalTo(first))
conf.Server.Scanner.ArtistSplitExceptions = []string{"AC/DC"}
second := ArtistSplitExceptionsRx()
Expect(second).ToNot(BeIdenticalTo(first))
Expect(second.MatchString("AC/DC")).To(BeTrue())
})
})
})