diff --git a/server/jellyfin/README.md b/server/jellyfin/README.md index 2e4c19950..8e0e08ce3 100644 --- a/server/jellyfin/README.md +++ b/server/jellyfin/README.md @@ -115,7 +115,7 @@ favorites-only (`Filters=IsFavorite` or the standalone `isFavorite=true`); `Sort | Area | Endpoints | |---|---| -| Handshake / system | `GET System/Info/Public`, `GET`/`POST System/Ping`, `GET QuickConnect/Enabled` | +| Handshake / system | `GET System/Info/Public`, `GET System/Info` (authenticated), `GET`/`POST System/Ping`, `GET QuickConnect/Enabled` | | Auth | `POST Users/AuthenticateByName`, `GET Users/Public` | | Users | `GET UserViews`, `GET Users/{userId}/Views`, `GET Users/Me`, `GET Users/{userId}` | | Browsing | `GET Items`, `GET Users/{userId}/Items`, `GET Items/{itemId}`, `GET Users/{userId}/Items/{itemId}`, `GET Users/{userId}/Items/Latest`, `DELETE Items/{itemId}` (playlists only) | diff --git a/server/jellyfin/api.go b/server/jellyfin/api.go index 0740901aa..3aa870704 100644 --- a/server/jellyfin/api.go +++ b/server/jellyfin/api.go @@ -92,6 +92,7 @@ func (api *Router) routes() http.Handler { // Subsonic's getPlayer, so Jellyfin clients show up in the players list (and scrobbling has a // player) even before the first playback report. r.Use(api.withPlayer) + r.Get("/system/info", api.getSystemInfo) r.Get("/userviews", api.getUserViews) r.Get("/users/{userId}/views", api.getUserViews) r.Get("/users/me", api.getCurrentUser) diff --git a/server/jellyfin/e2e/system_test.go b/server/jellyfin/e2e/system_test.go index d4d2f2777..c6f9145d5 100644 --- a/server/jellyfin/e2e/system_test.go +++ b/server/jellyfin/e2e/system_test.go @@ -30,6 +30,27 @@ var _ = Describe("System", func() { }) }) + Describe("GET /System/Info", func() { + It("returns system info to any authenticated user, matching the public Version", func() { + w := getAs(regularUser, "/System/Info") + var info map[string]any + parseInto(w, &info) + Expect(info["Version"]).ToNot(BeEmpty()) + Expect(info["SupportsLibraryMonitor"]).To(BeTrue()) + + pub := rawReq("GET", "/System/Info/Public", "") + var pubInfo map[string]any + parseInto(pub, &pubInfo) + Expect(info["Version"]).To(Equal(pubInfo["Version"])) + Expect(info["Id"]).To(Equal(pubInfo["Id"])) + }) + + It("rejects unauthenticated requests", func() { + w := rawReq("GET", "/System/Info", "") + Expect(w.Code).To(Equal(http.StatusUnauthorized)) + }) + }) + Describe("GET/POST /System/Ping", func() { It("answers GET with a plain-text server name", func() { w := rawReq("GET", "/System/Ping", "") diff --git a/server/jellyfin/system.go b/server/jellyfin/system.go index baa31fdeb..7e539be98 100644 --- a/server/jellyfin/system.go +++ b/server/jellyfin/system.go @@ -83,6 +83,13 @@ func (api *Router) getPublicSystemInfo(w http.ResponseWriter, r *http.Request) { api.ok(w, r, api.publicInfo(r)) } +func (api *Router) getSystemInfo(w http.ResponseWriter, r *http.Request) { + api.ok(w, r, dto.SystemInfo{ + PublicSystemInfo: api.publicInfo(r), + SupportsLibraryMonitor: true, + }) +} + // ping answers /System/Ping with a bare plain-text server name (not JSON-quoted): Jellyfin's // server does this and clients parse the raw body. func (api *Router) ping(w http.ResponseWriter, r *http.Request) { diff --git a/server/jellyfin/system_test.go b/server/jellyfin/system_test.go index 5ea525100..7846e4a20 100644 --- a/server/jellyfin/system_test.go +++ b/server/jellyfin/system_test.go @@ -38,6 +38,27 @@ var _ = Describe("System", func() { Expect(info.ServerName).To(HavePrefix("Navidrome")) }) + It("returns authenticated system info with the public fields plus library monitor support", func() { + DeferCleanup(configtest.SetupConfig()) + conf.Server.Jellyfin.ServerName = "" + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/System/Info", nil) + api.getSystemInfo(w, r) + + Expect(w.Code).To(Equal(http.StatusOK)) + Expect(w.Header().Get("Content-Type")).To(ContainSubstring("application/json")) + var info dto.SystemInfo + Expect(json.Unmarshal(w.Body.Bytes(), &info)).To(Succeed()) + Expect(info.Id).ToNot(BeEmpty()) + Expect(info.Version).To(Equal(jellyfinVersion)) + Expect(info.ProductName).To(Equal("Jellyfin Server")) + Expect(info.ServerName).To(HavePrefix("Navidrome")) + Expect(info.SupportsLibraryMonitor).To(BeTrue()) + Expect(info.HasPendingRestart).To(BeFalse()) + Expect(info.IsShuttingDown).To(BeFalse()) + }) + It("advertises a LocalAddress with the request scheme, host and Jellyfin base path", func() { DeferCleanup(configtest.SetupConfig())