From fc55e8bf16b517c124db89d2f70d77cf9477f99a Mon Sep 17 00:00:00 2001 From: Deluan Date: Thu, 23 Jul 2026 14:05:10 -0400 Subject: [PATCH] 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. --- conf/configuration.go | 10 ++++++---- core/artwork/worker.go | 6 +++--- core/artwork/worker_test.go | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/conf/configuration.go b/conf/configuration.go index 57ee4e4bc..9eeda89de 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -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) diff --git a/core/artwork/worker.go b/core/artwork/worker.go index 637af6efb..5ae40a6e7 100644 --- a/core/artwork/worker.go +++ b/core/artwork/worker.go @@ -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 { diff --git a/core/artwork/worker_test.go b/core/artwork/worker_test.go index aebbb31bc..e1b0c985d 100644 --- a/core/artwork/worker_test.go +++ b/core/artwork/worker_test.go @@ -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) })