feat(artwork): promote worker concurrency and external rate to real configs

The artwork worker's drain speed was governed by two hidden Dev flags,
DevArtworkWorkerConcurrency and DevArtworkExternalRPS, both defaulting to 2.
On a large library's one-time backfill the external rate limiter is the real
ceiling: every art-less item waits on it before the (rate-limited) external
lookup, so the drain crawls at ~RPS items/sec while local-art items are
unaffected.

Promote both to documented, supported options: ArtworkWorkerConcurrency
(default 4) sets local-resolution parallelism, ArtworkExternalMaxRPS
(default 2, 0 = unlimited) caps external-agent lookups to stay polite to
Last.fm/Deezer/etc. Operators can now trade first-backfill speed against
external-API rate limits. The old Dev names still map for backward compat.
This commit is contained in:
Deluan 2026-07-23 14:05:10 -04:00
parent 9ce51cf575
commit fc55e8bf16
3 changed files with 10 additions and 8 deletions

View File

@ -57,6 +57,8 @@ type configOptions struct {
ImageCacheSize string
AlbumPlayCountMode string
EnableArtworkPrecache bool
ArtworkWorkerConcurrency int
ArtworkExternalMaxRPS int
AutoImportPlaylists bool
DefaultPlaylistPublicVisibility bool
PlaylistsPath string
@ -139,8 +141,6 @@ type configOptions struct {
DevArtworkThrottleBacklogLimit int
DevArtworkThrottleBacklogTimeout time.Duration
DevArtworkThrottleBuffered bool
DevArtworkWorkerConcurrency int
DevArtworkExternalRPS int
DevArtistInfoTimeToLive time.Duration
DevAlbumInfoTimeToLive time.Duration
DevExternalScanner bool
@ -348,6 +348,8 @@ func Load(noConfigDump bool) {
mapDeprecatedOption("CoverJpegQuality", "CoverArtQuality")
mapDeprecatedOption("SimilarSongsMatchThreshold", "Matcher.FuzzyThreshold")
mapDeprecatedOption("EnableTranscodingCancellation", "Transcoding.EnableCancellation")
mapDeprecatedOption("DevArtworkWorkerConcurrency", "ArtworkWorkerConcurrency")
mapDeprecatedOption("DevArtworkExternalRPS", "ArtworkExternalMaxRPS")
err := viper.Unmarshal(&Server, viper.DecodeHook(
mapstructure.ComposeDecodeHookFunc(
@ -902,8 +904,8 @@ func setViperDefaults() {
viper.SetDefault("devartworkthrottlebackloglimit", consts.RequestThrottleBacklogLimit)
viper.SetDefault("devartworkthrottlebacklogtimeout", consts.RequestThrottleBacklogTimeout)
viper.SetDefault("devartworkthrottlebuffered", true)
viper.SetDefault("devartworkworkerconcurrency", 2)
viper.SetDefault("devartworkexternalrps", 2)
viper.SetDefault("artworkworkerconcurrency", 4)
viper.SetDefault("artworkexternalmaxrps", 2)
viper.SetDefault("devartistinfotimetolive", consts.ArtistInfoTimeToLive)
viper.SetDefault("devalbuminfotimetolive", consts.AlbumInfoTimeToLive)
viper.SetDefault("devexternalscanner", true)

View File

@ -43,8 +43,8 @@ type Worker struct {
}
func NewWorker(ds model.DataStore, store *ImageStore, prov external.Provider, ffmpeg ffmpeg.FFmpeg) *Worker {
rps := conf.Server.DevArtworkExternalRPS
limit := rate.Inf
rps := conf.Server.ArtworkExternalMaxRPS
limit := rate.Inf // 0 or negative disables the external throttle
if rps > 0 {
limit = rate.Limit(rps)
}
@ -64,7 +64,7 @@ func NewWorker(ds model.DataStore, store *ImageStore, prov external.Provider, ff
// leaked goroutines: each drain waits for its batch before the loop can return.
func (w *Worker) Run(ctx context.Context) error {
w.runCtx = ctx
concurrency := max(1, conf.Server.DevArtworkWorkerConcurrency)
concurrency := max(1, conf.Server.ArtworkWorkerConcurrency)
ticker := time.NewTicker(workerPollInterval)
defer ticker.Stop()
for {

View File

@ -85,7 +85,7 @@ var _ = Describe("Worker", func() {
ds.MockedAlbum = tests.CreateMockAlbumRepo()
store = NewImageStore(GinkgoT().TempDir())
conf.Server.CoverArtPriority = "cover.jpg, embedded"
conf.Server.DevArtworkExternalRPS = 1000 // keep the limiter out of the way of behavior tests
conf.Server.ArtworkExternalMaxRPS = 1000 // keep the limiter out of the way of behavior tests
w = NewWorker(ds, store, prov, ffm)
})