From cb396f3dba3084ab3d581c5f7c89c353291d5d44 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sun, 22 Mar 2026 14:54:28 -0400 Subject: [PATCH 1/6] feat(ui): increase cover art size to 600px and use CatmullRom scaling Increased the UI cover art request size from 300px to 600px for sharper images on high-DPI displays. Replaced BiLinear with CatmullRom (bicubic) interpolation for higher quality image resizing. Extracted the hardcoded size into a COVER_ART_SIZE constant in the frontend and consolidated backend sizes into a CacheWarmerImageSizes slice. Removed the unused UIThumbnailSize constant. Signed-off-by: Deluan --- consts/consts.go | 8 ++++++-- core/artwork/cache_warmer.go | 9 ++++----- core/artwork/cache_warmer_test.go | 4 ++-- core/artwork/reader_playlist.go | 2 +- core/artwork/reader_resized.go | 2 +- ui/src/album/AlbumDetails.jsx | 3 ++- ui/src/album/AlbumGridView.jsx | 4 ++-- ui/src/artist/DesktopArtistDetails.jsx | 3 ++- ui/src/artist/MobileArtistDetails.jsx | 3 ++- ui/src/common/CoverArtAvatar.jsx | 3 ++- ui/src/consts.js | 2 ++ ui/src/playlist/PlaylistDetails.jsx | 3 ++- ui/src/radio/RadioEdit.jsx | 4 ++-- ui/src/radio/helper.jsx | 4 ++-- ui/src/subsonic/index.test.js | 21 +++++++++++---------- 15 files changed, 43 insertions(+), 32 deletions(-) diff --git a/consts/consts.go b/consts/consts.go index 6fb6c5dac..f1010a872 100644 --- a/consts/consts.go +++ b/consts/consts.go @@ -70,8 +70,6 @@ const ( PlaceholderArtistArt = "artist-placeholder.webp" PlaceholderAlbumArt = "album-placeholder.webp" PlaceholderAvatar = "logo-192x192.png" - UICoverArtSize = 300 - UIThumbnailSize = 80 DefaultUIVolume = 100 DefaultUISearchDebounceMs = 200 @@ -86,6 +84,12 @@ const ( Zwsp = string('\u200b') ) +const ( + UICoverArtSize = 600 +) + +var CacheWarmerImageSizes = []int{UICoverArtSize} + // Prometheus options const ( PrometheusDefaultPath = "/metrics" diff --git a/core/artwork/cache_warmer.go b/core/artwork/cache_warmer.go index 83c98c806..bd1359b74 100644 --- a/core/artwork/cache_warmer.go +++ b/core/artwork/cache_warmer.go @@ -142,15 +142,14 @@ func (a *cacheWarmer) doCacheImage(ctx context.Context, id model.ArtworkID) erro ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() - for _, size := range []int{consts.UICoverArtSize, consts.UIThumbnailSize} { + for _, size := range consts.CacheWarmerImageSizes { r, _, err := a.artwork.Get(ctx, id, size, true) if err != nil { return fmt.Errorf("caching id='%s', size=%d: %w", id, size, err) } - defer r.Close() - if _, err = io.Copy(io.Discard, r); err != nil { - return err - } + _, err = io.Copy(io.Discard, r) + r.Close() + return err } return nil } diff --git a/core/artwork/cache_warmer_test.go b/core/artwork/cache_warmer_test.go index 6ddda00d6..9798ea8d6 100644 --- a/core/artwork/cache_warmer_test.go +++ b/core/artwork/cache_warmer_test.go @@ -176,13 +176,13 @@ var _ = Describe("CacheWarmer", func() { }).Should(Equal(0)) }) - It("pre-caches both UICoverArtSize and UIThumbnailSize", func() { + It("pre-caches UICoverArtSize", func() { cw := NewCacheWarmer(aw, fc).(*cacheWarmer) cw.PreCache(model.MustParseArtworkID("al-1")) Eventually(func() []int { return aw.getCachedSizes() - }).Should(ContainElements(consts.UICoverArtSize, consts.UIThumbnailSize)) + }).Should(ContainElements(consts.UICoverArtSize)) }) }) }) diff --git a/core/artwork/reader_playlist.go b/core/artwork/reader_playlist.go index 7919eead6..09707843d 100644 --- a/core/artwork/reader_playlist.go +++ b/core/artwork/reader_playlist.go @@ -264,6 +264,6 @@ func fillCenter(src image.Image, dstW, dstH int) image.Image { } dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH)) - xdraw.BiLinear.Scale(dst, dst.Bounds(), src, cropRect, draw.Src, nil) + xdraw.CatmullRom.Scale(dst, dst.Bounds(), src, cropRect, draw.Src, nil) return dst } diff --git a/core/artwork/reader_resized.go b/core/artwork/reader_resized.go index 7e90f10e9..72baad434 100644 --- a/core/artwork/reader_resized.go +++ b/core/artwork/reader_resized.go @@ -155,7 +155,7 @@ func resizeStaticImage(data []byte, size int, square bool) (io.Reader, int, erro dst = image.NewNRGBA(image.Rect(0, 0, dstW, dstH)) dstRect = dst.Bounds() } - xdraw.BiLinear.Scale(dst, dstRect, original, bounds, draw.Src, nil) + xdraw.CatmullRom.Scale(dst, dstRect, original, bounds, draw.Src, nil) buf := bufPool.Get().(*bytes.Buffer) buf.Reset() diff --git a/ui/src/album/AlbumDetails.jsx b/ui/src/album/AlbumDetails.jsx index c5d9a7ac4..2411b8611 100644 --- a/ui/src/album/AlbumDetails.jsx +++ b/ui/src/album/AlbumDetails.jsx @@ -18,6 +18,7 @@ import { useTranslate, } from 'react-admin' import Lightbox from 'react-image-lightbox' +import { COVER_ART_SIZE } from '../consts' import 'react-image-lightbox/style.css' import subsonic from '../subsonic' import { @@ -254,7 +255,7 @@ const AlbumDetails = (props) => { }) }, [record]) - const imageUrl = subsonic.getCoverArtUrl(record, 300) + const imageUrl = subsonic.getCoverArtUrl(record, COVER_ART_SIZE) const fullImageUrl = subsonic.getCoverArtUrl(record) return ( diff --git a/ui/src/album/AlbumGridView.jsx b/ui/src/album/AlbumGridView.jsx index 6b44c5fe9..e90e7a77b 100644 --- a/ui/src/album/AlbumGridView.jsx +++ b/ui/src/album/AlbumGridView.jsx @@ -19,7 +19,7 @@ import { ArtistLinkField, OverflowTooltip, } from '../common' -import { DraggableTypes } from '../consts' +import { COVER_ART_SIZE, DraggableTypes } from '../consts' import clsx from 'clsx' import { AlbumDatesField } from './AlbumDatesField.jsx' @@ -157,7 +157,7 @@ const Cover = withContentRect('bounds')(({
{record.name} { { { handleCloseLightbox, } = useImageLoadingState(record.id) - const imageUrl = subsonic.getCoverArtUrl(record, 300, true) + const imageUrl = subsonic.getCoverArtUrl(record, COVER_ART_SIZE, true) const fullImageUrl = subsonic.getCoverArtUrl(record) return ( diff --git a/ui/src/radio/RadioEdit.jsx b/ui/src/radio/RadioEdit.jsx index 6b1d2df79..5f804535a 100644 --- a/ui/src/radio/RadioEdit.jsx +++ b/ui/src/radio/RadioEdit.jsx @@ -11,7 +11,7 @@ import { makeStyles } from '@material-ui/core/styles' import { urlValidate } from '../utils/validations' import { Title, ImageUploadOverlay, useImageLoadingState } from '../common' import subsonic from '../subsonic' -import { RADIO_PLACEHOLDER_IMAGE } from '../consts' +import { COVER_ART_SIZE, RADIO_PLACEHOLDER_IMAGE } from '../consts' const useStyles = makeStyles({ coverParent: { @@ -83,7 +83,7 @@ const RadioCoverArt = ({ record }) => { {record.uploadedImage ? ( { @@ -30,10 +31,10 @@ describe('getCoverArtUrl', () => { updatedAt: '2023-01-01T00:00:00Z', } - const url = subsonic.getCoverArtUrl(playlistRecord, 300, true) + const url = subsonic.getCoverArtUrl(playlistRecord, COVER_ART_SIZE, true) expect(url).toContain('pl-playlist-123') - expect(url).toContain('size=300') + expect(url).toContain('size=600') expect(url).toContain('square=true') expect(url).toContain('_=2023-01-01T00%3A00%3A00Z') }) @@ -44,10 +45,10 @@ describe('getCoverArtUrl', () => { sync: true, } - const url = subsonic.getCoverArtUrl(playlistRecord, 300, true) + const url = subsonic.getCoverArtUrl(playlistRecord, COVER_ART_SIZE, true) expect(url).toContain('pl-playlist-123') - expect(url).toContain('size=300') + expect(url).toContain('size=600') expect(url).toContain('square=true') expect(url).not.toContain('_=') }) @@ -59,10 +60,10 @@ describe('getCoverArtUrl', () => { updatedAt: '2023-01-01T00:00:00Z', } - const url = subsonic.getCoverArtUrl(albumRecord, 300, true) + const url = subsonic.getCoverArtUrl(albumRecord, COVER_ART_SIZE, true) expect(url).toContain('al-album-123') - expect(url).toContain('size=300') + expect(url).toContain('size=600') expect(url).toContain('square=true') }) @@ -73,10 +74,10 @@ describe('getCoverArtUrl', () => { updatedAt: '2023-01-01T00:00:00Z', } - const url = subsonic.getCoverArtUrl(songRecord, 300, true) + const url = subsonic.getCoverArtUrl(songRecord, COVER_ART_SIZE, true) expect(url).toContain('mf-song-123') - expect(url).toContain('size=300') + expect(url).toContain('size=600') expect(url).toContain('square=true') }) @@ -86,10 +87,10 @@ describe('getCoverArtUrl', () => { updatedAt: '2023-01-01T00:00:00Z', } - const url = subsonic.getCoverArtUrl(artistRecord, 300, true) + const url = subsonic.getCoverArtUrl(artistRecord, COVER_ART_SIZE, true) expect(url).toContain('ar-artist-123') - expect(url).toContain('size=300') + expect(url).toContain('size=600') expect(url).toContain('square=true') }) From 03608d3eef7b62768983d10cd92c21a36e64ed18 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sun, 22 Mar 2026 15:20:15 -0400 Subject: [PATCH 2/6] feat(subsonic): add coverArt to internetRadioStation response Add OpenSubsonic coverArt extension to GetInternetRadios, showing uploaded radio images for non-legacy clients. Ref: https://github.com/opensubsonic/open-subsonic-api/pull/224 Signed-off-by: Deluan --- server/subsonic/radio.go | 12 ++ server/subsonic/radio_test.go | 146 +++++++++++++++++++++++++ server/subsonic/responses/responses.go | 13 ++- 3 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 server/subsonic/radio_test.go diff --git a/server/subsonic/radio.go b/server/subsonic/radio.go index c66268344..7121566f9 100644 --- a/server/subsonic/radio.go +++ b/server/subsonic/radio.go @@ -2,8 +2,11 @@ package subsonic import ( "net/http" + "strings" + "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/utils/req" ) @@ -66,6 +69,15 @@ func (api *Router) GetInternetRadios(r *http.Request) (*responses.Subsonic, erro StreamUrl: g.StreamUrl, HomepageUrl: g.HomePageUrl, } + + player, _ := request.PlayerFrom(ctx) + if strings.Contains(conf.Server.Subsonic.LegacyClients, player.Client) { + continue + } + // Add coverArt if not legacy client + res[i].OpenSubsonicRadio = &responses.OpenSubsonicRadio{ + CoverArt: g.UploadedImage, + } } response := newResponse() diff --git a/server/subsonic/radio_test.go b/server/subsonic/radio_test.go new file mode 100644 index 000000000..d5b764f60 --- /dev/null +++ b/server/subsonic/radio_test.go @@ -0,0 +1,146 @@ +package subsonic + +import ( + "context" + "net/http/httptest" + + "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/conf/configtest" + "github.com/navidrome/navidrome/core/auth" + "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/model/request" + "github.com/navidrome/navidrome/tests" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Radio", func() { + var api *Router + var ds *tests.MockDataStore + var ctx context.Context + var radioRepo *tests.MockedRadioRepo + + BeforeEach(func() { + ds = &tests.MockDataStore{} + auth.Init(ds) + api = &Router{ds: ds} + ctx = context.Background() + radioRepo = tests.CreateMockedRadioRepo() + ds.MockedRadio = radioRepo + }) + + Describe("GetInternetRadios", func() { + BeforeEach(func() { + radioRepo.All = model.Radios{ + {ID: "rd-1", Name: "Radio 1", StreamUrl: "http://stream1.example.com", HomePageUrl: "http://home1.example.com", UploadedImage: "rd-1_cover.jpg"}, + {ID: "rd-2", Name: "Radio 2", StreamUrl: "http://stream2.example.com"}, + } + }) + + It("returns all radios with basic fields", func() { + r := httptest.NewRequest("GET", "/rest/getInternetRadios", nil) + r = r.WithContext(ctx) + + response, err := api.GetInternetRadios(r) + + Expect(err).ToNot(HaveOccurred()) + Expect(response.InternetRadioStations).ToNot(BeNil()) + Expect(response.InternetRadioStations.Radios).To(HaveLen(2)) + Expect(response.InternetRadioStations.Radios[0].ID).To(Equal("rd-1")) + Expect(response.InternetRadioStations.Radios[0].Name).To(Equal("Radio 1")) + Expect(response.InternetRadioStations.Radios[0].StreamUrl).To(Equal("http://stream1.example.com")) + Expect(response.InternetRadioStations.Radios[0].HomepageUrl).To(Equal("http://home1.example.com")) + Expect(response.InternetRadioStations.Radios[1].ID).To(Equal("rd-2")) + Expect(response.InternetRadioStations.Radios[1].HomepageUrl).To(BeEmpty()) + }) + + Context("with a non-legacy client", func() { + BeforeEach(func() { + DeferCleanup(configtest.SetupConfig()) + conf.Server.Subsonic.LegacyClients = "legacy-client" + player := model.Player{Client: "modern-client"} + ctx = request.WithPlayer(ctx, player) + }) + + It("includes coverArt from UploadedImage", func() { + r := httptest.NewRequest("GET", "/rest/getInternetRadios", nil) + r = r.WithContext(ctx) + + response, err := api.GetInternetRadios(r) + + Expect(err).ToNot(HaveOccurred()) + Expect(response.InternetRadioStations.Radios).To(HaveLen(2)) + Expect(response.InternetRadioStations.Radios[0].OpenSubsonicRadio).ToNot(BeNil()) + Expect(response.InternetRadioStations.Radios[0].CoverArt).To(Equal("rd-1_cover.jpg")) + Expect(response.InternetRadioStations.Radios[1].OpenSubsonicRadio).ToNot(BeNil()) + Expect(response.InternetRadioStations.Radios[1].CoverArt).To(BeEmpty()) + }) + }) + + Context("with a legacy client", func() { + BeforeEach(func() { + DeferCleanup(configtest.SetupConfig()) + conf.Server.Subsonic.LegacyClients = "legacy-client" + player := model.Player{Client: "legacy-client"} + ctx = request.WithPlayer(ctx, player) + }) + + It("does not include coverArt", func() { + r := httptest.NewRequest("GET", "/rest/getInternetRadios", nil) + r = r.WithContext(ctx) + + response, err := api.GetInternetRadios(r) + + Expect(err).ToNot(HaveOccurred()) + Expect(response.InternetRadioStations.Radios).To(HaveLen(2)) + Expect(response.InternetRadioStations.Radios[0].OpenSubsonicRadio).To(BeNil()) + Expect(response.InternetRadioStations.Radios[1].OpenSubsonicRadio).To(BeNil()) + }) + }) + + Context("when no player in context", func() { + It("does not include coverArt (empty client matches legacy list)", func() { + DeferCleanup(configtest.SetupConfig()) + conf.Server.Subsonic.LegacyClients = "legacy-client" + + r := httptest.NewRequest("GET", "/rest/getInternetRadios", nil) + r = r.WithContext(ctx) + + response, err := api.GetInternetRadios(r) + + Expect(err).ToNot(HaveOccurred()) + Expect(response.InternetRadioStations.Radios[0].OpenSubsonicRadio).To(BeNil()) + }) + }) + + Context("when legacy clients list is empty", func() { + BeforeEach(func() { + DeferCleanup(configtest.SetupConfig()) + conf.Server.Subsonic.LegacyClients = "" + player := model.Player{Client: "any-client"} + ctx = request.WithPlayer(ctx, player) + }) + + It("includes coverArt for all clients", func() { + r := httptest.NewRequest("GET", "/rest/getInternetRadios", nil) + r = r.WithContext(ctx) + + response, err := api.GetInternetRadios(r) + + Expect(err).ToNot(HaveOccurred()) + Expect(response.InternetRadioStations.Radios[0].OpenSubsonicRadio).ToNot(BeNil()) + Expect(response.InternetRadioStations.Radios[0].CoverArt).To(Equal("rd-1_cover.jpg")) + }) + }) + + It("returns error when repository fails", func() { + radioRepo.SetError(true) + + r := httptest.NewRequest("GET", "/rest/getInternetRadios", nil) + r = r.WithContext(ctx) + + _, err := api.GetInternetRadios(r) + Expect(err).To(HaveOccurred()) + }) + }) +}) diff --git a/server/subsonic/responses/responses.go b/server/subsonic/responses/responses.go index b9a39b6f9..f0bb26f66 100644 --- a/server/subsonic/responses/responses.go +++ b/server/subsonic/responses/responses.go @@ -509,10 +509,15 @@ type InternetRadioStations struct { } type Radio struct { - ID string `xml:"id,attr" json:"id"` - Name string `xml:"name,attr" json:"name"` - StreamUrl string `xml:"streamUrl,attr" json:"streamUrl"` - HomepageUrl string `xml:"homePageUrl,omitempty,attr" json:"homePageUrl,omitempty"` + ID string `xml:"id,attr" json:"id"` + Name string `xml:"name,attr" json:"name"` + StreamUrl string `xml:"streamUrl,attr" json:"streamUrl"` + HomepageUrl string `xml:"homePageUrl,omitempty,attr" json:"homePageUrl,omitempty"` + *OpenSubsonicRadio `xml:",omitempty" json:",omitempty"` +} + +type OpenSubsonicRadio struct { + CoverArt string `xml:"coverArt,attr,omitempty" json:"coverArt"` } type JukeboxStatus struct { From d91b5e8f4daeae4ee9070b5672398aeed09ea419 Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 23 Mar 2026 11:39:52 -0400 Subject: [PATCH 3/6] refactor: simplify playlist name extraction using strings.CutPrefix --- core/playlists/parse_m3u.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/playlists/parse_m3u.go b/core/playlists/parse_m3u.go index 97ed7df6f..b9f5c92a2 100644 --- a/core/playlists/parse_m3u.go +++ b/core/playlists/parse_m3u.go @@ -31,8 +31,8 @@ func (s *playlists) parseM3U(ctx context.Context, pls *model.Playlist, folder *m filteredLines := make([]string, 0, len(lines)) for _, line := range lines { line := strings.TrimSpace(line) - if strings.HasPrefix(line, "#PLAYLIST:") { - pls.Name = line[len("#PLAYLIST:"):] + if after, ok := strings.CutPrefix(line, "#PLAYLIST:"); ok { + pls.Name = after continue } if after, ok := strings.CutPrefix(line, "#EXTALBUMARTURL:"); ok { From 4cca7bce4e36123ca8905115ca819c048f42f500 Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 23 Mar 2026 11:59:11 -0400 Subject: [PATCH 4/6] test: increase FlakeAttempts for library directory tests and remove flaky job test --- plugins/host_library_test.go | 4 ++-- scheduler/scheduler_test.go | 28 ---------------------------- 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/plugins/host_library_test.go b/plugins/host_library_test.go index d92abfe30..5746a3bed 100644 --- a/plugins/host_library_test.go +++ b/plugins/host_library_test.go @@ -544,7 +544,7 @@ var _ = Describe("LibraryService Integration", Ordered, func() { // Note: This test is slightly flaky due to a potential race condition in wazero's // WASI filesystem mounting. The test passes ~85% of the time. Using FlakeAttempts // to automatically retry on failure. - It("should read file from mounted library directory", FlakeAttempts(3), func() { + It("should read file from mounted library directory", FlakeAttempts(5), func() { ctx := GinkgoT().Context() output, err := callTestLibrary(ctx, testLibraryInput{ @@ -557,7 +557,7 @@ var _ = Describe("LibraryService Integration", Ordered, func() { }) // Note: Uses FlakeAttempts for the same reason as the read_file test above - It("should list files in mounted library directory", FlakeAttempts(3), func() { + It("should list files in mounted library directory", FlakeAttempts(5), func() { ctx := GinkgoT().Context() output, err := callTestLibrary(ctx, testLibraryInput{ diff --git a/scheduler/scheduler_test.go b/scheduler/scheduler_test.go index 795f07115..1a134a7f3 100644 --- a/scheduler/scheduler_test.go +++ b/scheduler/scheduler_test.go @@ -2,7 +2,6 @@ package scheduler import ( "testing" - "time" "github.com/navidrome/navidrome/log" . "github.com/onsi/ginkgo/v2" @@ -48,31 +47,4 @@ var _ = Describe("Scheduler", func() { Expect(id).ToNot(BeZero()) s.Remove(id) }) - - It("removes a job", func() { - done := make(chan struct{}) - - counter := 0 - id, err := s.Add("@every 50ms", func() { - counter++ - if counter == 1 { - close(done) - } - }) - Expect(err).ToNot(HaveOccurred()) - Expect(id).ToNot(BeZero()) - - // Verify job executed - Eventually(done).Should(BeClosed()) - Expect(counter).To(Equal(1)) - - // Remove the job - s.Remove(id) - - // Wait some time to ensure job doesn't execute again - time.Sleep(200 * time.Millisecond) - - // Verify counter didn't increase - Expect(counter).To(Equal(1)) - }) }) From 221d301c4249694e2b388dc1b6ef46dcb19804be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:19:16 -0400 Subject: [PATCH 5/6] chore(deps): bump nick-fields/retry from 3 to 4 in /.github/workflows (#5241) Bumps [nick-fields/retry](https://github.com/nick-fields/retry) from 3 to 4. - [Release notes](https://github.com/nick-fields/retry/releases) - [Commits](https://github.com/nick-fields/retry/compare/v3...v4) --- updated-dependencies: - dependency-name: nick-fields/retry dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index eb7523d4e..2529aaf36 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -338,7 +338,7 @@ jobs: hub_password: ${{ secrets.DOCKER_HUB_PASSWORD }} - name: Create manifest list and push to Docker Hub - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_minutes: 5 max_attempts: 3 From 8a19fa9991f486034dafcd9f259517ca465d55bb Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Mon, 23 Mar 2026 22:09:59 +0000 Subject: [PATCH 6/6] fix(server): require additional variable to enable systemd logging (#5222) * fix(logging): require additional variable to enable systemd logging * use a better name --- cmd/svc.go | 1 + conf/configuration.go | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/svc.go b/cmd/svc.go index e277bd459..89ca08056 100644 --- a/cmd/svc.go +++ b/cmd/svc.go @@ -248,6 +248,7 @@ ExecStart={{.Path|cmdEscape}}{{range .Arguments}} {{.|cmd}}{{end}} TimeoutStopSec=20 RestartSec=120 EnvironmentFile=-/etc/sysconfig/{{.Name}} +Environment="ND_SYSTEMD_PRIORITY_LOGGING=1" DevicePolicy=closed NoNewPrivileges=yes diff --git a/conf/configuration.go b/conf/configuration.go index a5c253746..5f74d6db0 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -341,9 +341,11 @@ func Load(noConfigDump bool) { os.Exit(1) } log.SetOutput(out) - } else if os.Getenv("JOURNAL_STREAM") != "" { + } else if os.Getenv("ND_SYSTEMD_PRIORITY_LOGGING") != "" && os.Getenv("JOURNAL_STREAM") != "" { // When running under systemd, prepend syslog priority prefixes so // journald assigns the correct severity to each log line. + // Note that we have an additional environment variable, as JOURNAL_STREAM + // can be present in a systemd environment even if not running as a systemd service log.EnableJournalFormat() }