diff --git a/server/jellyfin/api.go b/server/jellyfin/api.go index 956023e48..ae78b93c4 100644 --- a/server/jellyfin/api.go +++ b/server/jellyfin/api.go @@ -3,6 +3,7 @@ package jellyfin import ( "encoding/json" "net/http" + "sync" "github.com/go-chi/chi/v5" "github.com/navidrome/navidrome/core" @@ -23,6 +24,8 @@ type Router struct { players core.Players scrobbler scrobbler.PlayTracker playlists playlists.Playlists + serverIDOnce sync.Once + serverIDVal string } func New(ds model.DataStore, artwork artwork.Artwork, streamer stream.MediaStreamer, diff --git a/server/jellyfin/system.go b/server/jellyfin/system.go index 35a1f812b..207e2f366 100644 --- a/server/jellyfin/system.go +++ b/server/jellyfin/system.go @@ -22,18 +22,25 @@ func (api *Router) serverName() string { // the same way core/metrics/insights.go derives its InsightsID. Jellyfin clients cache // ServerId across sessions, so a per-process value would break re-authentication. // api.ds is nil only in unit tests that construct Router{} directly; New() always sets it. +// +// Resolution happens once per Router (sync.Once), so concurrent first-boot requests can't +// each generate and persist a different UUID before the row settles. func (api *Router) serverID(ctx context.Context) string { - if api.ds == nil { - return uuid.NewString() - } - id, err := api.ds.Property(ctx).Get(consts.JellyfinServerIDKey) - if err != nil { - id = uuid.NewString() - if err := api.ds.Property(ctx).Put(consts.JellyfinServerIDKey, id); err != nil { - log.Error(ctx, "Jellyfin API: could not persist server id", err) + api.serverIDOnce.Do(func() { + if api.ds == nil { + api.serverIDVal = uuid.NewString() + return } - } - return id + id, err := api.ds.Property(ctx).Get(consts.JellyfinServerIDKey) + if err != nil { + id = uuid.NewString() + if err := api.ds.Property(ctx).Put(consts.JellyfinServerIDKey, id); err != nil { + log.Error(ctx, "Jellyfin API: could not persist server id", err) + } + } + api.serverIDVal = id + }) + return api.serverIDVal } func (api *Router) publicInfo(ctx context.Context) dto.PublicSystemInfo { diff --git a/server/jellyfin/system_test.go b/server/jellyfin/system_test.go index a1aa4a353..7f82f5159 100644 --- a/server/jellyfin/system_test.go +++ b/server/jellyfin/system_test.go @@ -1,11 +1,13 @@ package jellyfin import ( + "context" "encoding/json" "net/http" "net/http/httptest" "github.com/navidrome/navidrome/server/jellyfin/dto" + "github.com/navidrome/navidrome/tests" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -49,4 +51,30 @@ var _ = Describe("System", func() { Expect(json.Unmarshal(w.Body.Bytes(), &enabled)).To(Succeed()) Expect(enabled).To(BeFalse()) }) + + Context("serverID with a real DataStore", func() { + var ctx context.Context + var ds *tests.MockDataStore + + BeforeEach(func() { + ctx = context.Background() + ds = &tests.MockDataStore{} + }) + + It("persists the generated id so it can be read back by another Router sharing the same DataStore", func() { + first := &Router{ds: ds} + id := first.serverID(ctx) + Expect(id).ToNot(BeEmpty()) + + second := &Router{ds: ds} + Expect(second.serverID(ctx)).To(Equal(id)) + }) + + It("memoizes the id across repeated calls on the same Router", func() { + r := &Router{ds: ds} + id := r.serverID(ctx) + Expect(r.serverID(ctx)).To(Equal(id)) + Expect(r.serverID(ctx)).To(Equal(id)) + }) + }) })