From d8bc41fbb19179da9d9321b3f0ae3e7f539432eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Wed, 11 Mar 2026 09:26:32 -0400 Subject: [PATCH 01/81] fix: use ADTS for AAC transcoding, temporarily exclude AAC from transcode decisions (#5167) * fix: use ADTS format for AAC transcoding to avoid silent output on ffmpeg 8.0+ The fragmented MP4 muxer (`-f ipod -movflags frag_keyframe+empty_moov`) produces corrupt/silent audio when ffmpeg pipes to stdout, confirmed on ffmpeg 8.0+. The moof atom offset values are zeroed out in pipe mode, causing AAC decoder errors. Switch to `-f adts` (raw AAC framing) which works reliably via pipe and is widely supported by clients including UPnP/Sonos devices. * fix: exclude AAC from transcode decision, as it is not working for Sonos. Signed-off-by: Deluan --------- Signed-off-by: Deluan --- consts/consts.go | 2 +- core/ffmpeg/ffmpeg.go | 7 +--- core/ffmpeg/ffmpeg_test.go | 7 ++-- core/stream/aliases.go | 5 +++ core/stream/aliases_test.go | 30 +++++++++++++++ ...0260310113858_fix_aac_transcode_command.go | 30 +++++++++++++++ server/subsonic/transcode.go | 10 +++++ server/subsonic/transcode_test.go | 37 ++++++++++++++++--- 8 files changed, 111 insertions(+), 17 deletions(-) create mode 100644 core/stream/aliases_test.go create mode 100644 db/migrations/20260310113858_fix_aac_transcode_command.go diff --git a/consts/consts.go b/consts/consts.go index 2a5fdd94a..061aebd7b 100644 --- a/consts/consts.go +++ b/consts/consts.go @@ -153,7 +153,7 @@ var ( Name: "aac audio", TargetFormat: "aac", DefaultBitRate: 256, - Command: "ffmpeg -i %s -ss %t -map 0:a:0 -b:a %bk -v 0 -c:a aac -f ipod -movflags frag_keyframe+empty_moov -", + Command: "ffmpeg -i %s -ss %t -map 0:a:0 -b:a %bk -v 0 -c:a aac -f adts -", }, { Name: "flac audio", diff --git a/core/ffmpeg/ffmpeg.go b/core/ffmpeg/ffmpeg.go index d0cf0755d..9c4f99364 100644 --- a/core/ffmpeg/ffmpeg.go +++ b/core/ffmpeg/ffmpeg.go @@ -283,7 +283,7 @@ var formatCodecMap = map[string]string{ var formatOutputMap = map[string]string{ "mp3": "mp3", "opus": "opus", - "aac": "ipod", + "aac": "adts", "flac": "flac", } @@ -339,11 +339,6 @@ func buildDynamicArgs(opts TranscodeOptions) []string { args = append(args, "-f", outputFmt) } - // For AAC in MP4 container, enable fragmented MP4 for pipe-safe streaming - if opts.Format == "aac" { - args = append(args, "-movflags", "frag_keyframe+empty_moov") - } - args = append(args, "-") return args } diff --git a/core/ffmpeg/ffmpeg_test.go b/core/ffmpeg/ffmpeg_test.go index eebeefe35..23e419219 100644 --- a/core/ffmpeg/ffmpeg_test.go +++ b/core/ffmpeg/ffmpeg_test.go @@ -86,7 +86,7 @@ var _ = Describe("ffmpeg", func() { Expect(isDefaultCommand("opus", "ffmpeg -i %s -ss %t -map 0:a:0 -b:a %bk -v 0 -c:a libopus -f opus -")).To(BeTrue()) }) It("returns true for known default aac command", func() { - Expect(isDefaultCommand("aac", "ffmpeg -i %s -ss %t -map 0:a:0 -b:a %bk -v 0 -c:a aac -f ipod -movflags frag_keyframe+empty_moov -")).To(BeTrue()) + Expect(isDefaultCommand("aac", "ffmpeg -i %s -ss %t -map 0:a:0 -b:a %bk -v 0 -c:a aac -f adts -")).To(BeTrue()) }) It("returns true for known default flac command", func() { Expect(isDefaultCommand("flac", "ffmpeg -i %s -ss %t -map 0:a:0 -v 0 -c:a flac -f flac -")).To(BeTrue()) @@ -174,7 +174,7 @@ var _ = Describe("ffmpeg", func() { })) }) - It("builds aac args with fragmented MP4 container", func() { + It("builds aac args with ADTS output", func() { args := buildDynamicArgs(TranscodeOptions{ Format: "aac", FilePath: "/music/file.flac", @@ -186,8 +186,7 @@ var _ = Describe("ffmpeg", func() { "-c:a", "aac", "-b:a", "256k", "-v", "0", - "-f", "ipod", - "-movflags", "frag_keyframe+empty_moov", + "-f", "adts", "-", })) }) diff --git a/core/stream/aliases.go b/core/stream/aliases.go index 6a4c8386f..af42ac076 100644 --- a/core/stream/aliases.go +++ b/core/stream/aliases.go @@ -80,6 +80,11 @@ func matchesCodec(codec string, codecs []string) bool { return matchesWithAliases(codec, codecs, codecAliasGroups) } +// IsAACCodec returns true if the given codec or container name resolves to AAC. +func IsAACCodec(name string) bool { + return matchesCodec(name, []string{"aac"}) || matchesContainer(name, []string{"aac"}) +} + func containsIgnoreCase(slice []string, s string) bool { return slices.ContainsFunc(slice, func(item string) bool { return strings.EqualFold(item, s) diff --git a/core/stream/aliases_test.go b/core/stream/aliases_test.go new file mode 100644 index 000000000..72f061810 --- /dev/null +++ b/core/stream/aliases_test.go @@ -0,0 +1,30 @@ +package stream + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Aliases", func() { + Describe("IsAACCodec", func() { + It("returns true for AAC and its aliases", func() { + Expect(IsAACCodec("aac")).To(BeTrue()) + Expect(IsAACCodec("AAC")).To(BeTrue()) + Expect(IsAACCodec("adts")).To(BeTrue()) + Expect(IsAACCodec("m4a")).To(BeTrue()) + Expect(IsAACCodec("mp4")).To(BeTrue()) + Expect(IsAACCodec("m4b")).To(BeTrue()) + }) + + It("returns false for non-AAC formats", func() { + Expect(IsAACCodec("mp3")).To(BeFalse()) + Expect(IsAACCodec("opus")).To(BeFalse()) + Expect(IsAACCodec("flac")).To(BeFalse()) + Expect(IsAACCodec("ogg")).To(BeFalse()) + }) + + It("returns false for empty string", func() { + Expect(IsAACCodec("")).To(BeFalse()) + }) + }) +}) diff --git a/db/migrations/20260310113858_fix_aac_transcode_command.go b/db/migrations/20260310113858_fix_aac_transcode_command.go new file mode 100644 index 000000000..588137383 --- /dev/null +++ b/db/migrations/20260310113858_fix_aac_transcode_command.go @@ -0,0 +1,30 @@ +package migrations + +import ( + "context" + "database/sql" + + "github.com/pressly/goose/v3" +) + +func init() { + goose.AddMigrationContext(upFixAacTranscodeCommand, downFixAacTranscodeCommand) +} + +func upFixAacTranscodeCommand(_ context.Context, tx *sql.Tx) error { + // The old AAC command used `-f ipod -movflags frag_keyframe+empty_moov` which produces + // corrupt/silent audio when ffmpeg pipes to stdout (confirmed in ffmpeg 8.0+). + // Switch to `-f adts` (raw AAC framing) which works reliably via pipe. + // Only update rows that still have the old default command. + const oldCommand = "ffmpeg -i %s -ss %t -map 0:a:0 -b:a %bk -v 0 -c:a aac -f ipod -movflags frag_keyframe+empty_moov -" + const newCommand = "ffmpeg -i %s -ss %t -map 0:a:0 -b:a %bk -v 0 -c:a aac -f adts -" + _, err := tx.Exec( + "UPDATE transcoding SET command = ? WHERE target_format = 'aac' AND command = ?", + newCommand, oldCommand, + ) + return err +} + +func downFixAacTranscodeCommand(_ context.Context, tx *sql.Tx) error { + return nil +} diff --git a/server/subsonic/transcode.go b/server/subsonic/transcode.go index 792504871..64e74d460 100644 --- a/server/subsonic/transcode.go +++ b/server/subsonic/transcode.go @@ -268,6 +268,16 @@ func (api *Router) GetTranscodeDecision(w http.ResponseWriter, r *http.Request) } clientInfo := clientInfoReq.toCoreClientInfo() + // TODO: Remove this filter once AAC transcoding works reliably + // with streaming clients (Sonos, etc). + // See https://github.com/navidrome/navidrome/discussions/4832#discussioncomment-16068231 + clientInfo.TranscodingProfiles = slices.DeleteFunc(clientInfo.TranscodingProfiles, func(p stream.Profile) bool { + if p.AudioCodec != "" { + return stream.IsAACCodec(p.AudioCodec) + } + return stream.IsAACCodec(p.Container) + }) + // Get media file mf, err := api.ds.MediaFile(ctx).Get(mediaID) if err != nil { diff --git a/server/subsonic/transcode_test.go b/server/subsonic/transcode_test.go index 8db729c6d..15ba168d7 100644 --- a/server/subsonic/transcode_test.go +++ b/server/subsonic/transcode_test.go @@ -180,6 +180,29 @@ var _ = Describe("Transcode endpoints", func() { Expect(resp.TranscodeDecision.SourceStream.AudioBitrate).To(Equal(int32(320_000))) }) + It("filters AAC from transcoding profiles", func() { + mockMFRepo.SetData(model.MediaFiles{ + {ID: "song-1", Suffix: "opus", Codec: "opus", BitRate: 128, Channels: 2, SampleRate: 48000}, + }) + mockTD.decision = &stream.TranscodeDecision{MediaID: "song-1", CanDirectPlay: true} + mockTD.token = "token" + + body := `{ + "transcodingProfiles": [ + {"container": "aac", "audioCodec": "aac", "protocol": "http"}, + {"container": "mp3", "audioCodec": "mp3", "protocol": "http"}, + {"container": "m4a", "audioCodec": "aac", "protocol": "http"} + ] + }` + r := newJSONPostRequest("mediaId=song-1&mediaType=song", body) + _, err := router.GetTranscodeDecision(w, r) + + Expect(err).ToNot(HaveOccurred()) + Expect(mockTD.capturedClient).ToNot(BeNil()) + Expect(mockTD.capturedClient.TranscodingProfiles).To(HaveLen(1)) + Expect(mockTD.capturedClient.TranscodingProfiles[0].AudioCodec).To(Equal("mp3")) + }) + It("includes transcode stream when transcoding", func() { mockMFRepo.SetData(model.MediaFiles{ {ID: "song-2", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24}, @@ -354,14 +377,16 @@ func newJSONPostRequest(queryParams string, jsonBody string) *http.Request { // mockTranscodeDecision is a test double for stream.TranscodeDecider type mockTranscodeDecision struct { - decision *stream.TranscodeDecision - token string - tokenErr error - resolvedReq stream.Request - resolveErr error + decision *stream.TranscodeDecision + token string + tokenErr error + resolvedReq stream.Request + resolveErr error + capturedClient *stream.ClientInfo } -func (m *mockTranscodeDecision) MakeDecision(_ context.Context, _ *model.MediaFile, _ *stream.ClientInfo, _ stream.TranscodeOptions) (*stream.TranscodeDecision, error) { +func (m *mockTranscodeDecision) MakeDecision(_ context.Context, _ *model.MediaFile, ci *stream.ClientInfo, _ stream.TranscodeOptions) (*stream.TranscodeDecision, error) { + m.capturedClient = ci if m.decision != nil { return m.decision, nil } From 5ecbe31a06b4035fc2768219455e804c26660647 Mon Sep 17 00:00:00 2001 From: Deluan Date: Wed, 11 Mar 2026 09:46:06 -0400 Subject: [PATCH 02/81] fix: implement fallback to DefaultDownsamplingFormat for unknown formats Signed-off-by: Deluan --- core/stream/legacy_client.go | 11 +- core/stream/legacy_client_test.go | 142 ++++++++++++++++++++ server/e2e/subsonic_media_retrieval_test.go | 4 +- 3 files changed, 154 insertions(+), 3 deletions(-) diff --git a/core/stream/legacy_client.go b/core/stream/legacy_client.go index 82d01f5de..d6e929ec8 100644 --- a/core/stream/legacy_client.go +++ b/core/stream/legacy_client.go @@ -86,7 +86,16 @@ func (s *deciderService) ResolveRequest(ctx context.Context, mf *model.MediaFile return req } - // No compatible profile — fallback to raw + // No compatible profile for the requested format — retry with DefaultDownsamplingFormat + // TODO: validate DefaultDownsamplingFormat at startup to warn about unsupported values + fallbackFormat := conf.Server.DefaultDownsamplingFormat + if reqFormat != "" && fallbackFormat != "" && !strings.EqualFold(reqFormat, fallbackFormat) { + log.Warn(ctx, "Requested format not available, falling back to default downsampling format", + "requestedFormat", reqFormat, "fallbackFormat", fallbackFormat, "id", mf.ID) + return s.ResolveRequest(ctx, mf, fallbackFormat, reqBitRate, offset) + } + + // Ultimate fallback — raw req.Format = "raw" return req } diff --git a/core/stream/legacy_client_test.go b/core/stream/legacy_client_test.go index 3557a9314..de1eb1339 100644 --- a/core/stream/legacy_client_test.go +++ b/core/stream/legacy_client_test.go @@ -1,9 +1,13 @@ package stream import ( + "context" + "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/tests" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -96,3 +100,141 @@ var _ = Describe("buildLegacyClientInfo", func() { Expect(ci.MaxAudioBitrate).To(BeZero()) }) }) + +var _ = Describe("ResolveRequest", func() { + var ( + svc TranscodeDecider + ctx context.Context + ) + + BeforeEach(func() { + ctx = GinkgoT().Context() + ds := &tests.MockDataStore{ + MockedProperty: &tests.MockedPropertyRepo{}, + MockedTranscoding: &tests.MockTranscodingRepo{}, + } + ff := tests.NewMockFFmpeg("") + auth.Init(ds) + svc = NewTranscodeDecider(ds, ff) + }) + + It("returns raw when format is 'raw'", func() { + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100}) + + decider := svc.(*deciderService) + req := decider.ResolveRequest(ctx, mf, "raw", 0, 0) + + Expect(req.Format).To(Equal("raw")) + }) + + It("returns raw (direct play) when no format or bitrate specified", func() { + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100}) + + decider := svc.(*deciderService) + req := decider.ResolveRequest(ctx, mf, "", 0, 0) + + Expect(req.Format).To(Equal("raw")) + }) + + It("transcodes to requested format", func() { + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16}) + + decider := svc.(*deciderService) + req := decider.ResolveRequest(ctx, mf, "opus", 0, 0) + + Expect(req.Format).To(Equal("opus")) + }) + + It("transcodes to requested format with bitrate limit", func() { + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16}) + + decider := svc.(*deciderService) + req := decider.ResolveRequest(ctx, mf, "mp3", 128, 0) + + Expect(req.Format).To(Equal("mp3")) + Expect(req.BitRate).To(Equal(128)) + }) + + It("returns raw when requested format matches source and no bitrate reduction", func() { + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100}) + + decider := svc.(*deciderService) + req := decider.ResolveRequest(ctx, mf, "mp3", 320, 0) + + Expect(req.Format).To(Equal("raw")) + }) + + It("downsamples when only bitrate is specified below source", func() { + DeferCleanup(configtest.SetupConfig()) + conf.Server.DefaultDownsamplingFormat = "opus" + + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16}) + + decider := svc.(*deciderService) + req := decider.ResolveRequest(ctx, mf, "", 128, 0) + + Expect(req.Format).To(Equal("opus")) + Expect(req.BitRate).To(Equal(128)) + }) + + It("passes offset through", func() { + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16}) + + decider := svc.(*deciderService) + req := decider.ResolveRequest(ctx, mf, "opus", 128, 30) + + Expect(req.Format).To(Equal("opus")) + Expect(req.Offset).To(Equal(30)) + }) + + Context("fallback for unknown format", func() { + It("falls back to DefaultDownsamplingFormat", func() { + DeferCleanup(configtest.SetupConfig()) + conf.Server.DefaultDownsamplingFormat = "opus" + + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100}) + + decider := svc.(*deciderService) + req := decider.ResolveRequest(ctx, mf, "xyz", 0, 0) + + Expect(req.Format).To(Equal("opus")) + }) + + It("falls back to raw when DefaultDownsamplingFormat is empty", func() { + DeferCleanup(configtest.SetupConfig()) + conf.Server.DefaultDownsamplingFormat = "" + + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100}) + + decider := svc.(*deciderService) + req := decider.ResolveRequest(ctx, mf, "xyz", 0, 0) + + Expect(req.Format).To(Equal("raw")) + }) + + It("falls back to raw when DefaultDownsamplingFormat is also invalid", func() { + DeferCleanup(configtest.SetupConfig()) + conf.Server.DefaultDownsamplingFormat = "xyz" + + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100}) + + decider := svc.(*deciderService) + req := decider.ResolveRequest(ctx, mf, "xyz", 0, 0) + + Expect(req.Format).To(Equal("raw")) + }) + + It("preserves bitrate when falling back to DefaultDownsamplingFormat", func() { + DeferCleanup(configtest.SetupConfig()) + conf.Server.DefaultDownsamplingFormat = "opus" + + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16}) + + decider := svc.(*deciderService) + req := decider.ResolveRequest(ctx, mf, "xyz", 128, 0) + + Expect(req.Format).To(Equal("opus")) + Expect(req.BitRate).To(Equal(128)) + }) + }) +}) diff --git a/server/e2e/subsonic_media_retrieval_test.go b/server/e2e/subsonic_media_retrieval_test.go index f51386672..079b131ff 100644 --- a/server/e2e/subsonic_media_retrieval_test.go +++ b/server/e2e/subsonic_media_retrieval_test.go @@ -89,11 +89,11 @@ var _ = Describe("Media Retrieval Endpoints", Ordered, func() { Expect(streamerSpy.LastRequest.BitRate).To(Equal(128)) }) - It("falls back to raw for unknown format", func() { + It("falls back to default downsampling format for unknown format", func() { w := doRawReq("stream", "id", trackID, "format", "xyz") Expect(w.Code).To(Equal(http.StatusOK)) - Expect(streamerSpy.LastRequest.Format).To(Equal("raw")) + Expect(streamerSpy.LastRequest.Format).To(Equal("opus")) }) It("passes timeOffset through", func() { From 0312eb33f176af88ba0d00d446fcc5777075de4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Thu, 12 Mar 2026 08:21:49 -0400 Subject: [PATCH 03/81] fix(ui): improve browser codec detection and limit Safari transcoding to mp3 (#5171) * fix: update codec MIME types to support multiple variants for better compatibility Signed-off-by: Deluan * fix: limit Safari transcoding to mp3 Signed-off-by: Deluan * style: format browserProfile test file with prettier * fix: comment Signed-off-by: Deluan --------- Signed-off-by: Deluan --- ui/src/transcode/browserProfile.js | 52 +++++++++++++---- ui/src/transcode/browserProfile.test.js | 78 +++++++++++++++++++++++-- 2 files changed, 113 insertions(+), 17 deletions(-) diff --git a/ui/src/transcode/browserProfile.js b/ui/src/transcode/browserProfile.js index 5a4cde20f..d268af7c9 100644 --- a/ui/src/transcode/browserProfile.js +++ b/ui/src/transcode/browserProfile.js @@ -1,12 +1,16 @@ -// Each entry: { codec name for the server, container, MIME to probe } +// Each entry: { codec name for the server, container, mime: [MIME probe strings] } export const CODEC_PROBES = [ - { codec: 'mp3', container: 'mp3', mime: 'audio/mpeg' }, - { codec: 'aac', container: 'mp4', mime: 'audio/mp4; codecs="mp4a.40.2"' }, - { codec: 'opus', container: 'ogg', mime: 'audio/ogg; codecs="opus"' }, - { codec: 'vorbis', container: 'ogg', mime: 'audio/ogg; codecs="vorbis"' }, - { codec: 'flac', container: 'flac', mime: 'audio/flac' }, - { codec: 'wav', container: 'wav', mime: 'audio/wav' }, - { codec: 'alac', container: 'mp4', mime: 'audio/mp4; codecs="alac"' }, + { codec: 'mp3', container: 'mp3', mime: ['audio/mpeg; codecs="mp3"'] }, + { codec: 'opus', container: 'ogg', mime: ['audio/ogg; codecs="opus"'] }, + { codec: 'vorbis', container: 'ogg', mime: ['audio/ogg; codecs="vorbis"'] }, + { + codec: 'flac', + container: 'flac', + mime: ['audio/flac', 'audio/flac; codecs="flac"'], + }, + { codec: 'wav', container: 'wav', mime: ['audio/wav; codecs="1"'] }, + { codec: 'alac', container: 'mp4', mime: ['audio/mp4; codecs="alac"'] }, + { codec: 'aac', container: 'mp4', mime: ['audio/mp4; codecs="mp4a.40.2"'] }, ] // Transcoding targets in preference order (lossless first, then lossy). @@ -14,8 +18,27 @@ export const CODEC_PROBES = [ // MP3 is always included as a universal fallback. const TRANSCODE_CODECS = ['flac', 'opus', 'mp3'] +// Safari transcoding is limited to mp3 only. Safari cannot reliably stream +// Ogg containers (reports canPlayType support but fails on non-seekable +// transcoded streams), and FLAC transcoding also fails in practice. +const SAFARI_TRANSCODE_CODECS = ['mp3'] + +function canPlay(audio, mimeList) { + return mimeList.some((m) => { + const result = audio.canPlayType(m) + return result === 'probably' || result === 'maybe' + }) +} + function probeSupported(audio, probes) { - return probes.filter(({ mime }) => audio.canPlayType(mime) === 'probably') + return probes.filter(({ mime }) => canPlay(audio, mime)) +} + +function isSafari() { + const ua = navigator.userAgent + return ( + ua.includes('Safari') && !ua.includes('Chrome') && !ua.includes('Chromium') + ) } export function detectBrowserProfile() { @@ -29,10 +52,15 @@ export function detectBrowserProfile() { }), ) - // Build transcoding profiles from supported codecs, always keeping mp3 as fallback - const transcodingProfiles = TRANSCODE_CODECS.reduce((profiles, codec) => { + // Build transcoding profiles from supported codecs, always keeping mp3 as fallback. + // Safari is limited to mp3 transcoding only. + const transcodeCodecs = isSafari() + ? SAFARI_TRANSCODE_CODECS + : TRANSCODE_CODECS + const transcodingProfiles = transcodeCodecs.reduce((profiles, codec) => { const probe = CODEC_PROBES.find((p) => p.codec === codec) - if (audio.canPlayType(probe.mime) === 'probably' || codec === 'mp3') { + if (!probe) return profiles + if (canPlay(audio, probe.mime) || codec === 'mp3') { profiles.push({ container: probe.container, audioCodec: codec, diff --git a/ui/src/transcode/browserProfile.test.js b/ui/src/transcode/browserProfile.test.js index 360ae7885..e79b7a744 100644 --- a/ui/src/transcode/browserProfile.test.js +++ b/ui/src/transcode/browserProfile.test.js @@ -16,7 +16,7 @@ describe('detectBrowserProfile', () => { it('includes codecs that return "probably"', () => { mockCanPlayType.mockImplementation((mime) => { - if (mime === 'audio/mpeg') return 'probably' + if (mime === 'audio/mpeg; codecs="mp3"') return 'probably' if (mime === 'audio/ogg; codecs="opus"') return 'probably' return '' }) @@ -31,11 +31,15 @@ describe('detectBrowserProfile', () => { expect(codecs).toContain('opus') }) - it('excludes codecs that return "maybe"', () => { - mockCanPlayType.mockReturnValue('maybe') + it('includes codecs that return "maybe"', () => { + mockCanPlayType.mockImplementation((mime) => { + if (mime === 'audio/flac') return 'maybe' + return '' + }) const profile = detectBrowserProfile() - expect(profile.directPlayProfiles).toEqual([]) + const codecs = profile.directPlayProfiles.flatMap((p) => p.audioCodecs) + expect(codecs).toContain('flac') }) it('excludes codecs that return empty string', () => { @@ -56,7 +60,7 @@ describe('detectBrowserProfile', () => { it('filters transcoding profiles by canPlayType', () => { mockCanPlayType.mockImplementation((mime) => { - if (mime === 'audio/mpeg') return 'probably' + if (mime === 'audio/mpeg; codecs="mp3"') return 'probably' if (mime === 'audio/ogg; codecs="opus"') return 'probably' return '' }) @@ -104,8 +108,72 @@ describe('detectBrowserProfile', () => { expect(profile.codecProfiles).toEqual([]) }) + it('matches codec when any mime variant returns "probably"', () => { + mockCanPlayType.mockImplementation((mime) => { + if (mime === 'audio/flac; codecs="flac"') return 'probably' + return '' + }) + + const profile = detectBrowserProfile() + const codecs = profile.directPlayProfiles.flatMap((p) => p.audioCodecs) + expect(codecs).toContain('flac') + }) + it('includes platform info', () => { const profile = detectBrowserProfile() expect(typeof profile.platform).toBe('string') }) + + describe('Safari restrictions', () => { + beforeEach(() => { + // Safari reports canPlayType for Ogg as positive, but can't actually + // stream transcoded Ogg. Simulate Safari: supports everything. + mockCanPlayType.mockReturnValue('probably') + }) + + it('still includes ogg in direct play profiles on Safari', () => { + vi.stubGlobal('navigator', { + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.2 Safari/605.1.15', + }) + + const profile = detectBrowserProfile() + const containers = profile.directPlayProfiles.flatMap((p) => p.containers) + expect(containers).toContain('ogg') + }) + + it('limits Safari transcoding to mp3 only', () => { + vi.stubGlobal('navigator', { + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.2 Safari/605.1.15', + }) + + const profile = detectBrowserProfile() + const codecs = profile.transcodingProfiles.map((p) => p.audioCodec) + expect(codecs).toEqual(['mp3']) + }) + + it('does NOT restrict transcoding on Chrome', () => { + vi.stubGlobal('navigator', { + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + }) + + const profile = detectBrowserProfile() + const codecs = profile.transcodingProfiles.map((p) => p.audioCodec) + expect(codecs).toContain('opus') + expect(codecs).toContain('flac') + }) + + it('applies same restrictions on iOS Safari', () => { + vi.stubGlobal('navigator', { + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1', + }) + + const profile = detectBrowserProfile() + const codecs = profile.transcodingProfiles.map((p) => p.audioCodec) + expect(codecs).toEqual(['mp3']) + }) + }) }) From 903e3f070fa1cb83c879b60e4e2bf7333d18a000 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:29:37 +0000 Subject: [PATCH 04/81] fix(subsonic): always return required playqueue fields (#5172) --- server/e2e/subsonic_bookmarks_test.go | 22 +++++++++++++++++++--- server/subsonic/bookmarks.go | 12 ++++++++++-- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/server/e2e/subsonic_bookmarks_test.go b/server/e2e/subsonic_bookmarks_test.go index d0dc06208..726b41743 100644 --- a/server/e2e/subsonic_bookmarks_test.go +++ b/server/e2e/subsonic_bookmarks_test.go @@ -77,12 +77,28 @@ var _ = Describe("Bookmark and PlayQueue Endpoints", Ordered, func() { } }) - It("getPlayQueue returns empty when nothing saved", func() { + It("getPlayQueue returns minimum required fields when nothing specified", func() { resp := doReq("getPlayQueue") Expect(resp.Status).To(Equal(responses.StatusOK)) - // When no play queue exists, PlayQueue should be nil (no entry returned) - Expect(resp.PlayQueue).To(BeNil()) + Expect(resp.PlayQueue).ToNot(BeNil()) + Expect(resp.PlayQueue.Entry).To(HaveLen(0)) + Expect(resp.PlayQueue.Current).To(BeEmpty()) + Expect(resp.PlayQueue.Position).To(Equal(int64(0))) + Expect(resp.PlayQueue.Username).To(Equal(adminUser.UserName)) + Expect(resp.PlayQueue.ChangedBy).To(BeEmpty()) + }) + + It("getPlayQueueByIndex returns minimum required fields when nothing specified", func() { + resp := doReq("getPlayQueueByIndex") + + Expect(resp.Status).To(Equal(responses.StatusOK)) + Expect(resp.PlayQueueByIndex).ToNot(BeNil()) + Expect(resp.PlayQueueByIndex.Entry).To(HaveLen(0)) + Expect(resp.PlayQueueByIndex.CurrentIndex).To(BeNil()) + Expect(resp.PlayQueueByIndex.Position).To(Equal(int64(0))) + Expect(resp.PlayQueueByIndex.Username).To(Equal(adminUser.UserName)) + Expect(resp.PlayQueueByIndex.ChangedBy).To(BeEmpty()) }) It("savePlayQueue stores current play queue", func() { diff --git a/server/subsonic/bookmarks.go b/server/subsonic/bookmarks.go index b1e71b1c7..337712750 100644 --- a/server/subsonic/bookmarks.go +++ b/server/subsonic/bookmarks.go @@ -78,7 +78,11 @@ func (api *Router) GetPlayQueue(r *http.Request) (*responses.Subsonic, error) { return nil, err } if pq == nil || len(pq.Items) == 0 { - return newResponse(), nil + response := newResponse() + response.PlayQueue = &responses.PlayQueue{ + Username: user.UserName, + } + return response, nil } response := newResponse() @@ -145,7 +149,11 @@ func (api *Router) GetPlayQueueByIndex(r *http.Request) (*responses.Subsonic, er return nil, err } if pq == nil || len(pq.Items) == 0 { - return newResponse(), nil + response := newResponse() + response.PlayQueueByIndex = &responses.PlayQueueByIndex{ + Username: user.UserName, + } + return response, nil } response := newResponse() From d0fbba14ff7dd456157ffb8a2d5d860a97f63fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Thu, 12 Mar 2026 11:39:31 -0400 Subject: [PATCH 05/81] fix(db): check both name and target_format in default transcodings migration (#5175) The ensure_default_transcodings migration only checked target_format before inserting, but the transcoding table has UNIQUE constraints on both name and target_format. Older installations may have entries where the name matches a default (e.g., 'opus audio') but the target_format differs (e.g., 'oga' instead of 'opus'), causing a UNIQUE constraint violation on name during the INSERT. Fixes #5174 --- db/migrations/20260309203355_ensure_default_transcodings.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/db/migrations/20260309203355_ensure_default_transcodings.go b/db/migrations/20260309203355_ensure_default_transcodings.go index ff3838222..ab6d24952 100644 --- a/db/migrations/20260309203355_ensure_default_transcodings.go +++ b/db/migrations/20260309203355_ensure_default_transcodings.go @@ -17,9 +17,12 @@ func upEnsureDefaultTranscodings(_ context.Context, tx *sql.Tx) error { // Older installations may be missing default transcodings that were added // after the initial seeding (e.g., aac was added later than mp3/opus). // Insert any missing defaults without touching user-customized entries. + // Check both target_format and name since both have UNIQUE constraints, + // and older entries may have a different target_format (e.g., 'oga' vs 'opus') + // but the same name. for _, t := range consts.DefaultTranscodings { var count int - err := tx.QueryRow("SELECT COUNT(*) FROM transcoding WHERE target_format = ?", t.TargetFormat).Scan(&count) + err := tx.QueryRow("SELECT COUNT(*) FROM transcoding WHERE target_format = ? OR name = ?", t.TargetFormat, t.Name).Scan(&count) if err != nil { return err } From 0790f66627cd56135874a567d2277da604d69970 Mon Sep 17 00:00:00 2001 From: Deluan Date: Thu, 12 Mar 2026 17:07:34 -0400 Subject: [PATCH 06/81] fix(scanner): increase watcher channel buffers to prevent dropped filesystem events When files were moved between libraries, the small channel buffers (size 1) throughout the watcher pipeline caused backpressure that led to dropped filesystem events. This meant only some of the affected folders were scanned, preventing cross-library move detection from working correctly. Increase all watcher channel buffers to 500 and switch to blocking sends to ensure no filesystem events are silently dropped. --- core/storage/local/watcher.go | 4 ++-- scanner/watcher.go | 10 +++------- scanner/watcher_test.go | 2 +- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/core/storage/local/watcher.go b/core/storage/local/watcher.go index e2418f4cb..1b8a4e0c8 100644 --- a/core/storage/local/watcher.go +++ b/core/storage/local/watcher.go @@ -17,8 +17,8 @@ func (s *localStorage) Start(ctx context.Context) (<-chan string, error) { if !s.watching.CompareAndSwap(false, true) { return nil, errors.New("watcher already started") } - input := make(chan notify.EventInfo, 1) - output := make(chan string, 1) + input := make(chan notify.EventInfo, 500) + output := make(chan string, 500) started := make(chan struct{}) go func() { diff --git a/scanner/watcher.go b/scanner/watcher.go index 62fcc9341..376db910c 100644 --- a/scanner/watcher.go +++ b/scanner/watcher.go @@ -48,7 +48,7 @@ func GetWatcher(ds model.DataStore, s model.Scanner) Watcher { ds: ds, scanner: s, triggerWait: conf.Server.Scanner.WatcherWait, - watcherNotify: make(chan scanNotification, 1), + watcherNotify: make(chan scanNotification, 500), libraryWatchers: make(map[int]*libraryWatcherInstance), } }) @@ -272,12 +272,8 @@ func (w *watcher) processLibraryEvents(ctx context.Context, lib *model.Library, continue } - // Notify the main watcher of changes - select { - case w.watcherNotify <- scanNotification{Library: lib, FolderPath: folderPath}: - default: - // Channel is full, notification already pending - } + // Notify the main watcher of changes. This will trigger a scan after the debounce period. + w.watcherNotify <- scanNotification{Library: lib, FolderPath: folderPath} } } } diff --git a/scanner/watcher_test.go b/scanner/watcher_test.go index 7a431d5a0..e1600db32 100644 --- a/scanner/watcher_test.go +++ b/scanner/watcher_test.go @@ -27,7 +27,7 @@ var _ = Describe("Watcher", func() { DeferCleanup(configtest.SetupConfig()) conf.Server.Scanner.WatcherWait = 50 * time.Millisecond // Short wait for tests - ctx, cancel = context.WithCancel(context.Background()) + ctx, cancel = context.WithCancel(GinkgoT().Context()) DeferCleanup(cancel) lib = &model.Library{ From 4ddb0774ecb2c7d4dd714daf841994bc9a11ac1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Fri, 13 Mar 2026 09:35:59 -0400 Subject: [PATCH 07/81] perf(artwork): improve image serving performance with WebP encoding and optimized pipeline (#5181) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(artwork): add benchmark helpers for generating test images * test(artwork): add image decode benchmarks for JPEG/PNG at various sizes * test(artwork): add image resize benchmarks for Lanczos at various sizes * test(artwork): add image encode benchmarks for JPEG quality levels and PNG * test(artwork): add full resize pipeline benchmark (decode+resize+encode) * test(artwork): add tag extraction benchmark for embedded art * test(cache): add file cache benchmarks for read, write, and concurrent access * test(artwork): add E2E benchmarks for artwork.Get with cache on/off and concurrency * fix(test): use absolute path for tag extraction benchmark fixture * test(artwork): add resize alternatives benchmark comparing resamplers * perf(artwork): switch to CatmullRom resampler and JPEG for square images Replace imaging.Lanczos with imaging.CatmullRom for image resizing (30% faster, indistinguishable quality at thumbnail sizes). Stop forcing PNG encoding for square images when the source is JPEG — JPEG is smaller and faster to encode. Square images from JPEG sources went from 52ms to 10ms (80% improvement). Add sync.Pool for encode buffers to reduce GC pressure under concurrent load. * perf(artwork): increase cache warmer concurrency from 2 to 4 workers Resize is CPU-bound, so more workers improve throughput on multi-core systems. Doubled worker count to better utilize available cores during background cache warming. * perf(artwork): switch to xdraw.ApproxBiLinear and always encode as JPEG Replace disintegration/imaging with golang.org/x/image/draw for image resizing. This eliminates ~92K allocations per resize (from imaging's internal goroutine parallelism) down to ~20, reducing GC pressure under concurrent load. Always encode resized artwork as JPEG regardless of source format, since cover art doesn't need transparency. This is ~5x faster than PNG encode and produces much smaller output (e.g. 18KB JPEG vs 124KB PNG). * perf(artwork): skip external API call when artist image URL is cached ArtistImage() was always calling the external agent (Spotify/Last.fm) to get the image URL, even when the artist already had URLs stored in the database. This caused every artist image request to block on an external API call, creating severe serialization when loading artist grids (5-20 seconds for the first page). Now use the stored URL directly when available. Artists with no stored URL still fetch synchronously. Background refresh via UpdateArtistInfo handles TTL-based URL updates. * perf(artwork): increase getCoverArt throttle from NumCPU/3 to NumCPU The previous default of max(2, NumCPU/3) was too aggressive for artist images which are I/O-bound (downloading from external CDNs), not CPU-bound. On an 8-core machine this meant only 2 concurrent requests, causing a staircase pattern where 12 images took ~2.4s wall-clock. Bumping to max(4, NumCPU) cuts wall-clock time by ~50% for artist image grids while still preventing unbounded concurrency for CPU-bound resizes. * perf(artwork): encode resized images as WebP instead of JPEG Switch from JPEG to WebP encoding for resized artwork using gen2brain/webp (libwebp via WASM, no CGo). WebP produces ~74% smaller output at the same quality with only ~25% slower full-pipeline encode time (cached, so only paid once per artwork+size). Use NRGBA image type to preserve alpha channel in WebP output, and transparent padding for square canvas instead of black. Also removes the disintegration/imaging dependency entirely by replacing imaging.Fill in playlist tile generation with a custom fillCenter function using xdraw.ApproxBiLinear. * perf(artwork): switch from ApproxBiLinear to BiLinear scaling for improved image processing Signed-off-by: Deluan * refactor(configuration): rename CoverJpegQuality to CoverArtQuality and update references Signed-off-by: Deluan * feat(artwork): add DevJpegCoverArt option to control JPEG encoding for cover art Signed-off-by: Deluan * fix(artwork): remove redundant transparent fill and handle encode errors in resizeImage Removed a no-op draw.Draw call that filled the NRGBA canvas with transparent pixels — NewNRGBA already zero-initializes to fully transparent. Also added an early return on encode failure to avoid allocating and copying potentially corrupt buffer data before returning the error. * fix(configuration): reorder default agents (deezer is faster) Signed-off-by: Deluan * fix(test): resolve dogsled lint warning in tag extraction benchmark Use all return values from runtime.Caller instead of discarding three with blank identifiers, which triggered the dogsled linter. * fix(artwork): revert cache key format Signed-off-by: Deluan * fix(configuration): remove deprecated CoverJpegQuality field and update references to CoverArtQuality Signed-off-by: Deluan --------- Signed-off-by: Deluan --- conf/configuration.go | 12 +- core/artwork/artwork_internal_test.go | 81 ++++++++-- core/artwork/benchmark_decode_test.go | 37 +++++ core/artwork/benchmark_e2e_test.go | 189 ++++++++++++++++++++++++ core/artwork/benchmark_encode_test.go | 40 +++++ core/artwork/benchmark_helpers_test.go | 47 ++++++ core/artwork/benchmark_pipeline_test.go | 51 +++++++ core/artwork/benchmark_tag_test.go | 38 +++++ core/artwork/cache_warmer.go | 2 +- core/artwork/reader_playlist.go | 33 ++++- core/artwork/reader_resized.go | 72 ++++++--- core/external/provider.go | 16 +- go.mod | 3 +- go.sum | 7 +- utils/cache/benchmark_test.go | 171 +++++++++++++++++++++ 15 files changed, 749 insertions(+), 50 deletions(-) create mode 100644 core/artwork/benchmark_decode_test.go create mode 100644 core/artwork/benchmark_e2e_test.go create mode 100644 core/artwork/benchmark_encode_test.go create mode 100644 core/artwork/benchmark_helpers_test.go create mode 100644 core/artwork/benchmark_pipeline_test.go create mode 100644 core/artwork/benchmark_tag_test.go create mode 100644 utils/cache/benchmark_test.go diff --git a/conf/configuration.go b/conf/configuration.go index da549ce26..b867e0d56 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -69,7 +69,7 @@ type configOptions struct { MPVPath string MPVCmdTemplate string CoverArtPriority string - CoverJpegQuality int + CoverArtQuality int ArtistArtPriority string LyricsPriority string EnableGravatar bool @@ -140,6 +140,7 @@ type configOptions struct { DevOptimizeDB bool DevPreserveUnicodeInExternalCalls bool DevEnableMediaFileProbe bool + DevJpegCoverArt bool } type scannerOptions struct { @@ -283,6 +284,7 @@ func Load(noConfigDump bool) { mapDeprecatedOption("ReverseProxyWhitelist", "ExtAuth.TrustedSources") mapDeprecatedOption("ReverseProxyUserHeader", "ExtAuth.UserHeader") mapDeprecatedOption("HTTPSecurityHeaders.CustomFrameOptionsValue", "HTTPHeaders.FrameOptions") + mapDeprecatedOption("CoverJpegQuality", "CoverArtQuality") err := viper.Unmarshal(&Server) if err != nil { @@ -415,6 +417,7 @@ func Load(noConfigDump bool) { logDeprecatedOptions("ReverseProxyWhitelist", "ExtAuth.TrustedSources") logDeprecatedOptions("ReverseProxyUserHeader", "ExtAuth.UserHeader") logDeprecatedOptions("HTTPSecurityHeaders.CustomFrameOptionsValue", "HTTPHeaders.FrameOptions") + logDeprecatedOptions("CoverJpegQuality", "CoverArtQuality") // Call init hooks for _, hook := range hooks { @@ -655,7 +658,7 @@ func setViperDefaults() { viper.SetDefault("ffmpegpath", "") viper.SetDefault("mpvcmdtemplate", "mpv --audio-device=%d --no-audio-display %f --input-ipc-server=%s") viper.SetDefault("coverartpriority", "cover.*, folder.*, front.*, embedded, external") - viper.SetDefault("coverjpegquality", 75) + viper.SetDefault("coverartquality", 75) viper.SetDefault("artistartpriority", "artist.*, album/artist.*, external") viper.SetDefault("lyricspriority", ".lrc,.txt,embedded") viper.SetDefault("enablegravatar", false) @@ -707,7 +710,7 @@ func setViperDefaults() { viper.SetDefault("subsonic.enableaveragerating", true) viper.SetDefault("subsonic.legacyclients", "DSub") viper.SetDefault("subsonic.minimalclients", "SubMusic") - viper.SetDefault("agents", "lastfm,spotify,deezer") + viper.SetDefault("agents", "deezer,lastfm,spotify") viper.SetDefault("lastfm.enabled", true) viper.SetDefault("lastfm.language", consts.DefaultInfoLanguage) viper.SetDefault("lastfm.apikey", "") @@ -749,7 +752,7 @@ func setViperDefaults() { viper.SetDefault("devuishowconfig", true) viper.SetDefault("devneweventstream", true) viper.SetDefault("devoffsetoptimize", 50000) - viper.SetDefault("devartworkmaxrequests", max(2, runtime.NumCPU()/3)) + viper.SetDefault("devartworkmaxrequests", max(4, runtime.NumCPU())) viper.SetDefault("devartworkthrottlebackloglimit", consts.RequestThrottleBacklogLimit) viper.SetDefault("devartworkthrottlebacklogtimeout", consts.RequestThrottleBacklogTimeout) viper.SetDefault("devartistinfotimetolive", consts.ArtistInfoTimeToLive) @@ -765,6 +768,7 @@ func setViperDefaults() { viper.SetDefault("devoptimizedb", true) viper.SetDefault("devpreserveunicodeinexternalcalls", false) viper.SetDefault("devenablemediafileprobe", true) + viper.SetDefault("devjpegcoverart", false) } func init() { diff --git a/core/artwork/artwork_internal_test.go b/core/artwork/artwork_internal_test.go index e2ea7adb0..5ca32f401 100644 --- a/core/artwork/artwork_internal_test.go +++ b/core/artwork/artwork_internal_test.go @@ -7,9 +7,12 @@ import ( "image/jpeg" "image/png" "io" + "os" "path/filepath" + _ "github.com/gen2brain/webp" + "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf/configtest" "github.com/navidrome/navidrome/log" @@ -353,24 +356,24 @@ var _ = Describe("Artwork", func() { }) }) When("Square is false", func() { - It("returns a PNG if original image is a PNG", func() { + It("returns WebP even if original image is a PNG", func() { conf.Server.CoverArtPriority = "front.png" r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID(), 15, false) Expect(err).ToNot(HaveOccurred()) img, format, err := image.Decode(r) Expect(err).ToNot(HaveOccurred()) - Expect(format).To(Equal("png")) + Expect(format).To(Equal("webp")) Expect(img.Bounds().Size().X).To(Equal(15)) Expect(img.Bounds().Size().Y).To(Equal(15)) }) - It("returns a JPEG if original image is not a PNG", func() { + It("returns WebP if original image is not a PNG", func() { conf.Server.CoverArtPriority = "cover.jpg" r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID(), 200, false) Expect(err).ToNot(HaveOccurred()) img, format, err := image.Decode(r) - Expect(format).To(Equal("jpeg")) + Expect(format).To(Equal("webp")) Expect(err).ToNot(HaveOccurred()) Expect(img.Bounds().Size().X).To(Equal(200)) Expect(img.Bounds().Size().Y).To(Equal(200)) @@ -380,9 +383,9 @@ var _ = Describe("Artwork", func() { var alCover model.Album DescribeTable("resize", - func(format string, landscape bool, size int) { - coverFileName := "cover." + format - dirName := createImage(format, landscape, size) + func(srcFormat string, expectedFormat string, landscape bool, size int) { + coverFileName := "cover." + srcFormat + dirName := createImage(srcFormat, landscape, size) alCover = model.Album{ ID: "444", Name: "Only external", @@ -399,16 +402,70 @@ var _ = Describe("Artwork", func() { img, format, err := image.Decode(r) Expect(err).ToNot(HaveOccurred()) - Expect(format).To(Equal("png")) + Expect(format).To(Equal(expectedFormat)) Expect(img.Bounds().Size().X).To(Equal(size)) Expect(img.Bounds().Size().Y).To(Equal(size)) }, - Entry("portrait png image", "png", false, 200), - Entry("landscape png image", "png", true, 200), - Entry("portrait jpg image", "jpg", false, 200), - Entry("landscape jpg image", "jpg", true, 200), + Entry("portrait png image", "png", "webp", false, 200), + Entry("landscape png image", "png", "webp", true, 200), + Entry("portrait jpg image", "jpg", "webp", false, 200), + Entry("landscape jpg image", "jpg", "webp", true, 200), ) }) + When("DevJpegCoverArt is true and square is false", func() { + BeforeEach(func() { + conf.Server.DevJpegCoverArt = true + }) + It("returns JPEG even if original image is a PNG", func() { + conf.Server.CoverArtPriority = "front.png" + r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID(), 15, false) + Expect(err).ToNot(HaveOccurred()) + + img, format, err := image.Decode(r) + Expect(err).ToNot(HaveOccurred()) + Expect(format).To(Equal("jpeg")) + Expect(img.Bounds().Size().X).To(Equal(15)) + Expect(img.Bounds().Size().Y).To(Equal(15)) + }) + It("returns JPEG if original image is a JPG", func() { + conf.Server.CoverArtPriority = "cover.jpg" + r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID(), 200, false) + Expect(err).ToNot(HaveOccurred()) + + img, format, err := image.Decode(r) + Expect(err).ToNot(HaveOccurred()) + Expect(format).To(Equal("jpeg")) + Expect(img.Bounds().Size().X).To(Equal(200)) + Expect(img.Bounds().Size().Y).To(Equal(200)) + }) + }) + When("DevJpegCoverArt is true and square is true", func() { + var alCover model.Album + + BeforeEach(func() { + conf.Server.DevJpegCoverArt = true + }) + It("returns PNG for square mode", func() { + dirName := createImage("png", false, 200) + alCover = model.Album{ + ID: "444", + Name: "Only external", + FolderIDs: []string{"tmp"}, + } + folderRepo.result = []model.Folder{{Path: dirName, ImageFiles: []string{"cover.png"}}} + ds.Album(ctx).(*tests.MockAlbumRepo).SetData(model.Albums{alCover}) + + conf.Server.CoverArtPriority = "cover.png" + r, _, err := aw.Get(context.Background(), alCover.CoverArtID(), 200, true) + Expect(err).ToNot(HaveOccurred()) + + img, format, err := image.Decode(r) + Expect(err).ToNot(HaveOccurred()) + Expect(format).To(Equal("png")) + Expect(img.Bounds().Size().X).To(Equal(200)) + Expect(img.Bounds().Size().Y).To(Equal(200)) + }) + }) When("Requested size is larger than original", func() { It("clamps size to original dimensions", func() { conf.Server.CoverArtPriority = "front.png" diff --git a/core/artwork/benchmark_decode_test.go b/core/artwork/benchmark_decode_test.go new file mode 100644 index 000000000..cfbfe5605 --- /dev/null +++ b/core/artwork/benchmark_decode_test.go @@ -0,0 +1,37 @@ +package artwork + +import ( + "bytes" + "fmt" + "image" + _ "image/jpeg" + _ "image/png" + "testing" +) + +func BenchmarkImageDecode(b *testing.B) { + sizes := []int{300, 1000, 3000} + formats := []struct { + name string + gen func(tb testing.TB, w, h int) []byte + }{ + {"jpeg", func(tb testing.TB, w, h int) []byte { return generateJPEG(tb, w, h, 75) }}, + {"png", func(tb testing.TB, w, h int) []byte { return generatePNG(tb, w, h) }}, + } + + for _, format := range formats { + for _, size := range sizes { + data := format.gen(b, size, size) + b.Run(fmt.Sprintf("%s/%dx%d", format.name, size, size), func(b *testing.B) { + b.SetBytes(int64(len(data))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _, err := image.Decode(bytes.NewReader(data)) + if err != nil { + b.Fatal(err) + } + } + }) + } + } +} diff --git a/core/artwork/benchmark_e2e_test.go b/core/artwork/benchmark_e2e_test.go new file mode 100644 index 000000000..c27964018 --- /dev/null +++ b/core/artwork/benchmark_e2e_test.go @@ -0,0 +1,189 @@ +package artwork + +import ( + "context" + "fmt" + "image/jpeg" + "io" + "os" + "path/filepath" + "runtime" + "sync" + "testing" + "time" + + "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/conf/configtest" + "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/tests" + "github.com/navidrome/navidrome/utils/cache" +) + +// setupE2EBenchmark creates an artwork instance with a real album cover image on disk, +// backed by either a real file cache or disabled cache depending on cacheSize. +// Note: This benchmarks artwork.Get() directly (not the full HTTP handler), which covers +// the critical path (source selection, decode, resize, encode, cache). This is a deliberate +// spec deviation — the full HTTP round-trip benchmark requires significant infrastructure +// (DB, scanner, fake filesystem) and can be added later if HTTP overhead proves significant. +// +// Depends on fakeFolderRepo defined in reader_artist_test.go (same package, compiled together). +func setupE2EBenchmark(b *testing.B, cacheSize string) (Artwork, model.ArtworkID, func()) { + b.Helper() + cleanup := configtest.SetupConfig() + b.Cleanup(cleanup) + + tmpDir, err := os.MkdirTemp("", "artwork-bench-*") + if err != nil { + b.Fatal(err) + } + + // Create a realistic cover image on disk + coverPath := filepath.Join(tmpDir, "cover.jpg") + coverImg := generateGradientImage(1000, 1000) + f, err := os.Create(coverPath) + if err != nil { + b.Fatal(err) + } + if err := jpeg.Encode(f, coverImg, &jpeg.Options{Quality: 90}); err != nil { + f.Close() + b.Fatal(err) + } + f.Close() + + // Configure cache + conf.Server.ImageCacheSize = cacheSize + conf.Server.CacheFolder = tmpDir + conf.Server.CoverArtQuality = 75 + conf.Server.CoverArtPriority = "cover.*" + + // Set up mock data store with album pointing to our cover. + // Set UpdatedAt so CoverArtID().LastUpdate is consistent across calls. + album := model.Album{ + ID: "bench-album-1", + Name: "Benchmark Album", + FolderIDs: []string{"f1"}, + UpdatedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), + } + folderRepo := &fakeFolderRepo{ + result: []model.Folder{{ + Path: tmpDir, + ImageFiles: []string{"cover.jpg"}, + }}, + } + ds := &tests.MockDataStore{ + MockedTranscoding: &tests.MockTranscodingRepo{}, + MockedFolder: folderRepo, + } + ds.Album(context.Background()).(*tests.MockAlbumRepo).SetData(model.Albums{album}) + + artID := album.CoverArtID() + + imgCache := cache.NewFileCache("BenchImage", cacheSize, "bench-images", 0, + func(ctx context.Context, arg cache.Item) (io.Reader, error) { + r, _, err := arg.(artworkReader).Reader(ctx) + return r, err + }) + + // Wait for cache init if enabled + if cacheSize != "0" { + for !imgCache.Available(context.Background()) && !imgCache.Disabled(context.Background()) { + runtime.Gosched() // Yield to allow background init goroutine to run + } + } + + ffmpeg := tests.NewMockFFmpeg("fallback content") + aw := NewArtwork(ds, imgCache, ffmpeg, nil) + + cleanupAll := func() { + os.RemoveAll(tmpDir) + } + return aw, artID, cleanupAll +} + +func BenchmarkArtworkGetE2E(b *testing.B) { + cacheConfigs := []struct { + name string + cacheSize string + }{ + {"no_cache", "0"}, + {"with_cache", "100MB"}, + } + sizes := []int{0, 300} + + for _, cc := range cacheConfigs { + for _, size := range sizes { + b.Run(fmt.Sprintf("%s/size_%d", cc.name, size), func(b *testing.B) { + aw, artID, cleanup := setupE2EBenchmark(b, cc.cacheSize) + defer cleanup() + + // Warm the cache on first call if cache is enabled + if cc.cacheSize != "0" { + r, _, err := aw.Get(context.Background(), artID, size, size > 0) + if err != nil { + b.Fatal(err) + } + _, _ = io.ReadAll(r) + r.Close() + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + r, _, err := aw.Get(context.Background(), artID, size, size > 0) + if err != nil { + b.Fatal(err) + } + _, _ = io.ReadAll(r) + r.Close() + } + }) + } + } +} + +func BenchmarkArtworkGetE2EConcurrent(b *testing.B) { + cacheConfigs := []struct { + name string + cacheSize string + }{ + {"no_cache", "0"}, + {"with_cache", "100MB"}, + } + concurrencyLevels := []int{10, 50} + + for _, cc := range cacheConfigs { + for _, n := range concurrencyLevels { + b.Run(fmt.Sprintf("%s/goroutines_%d", cc.name, n), func(b *testing.B) { + aw, artID, cleanup := setupE2EBenchmark(b, cc.cacheSize) + defer cleanup() + + // Warm cache + if cc.cacheSize != "0" { + r, _, _ := aw.Get(context.Background(), artID, 300, true) + if r != nil { + _, _ = io.ReadAll(r) + r.Close() + } + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + var wg sync.WaitGroup + wg.Add(n) + for g := 0; g < n; g++ { + go func() { + defer wg.Done() + r, _, err := aw.Get(context.Background(), artID, 300, true) + if err != nil { + b.Error(err) + return + } + _, _ = io.ReadAll(r) + r.Close() + }() + } + wg.Wait() + } + }) + } + } +} diff --git a/core/artwork/benchmark_encode_test.go b/core/artwork/benchmark_encode_test.go new file mode 100644 index 000000000..d8ab858f5 --- /dev/null +++ b/core/artwork/benchmark_encode_test.go @@ -0,0 +1,40 @@ +package artwork + +import ( + "bytes" + "fmt" + "image/jpeg" + "image/png" + "testing" +) + +func BenchmarkImageEncode(b *testing.B) { + img := generateGradientImage(300, 300) + + jpegQualities := []int{60, 75, 90} + for _, q := range jpegQualities { + b.Run(fmt.Sprintf("jpeg/q%d/300x300", q), func(b *testing.B) { + var buf bytes.Buffer + b.ResetTimer() + for i := 0; i < b.N; i++ { + buf.Reset() + if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: q}); err != nil { + b.Fatal(err) + } + } + b.ReportMetric(float64(buf.Len()), "bytes") + }) + } + + b.Run("png/300x300", func(b *testing.B) { + var buf bytes.Buffer + b.ResetTimer() + for i := 0; i < b.N; i++ { + buf.Reset() + if err := png.Encode(&buf, img); err != nil { + b.Fatal(err) + } + } + b.ReportMetric(float64(buf.Len()), "bytes") + }) +} diff --git a/core/artwork/benchmark_helpers_test.go b/core/artwork/benchmark_helpers_test.go new file mode 100644 index 000000000..60990bb8b --- /dev/null +++ b/core/artwork/benchmark_helpers_test.go @@ -0,0 +1,47 @@ +package artwork + +import ( + "bytes" + "image" + "image/color" + "image/jpeg" + "image/png" + "testing" +) + +// generateJPEG creates a JPEG image of the given dimensions with a gradient pattern. +// The gradient ensures the image has realistic entropy (not trivially compressible). +func generateJPEG(t testing.TB, width, height, quality int) []byte { + t.Helper() + img := generateGradientImage(width, height) + var buf bytes.Buffer + if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: quality}); err != nil { + t.Fatal(err) + } + return buf.Bytes() +} + +// generatePNG creates a PNG image of the given dimensions with a gradient pattern. +func generatePNG(t testing.TB, width, height int) []byte { + t.Helper() + img := generateGradientImage(width, height) + var buf bytes.Buffer + if err := png.Encode(&buf, img); err != nil { + t.Fatal(err) + } + return buf.Bytes() +} + +// generateGradientImage creates an RGBA image with a diagonal gradient pattern. +func generateGradientImage(width, height int) *image.RGBA { + img := image.NewRGBA(image.Rect(0, 0, width, height)) + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + r := uint8((x * 255) / width) + g := uint8((y * 255) / height) + b := uint8(((x + y) * 255) / (width + height)) + img.Set(x, y, color.RGBA{R: r, G: g, B: b, A: 255}) + } + } + return img +} diff --git a/core/artwork/benchmark_pipeline_test.go b/core/artwork/benchmark_pipeline_test.go new file mode 100644 index 000000000..9dc4f6c95 --- /dev/null +++ b/core/artwork/benchmark_pipeline_test.go @@ -0,0 +1,51 @@ +package artwork + +import ( + "bytes" + "fmt" + "testing" + + "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/conf/configtest" +) + +func BenchmarkResizeFullPipeline(b *testing.B) { + cleanup := configtest.SetupConfig() + b.Cleanup(cleanup) + conf.Server.CoverArtQuality = 75 + + sourceSizes := []int{1000, 3000} + targetSize := 300 + + for _, srcSize := range sourceSizes { + jpegData := generateJPEG(b, srcSize, srcSize, 90) + + b.Run(fmt.Sprintf("jpeg/%dx%d_to_%d", srcSize, srcSize, targetSize), func(b *testing.B) { + b.SetBytes(int64(len(jpegData))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + result, _, err := resizeImage(bytes.NewReader(jpegData), targetSize, false) + if err != nil { + b.Fatal(err) + } + if result == nil { + b.Fatal("expected non-nil resized image") + } + } + }) + + b.Run(fmt.Sprintf("jpeg/%dx%d_to_%d_square", srcSize, srcSize, targetSize), func(b *testing.B) { + b.SetBytes(int64(len(jpegData))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + result, _, err := resizeImage(bytes.NewReader(jpegData), targetSize, true) + if err != nil { + b.Fatal(err) + } + if result == nil { + b.Fatal("expected non-nil resized image") + } + } + }) + } +} diff --git a/core/artwork/benchmark_tag_test.go b/core/artwork/benchmark_tag_test.go new file mode 100644 index 000000000..fd649beab --- /dev/null +++ b/core/artwork/benchmark_tag_test.go @@ -0,0 +1,38 @@ +package artwork + +import ( + "path/filepath" + "runtime" + "testing" + + "go.senan.xyz/taglib" +) + +func BenchmarkTagExtraction(b *testing.B) { + // Ensure working directory is the project root (tests.Init not called with -run='^$') + _, file, _, ok := runtime.Caller(0) + if !ok { + b.Fatal("runtime.Caller failed") + } + appPath, _ := filepath.Abs(filepath.Join(filepath.Dir(file), "..", "..")) + + // Use existing test fixture with embedded artwork + testFile := filepath.Join(appPath, "tests/fixtures/artist/an-album/test.mp3") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + f, err := taglib.OpenReadOnly(testFile, taglib.WithReadStyle(taglib.ReadStyleFast)) + if err != nil { + b.Fatal(err) + } + images := f.Properties().Images + if len(images) == 0 { + b.Fatal("no images found in test file") + } + data, err := f.Image(0) + if err != nil || len(data) == 0 { + b.Fatal("failed to extract image data") + } + f.Close() + } +} diff --git a/core/artwork/cache_warmer.go b/core/artwork/cache_warmer.go index 909d299d8..f13820d00 100644 --- a/core/artwork/cache_warmer.go +++ b/core/artwork/cache_warmer.go @@ -132,7 +132,7 @@ func (a *cacheWarmer) waitSignal(ctx context.Context, timeout time.Duration) { func (a *cacheWarmer) processBatch(ctx context.Context, batch []model.ArtworkID) { log.Trace(ctx, "PreCaching a new batch of artwork", "batchSize", len(batch)) input := pl.FromSlice(ctx, batch) - errs := pl.Sink(ctx, 2, input, a.doCacheImage) + errs := pl.Sink(ctx, 4, input, a.doCacheImage) for err := range errs { log.Debug(ctx, "Error warming cache", err) } diff --git a/core/artwork/reader_playlist.go b/core/artwork/reader_playlist.go index 91d47b0b0..7919eead6 100644 --- a/core/artwork/reader_playlist.go +++ b/core/artwork/reader_playlist.go @@ -14,11 +14,11 @@ import ( "strings" "time" - "github.com/disintegration/imaging" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/utils/slice" + xdraw "golang.org/x/image/draw" ) type playlistArtworkReader struct { @@ -200,7 +200,7 @@ func (a *playlistArtworkReader) createTile(_ context.Context, r io.ReadCloser) ( if err != nil { return nil, err } - return imaging.Fill(img, tileSize/2, tileSize/2, imaging.Center, imaging.Lanczos), nil + return fillCenter(img, tileSize/2, tileSize/2), nil } func (a *playlistArtworkReader) createTiledImage(_ context.Context, tiles []image.Image) (io.ReadCloser, error) { @@ -238,3 +238,32 @@ func rect(pos int) image.Rectangle { r.Max.Y = r.Min.Y + tileSize/2 return r } + +// fillCenter crops the source image from the center and scales it to fill dstW x dstH exactly, +// equivalent to imaging.Fill with Center anchor. +func fillCenter(src image.Image, dstW, dstH int) image.Image { + srcBounds := src.Bounds() + srcW := srcBounds.Dx() + srcH := srcBounds.Dy() + + // Calculate crop rectangle (center crop to match destination aspect ratio) + srcAspect := float64(srcW) / float64(srcH) + dstAspect := float64(dstW) / float64(dstH) + + var cropRect image.Rectangle + if srcAspect > dstAspect { + // Source is wider — crop horizontally + cropW := int(float64(srcH) * dstAspect) + cropX := (srcW - cropW) / 2 + cropRect = image.Rect(srcBounds.Min.X+cropX, srcBounds.Min.Y, srcBounds.Min.X+cropX+cropW, srcBounds.Max.Y) + } else { + // Source is taller — crop vertically + cropH := int(float64(srcW) / dstAspect) + cropY := (srcH - cropH) / 2 + cropRect = image.Rect(srcBounds.Min.X, srcBounds.Min.Y+cropY, srcBounds.Max.X, srcBounds.Min.Y+cropY+cropH) + } + + dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH)) + xdraw.BiLinear.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 6de983baf..dffcba8e3 100644 --- a/core/artwork/reader_resized.go +++ b/core/artwork/reader_resized.go @@ -5,17 +5,26 @@ import ( "context" "fmt" "image" + "image/draw" "image/jpeg" "image/png" "io" + "sync" "time" - "github.com/disintegration/imaging" + "github.com/gen2brain/webp" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" + xdraw "golang.org/x/image/draw" ) +var bufPool = sync.Pool{ + New: func() any { + return new(bytes.Buffer) + }, +} + type resizedArtworkReader struct { artID model.ArtworkID cacheKey string @@ -46,7 +55,7 @@ func (a *resizedArtworkReader) Key() string { if a.square { return baseKey + ".square" } - return fmt.Sprintf("%s.%d", baseKey, conf.Server.CoverJpegQuality) + return fmt.Sprintf("%s.%d", baseKey, conf.Server.CoverArtQuality) } func (a *resizedArtworkReader) LastUpdated() time.Time { @@ -79,7 +88,7 @@ func (a *resizedArtworkReader) Reader(ctx context.Context) (io.ReadCloser, strin } func resizeImage(reader io.Reader, size int, square bool) (io.Reader, int, error) { - original, format, err := image.Decode(reader) + original, _, err := image.Decode(reader) if err != nil { return nil, 0, err } @@ -96,26 +105,45 @@ func resizeImage(reader io.Reader, size int, square bool) (io.Reader, int, error return nil, originalSize, nil } - var resized image.Image - if originalSize >= size { - resized = imaging.Fit(original, size, size, imaging.Lanczos) - } else { - if bounds.Max.Y < bounds.Max.X { - resized = imaging.Resize(original, size, 0, imaging.Lanczos) - } else { - resized = imaging.Resize(original, 0, size, imaging.Lanczos) - } - } - if square { - bg := image.NewRGBA(image.Rect(0, 0, size, size)) - resized = imaging.OverlayCenter(bg, resized, 1) - } + // Calculate aspect-fit dimensions + srcW, srcH := bounds.Dx(), bounds.Dy() + scale := float64(size) / float64(max(srcW, srcH)) + dstW := int(float64(srcW) * scale) + dstH := int(float64(srcH) * scale) - buf := new(bytes.Buffer) - if format == "png" || square { - err = png.Encode(buf, resized) + var dst *image.NRGBA + var dstRect image.Rectangle + if square { + // Square canvas with image centered (transparent padding via zero-initialized NRGBA) + dst = image.NewNRGBA(image.Rect(0, 0, size, size)) + offsetX := (size - dstW) / 2 + offsetY := (size - dstH) / 2 + dstRect = image.Rect(offsetX, offsetY, offsetX+dstW, offsetY+dstH) } else { - err = jpeg.Encode(buf, resized, &jpeg.Options{Quality: conf.Server.CoverJpegQuality}) + // Tight-fit canvas + dst = image.NewNRGBA(image.Rect(0, 0, dstW, dstH)) + dstRect = dst.Bounds() } - return buf, originalSize, err + xdraw.BiLinear.Scale(dst, dstRect, original, bounds, draw.Src, nil) + + buf := bufPool.Get().(*bytes.Buffer) + buf.Reset() + if conf.Server.DevJpegCoverArt { + if square { + err = png.Encode(buf, dst) + } else { + err = jpeg.Encode(buf, dst, &jpeg.Options{Quality: conf.Server.CoverArtQuality}) + } + } else { + err = webp.Encode(buf, dst, webp.Options{Quality: conf.Server.CoverArtQuality}) + } + if err != nil { + bufPool.Put(buf) + return nil, originalSize, err + } + // Copy bytes before returning buffer to pool (pool may reuse the buffer) + encoded := make([]byte, buf.Len()) + copy(encoded, buf.Bytes()) + bufPool.Put(buf) + return bytes.NewReader(encoded), originalSize, nil } diff --git a/core/external/provider.go b/core/external/provider.go index 6a4f4f5e0..a30afed15 100644 --- a/core/external/provider.go +++ b/core/external/provider.go @@ -374,13 +374,19 @@ func (e *provider) ArtistImage(ctx context.Context, id string) (*url.URL, error) return nil, err } - e.callGetImage(ctx, e.ag, &artist) - if utils.IsCtxDone(ctx) { - log.Warn(ctx, "ArtistImage call canceled", ctx.Err()) - return nil, ctx.Err() + // Use already-stored image URL if available, avoiding expensive external API calls. + // If the info is expired, the background refresh (via UpdateArtistInfo/artistQueue) will update it. + imageUrl := artist.ArtistImageUrl() + if imageUrl == "" { + // No cached URL — must fetch from external source synchronously + e.callGetImage(ctx, e.ag, &artist) + if utils.IsCtxDone(ctx) { + log.Warn(ctx, "ArtistImage call canceled", ctx.Err()) + return nil, ctx.Err() + } + imageUrl = artist.ArtistImageUrl() } - imageUrl := artist.ArtistImageUrl() if imageUrl == "" { return nil, model.ErrNotFound } diff --git a/go.mod b/go.mod index d50f738b7..424c52421 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,6 @@ require ( github.com/deluan/rest v0.0.0-20211102003136-6260bc399cbf github.com/deluan/sanitize v0.0.0-20241120162836-fdfd8fdfaa55 github.com/dexterlb/mpvipc v0.0.0-20241005113212-7cdefca0e933 - github.com/disintegration/imaging v1.6.2 github.com/djherbis/atime v1.1.0 github.com/djherbis/fscache v0.10.2-0.20231127215153-442a07e326c4 github.com/djherbis/stream v1.4.0 @@ -22,6 +21,7 @@ require ( github.com/dustin/go-humanize v1.0.1 github.com/extism/go-sdk v1.7.1 github.com/fatih/structs v1.1.0 + github.com/gen2brain/webp v0.5.5 github.com/go-chi/chi/v5 v5.2.5 github.com/go-chi/cors v1.2.2 github.com/go-chi/httprate v0.15.0 @@ -84,6 +84,7 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1 // indirect + github.com/ebitengine/purego v0.8.3 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect diff --git a/go.sum b/go.sum index d92075234..a28900a41 100644 --- a/go.sum +++ b/go.sum @@ -44,8 +44,6 @@ github.com/deluan/sanitize v0.0.0-20241120162836-fdfd8fdfaa55 h1:wSCnggTs2f2ji6n github.com/deluan/sanitize v0.0.0-20241120162836-fdfd8fdfaa55/go.mod h1:ZNCLJfehvEf34B7BbLKjgpsL9lyW7q938w/GY1XgV4E= github.com/dexterlb/mpvipc v0.0.0-20241005113212-7cdefca0e933 h1:r4hxcT6GBIA/j8Ox4OXI5MNgMKfR+9plcAWYi1OnmOg= github.com/dexterlb/mpvipc v0.0.0-20241005113212-7cdefca0e933/go.mod h1:RkQWLNITKkXHLP7LXxZSgEq+uFWU25M5qW7qfEhL9Wc= -github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= -github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/djherbis/atime v1.1.0 h1:rgwVbP/5by8BvvjBNrbh64Qz33idKT3pSnMSJsxhi0g= github.com/djherbis/atime v1.1.0/go.mod h1:28OF6Y8s3NQWwacXc5eZTsEsiMzp7LF8MbXE+XJPdBE= github.com/djherbis/fscache v0.10.2-0.20231127215153-442a07e326c4 h1:wdZllsLrDJtYfHiAKogB4PNHSDeO+v+5S3eqSWHGDlc= @@ -60,6 +58,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1 h1:idfl8M8rPW93NehFw5H1qqH8yG158t5POr+LX9avbJY= github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q= +github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc= +github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw= github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= @@ -69,6 +69,8 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gen2brain/webp v0.5.5 h1:MvQR75yIPU/9nSqYT5h13k4URaJK3gf9tgz/ksRbyEg= +github.com/gen2brain/webp v0.5.5/go.mod h1:xOSMzp4aROt2KFW++9qcK/RBTOVC2S9tJG66ip/9Oc0= github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= @@ -325,7 +327,6 @@ golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= -golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc= golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= diff --git a/utils/cache/benchmark_test.go b/utils/cache/benchmark_test.go new file mode 100644 index 000000000..1fe448f84 --- /dev/null +++ b/utils/cache/benchmark_test.go @@ -0,0 +1,171 @@ +package cache + +import ( + "context" + "fmt" + "io" + "os" + "runtime" + "strings" + "sync" + "testing" + + "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/conf/configtest" +) + +type benchItem struct { + key string +} + +func (b *benchItem) Key() string { return b.key } + +// setupBenchCache creates a file cache in a temp directory. Returns the cache and cleanup function. +func setupBenchCache(b *testing.B, cacheSize string, getReader ReadFunc) (*fileCache, func()) { + b.Helper() + tmpDir, err := os.MkdirTemp("", "bench-cache-*") + if err != nil { + b.Fatal(err) + } + b.Cleanup(configtest.SetupConfig()) + conf.Server.CacheFolder = tmpDir + + fc := NewFileCache("bench", cacheSize, "bench", 0, getReader).(*fileCache) + + // Wait for cache to be ready + for !fc.ready.Load() { + runtime.Gosched() // Yield to allow background init goroutine to run + } + + teardown := func() { + os.RemoveAll(tmpDir) + } + return fc, teardown +} + +func BenchmarkCacheWrite(b *testing.B) { + // Simulate writing 50KB images (typical 300px JPEG) + imageData := strings.Repeat("x", 50*1024) + + fc, cleanup := setupBenchCache(b, "100MB", func(ctx context.Context, item Item) (io.Reader, error) { + return strings.NewReader(imageData), nil + }) + defer cleanup() + + b.SetBytes(int64(len(imageData))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := fmt.Sprintf("write-bench-%d", i) + s, err := fc.Get(context.Background(), &benchItem{key: key}) + if err != nil { + b.Fatal(err) + } + _, _ = io.ReadAll(s) + s.Close() + } +} + +func BenchmarkCacheRead(b *testing.B) { + imageData := strings.Repeat("x", 50*1024) + + fc, cleanup := setupBenchCache(b, "100MB", func(ctx context.Context, item Item) (io.Reader, error) { + return strings.NewReader(imageData), nil + }) + defer cleanup() + + // Pre-populate cache + item := &benchItem{key: "read-bench"} + s, err := fc.Get(context.Background(), item) + if err != nil { + b.Fatal(err) + } + _, _ = io.ReadAll(s) + s.Close() + + b.SetBytes(int64(len(imageData))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + s, err := fc.Get(context.Background(), item) + if err != nil { + b.Fatal(err) + } + _, _ = io.ReadAll(s) + s.Close() + } +} + +func BenchmarkConcurrentCacheRead(b *testing.B) { + imageData := strings.Repeat("x", 50*1024) + + fc, cleanup := setupBenchCache(b, "100MB", func(ctx context.Context, item Item) (io.Reader, error) { + return strings.NewReader(imageData), nil + }) + defer cleanup() + + // Pre-populate cache + item := &benchItem{key: "concurrent-read"} + s, _ := fc.Get(context.Background(), item) + _, _ = io.ReadAll(s) + s.Close() + + concurrencyLevels := []int{1, 10, 50} + for _, n := range concurrencyLevels { + b.Run(fmt.Sprintf("goroutines_%d", n), func(b *testing.B) { + b.SetBytes(int64(len(imageData))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + var wg sync.WaitGroup + wg.Add(n) + for g := 0; g < n; g++ { + go func() { + defer wg.Done() + s, err := fc.Get(context.Background(), item) + if err != nil { + b.Error(err) + return + } + _, _ = io.ReadAll(s) + s.Close() + }() + } + wg.Wait() + } + }) + } +} + +func BenchmarkConcurrentCacheMiss(b *testing.B) { + imageData := strings.Repeat("x", 50*1024) + + concurrencyLevels := []int{1, 10, 50} + for _, n := range concurrencyLevels { + b.Run(fmt.Sprintf("goroutines_%d", n), func(b *testing.B) { + fc, cleanup := setupBenchCache(b, "100MB", func(ctx context.Context, item Item) (io.Reader, error) { + return strings.NewReader(imageData), nil + }) + defer cleanup() + + b.SetBytes(int64(len(imageData))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + var wg sync.WaitGroup + wg.Add(n) + // All goroutines request the SAME key (not yet cached) + item := &benchItem{key: fmt.Sprintf("miss-%d", i)} + for g := 0; g < n; g++ { + go func() { + defer wg.Done() + s, err := fc.Get(context.Background(), item) + if err != nil { + b.Error(err) + return + } + _, _ = io.ReadAll(s) + s.Close() + }() + } + wg.Wait() + } + }) + } +} From a50b2a1e72129aba2401149814f193b20969ef1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Fri, 13 Mar 2026 18:11:12 -0400 Subject: [PATCH 08/81] feat(artwork): preserve animated image artwork during resize (#5184) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(artwork): preserve animated image artwork during resize Detect animated GIFs, WebPs, and APNGs via lightweight byte scanning and preserve their animation when serving resized artwork. Animated GIFs are converted to animated WebP via ffmpeg with optional downscaling; animated WebP/APNG are returned as-is since ffmpeg cannot re-encode them. Adds ConvertAnimatedImage to the FFmpeg interface for piping stdin data through ffmpeg with animated WebP output. * fix(artwork): address code review feedback for animated artwork Fix ReadCloser leak where ffmpeg pipe's Close was discarded by io.NopCloser wrapping — now preserves ReadCloser semantics when the resized reader already supports Close. Use uint64 for PNG chunk position to prevent potential overflow on 32-bit platforms. Add integration tests for the animation branching logic in resizeImage. --- core/artwork/animation.go | 120 +++++++++++++++++ core/artwork/animation_test.go | 161 ++++++++++++++++++++++ core/artwork/benchmark_pipeline_test.go | 5 +- core/artwork/reader_resized.go | 37 +++++- core/artwork/reader_resized_test.go | 170 ++++++++++++++++++++++++ core/ffmpeg/ffmpeg.go | 33 ++++- server/e2e/e2e_suite_test.go | 4 + tests/mock_ffmpeg.go | 12 ++ 8 files changed, 532 insertions(+), 10 deletions(-) create mode 100644 core/artwork/animation.go create mode 100644 core/artwork/animation_test.go create mode 100644 core/artwork/reader_resized_test.go diff --git a/core/artwork/animation.go b/core/artwork/animation.go new file mode 100644 index 000000000..07f493eb4 --- /dev/null +++ b/core/artwork/animation.go @@ -0,0 +1,120 @@ +package artwork + +import ( + "bytes" + "encoding/binary" +) + +// isAnimatedGIF checks for multiple image descriptor blocks (0x2C) in a GIF file. +// Animated GIFs use GIF89a and contain multiple image blocks. +func isAnimatedGIF(data []byte) bool { + // GIF header: "GIF87a" or "GIF89a" + if !bytes.HasPrefix(data, []byte("GIF")) { + return false + } + + // Skip header (6 bytes) + logical screen descriptor (7 bytes) + pos := 13 + if pos >= len(data) { + return false + } + + // Skip Global Color Table if present (bit 7 of packed byte at offset 10) + if len(data) > 10 && data[10]&0x80 != 0 { + // GCT size = 3 * 2^(N+1) where N = bits 0-2 of packed byte + gctSize := 3 * (1 << ((data[10] & 0x07) + 1)) + pos += gctSize + } + + frameCount := 0 + for pos < len(data) { + switch data[pos] { + case 0x2C: // Image Descriptor - marks a frame + frameCount++ + if frameCount > 1 { + return true + } + pos++ // skip introducer + if pos+8 >= len(data) { + return false + } + pos += 8 // skip x, y, w, h (each 2 bytes) + packed := data[pos] + pos++ // skip packed byte + // Skip Local Color Table if present + if packed&0x80 != 0 { + lctSize := 3 * (1 << ((packed & 0x07) + 1)) + pos += lctSize + } + // Skip LZW minimum code size + pos++ + // Skip sub-blocks + pos = skipGIFSubBlocks(data, pos) + case 0x21: // Extension block + pos++ // skip introducer + if pos >= len(data) { + return false + } + pos++ // skip extension label + // Skip sub-blocks + pos = skipGIFSubBlocks(data, pos) + case 0x3B: // Trailer + return false + default: + // Unknown block, bail + return false + } + } + return false +} + +// skipGIFSubBlocks advances past a sequence of GIF sub-blocks (terminated by a zero-length block). +func skipGIFSubBlocks(data []byte, pos int) int { + for pos < len(data) { + blockSize := int(data[pos]) + pos++ // skip size byte + if blockSize == 0 { + break + } + pos += blockSize + } + return pos +} + +// isAnimatedWebP checks for ANMF (animation frame) chunks in a WebP RIFF container. +func isAnimatedWebP(data []byte) bool { + // WebP header: "RIFF" + 4 bytes size + "WEBP" + if !bytes.HasPrefix(data, []byte("RIFF")) || len(data) < 12 { + return false + } + if !bytes.Equal(data[8:12], []byte("WEBP")) { + return false + } + // Scan for ANMF chunk identifier + return bytes.Contains(data[12:], []byte("ANMF")) +} + +// isAnimatedPNG checks for the acTL (animation control) chunk in a PNG file. +// APNG files contain an acTL chunk that is not present in static PNGs. +func isAnimatedPNG(data []byte) bool { + // PNG signature: 8 bytes + pngSig := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A} + if !bytes.HasPrefix(data, pngSig) { + return false + } + + // Scan chunks for "acTL" (animation control) + pos := uint64(8) + dataLen := uint64(len(data)) + for pos+8 <= dataLen { + chunkLen := uint64(binary.BigEndian.Uint32(data[pos : pos+4])) + chunkType := string(data[pos+4 : pos+8]) + + if chunkType == "acTL" { + return true + } + // Move to next chunk: 4 (length) + 4 (type) + chunkLen (data) + 4 (CRC) + pos += 12 + chunkLen + } + return false +} diff --git a/core/artwork/animation_test.go b/core/artwork/animation_test.go new file mode 100644 index 000000000..9000a8511 --- /dev/null +++ b/core/artwork/animation_test.go @@ -0,0 +1,161 @@ +package artwork + +import ( + "bytes" + "encoding/binary" + "image" + "image/color" + "image/gif" + "image/png" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Animation detection", func() { + Describe("isAnimatedGIF", func() { + It("detects an animated GIF with multiple frames", func() { + Expect(isAnimatedGIF(createAnimatedGIF(2))).To(BeTrue()) + }) + + It("detects an animated GIF with many frames", func() { + Expect(isAnimatedGIF(createAnimatedGIF(5))).To(BeTrue()) + }) + + It("does not flag a static GIF (single frame)", func() { + Expect(isAnimatedGIF(createAnimatedGIF(1))).To(BeFalse()) + }) + + It("returns false for non-GIF data", func() { + Expect(isAnimatedGIF(nil)).To(BeFalse()) + Expect(isAnimatedGIF([]byte{0xFF, 0xD8})).To(BeFalse()) + }) + }) + + Describe("isAnimatedWebP", func() { + It("detects an animated WebP with ANMF chunk", func() { + Expect(isAnimatedWebP(createAnimatedWebPBytes())).To(BeTrue()) + }) + + It("does not flag a static WebP (no ANMF chunk)", func() { + Expect(isAnimatedWebP(createStaticWebPBytes())).To(BeFalse()) + }) + + It("returns false for non-WebP data", func() { + Expect(isAnimatedWebP(nil)).To(BeFalse()) + Expect(isAnimatedWebP([]byte{0xFF, 0xD8})).To(BeFalse()) + }) + }) + + Describe("isAnimatedPNG", func() { + It("detects an APNG with acTL chunk", func() { + Expect(isAnimatedPNG(createAPNGBytes())).To(BeTrue()) + }) + + It("does not flag a static PNG (no acTL chunk)", func() { + Expect(isAnimatedPNG(createStaticPNGBytes())).To(BeFalse()) + }) + + It("returns false for non-PNG data", func() { + Expect(isAnimatedPNG(nil)).To(BeFalse()) + Expect(isAnimatedPNG([]byte{0xFF, 0xD8})).To(BeFalse()) + }) + }) +}) + +// createAnimatedGIF creates a minimal animated GIF with the given number of frames. +func createAnimatedGIF(frames int) []byte { + g := &gif.GIF{ + LoopCount: 0, + } + for range frames { + img := image.NewPaletted(image.Rect(0, 0, 2, 2), color.Palette{color.Black, color.White}) + g.Image = append(g.Image, img) + g.Delay = append(g.Delay, 10) + } + var buf bytes.Buffer + err := gif.EncodeAll(&buf, g) + if err != nil { + panic(err) + } + return buf.Bytes() +} + +// writeUint32LE appends a little-endian uint32 to the buffer. +func writeUint32LE(buf *bytes.Buffer, v uint32) { + b := make([]byte, 4) + binary.LittleEndian.PutUint32(b, v) + buf.Write(b) +} + +// writeUint32BE appends a big-endian uint32 to the buffer. +func writeUint32BE(buf *bytes.Buffer, v uint32) { + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, v) + buf.Write(b) +} + +// createAnimatedWebPBytes creates a minimal RIFF/WEBP container with an ANMF chunk. +func createAnimatedWebPBytes() []byte { + var buf bytes.Buffer + buf.WriteString("RIFF") + writeUint32LE(&buf, 100) // file size placeholder + buf.WriteString("WEBP") + // VP8X chunk (extended format, required for animation) + buf.WriteString("VP8X") + writeUint32LE(&buf, 10) + buf.Write(make([]byte, 10)) + // ANIM chunk (animation parameters) + buf.WriteString("ANIM") + writeUint32LE(&buf, 6) + buf.Write(make([]byte, 6)) + // ANMF chunk (animation frame) + buf.WriteString("ANMF") + writeUint32LE(&buf, 16) + buf.Write(make([]byte, 16)) + return buf.Bytes() +} + +// createStaticWebPBytes creates a minimal RIFF/WEBP container without ANMF chunks. +func createStaticWebPBytes() []byte { + var buf bytes.Buffer + buf.WriteString("RIFF") + writeUint32LE(&buf, 20) // file size + buf.WriteString("WEBP") + // VP8 chunk (simple lossy format) + buf.WriteString("VP8 ") + writeUint32LE(&buf, 4) + buf.Write(make([]byte, 4)) + return buf.Bytes() +} + +// createAPNGBytes creates a minimal PNG with an acTL chunk (making it APNG). +func createAPNGBytes() []byte { + // Start with a real PNG + staticPNG := createStaticPNGBytes() + + // Insert an acTL chunk after the IHDR chunk. + // PNG structure: signature (8) + IHDR chunk (4 len + 4 type + 13 data + 4 crc = 25) + ihdrEnd := 8 + 25 + var buf bytes.Buffer + buf.Write(staticPNG[:ihdrEnd]) + // Write acTL chunk: length=8, type="acTL", data=num_frames(4)+num_plays(4), CRC=4 + writeUint32BE(&buf, 8) // chunk data length + buf.WriteString("acTL") + writeUint32BE(&buf, 2) // num_frames + writeUint32BE(&buf, 0) // num_plays (0 = infinite) + writeUint32BE(&buf, 0) // CRC placeholder + buf.Write(staticPNG[ihdrEnd:]) + return buf.Bytes() +} + +// createStaticPNGBytes creates a minimal valid static PNG. +func createStaticPNGBytes() []byte { + img := image.NewRGBA(image.Rect(0, 0, 2, 2)) + var buf bytes.Buffer + err := png.Encode(&buf, img) + if err != nil { + panic(err) + } + return buf.Bytes() +} diff --git a/core/artwork/benchmark_pipeline_test.go b/core/artwork/benchmark_pipeline_test.go index 9dc4f6c95..23d5954df 100644 --- a/core/artwork/benchmark_pipeline_test.go +++ b/core/artwork/benchmark_pipeline_test.go @@ -1,7 +1,6 @@ package artwork import ( - "bytes" "fmt" "testing" @@ -24,7 +23,7 @@ func BenchmarkResizeFullPipeline(b *testing.B) { b.SetBytes(int64(len(jpegData))) b.ResetTimer() for i := 0; i < b.N; i++ { - result, _, err := resizeImage(bytes.NewReader(jpegData), targetSize, false) + result, _, err := resizeStaticImage(jpegData, targetSize, false) if err != nil { b.Fatal(err) } @@ -38,7 +37,7 @@ func BenchmarkResizeFullPipeline(b *testing.B) { b.SetBytes(int64(len(jpegData))) b.ResetTimer() for i := 0; i < b.N; i++ { - result, _, err := resizeImage(bytes.NewReader(jpegData), targetSize, true) + result, _, err := resizeStaticImage(jpegData, targetSize, true) if err != nil { b.Fatal(err) } diff --git a/core/artwork/reader_resized.go b/core/artwork/reader_resized.go index dffcba8e3..7e90f10e9 100644 --- a/core/artwork/reader_resized.go +++ b/core/artwork/reader_resized.go @@ -70,7 +70,7 @@ func (a *resizedArtworkReader) Reader(ctx context.Context) (io.ReadCloser, strin } defer orig.Close() - resized, origSize, err := resizeImage(orig, a.size, a.square) + resized, origSize, err := a.resizeImage(ctx, orig) if resized == nil { log.Trace(ctx, "Image smaller than requested size", "artID", a.artID, "original", origSize, "resized", a.size, "square", a.square) } else { @@ -84,11 +84,42 @@ func (a *resizedArtworkReader) Reader(ctx context.Context) (io.ReadCloser, strin orig, _, err = a.a.Get(ctx, a.artID, 0, false) return orig, "", err } + // Preserve ReadCloser semantics if the resized reader already supports Close + // (e.g., ffmpeg pipe), otherwise wrap with NopCloser + if rc, ok := resized.(io.ReadCloser); ok { + return rc, fmt.Sprintf("%s@%d", a.artID, a.size), nil + } return io.NopCloser(resized), fmt.Sprintf("%s@%d", a.artID, a.size), nil } -func resizeImage(reader io.Reader, size int, square bool) (io.Reader, int, error) { - original, _, err := image.Decode(reader) +func (a *resizedArtworkReader) resizeImage(ctx context.Context, reader io.Reader) (io.Reader, int, error) { + data, err := io.ReadAll(reader) + if err != nil { + return nil, 0, fmt.Errorf("reading image data: %w", err) + } + + // Preserve animation for animated images (skip for square thumbnails) + if !a.square { + if isAnimatedGIF(data) { + if a.a.ffmpeg.IsAvailable() { + // Animated GIF: convert to animated WebP via ffmpeg (with optional resize) + r, err := a.a.ffmpeg.ConvertAnimatedImage(ctx, bytes.NewReader(data), a.size, conf.Server.CoverArtQuality) + if err == nil { + return r, 0, nil + } + log.Warn(ctx, "Could not convert animated GIF, falling back to static", err) + } + } else if isAnimatedWebP(data) || isAnimatedPNG(data) { + // Animated WebP/APNG: return original as-is (ffmpeg can't re-encode these) + return bytes.NewReader(data), 0, nil + } + } + + return resizeStaticImage(data, a.size, a.square) +} + +func resizeStaticImage(data []byte, size int, square bool) (io.Reader, int, error) { + original, _, err := image.Decode(bytes.NewReader(data)) if err != nil { return nil, 0, err } diff --git a/core/artwork/reader_resized_test.go b/core/artwork/reader_resized_test.go new file mode 100644 index 000000000..7c9e21c0a --- /dev/null +++ b/core/artwork/reader_resized_test.go @@ -0,0 +1,170 @@ +package artwork + +import ( + "bytes" + "context" + "errors" + "io" + + "github.com/navidrome/navidrome/core/ffmpeg" + "github.com/navidrome/navidrome/tests" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("resizeImage", func() { + var mockFF *tests.MockFFmpeg + var r *resizedArtworkReader + + BeforeEach(func() { + mockFF = tests.NewMockFFmpeg("converted-animated-data") + r = &resizedArtworkReader{ + size: 300, + square: false, + a: &artwork{ffmpeg: mockFF}, + } + }) + + Describe("animated GIF handling", func() { + It("converts animated GIF via ffmpeg when available", func() { + data := createAnimatedGIF(3) + result, _, err := r.resizeImage(context.Background(), bytes.NewReader(data)) + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(BeNil()) + + // Should have been processed by ffmpeg (mock returns "converted-animated-data") + output, err := io.ReadAll(result) + Expect(err).ToNot(HaveOccurred()) + Expect(output).To(Equal(data)) // MockFFmpeg echoes input back + }) + + It("falls back to static resize when ffmpeg fails for animated GIF", func() { + mockFF.Error = errors.New("ffmpeg failed") + // Use size smaller than image so static resize actually produces output + r.size = 1 + data := createAnimatedGIF(3) + result, _, err := r.resizeImage(context.Background(), bytes.NewReader(data)) + // Should fall through to static resize successfully (no ffmpeg error propagated) + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(BeNil()) + + // Verify it's a static image (WebP encoded), not the ffmpeg error + output, err := io.ReadAll(result) + Expect(err).ToNot(HaveOccurred()) + Expect(len(output)).To(BeNumerically(">", 0)) + }) + + It("skips animation for square thumbnails even with animated GIF", func() { + r.square = true + data := createAnimatedGIF(3) + result, _, err := r.resizeImage(context.Background(), bytes.NewReader(data)) + // Should fall through to static resize (not ffmpeg conversion) + // The minimal test GIF may or may not resize successfully, + // but ffmpeg should NOT have been called for animated conversion + _ = result + _ = err + // Verify by checking the mock wasn't used for animated conversion: + // If ffmpeg was called, it would return mock data, not static resize result + }) + }) + + Describe("animated WebP handling", func() { + It("returns animated WebP data as-is when not square", func() { + data := createAnimatedWebPBytes() + result, _, err := r.resizeImage(context.Background(), bytes.NewReader(data)) + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(BeNil()) + + // Should return original data unchanged + output, err := io.ReadAll(result) + Expect(err).ToNot(HaveOccurred()) + Expect(output).To(Equal(data)) + }) + + It("does not passthrough animated WebP for square thumbnails", func() { + r.square = true + data := createAnimatedWebPBytes() + // Should fall through to static resize, which will fail on fake WebP data + _, _, err := r.resizeImage(context.Background(), bytes.NewReader(data)) + // Static decode will fail on our minimal test WebP bytes (not a real image) + Expect(err).To(HaveOccurred()) + }) + }) + + Describe("animated PNG handling", func() { + It("returns animated PNG data as-is when not square", func() { + data := createAPNGBytes() + result, _, err := r.resizeImage(context.Background(), bytes.NewReader(data)) + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(BeNil()) + + // Should return original data unchanged + output, err := io.ReadAll(result) + Expect(err).ToNot(HaveOccurred()) + Expect(output).To(Equal(data)) + }) + + It("does not passthrough animated PNG for square thumbnails", func() { + r.square = true + data := createAPNGBytes() + // Should fall through to static resize + result, _, err := r.resizeImage(context.Background(), bytes.NewReader(data)) + // Static PNG decode should succeed on our APNG (it's a valid PNG) + if err == nil { + Expect(result).ToNot(BeNil()) + } + }) + }) + + Describe("static image handling", func() { + It("resizes a static PNG normally", func() { + data := createStaticPNGBytes() + result, _, err := r.resizeImage(context.Background(), bytes.NewReader(data)) + // Static PNG is 2x2, size 300 is larger, so should return nil (no upscale) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(BeNil()) + }) + }) + + Describe("ReadCloser preservation", func() { + It("preserves Close semantics from ffmpeg ReadCloser", func() { + // Create a trackable ReadCloser + tracker := &closeTracker{Reader: bytes.NewReader([]byte("test data"))} + mockFF2 := &mockFFmpegWithCloser{tracker: tracker} + r.a = &artwork{ffmpeg: mockFF2} + + data := createAnimatedGIF(3) + result, _, err := r.resizeImage(context.Background(), bytes.NewReader(data)) + Expect(err).ToNot(HaveOccurred()) + + // The result should be an io.ReadCloser (the tracker) + rc, ok := result.(io.ReadCloser) + Expect(ok).To(BeTrue()) + Expect(rc.Close()).ToNot(HaveOccurred()) + Expect(tracker.closed).To(BeTrue()) + }) + }) +}) + +// closeTracker is an io.ReadCloser that tracks whether Close was called. +type closeTracker struct { + io.Reader + closed bool +} + +func (c *closeTracker) Close() error { + c.closed = true + return nil +} + +// mockFFmpegWithCloser is a minimal FFmpeg mock that returns a specific ReadCloser +// for ConvertAnimatedImage, allowing us to verify Close propagation. +type mockFFmpegWithCloser struct { + ffmpeg.FFmpeg + tracker *closeTracker +} + +func (m *mockFFmpegWithCloser) IsAvailable() bool { return true } +func (m *mockFFmpegWithCloser) ConvertAnimatedImage(_ context.Context, _ io.Reader, _ int, _ int) (io.ReadCloser, error) { + return m.tracker, nil +} diff --git a/core/ffmpeg/ffmpeg.go b/core/ffmpeg/ffmpeg.go index 9c4f99364..2b06250a1 100644 --- a/core/ffmpeg/ffmpeg.go +++ b/core/ffmpeg/ffmpeg.go @@ -43,6 +43,7 @@ type AudioProbeResult struct { type FFmpeg interface { Transcode(ctx context.Context, opts TranscodeOptions) (io.ReadCloser, error) ExtractImage(ctx context.Context, path string) (io.ReadCloser, error) + ConvertAnimatedImage(ctx context.Context, reader io.Reader, maxSize int, quality int) (io.ReadCloser, error) Probe(ctx context.Context, files []string) (string, error) ProbeAudioStream(ctx context.Context, filePath string) (*AudioProbeResult, error) CmdPath() (string, error) @@ -78,6 +79,23 @@ func (e *ffmpeg) Transcode(ctx context.Context, opts TranscodeOptions) (io.ReadC return e.start(ctx, args) } +func (e *ffmpeg) ConvertAnimatedImage(ctx context.Context, reader io.Reader, maxSize int, quality int) (io.ReadCloser, error) { + cmdPath, err := ffmpegCmd() + if err != nil { + return nil, err + } + + args := []string{cmdPath, "-i", "pipe:0"} + if maxSize > 0 { + vf := fmt.Sprintf("scale='min(%d,iw)':'min(%d,ih)':force_original_aspect_ratio=decrease", maxSize, maxSize) + args = append(args, "-vf", vf) + } + args = append(args, "-loop", "0", "-c:v", "libwebp_anim", + "-quality", strconv.Itoa(quality), "-f", "webp", "-") + + return e.start(ctx, args, reader) +} + func (e *ffmpeg) ExtractImage(ctx context.Context, path string) (io.ReadCloser, error) { if _, err := ffmpegCmd(); err != nil { return nil, err @@ -223,9 +241,12 @@ func (e *ffmpeg) Version() string { return parts[2] } -func (e *ffmpeg) start(ctx context.Context, args []string) (io.ReadCloser, error) { +func (e *ffmpeg) start(ctx context.Context, args []string, input ...io.Reader) (io.ReadCloser, error) { log.Trace(ctx, "Executing ffmpeg command", "cmd", args) j := &ffCmd{args: args} + if len(input) > 0 { + j.input = input[0] + } j.PipeReader, j.out = io.Pipe() err := j.start(ctx) if err != nil { @@ -237,14 +258,18 @@ func (e *ffmpeg) start(ctx context.Context, args []string) (io.ReadCloser, error type ffCmd struct { *io.PipeReader - out *io.PipeWriter - args []string - cmd *exec.Cmd + out *io.PipeWriter + args []string + cmd *exec.Cmd + input io.Reader // optional stdin source } func (j *ffCmd) start(ctx context.Context) error { cmd := exec.CommandContext(ctx, j.args[0], j.args[1:]...) // #nosec cmd.Stdout = j.out + if j.input != nil { + cmd.Stdin = j.input + } if log.IsGreaterOrEqualTo(log.LevelTrace) { cmd.Stderr = os.Stderr } else { diff --git a/server/e2e/e2e_suite_test.go b/server/e2e/e2e_suite_test.go index dc8b178d8..ef379ad05 100644 --- a/server/e2e/e2e_suite_test.go +++ b/server/e2e/e2e_suite_test.go @@ -320,6 +320,10 @@ func (n noopFFmpeg) ProbeAudioStream(context.Context, string) (*ffmpeg.AudioProb return nil, errors.New("noop ffmpeg: probe not supported") } +func (n noopFFmpeg) ConvertAnimatedImage(context.Context, io.Reader, int, int) (io.ReadCloser, error) { + return nil, errors.New("noop ffmpeg: convert animated image not supported") +} + func (n noopFFmpeg) CmdPath() (string, error) { return "", nil } func (n noopFFmpeg) IsAvailable() bool { return false } func (n noopFFmpeg) Version() string { return "noop" } diff --git a/tests/mock_ffmpeg.go b/tests/mock_ffmpeg.go index a35defeae..346209b71 100644 --- a/tests/mock_ffmpeg.go +++ b/tests/mock_ffmpeg.go @@ -1,6 +1,7 @@ package tests import ( + "bytes" "context" "io" "strings" @@ -40,6 +41,17 @@ func (ff *MockFFmpeg) ExtractImage(context.Context, string) (io.ReadCloser, erro return ff, nil } +func (ff *MockFFmpeg) ConvertAnimatedImage(_ context.Context, reader io.Reader, _ int, _ int) (io.ReadCloser, error) { + if ff.Error != nil { + return nil, ff.Error + } + data, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + return io.NopCloser(bytes.NewReader(data)), nil +} + func (ff *MockFFmpeg) Probe(context.Context, []string) (string, error) { if ff.Error != nil { return "", ff.Error From 49a14d45838ff6f95157ea3d55b70e28d0f66866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Fri, 13 Mar 2026 18:33:18 -0400 Subject: [PATCH 09/81] feat(artwork): add per-disc cover art support (#5182) * feat(artwork): add KindDiscArtwork and ParseDiscArtworkID Add new disc artwork kind with 'dc' prefix for per-disc cover art support. The composite ID format is albumID:discNumber, parsed by the new ParseDiscArtworkID helper. * feat(conf): add DiscArtPriority configuration option Default: 'disc*.*, cd*.*, embedded'. Controls how per-disc cover art is resolved, following the same pattern as CoverArtPriority and ArtistArtPriority. * feat(artwork): implement extractDiscNumber helper Extracts disc number from filenames based on glob patterns by parsing leading digits from the wildcard-matched portion. Used for matching disc-specific artwork files like disc1.jpg. * feat(artwork): implement fromDiscExternalFile source function Disc-aware variant of fromExternalFile that filters image files by disc number (extracted from filename) or folder association (for multi-folder albums). * feat(artwork): implement discArtworkReader Resolves disc artwork using DiscArtPriority config patterns. Supports glob patterns with disc number extraction, embedded images from first track, and falls back to album cover art. Handles both multi-folder and single-folder multi-disc albums. * feat(artwork): register disc artwork reader in dispatcher Add KindDiscArtwork case to getArtworkReader switch, routing disc artwork requests to the new discArtworkReader. * feat(subsonic): add CoverArt field to DiscTitle response Implements OpenSubsonic PR #220: optional cover art ID in DiscTitle responses for per-disc artwork support. * feat(subsonic): populate CoverArt in DiscTitle responses Each DiscTitle now includes a disc artwork ID (dc-albumID:discNum) that clients can use with getCoverArt to retrieve per-disc artwork. * style: fix file permission in test to satisfy gosec * feat(ui): add disc cover art display and lightbox functionality Signed-off-by: Deluan * refactor: simplify disc artwork code - Add DiscArtworkID constructor to encapsulate the "albumID:discNumber" format in one place - Convert fromDiscExternalFile to a method on discArtworkReader, reducing parameter count from 6 to 2 - Remove unused rootFolder field from discArtworkReader * style: fix prettier formatting in subsonic index * style(ui): move cursor style to makeStyles in SongDatagrid * feat(artwork): add discsubtitle option to DiscArtPriority Allow matching disc cover art by the disc's subtitle/name. When the "discsubtitle" keyword is in the priority list, image files whose stem matches the disc subtitle (case-insensitive) are used. This is useful for box sets with named discs (e.g., "The Blue Disc.jpg"). * feat(configuration): update discartpriority to include cover art options Signed-off-by: Deluan --------- Signed-off-by: Deluan --- conf/configuration.go | 2 + core/artwork/artwork.go | 2 + core/artwork/reader_disc.go | 268 +++++++++++++++++++++++ core/artwork/reader_disc_test.go | 285 +++++++++++++++++++++++++ model/artwork_id.go | 18 ++ model/artwork_id_test.go | 34 +++ server/subsonic/helpers.go | 8 +- server/subsonic/helpers_test.go | 33 ++- server/subsonic/responses/responses.go | 5 +- ui/src/common/SongDatagrid.jsx | 88 ++++++-- ui/src/i18n/en.json | 1 + ui/src/subsonic/index.js | 11 + ui/src/subsonic/index.test.js | 50 +++++ 13 files changed, 782 insertions(+), 23 deletions(-) create mode 100644 core/artwork/reader_disc.go create mode 100644 core/artwork/reader_disc_test.go diff --git a/conf/configuration.go b/conf/configuration.go index b867e0d56..dc5fd1b59 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -71,6 +71,7 @@ type configOptions struct { CoverArtPriority string CoverArtQuality int ArtistArtPriority string + DiscArtPriority string LyricsPriority string EnableGravatar bool EnableFavourites bool @@ -660,6 +661,7 @@ func setViperDefaults() { viper.SetDefault("coverartpriority", "cover.*, folder.*, front.*, embedded, external") viper.SetDefault("coverartquality", 75) viper.SetDefault("artistartpriority", "artist.*, album/artist.*, external") + viper.SetDefault("discartpriority", "disc*.*, cd*.*, cover.*, folder.*, front.*, discsubtitle, embedded") viper.SetDefault("lyricspriority", ".lrc,.txt,embedded") viper.SetDefault("enablegravatar", false) viper.SetDefault("enablefavourites", true) diff --git a/core/artwork/artwork.go b/core/artwork/artwork.go index 2e92b24c8..4a0a32afc 100644 --- a/core/artwork/artwork.go +++ b/core/artwork/artwork.go @@ -122,6 +122,8 @@ func (a *artwork) getArtworkReader(ctx context.Context, artID model.ArtworkID, s artReader, err = newMediafileArtworkReader(ctx, a, artID) case model.KindPlaylistArtwork: artReader, err = newPlaylistArtworkReader(ctx, a, artID) + case model.KindDiscArtwork: + artReader, err = newDiscArtworkReader(ctx, a, artID) default: return nil, ErrUnavailable } diff --git a/core/artwork/reader_disc.go b/core/artwork/reader_disc.go new file mode 100644 index 000000000..7548f76d2 --- /dev/null +++ b/core/artwork/reader_disc.go @@ -0,0 +1,268 @@ +package artwork + +import ( + "context" + "crypto/md5" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/Masterminds/squirrel" + "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/core" + "github.com/navidrome/navidrome/core/ffmpeg" + "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/model" +) + +type discArtworkReader struct { + cacheKey + a *artwork + album model.Album + discNumber int + imgFiles []string + discFolders map[string]bool + isMultiFolder bool + firstTrackPath string + updatedAt *time.Time +} + +func newDiscArtworkReader(ctx context.Context, a *artwork, artID model.ArtworkID) (*discArtworkReader, error) { + albumID, discNumber, err := model.ParseDiscArtworkID(artID.ID) + if err != nil { + return nil, fmt.Errorf("invalid disc artwork id '%s': %w", artID.ID, err) + } + + al, err := a.ds.Album(ctx).Get(albumID) + if err != nil { + return nil, err + } + + _, imgFiles, imagesUpdatedAt, err := loadAlbumFoldersPaths(ctx, a.ds, *al) + if err != nil { + return nil, err + } + + // Query mediafiles for this album + disc to find folder associations and first track + mfs, err := a.ds.MediaFile(ctx).GetAll(model.QueryOptions{ + Sort: "track_number", + Order: "ASC", + Filters: squirrel.Eq{"album_id": albumID, "disc_number": discNumber}, + }) + if err != nil { + return nil, err + } + + // Build disc folder set and find first track + discFolders := make(map[string]bool) + var firstTrackPath string + allFolderIDs := make(map[string]bool) + for _, mf := range mfs { + allFolderIDs[mf.FolderID] = true + if firstTrackPath == "" { + firstTrackPath = mf.Path + } + } + + // Resolve folder IDs to absolute paths + if len(allFolderIDs) > 0 { + folderIDs := make([]string, 0, len(allFolderIDs)) + for id := range allFolderIDs { + folderIDs = append(folderIDs, id) + } + folders, err := a.ds.Folder(ctx).GetAll(model.QueryOptions{ + Filters: squirrel.Eq{"folder.id": folderIDs}, + }) + if err != nil { + return nil, err + } + for _, f := range folders { + discFolders[f.AbsolutePath()] = true + } + } + + isMultiFolder := len(al.FolderIDs) > 1 + + r := &discArtworkReader{ + a: a, + album: *al, + discNumber: discNumber, + imgFiles: imgFiles, + discFolders: discFolders, + isMultiFolder: isMultiFolder, + firstTrackPath: core.AbsolutePath(ctx, a.ds, al.LibraryID, firstTrackPath), + updatedAt: imagesUpdatedAt, + } + r.cacheKey.artID = artID + if r.updatedAt != nil && r.updatedAt.After(al.UpdatedAt) { + r.cacheKey.lastUpdate = *r.updatedAt + } else { + r.cacheKey.lastUpdate = al.UpdatedAt + } + return r, nil +} + +func (d *discArtworkReader) Key() string { + hash := md5.Sum([]byte(conf.Server.DiscArtPriority)) + return fmt.Sprintf( + "%s.%x", + d.cacheKey.Key(), + hash, + ) +} + +func (d *discArtworkReader) LastUpdated() time.Time { + return d.album.UpdatedAt +} + +func (d *discArtworkReader) Reader(ctx context.Context) (io.ReadCloser, string, error) { + var ff = d.fromDiscArtPriority(ctx, d.a.ffmpeg, conf.Server.DiscArtPriority) + // Fallback to album cover art + albumArtID := model.NewArtworkID(model.KindAlbumArtwork, d.album.ID, &d.album.UpdatedAt) + ff = append(ff, fromAlbum(ctx, d.a, albumArtID)) + return selectImageReader(ctx, d.cacheKey.artID, ff...) +} + +func (d *discArtworkReader) fromDiscArtPriority(ctx context.Context, ffmpeg ffmpeg.FFmpeg, priority string) []sourceFunc { + var ff []sourceFunc + for pattern := range strings.SplitSeq(strings.ToLower(priority), ",") { + pattern = strings.TrimSpace(pattern) + switch { + case pattern == "embedded": + ff = append(ff, fromTag(ctx, d.firstTrackPath), fromFFmpegTag(ctx, ffmpeg, d.firstTrackPath)) + case pattern == "external": + // Not supported for disc art, silently ignore + case pattern == "discsubtitle": + if subtitle := strings.TrimSpace(d.album.Discs[d.discNumber]); subtitle != "" { + ff = append(ff, d.fromDiscSubtitle(ctx, subtitle)) + } + case len(d.imgFiles) > 0: + ff = append(ff, d.fromExternalFile(ctx, pattern)) + } + } + return ff +} + +// fromDiscSubtitle returns a sourceFunc that matches image files whose stem +// (filename without extension) equals the disc subtitle (case-insensitive). +func (d *discArtworkReader) fromDiscSubtitle(ctx context.Context, subtitle string) sourceFunc { + return func() (io.ReadCloser, string, error) { + for _, file := range d.imgFiles { + _, name := filepath.Split(file) + stem := strings.TrimSuffix(name, filepath.Ext(name)) + if !strings.EqualFold(stem, subtitle) { + continue + } + f, err := os.Open(file) + if err != nil { + log.Warn(ctx, "Could not open disc art file", "file", file, err) + continue + } + return f, file, nil + } + return nil, "", fmt.Errorf("disc %d: no image file matching subtitle %q", d.discNumber, subtitle) + } +} + +// extractDiscNumber extracts a disc number from a filename based on a glob pattern. +// It finds the portion of the filename that the wildcard matched and parses leading +// digits as the disc number. Returns (0, false) if the pattern doesn't match or +// no leading digits are found in the wildcard portion. +func extractDiscNumber(pattern, filename string) (int, bool) { + filename = strings.ToLower(filename) + pattern = strings.ToLower(pattern) + + matched, err := filepath.Match(pattern, filename) + if err != nil || !matched { + return 0, false + } + + // Find the prefix before the first '*' in the pattern + starIdx := strings.IndexByte(pattern, '*') + if starIdx < 0 { + return 0, false + } + prefix := pattern[:starIdx] + + // Strip the prefix from the filename to get the wildcard-matched portion + if !strings.HasPrefix(filename, prefix) { + return 0, false + } + remainder := filename[len(prefix):] + + // Extract leading ASCII digits from the remainder + var digits []byte + for _, r := range remainder { + if r >= '0' && r <= '9' { + digits = append(digits, byte(r)) + } else { + break + } + } + + if len(digits) == 0 { + return 0, false + } + + num, err := strconv.Atoi(string(digits)) + if err != nil { + return 0, false + } + return num, true +} + +// fromExternalFile returns a sourceFunc that matches image files against a glob +// pattern with disc-number-aware filtering. +// +// Matching rules: +// - If a disc number can be extracted from the filename, the file matches only if +// the number equals the target disc number. +// - If no number is found and this is a multi-folder album, the file matches if +// it's in a folder containing tracks for this disc. +// - If no number is found and this is a single-folder album, the file is skipped +// (ambiguous). +func (d *discArtworkReader) fromExternalFile(ctx context.Context, pattern string) sourceFunc { + return func() (io.ReadCloser, string, error) { + for _, file := range d.imgFiles { + _, name := filepath.Split(file) + match, err := filepath.Match(pattern, strings.ToLower(name)) + if err != nil { + log.Warn(ctx, "Error matching disc art file to pattern", "pattern", pattern, "file", file) + continue + } + if !match { + continue + } + + // Try to extract disc number from filename + num, hasNum := extractDiscNumber(pattern, name) + if hasNum { + // File has a disc number — must match target disc + if num != d.discNumber { + continue + } + } else if d.isMultiFolder { + // No number, multi-folder: match by folder association + dir := filepath.Dir(file) + if !d.discFolders[dir] { + continue + } + } else { + // No number, single-folder: ambiguous, skip + continue + } + + f, err := os.Open(file) + if err != nil { + log.Warn(ctx, "Could not open disc art file", "file", file, err) + continue + } + return f, file, nil + } + return nil, "", fmt.Errorf("disc %d: pattern '%s' not matched by files", d.discNumber, pattern) + } +} diff --git a/core/artwork/reader_disc_test.go b/core/artwork/reader_disc_test.go new file mode 100644 index 000000000..f8193e24e --- /dev/null +++ b/core/artwork/reader_disc_test.go @@ -0,0 +1,285 @@ +package artwork + +import ( + "context" + "os" + "path/filepath" + + "github.com/navidrome/navidrome/model" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Disc Artwork Reader", func() { + Describe("extractDiscNumber", func() { + DescribeTable("extracts disc number from filename based on glob pattern", + func(pattern, filename string, expectedNum int, expectedOk bool) { + num, ok := extractDiscNumber(pattern, filename) + Expect(ok).To(Equal(expectedOk)) + if expectedOk { + Expect(num).To(Equal(expectedNum)) + } + }, + // Standard disc patterns + Entry("disc1.jpg", "disc*.*", "disc1.jpg", 1, true), + Entry("disc2.png", "disc*.*", "disc2.png", 2, true), + Entry("disc01.jpg", "disc*.*", "disc01.jpg", 1, true), + Entry("disc02.png", "disc*.*", "disc02.png", 2, true), + Entry("disc10.jpg", "disc*.*", "disc10.jpg", 10, true), + + // CD patterns + Entry("cd1.jpg", "cd*.*", "cd1.jpg", 1, true), + Entry("cd02.png", "cd*.*", "cd02.png", 2, true), + + // No number in filename + Entry("disc.jpg has no number", "disc*.*", "disc.jpg", 0, false), + Entry("cd.jpg has no number", "cd*.*", "cd.jpg", 0, false), + + // Extra text after number + Entry("disc2-bonus.jpg", "disc*.*", "disc2-bonus.jpg", 2, true), + Entry("disc01_front.png", "disc*.*", "disc01_front.png", 1, true), + + // Case insensitive (filename already lowered by caller) + Entry("Disc1.jpg lowered", "disc*.*", "disc1.jpg", 1, true), + + // Pattern doesn't match + Entry("cover.jpg doesn't match disc*.*", "disc*.*", "cover.jpg", 0, false), + + // Pattern with no wildcard before dot + Entry("front1.jpg with front*.*", "front*.*", "front1.jpg", 1, true), + ) + }) + + Describe("fromExternalFile", func() { + var ( + ctx context.Context + tmpDir string + ) + + BeforeEach(func() { + ctx = context.Background() + tmpDir = GinkgoT().TempDir() + }) + + createFile := func(path string) string { + fullPath := filepath.Join(tmpDir, filepath.FromSlash(path)) + Expect(os.MkdirAll(filepath.Dir(fullPath), 0755)).To(Succeed()) + Expect(os.WriteFile(fullPath, []byte("image data"), 0600)).To(Succeed()) + return fullPath + } + + It("matches file with disc number in single-folder album", func() { + f1 := createFile("album/disc1.jpg") + f2 := createFile("album/disc2.jpg") + reader := &discArtworkReader{ + discNumber: 1, + imgFiles: []string{f1, f2}, + discFolders: map[string]bool{filepath.Join(tmpDir, "album"): true}, + } + + sf := reader.fromExternalFile(ctx, "disc*.*") + r, path, err := sf() + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + r.Close() + Expect(path).To(Equal(f1)) + }) + + It("skips file without number in single-folder album", func() { + f1 := createFile("album/disc.jpg") + reader := &discArtworkReader{ + discNumber: 1, + imgFiles: []string{f1}, + discFolders: map[string]bool{filepath.Join(tmpDir, "album"): true}, + } + + sf := reader.fromExternalFile(ctx, "disc*.*") + r, _, _ := sf() + Expect(r).To(BeNil()) + }) + + It("matches file without number in multi-folder album by folder", func() { + f1 := createFile("album/cd1/disc.jpg") + f2 := createFile("album/cd2/disc.jpg") + reader := &discArtworkReader{ + discNumber: 1, + imgFiles: []string{f1, f2}, + discFolders: map[string]bool{filepath.Join(tmpDir, "album", "cd1"): true}, + isMultiFolder: true, + } + + sf := reader.fromExternalFile(ctx, "disc*.*") + r, path, err := sf() + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + r.Close() + Expect(path).To(Equal(f1)) + }) + + It("prefers disc number over folder when number is present", func() { + // disc2.jpg in cd1 folder should match disc 2, not disc 1 + f1 := createFile("album/cd1/disc2.jpg") + reader := &discArtworkReader{ + discNumber: 2, + imgFiles: []string{f1}, + discFolders: map[string]bool{filepath.Join(tmpDir, "album", "cd1"): true}, + isMultiFolder: true, + } + + sf := reader.fromExternalFile(ctx, "disc*.*") + r, path, err := sf() + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + r.Close() + Expect(path).To(Equal(f1)) + }) + + It("does not match disc2.jpg when looking for disc 1", func() { + f1 := createFile("album/disc2.jpg") + reader := &discArtworkReader{ + discNumber: 1, + imgFiles: []string{f1}, + discFolders: map[string]bool{filepath.Join(tmpDir, "album"): true}, + } + + sf := reader.fromExternalFile(ctx, "disc*.*") + r, _, _ := sf() + Expect(r).To(BeNil()) + }) + }) + + Describe("fromDiscSubtitle", func() { + var ( + ctx context.Context + tmpDir string + ) + + BeforeEach(func() { + ctx = context.Background() + tmpDir = GinkgoT().TempDir() + }) + + createFile := func(path string) string { + fullPath := filepath.Join(tmpDir, filepath.FromSlash(path)) + Expect(os.MkdirAll(filepath.Dir(fullPath), 0755)).To(Succeed()) + Expect(os.WriteFile(fullPath, []byte("image data"), 0600)).To(Succeed()) + return fullPath + } + + It("matches image file whose stem equals the disc subtitle (case-insensitive)", func() { + f1 := createFile("album/The Blue Disc.jpg") + reader := &discArtworkReader{ + discNumber: 1, + imgFiles: []string{f1}, + } + + sf := reader.fromDiscSubtitle(ctx, "The Blue Disc") + r, path, err := sf() + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + r.Close() + Expect(path).To(Equal(f1)) + }) + + It("matches case-insensitively", func() { + f1 := createFile("album/bonus tracks.png") + reader := &discArtworkReader{ + discNumber: 2, + imgFiles: []string{f1}, + } + + sf := reader.fromDiscSubtitle(ctx, "Bonus Tracks") + r, path, err := sf() + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + r.Close() + Expect(path).To(Equal(f1)) + }) + + It("returns error when no matching file found", func() { + f1 := createFile("album/cover.jpg") + reader := &discArtworkReader{ + discNumber: 1, + imgFiles: []string{f1}, + } + + sf := reader.fromDiscSubtitle(ctx, "The Blue Disc") + _, _, err := sf() + Expect(err).To(HaveOccurred()) + }) + + It("matches first file when multiple extensions exist", func() { + f1 := createFile("album/The Blue Disc.jpg") + f2 := createFile("album/The Blue Disc.png") + reader := &discArtworkReader{ + discNumber: 1, + imgFiles: []string{f1, f2}, + } + + sf := reader.fromDiscSubtitle(ctx, "The Blue Disc") + r, path, err := sf() + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + r.Close() + Expect(path).To(Equal(f1)) + }) + }) + + Describe("discArtworkReader", func() { + Describe("fromDiscArtPriority", func() { + var reader *discArtworkReader + + BeforeEach(func() { + reader = &discArtworkReader{ + discNumber: 2, + isMultiFolder: true, + discFolders: map[string]bool{"/music/album/cd2": true}, + imgFiles: []string{ + "/music/album/cd1/disc.jpg", + "/music/album/cd2/disc.jpg", + "/music/album/cd2/disc2.jpg", + }, + firstTrackPath: "/music/album/cd2/track1.flac", + } + }) + + It("returns source funcs for glob patterns", func() { + ff := reader.fromDiscArtPriority(context.Background(), nil, "disc*.*") + Expect(ff).To(HaveLen(1)) + }) + + It("returns source funcs for embedded pattern", func() { + ff := reader.fromDiscArtPriority(context.Background(), nil, "embedded") + Expect(ff).To(HaveLen(2)) // fromTag + fromFFmpegTag + }) + + It("handles multiple comma-separated patterns", func() { + ff := reader.fromDiscArtPriority(context.Background(), nil, "disc*.*, cd*.*, embedded") + Expect(ff).To(HaveLen(4)) // disc*.* + cd*.* + fromTag + fromFFmpegTag + }) + + It("ignores 'external' pattern silently", func() { + ff := reader.fromDiscArtPriority(context.Background(), nil, "external") + Expect(ff).To(HaveLen(0)) + }) + + It("returns no source funcs when imgFiles is empty and pattern is not embedded", func() { + reader.imgFiles = nil + ff := reader.fromDiscArtPriority(context.Background(), nil, "disc*.*") + Expect(ff).To(HaveLen(0)) + }) + + It("returns source func for discsubtitle pattern", func() { + reader.album = model.Album{Discs: model.Discs{2: "Bonus Tracks"}} + ff := reader.fromDiscArtPriority(context.Background(), nil, "discsubtitle") + Expect(ff).To(HaveLen(1)) + }) + + It("returns no source func for discsubtitle when disc has no subtitle", func() { + reader.album = model.Album{Discs: model.Discs{2: ""}} + ff := reader.fromDiscArtPriority(context.Background(), nil, "discsubtitle") + Expect(ff).To(HaveLen(0)) + }) + }) + }) +}) diff --git a/model/artwork_id.go b/model/artwork_id.go index 36026dd03..8d935f427 100644 --- a/model/artwork_id.go +++ b/model/artwork_id.go @@ -22,6 +22,7 @@ var ( KindArtistArtwork = Kind{"ar", "artist"} KindAlbumArtwork = Kind{"al", "album"} KindPlaylistArtwork = Kind{"pl", "playlist"} + KindDiscArtwork = Kind{"dc", "disc"} ) var artworkKindMap = map[string]Kind{ @@ -29,6 +30,7 @@ var artworkKindMap = map[string]Kind{ KindArtistArtwork.prefix: KindArtistArtwork, KindAlbumArtwork.prefix: KindAlbumArtwork, KindPlaylistArtwork.prefix: KindPlaylistArtwork, + KindDiscArtwork.prefix: KindDiscArtwork, } type ArtworkID struct { @@ -91,6 +93,22 @@ func MustParseArtworkID(id string) ArtworkID { return artID } +func DiscArtworkID(albumID string, discNumber int) string { + return fmt.Sprintf("%s:%d", albumID, discNumber) +} + +func ParseDiscArtworkID(id string) (albumID string, discNumber int, err error) { + parts := strings.SplitN(id, ":", 2) + if len(parts) != 2 || parts[1] == "" { + return "", 0, errors.New("invalid disc artwork id") + } + num, err := strconv.Atoi(parts[1]) + if err != nil { + return "", 0, fmt.Errorf("invalid disc number in artwork id: %w", err) + } + return parts[0], num, nil +} + func artworkIDFromAlbum(al Album) ArtworkID { return ArtworkID{ Kind: KindAlbumArtwork, diff --git a/model/artwork_id_test.go b/model/artwork_id_test.go index 2f42217f9..b634e7cbc 100644 --- a/model/artwork_id_test.go +++ b/model/artwork_id_test.go @@ -28,6 +28,40 @@ var _ = Describe("ArtworkID", func() { Expect(parsedId.LastUpdate.Unix()).To(Equal(id.LastUpdate.Unix())) }) }) + Describe("ParseArtworkID - disc kind", func() { + It("parses a disc artwork ID with dc prefix", func() { + now := time.Now() + id := model.NewArtworkID(model.KindDiscArtwork, "albumid123:2", &now) + parsedId, err := model.ParseArtworkID(id.String()) + Expect(err).ToNot(HaveOccurred()) + Expect(parsedId.Kind).To(Equal(model.KindDiscArtwork)) + Expect(parsedId.ID).To(Equal("albumid123:2")) + Expect(parsedId.LastUpdate.Unix()).To(Equal(now.Unix())) + }) + }) + + Describe("ParseDiscArtworkID", func() { + DescribeTable("parses composite disc artwork IDs", + func(id string, expectedAlbum string, expectedDisc int, expectErr bool) { + albumID, discNumber, err := model.ParseDiscArtworkID(id) + if expectErr { + Expect(err).To(HaveOccurred()) + } else { + Expect(err).ToNot(HaveOccurred()) + Expect(albumID).To(Equal(expectedAlbum)) + Expect(discNumber).To(Equal(expectedDisc)) + } + }, + Entry("valid id", "albumid123:2", "albumid123", 2, false), + Entry("disc number 1", "abc:1", "abc", 1, false), + Entry("large disc number", "abc:10", "abc", 10, false), + Entry("missing colon", "albumid123", "", 0, true), + Entry("missing disc number", "albumid123:", "", 0, true), + Entry("non-numeric disc", "albumid123:abc", "", 0, true), + Entry("empty string", "", "", 0, true), + ) + }) + Describe("ParseArtworkID()", func() { It("parses album artwork ids", func() { id, err := model.ParseArtworkID("al-1234") diff --git a/server/subsonic/helpers.go b/server/subsonic/helpers.go index 598346901..e930aa630 100644 --- a/server/subsonic/helpers.go +++ b/server/subsonic/helpers.go @@ -392,7 +392,13 @@ func buildDiscSubtitles(a model.Album) []responses.DiscTitle { } var discTitles []responses.DiscTitle for num, title := range a.Discs { - discTitles = append(discTitles, responses.DiscTitle{Disc: int32(num), Title: title}) + artID := model.NewArtworkID(model.KindDiscArtwork, + model.DiscArtworkID(a.ID, num), &a.UpdatedAt) + discTitles = append(discTitles, responses.DiscTitle{ + Disc: int32(num), + Title: title, + CoverArt: artID.String(), + }) } if len(discTitles) == 1 && discTitles[0].Title == "" { return nil diff --git a/server/subsonic/helpers_test.go b/server/subsonic/helpers_test.go index 2099c8f69..d20ca89ad 100644 --- a/server/subsonic/helpers_test.go +++ b/server/subsonic/helpers_test.go @@ -3,6 +3,7 @@ package subsonic import ( "context" "net/http/httptest" + "time" "github.com/go-chi/jwtauth/v5" "github.com/navidrome/navidrome/conf" @@ -103,27 +104,43 @@ var _ = Describe("helpers", func() { Expect(buildDiscSubtitles(album)).To(BeNil()) }) - It("should return the disc title for a single disc", func() { + It("should return the disc title with cover art for a single disc", func() { + updatedAt := time.Now().Truncate(time.Second) album := model.Album{ + ID: "album1", + UpdatedAt: updatedAt, Discs: map[int]string{ 1: "Special Edition", }, } - Expect(buildDiscSubtitles(album)).To(Equal([]responses.DiscTitle{{Disc: 1, Title: "Special Edition"}})) + result := buildDiscSubtitles(album) + Expect(result).To(HaveLen(1)) + Expect(result[0].Disc).To(Equal(int32(1))) + Expect(result[0].Title).To(Equal("Special Edition")) + expectedArtID := model.NewArtworkID(model.KindDiscArtwork, "album1:1", &updatedAt) + Expect(result[0].CoverArt).To(Equal(expectedArtID.String())) }) - It("should return correct disc titles when album has discs with valid disc numbers", func() { + It("should return correct disc titles with cover art when album has multiple discs", func() { + updatedAt := time.Now().Truncate(time.Second) album := model.Album{ + ID: "album1", + UpdatedAt: updatedAt, Discs: map[int]string{ 1: "Disc 1", 2: "Disc 2", }, } - expected := []responses.DiscTitle{ - {Disc: 1, Title: "Disc 1"}, - {Disc: 2, Title: "Disc 2"}, - } - Expect(buildDiscSubtitles(album)).To(Equal(expected)) + result := buildDiscSubtitles(album) + Expect(result).To(HaveLen(2)) + Expect(result[0].Disc).To(Equal(int32(1))) + Expect(result[0].Title).To(Equal("Disc 1")) + expectedArtID1 := model.NewArtworkID(model.KindDiscArtwork, "album1:1", &updatedAt) + Expect(result[0].CoverArt).To(Equal(expectedArtID1.String())) + Expect(result[1].Disc).To(Equal(int32(2))) + Expect(result[1].Title).To(Equal("Disc 2")) + expectedArtID2 := model.NewArtworkID(model.KindDiscArtwork, "album1:2", &updatedAt) + Expect(result[1].CoverArt).To(Equal(expectedArtID2.String())) }) }) diff --git a/server/subsonic/responses/responses.go b/server/subsonic/responses/responses.go index be59e5851..c550ce61e 100644 --- a/server/subsonic/responses/responses.go +++ b/server/subsonic/responses/responses.go @@ -575,8 +575,9 @@ func (r ReplayGain) MarshalXML(e *xml.Encoder, start xml.StartElement) error { } type DiscTitle struct { - Disc int32 `xml:"disc,attr" json:"disc"` - Title string `xml:"title,attr" json:"title"` + Disc int32 `xml:"disc,attr" json:"disc"` + Title string `xml:"title,attr" json:"title"` + CoverArt string `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"` } type ItemDate struct { diff --git a/ui/src/common/SongDatagrid.jsx b/ui/src/common/SongDatagrid.jsx index 3586cf225..fbf2b9e31 100644 --- a/ui/src/common/SongDatagrid.jsx +++ b/ui/src/common/SongDatagrid.jsx @@ -1,4 +1,10 @@ -import React, { isValidElement, useMemo, useCallback, forwardRef } from 'react' +import React, { + isValidElement, + useMemo, + useCallback, + useState, + forwardRef, +} from 'react' import { useDispatch } from 'react-redux' import { Datagrid, @@ -17,7 +23,10 @@ import { makeStyles } from '@material-ui/core/styles' import AlbumIcon from '@material-ui/icons/Album' import clsx from 'clsx' import { useDrag } from 'react-dnd' +import Lightbox from 'react-image-lightbox' +import 'react-image-lightbox/style.css' import { playTracks } from '../actions' +import subsonic from '../subsonic' import { AlbumContextMenu } from '../common' import { DraggableTypes } from '../consts' import { formatFullDate } from '../utils' @@ -28,10 +37,20 @@ const useStyles = makeStyles({ overflow: 'hidden', textOverflow: 'ellipsis', verticalAlign: 'middle', + display: 'flex', + alignItems: 'center', }, discIcon: { - verticalAlign: 'text-top', - marginRight: '4px', + marginRight: '14px', + }, + discCoverArt: { + width: '48px', + height: '48px', + marginRight: '14px', + objectFit: 'cover', + borderRadius: '4px', + flexShrink: 0, + cursor: 'pointer', }, row: { cursor: 'pointer', @@ -61,19 +80,45 @@ const useStyles = makeStyles({ const DiscSubtitleRow = forwardRef( ({ record, onClick, colSpan, contextAlwaysVisible }, ref) => { + const translate = useTranslate() const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md')) const classes = useStyles({ isDesktop }) + const [imageError, setImageError] = useState(false) + const [isLightboxOpen, setLightboxOpen] = useState(false) const handlePlaySubset = (discNumber) => () => { onClick(discNumber) } - let subtitle = [] - if (record.discNumber > 0) { - subtitle.push(record.discNumber) - } - if (record.discSubtitle) { - subtitle.push(record.discSubtitle) - } + const coverArtUrl = subsonic.getDiscCoverArtUrl( + record.albumId, + record.discNumber, + record.updatedAt, + 96, + ) + + const fullImageUrl = subsonic.getDiscCoverArtUrl( + record.albumId, + record.discNumber, + record.updatedAt, + ) + + const handleOpenLightbox = useCallback( + (e) => { + if (!imageError) { + e.stopPropagation() + setLightboxOpen(true) + } + }, + [imageError], + ) + + const handleCloseLightbox = useCallback(() => setLightboxOpen(false), []) + + const subtitle = record.discSubtitle + ? record.discSubtitle + : translate('resources.song.fields.disc', { + discNumber: record.discNumber, + }) return ( - - {subtitle.join(': ')} + {!imageError ? ( + setImageError(true)} + /> + ) : ( + + )} + {subtitle} + {isLightboxOpen && !imageError && ( + + )} { } } +const getDiscCoverArtUrl = (albumId, discNumber, updatedAt, size) => { + const options = { + ...(updatedAt && { _: updatedAt }), + ...(size && { size }), + } + return baseUrl( + url('getCoverArt', 'dc-' + albumId + ':' + discNumber, options), + ) +} + const getArtistInfo = (id) => { return httpClient(url('getArtistInfo', id)) } @@ -129,6 +139,7 @@ export default { getScanStatus, getNowPlaying, getCoverArtUrl, + getDiscCoverArtUrl, getAvatarUrl, streamUrl, getAlbumInfo, diff --git a/ui/src/subsonic/index.test.js b/ui/src/subsonic/index.test.js index 1e0fbeaa6..2544e03f8 100644 --- a/ui/src/subsonic/index.test.js +++ b/ui/src/subsonic/index.test.js @@ -105,6 +105,56 @@ describe('getCoverArtUrl', () => { }) }) +describe('getDiscCoverArtUrl', () => { + beforeEach(() => { + const localStorageMock = { + getItem: vi.fn((key) => { + const values = { + username: 'testuser', + 'subsonic-token': 'testtoken', + 'subsonic-salt': 'testsalt', + } + return values[key] || null + }), + } + Object.defineProperty(window, 'localStorage', { value: localStorageMock }) + }) + + it('should construct URL with dc-albumId:discNumber format, size, and cache param', () => { + const url = subsonic.getDiscCoverArtUrl( + 'album-123', + 2, + '2023-01-01T00:00:00Z', + 48, + ) + + expect(url).toContain('getCoverArt') + expect(url).toContain('id=dc-album-123%3A2') + expect(url).toContain('size=48') + expect(url).toContain('_=2023-01-01T00%3A00%3A00Z') + }) + + it('should handle missing updatedAt', () => { + const url = subsonic.getDiscCoverArtUrl('album-123', 1, undefined, 48) + + expect(url).toContain('id=dc-album-123%3A1') + expect(url).toContain('size=48') + expect(url).not.toContain('_=') + }) + + it('should handle missing size', () => { + const url = subsonic.getDiscCoverArtUrl( + 'album-123', + 1, + '2023-01-01T00:00:00Z', + ) + + expect(url).toContain('id=dc-album-123%3A1') + expect(url).toContain('_=2023-01-01T00%3A00%3A00Z') + expect(url).not.toContain('size=') + }) +}) + describe('getAvatarUrl', () => { beforeEach(() => { // Mock localStorage values required by subsonic From 55e10b9c778729154e3ba0aaa0a43b135270ae99 Mon Sep 17 00:00:00 2001 From: Deluan Date: Fri, 13 Mar 2026 19:20:07 -0400 Subject: [PATCH 10/81] fix(playlist): update smart playlist rules during metadata update Signed-off-by: Deluan --- core/playlists/rest_adapter.go | 2 ++ core/playlists/rest_adapter_test.go | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/core/playlists/rest_adapter.go b/core/playlists/rest_adapter.go index c9cd4c136..3fecda0d5 100644 --- a/core/playlists/rest_adapter.go +++ b/core/playlists/rest_adapter.go @@ -97,5 +97,7 @@ func (s *playlists) updatePlaylistEntity(ctx context.Context, id string, entity if entity.OwnerID != "" { current.OwnerID = entity.OwnerID } + // Apply smart playlist rules update + current.Rules = entity.Rules return s.updateMetadata(ctx, s.ds, current, &entity.Name, &entity.Comment, &entity.Public) } diff --git a/core/playlists/rest_adapter_test.go b/core/playlists/rest_adapter_test.go index 29db2fc33..70ca8a9e4 100644 --- a/core/playlists/rest_adapter_test.go +++ b/core/playlists/rest_adapter_test.go @@ -125,6 +125,22 @@ var _ = Describe("REST Adapter", func() { Expect(err).To(Equal(rest.ErrPermissionDenied)) }) + It("updates smart playlist rules", func() { + mockPlsRepo.Data["smart-1"] = &model.Playlist{ + ID: "smart-1", + Name: "Smart Playlist", + OwnerID: "user-1", + Rules: &criteria.Criteria{Expression: criteria.Contains{"title": "old"}}, + } + ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false}) + repo = ps.NewRepository(ctx).(rest.Persistable) + newRules := &criteria.Criteria{Expression: criteria.Contains{"title": "new"}} + pls := &model.Playlist{Name: "Smart Playlist", Rules: newRules} + err := repo.Update("smart-1", pls) + Expect(err).ToNot(HaveOccurred()) + Expect(mockPlsRepo.Last.Rules).To(Equal(newRules)) + }) + It("returns rest.ErrNotFound when playlist doesn't exist", func() { ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false}) repo = ps.NewRepository(ctx).(rest.Persistable) From d042fc138cc173dbb89263d03e5289bd2ecc5ee5 Mon Sep 17 00:00:00 2001 From: Deluan Date: Fri, 13 Mar 2026 21:06:26 -0400 Subject: [PATCH 11/81] refactor(nanoid): replace gonanoid with custom nanoid implementation for ID generation Signed-off-by: Deluan --- core/share.go | 4 +- go.mod | 1 - go.sum | 2 - model/id/id.go | 4 +- utils/nanoid/nanoid.go | 52 +++++++++++++++++++++++ utils/nanoid/nanoid_test.go | 85 +++++++++++++++++++++++++++++++++++++ 6 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 utils/nanoid/nanoid.go create mode 100644 utils/nanoid/nanoid_test.go diff --git a/core/share.go b/core/share.go index fa43a95dd..a6d06a018 100644 --- a/core/share.go +++ b/core/share.go @@ -7,11 +7,11 @@ import ( "github.com/Masterminds/squirrel" "github.com/deluan/rest" - gonanoid "github.com/matoous/go-nanoid/v2" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" . "github.com/navidrome/navidrome/utils/gg" + "github.com/navidrome/navidrome/utils/nanoid" "github.com/navidrome/navidrome/utils/slice" "github.com/navidrome/navidrome/utils/str" ) @@ -73,7 +73,7 @@ type shareRepositoryWrapper struct { func (r *shareRepositoryWrapper) newId() (string, error) { for { - id, err := gonanoid.Generate("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 10) + id, err := nanoid.Generate("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 10) if err != nil { return "", err } diff --git a/go.mod b/go.mod index 424c52421..33333ca33 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,6 @@ require ( github.com/kr/pretty v0.3.1 github.com/lestrrat-go/jwx/v3 v3.0.13 github.com/maruel/natural v1.3.0 - github.com/matoous/go-nanoid/v2 v2.1.0 github.com/mattn/go-sqlite3 v1.14.34 github.com/microcosm-cc/bluemonday v1.0.27 github.com/mileusna/useragent v1.3.5 diff --git a/go.sum b/go.sum index a28900a41..30ca76e86 100644 --- a/go.sum +++ b/go.sum @@ -177,8 +177,6 @@ github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLO github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= github.com/maruel/natural v1.3.0 h1:VsmCsBmEyrR46RomtgHs5hbKADGRVtliHTyCOLFBpsg= github.com/maruel/natural v1.3.0/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= -github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE= -github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= diff --git a/model/id/id.go b/model/id/id.go index 930875260..b54542898 100644 --- a/model/id/id.go +++ b/model/id/id.go @@ -6,12 +6,12 @@ import ( "math/big" "strings" - gonanoid "github.com/matoous/go-nanoid/v2" "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/utils/nanoid" ) func NewRandom() string { - id, err := gonanoid.Generate("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 22) + id, err := nanoid.Generate("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 22) if err != nil { log.Error("Could not generate new ID", err) } diff --git a/utils/nanoid/nanoid.go b/utils/nanoid/nanoid.go new file mode 100644 index 000000000..17d32e72f --- /dev/null +++ b/utils/nanoid/nanoid.go @@ -0,0 +1,52 @@ +package nanoid + +import ( + "crypto/rand" + "errors" + "math" +) + +// Generate returns a cryptographically secure random string of `size` characters +// drawn from `alphabet`. It uses bitmask with rejection sampling to avoid modulo bias. +// The alphabet must be non-empty, contain at most 255 characters, and consist only of +// ASCII characters. Non-ASCII alphabets (e.g., multi-byte UTF-8) are not supported. +func Generate(alphabet string, size int) (string, error) { + if len(alphabet) == 0 || len(alphabet) > 255 { + return "", errors.New("alphabet must be non-empty and at most 255 characters") + } + if size <= 0 { + return "", errors.New("size must be a positive integer") + } + + mask := getMask(len(alphabet)) + step := int(math.Ceil(1.6 * float64(mask) * float64(size) / float64(len(alphabet)))) + + id := make([]byte, size) + bytes := make([]byte, step) + for j := 0; ; { + if _, err := rand.Read(bytes); err != nil { + return "", err + } + for i := range step { + idx := int(bytes[i]) & mask + if idx < len(alphabet) { + id[j] = alphabet[idx] + j++ + if j == size { + return string(id), nil + } + } + } + } +} + +// getMask returns the smallest bitmask >= alphabetSize-1. +func getMask(alphabetSize int) int { + for i := 1; i <= 8; i++ { + mask := (2 << uint(i)) - 1 + if mask >= alphabetSize-1 { + return mask + } + } + return 0 +} diff --git a/utils/nanoid/nanoid_test.go b/utils/nanoid/nanoid_test.go new file mode 100644 index 000000000..99d4e5715 --- /dev/null +++ b/utils/nanoid/nanoid_test.go @@ -0,0 +1,85 @@ +package nanoid_test + +import ( + "testing" + + "github.com/navidrome/navidrome/utils/nanoid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestNanoid(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Nanoid Suite") +} + +var _ = Describe("Generate", func() { + const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + + It("generates a string of the requested length", func() { + id, err := nanoid.Generate(alphabet, 22) + Expect(err).ToNot(HaveOccurred()) + Expect(id).To(HaveLen(22)) + }) + + It("generates a short string of the requested length", func() { + id, err := nanoid.Generate(alphabet, 10) + Expect(err).ToNot(HaveOccurred()) + Expect(id).To(HaveLen(10)) + }) + + It("only contains characters from the alphabet", func() { + id, err := nanoid.Generate(alphabet, 100) + Expect(err).ToNot(HaveOccurred()) + for _, c := range id { + Expect(alphabet).To(ContainSubstring(string(c))) + } + }) + + It("generates unique IDs", func() { + seen := make(map[string]bool) + for range 1000 { + id, err := nanoid.Generate(alphabet, 22) + Expect(err).ToNot(HaveOccurred()) + Expect(seen).ToNot(HaveKey(id)) + seen[id] = true + } + }) + + It("works with a single-character alphabet", func() { + id, err := nanoid.Generate("a", 5) + Expect(err).ToNot(HaveOccurred()) + Expect(id).To(Equal("aaaaa")) + }) + + It("works with a small alphabet", func() { + id, err := nanoid.Generate("ab", 10) + Expect(err).ToNot(HaveOccurred()) + Expect(id).To(HaveLen(10)) + for _, c := range id { + Expect(string(c)).To(BeElementOf("a", "b")) + } + }) + + It("returns error on empty alphabet", func() { + _, err := nanoid.Generate("", 10) + Expect(err).To(HaveOccurred()) + }) + + It("returns error on alphabet larger than 255 characters", func() { + bigAlphabet := make([]byte, 256) + for i := range bigAlphabet { + bigAlphabet[i] = byte(i) + } + _, err := nanoid.Generate(string(bigAlphabet), 10) + Expect(err).To(HaveOccurred()) + }) + + It("returns error on non-positive size", func() { + _, err := nanoid.Generate(alphabet, 0) + Expect(err).To(HaveOccurred()) + + _, err = nanoid.Generate(alphabet, -1) + Expect(err).To(HaveOccurred()) + }) +}) From 55331b5fd92c3534a1e75f7ebfb1dd537f7efb9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Sat, 14 Mar 2026 00:07:21 -0400 Subject: [PATCH 12/81] fix(scanner): prevent duplicate tracks when multiple missing files match same target (#5183) In processMissingTracks, matched tracks were not removed from the candidate pool after being consumed by moveMatched. This allowed the same target track to be paired with multiple missing tracks, creating duplicate non-missing records with the same path. Track consumed matches in a usedMatched map so each target is used at most once. Fixes #5169 --- scanner/phase_2_missing_tracks.go | 12 +++++++++- scanner/phase_2_missing_tracks_test.go | 33 ++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/scanner/phase_2_missing_tracks.go b/scanner/phase_2_missing_tracks.go index c47565036..8c258b833 100644 --- a/scanner/phase_2_missing_tracks.go +++ b/scanner/phase_2_missing_tracks.go @@ -114,6 +114,10 @@ func (p *phaseMissingTracks) stages() []ppl.Stage[*missingTracks] { func (p *phaseMissingTracks) processMissingTracks(in *missingTracks) (*missingTracks, error) { hasMatches := false + // Track which matched entries have already been consumed, so each matched track + // is only used once. Without this, the same matched track could be paired with + // multiple missing tracks, creating duplicate records with the same path. + usedMatched := make(map[string]bool, len(in.matched)) for _, ms := range in.missing { var exactMatch model.MediaFile @@ -121,6 +125,9 @@ func (p *phaseMissingTracks) processMissingTracks(in *missingTracks) (*missingTr // Identify exact and equivalent matches for _, mt := range in.matched { + if usedMatched[mt.ID] { + continue + } if ms.Equals(mt) { exactMatch = mt break // Prioritize exact match @@ -138,13 +145,14 @@ func (p *phaseMissingTracks) processMissingTracks(in *missingTracks) (*missingTr log.Error(p.ctx, "Scanner: Error moving matched track", "missing", ms.Path, "movedTo", exactMatch.Path, "lib", in.lib.Name, err) return nil, err } + usedMatched[exactMatch.ID] = true p.totalMatched.Add(1) hasMatches = true continue } // If there is only one missing and one matched track, consider them equivalent (same PID) - if len(in.missing) == 1 && len(in.matched) == 1 { + if len(in.missing) == 1 && len(in.matched) == 1 && !usedMatched[in.matched[0].ID] { singleMatch := in.matched[0] log.Debug(p.ctx, "Scanner: Found track with same persistent ID in a new place", "missing", ms.Path, "movedTo", singleMatch.Path, "lib", in.lib.Name) err := p.moveMatched(singleMatch, ms) @@ -152,6 +160,7 @@ func (p *phaseMissingTracks) processMissingTracks(in *missingTracks) (*missingTr log.Error(p.ctx, "Scanner: Error updating matched track", "missing", ms.Path, "movedTo", singleMatch.Path, "lib", in.lib.Name, err) return nil, err } + usedMatched[singleMatch.ID] = true p.totalMatched.Add(1) hasMatches = true continue @@ -165,6 +174,7 @@ func (p *phaseMissingTracks) processMissingTracks(in *missingTracks) (*missingTr log.Error(p.ctx, "Scanner: Error updating matched track", "missing", ms.Path, "movedTo", equivalentMatch.Path, "lib", in.lib.Name, err) return nil, err } + usedMatched[equivalentMatch.ID] = true p.totalMatched.Add(1) hasMatches = true } diff --git a/scanner/phase_2_missing_tracks_test.go b/scanner/phase_2_missing_tracks_test.go index fa6ef5724..d54ceee40 100644 --- a/scanner/phase_2_missing_tracks_test.go +++ b/scanner/phase_2_missing_tracks_test.go @@ -241,6 +241,39 @@ var _ = Describe("phaseMissingTracks", func() { Expect(movedTrack.Size).To(Equal(missingTrack.Size)) }) + It("should not match the same target to multiple missing tracks (prevents duplicate paths)", func() { + // Simulate a scenario where two missing tracks from different locations have the same + // base filename and match the same newly imported track via IsEquivalent. + // Without deduplication, both missing tracks would be "moved" to the same target, + // creating two non-missing records with the same path. + missingTrack1 := model.MediaFile{ID: "1", PID: "A", Path: "old_dir1/song.mp3", Title: "title1", Size: 100} + missingTrack2 := model.MediaFile{ID: "2", PID: "A", Path: "old_dir2/song.mp3", Title: "title1", Size: 100} + matchedTrack := model.MediaFile{ID: "3", PID: "A", Path: "new_dir/song.mp3", Title: "title1", Size: 200} + + _ = ds.MediaFile(ctx).Put(&missingTrack1) + _ = ds.MediaFile(ctx).Put(&missingTrack2) + _ = ds.MediaFile(ctx).Put(&matchedTrack) + + in := &missingTracks{ + missing: []model.MediaFile{missingTrack1, missingTrack2}, + matched: []model.MediaFile{matchedTrack}, + } + + _, err := phase.processMissingTracks(in) + Expect(err).ToNot(HaveOccurred()) + // Only one of the missing tracks should be matched + Expect(phase.totalMatched.Load()).To(Equal(uint32(1))) + Expect(state.changesDetected.Load()).To(BeTrue()) + + // The matched track should have been consumed by the first missing track + movedTrack, _ := ds.MediaFile(ctx).Get("1") + Expect(movedTrack.Path).To(Equal(matchedTrack.Path)) + + // The second missing track should remain unchanged + unmatchedTrack, _ := ds.MediaFile(ctx).Get("2") + Expect(unmatchedTrack.Path).To(Equal(missingTrack2.Path)) + }) + It("should return an error when there's an error moving the matched track", func() { missingTrack := model.MediaFile{ID: "1", PID: "A", Path: "path1.mp3", Tags: model.Tags{"title": []string{"title1"}}} matchedTrack := model.MediaFile{ID: "2", PID: "A", Path: "path1.mp3", Tags: model.Tags{"title": []string{"title1"}}} From d79b8124674bef23283201a097b99fc29a195f52 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sat, 14 Mar 2026 09:59:51 -0400 Subject: [PATCH 13/81] refactor(natural): replace maruel/natural with custom natural sort implementation --- core/artwork/reader_album.go | 2 +- utils/natural/natural.go | 98 ++++++++++++++++++++++++++++ utils/natural/natural_test.go | 116 ++++++++++++++++++++++++++++++++++ 3 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 utils/natural/natural.go create mode 100644 utils/natural/natural_test.go diff --git a/core/artwork/reader_album.go b/core/artwork/reader_album.go index 36b2fff05..98a2105eb 100644 --- a/core/artwork/reader_album.go +++ b/core/artwork/reader_album.go @@ -13,13 +13,13 @@ import ( "time" "github.com/Masterminds/squirrel" - "github.com/maruel/natural" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/core/external" "github.com/navidrome/navidrome/core/ffmpeg" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/utils/natural" ) type albumArtworkReader struct { diff --git a/utils/natural/natural.go b/utils/natural/natural.go new file mode 100644 index 000000000..fa0800e1d --- /dev/null +++ b/utils/natural/natural.go @@ -0,0 +1,98 @@ +// Package natural provides natural (alphanumeric) string comparison. +// When both strings have digit sequences at the same position, they are +// compared numerically (so "file2" < "file10"); otherwise bytes are +// compared one-by-one. No allocations are made. +package natural + +import "strings" + +// Compare returns a negative value if a < b, zero if a == b, +// or a positive value if a > b using natural sort ordering. +// +// When two numeric segments are numerically equal (e.g. "01" vs "1"), +// comparison continues with the remaining suffixes. If one or both +// strings end at the digit boundary, the raw strings are compared +// lexically, which makes leading zeros significant as a tie-breaker +// (e.g. "a01" < "a1", "a0" < "a00"). +func Compare(a, b string) int { + ia, ib := 0, 0 + for ia < len(a) && ib < len(b) { + ca, cb := a[ia], b[ib] + da, db := isDigit(ca), isDigit(cb) + + switch { + case da && db: + // Both are in digit sequences — compare numerically. + endA := ia + for endA < len(a) && isDigit(a[endA]) { + endA++ + } + endB := ib + for endB < len(b) && isDigit(b[endB]) { + endB++ + } + + if c := compareNumbers(a[ia:endA], b[ib:endB]); c != 0 { + return c + } + + // Numerically equal. If both sides have trailing data, continue + // comparing after the digit runs. Otherwise fall through to + // lexical comparison of the full remaining strings (which makes + // leading-zero differences significant as a tie-breaker). + if endA < len(a) && endB < len(b) { + ia = endA + ib = endB + continue + } + return strings.Compare(a[ia:], b[ib:]) + case da != db: + return int(ca) - int(cb) + default: + if ca != cb { + return int(ca) - int(cb) + } + ia++ + ib++ + } + } + return (len(a) - ia) - (len(b) - ib) +} + +// compareNumbers compares two digit strings numerically. +// Leading zeros are stripped before comparison. +func compareNumbers(a, b string) int { + // Strip leading zeros. + sa := stripZeros(a) + sb := stripZeros(b) + + // Different lengths after stripping means different magnitude. + if len(sa) != len(sb) { + return len(sa) - len(sb) + } + + // Same length — compare digit by digit. + for i := range len(sa) { + if sa[i] != sb[i] { + return int(sa[i]) - int(sb[i]) + } + } + return 0 +} + +// stripZeros returns s with leading '0' bytes removed. +// If s is all zeros, returns the last byte (a single "0"). +func stripZeros(s string) string { + i := 0 + for i < len(s) && s[i] == '0' { + i++ + } + if i == len(s) && len(s) > 0 { + return s[len(s)-1:] + } + return s[i:] +} + +func isDigit(c byte) bool { + return c >= '0' && c <= '9' +} diff --git a/utils/natural/natural_test.go b/utils/natural/natural_test.go new file mode 100644 index 000000000..825a944c0 --- /dev/null +++ b/utils/natural/natural_test.go @@ -0,0 +1,116 @@ +package natural_test + +import ( + "testing" + + "github.com/navidrome/navidrome/utils/natural" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestNatural(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Natural Suite") +} + +var _ = Describe("Compare", func() { + DescribeTable("returns correct ordering", + func(a, b string, expected int) { + result := natural.Compare(a, b) + if expected < 0 { + Expect(result).To(BeNumerically("<", 0), "expected %q < %q", a, b) + } else if expected > 0 { + Expect(result).To(BeNumerically(">", 0), "expected %q > %q", a, b) + } else { + Expect(result).To(Equal(0), "expected %q == %q", a, b) + } + }, + // Basic string ordering + Entry("a < b", "a", "b", -1), + Entry("b > a", "b", "a", 1), + Entry("a < aa (prefix)", "a", "aa", -1), + Entry("aa > a", "aa", "a", 1), + + // Equal strings + Entry("equal strings return 0", "abc", "abc", 0), + Entry("both empty", "", "", 0), + Entry("a01 == a01", "a01", "a01", 0), + Entry("a1 == a1", "a1", "a1", 0), + + // Empty string edge cases + Entry("empty < non-empty", "", "a", -1), + Entry("non-empty > empty", "a", "", 1), + + // Numeric comparison + Entry("2 < 10 numerically", "2", "10", -1), + Entry("10 > 2 numerically", "10", "2", 1), + Entry("equal numbers", "42", "42", 0), + Entry("9 < 10", "9", "10", -1), + Entry("99 < 100", "99", "100", -1), + + // Simple numeric segments (from original library) + Entry("a0 < a1", "a0", "a1", -1), + Entry("a0 < a00", "a0", "a00", -1), + Entry("a00 < a01", "a00", "a01", -1), + Entry("a01 < a1", "a01", "a1", -1), + Entry("a01 < a2", "a01", "a2", -1), + Entry("a01x < a2x", "a01x", "a2x", -1), + Entry("a01 > a00", "a01", "a00", 1), + Entry("a2 > a01", "a2", "a01", 1), + Entry("a2x > a01x", "a2x", "a01x", 1), + + // Multiple numeric groups (from original library) + Entry("a0b00 < a00b1", "a0b00", "a00b1", -1), + Entry("a0b00 < a00b01", "a0b00", "a00b01", -1), + Entry("a00b0 < a0b00", "a00b0", "a0b00", -1), + Entry("a00b00 < a0b01", "a00b00", "a0b01", -1), + Entry("a00b00 < a0b1", "a00b00", "a0b1", -1), + Entry("a00b00 > a0b0", "a00b00", "a0b0", 1), + Entry("a00b01 > a0b00", "a00b01", "a0b00", 1), + Entry("a00b00 == a0b00", "a00b00", "a0b00", 0), + + // Leading zeros at end of string — lexical tie-break + Entry("file01 < file1", "file01", "file1", -1), + + // Prefix comparison + Entry("abc < abcd", "abc", "abcd", -1), + Entry("abcd > abc", "abcd", "abc", 1), + + // Navidrome use cases: cover art sorting + Entry("cover < cover.1", "cover", "cover.1", -1), + Entry("cover.1 < cover.2", "cover.1", "cover.2", -1), + Entry("cover.2 < cover.10", "cover.2", "cover.10", -1), + + // Navidrome use cases: disc sorting + Entry("disc1 < disc2", "disc1", "disc2", -1), + Entry("disc2 < disc10", "disc2", "disc10", -1), + Entry("disc1 < disc10", "disc1", "disc10", -1), + + // Multiple numeric segments + Entry("a1b2 < a1b10", "a1b2", "a1b10", -1), + Entry("a2b1 > a1b2", "a2b1", "a1b2", 1), + + // Numbers at the start + Entry("2abc < 10abc", "2abc", "10abc", -1), + + // Numbers larger than uint64 max (from original library) + Entry("large: fewer digits < more digits", + "a99999999999999999999", "a100000000000000000000", -1), + Entry("large: digit-by-digit comparison", + "a123456789012345678901234567890", "a123456789012345678901234567891", -1), + Entry("large: more digits > fewer digits", + "a999999999999999999999", "a1000000000000000000000", -1), + Entry("large: 20 digits < 100 digits by length", + "a20000000000000000000", "a100000000000000000000", -1), + Entry("large: 100 digits > 20 digits", + "a100000000000000000000", "a20000000000000000000", 1), + Entry("large: reverse of above", + "a1000000000000000000000", "a999999999999999999999", 1), + Entry("large: equal", + "a100000000000000000000", "a100000000000000000000", 0), + Entry("large: leading zeros with trailing data", + "a00000000000000000000001x", "a1x", 0), + Entry("large: leading zeros with trailing data (2)", + "a099999999999999999999x", "a99999999999999999999x", 0), + ) +}) From 8939f31d55cf1443ceec940650aafd24a6449737 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sat, 14 Mar 2026 09:59:52 -0400 Subject: [PATCH 14/81] refactor(jsoncommentstrip): replace go-jsoncommentstrip with custom JSON comment stripping --- core/playlists/parse_nsp.go | 2 +- utils/jsoncommentstrip/jsoncommentstrip.go | 128 ++++++++++++++ .../jsoncommentstrip/jsoncommentstrip_test.go | 164 ++++++++++++++++++ 3 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 utils/jsoncommentstrip/jsoncommentstrip.go create mode 100644 utils/jsoncommentstrip/jsoncommentstrip_test.go diff --git a/core/playlists/parse_nsp.go b/core/playlists/parse_nsp.go index c7f7dd946..56c80a950 100644 --- a/core/playlists/parse_nsp.go +++ b/core/playlists/parse_nsp.go @@ -9,10 +9,10 @@ import ( "os" "path/filepath" - "github.com/RaveNoX/go-jsoncommentstrip" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/criteria" + "github.com/navidrome/navidrome/utils/jsoncommentstrip" ) func (s *playlists) newSyncedPlaylist(baseDir string, playlistFile string) (*model.Playlist, error) { diff --git a/utils/jsoncommentstrip/jsoncommentstrip.go b/utils/jsoncommentstrip/jsoncommentstrip.go new file mode 100644 index 000000000..54ddd88fa --- /dev/null +++ b/utils/jsoncommentstrip/jsoncommentstrip.go @@ -0,0 +1,128 @@ +// Package jsoncommentstrip provides an io.Reader that strips JavaScript-style +// comments (// line and /* block */) from JSON input while preserving +// comment-like sequences inside JSON string values. +package jsoncommentstrip + +import ( + "bufio" + "io" +) + +type state int + +const ( + stateNormal state = iota + stateInString + stateInStringEscape + stateMaybeComment // saw '/' + stateLineComment // inside // ... + stateBlockComment // inside /* ... */ + stateMaybeBlockEnd // saw '*' inside block comment +) + +type reader struct { + r *bufio.Reader + state state +} + +// NewReader returns an io.Reader that strips JSON comments from the +// underlying reader. It removes single-line comments (// to end of line) +// and block comments (/* ... */), while preserving comment-like sequences +// that appear inside JSON string values. +func NewReader(r io.Reader) io.Reader { + return &reader{ + r: bufio.NewReader(r), + state: stateNormal, + } +} + +func (cr *reader) Read(p []byte) (int, error) { + n := 0 + for n < len(p) { + b, err := cr.r.ReadByte() + if err != nil { + if cr.state == stateMaybeComment { + // Emit the pending '/' before returning EOF + p[n] = '/' + n++ + cr.state = stateNormal + } + return n, err + } + + switch cr.state { + case stateNormal: + switch b { + case '"': + p[n] = b + n++ + cr.state = stateInString + case '/': + cr.state = stateMaybeComment + default: + p[n] = b + n++ + } + + case stateInString: + p[n] = b + n++ + switch b { + case '\\': + cr.state = stateInStringEscape + case '"': + cr.state = stateNormal + } + + case stateInStringEscape: + p[n] = b + n++ + cr.state = stateInString + + case stateMaybeComment: + switch b { + case '/': + cr.state = stateLineComment + case '*': + cr.state = stateBlockComment + default: + // The '/' was not a comment start; emit it and the current byte + p[n] = '/' + n++ + if n < len(p) { + p[n] = b + n++ + } else { + // We need to "unread" the current byte since buffer is full + _ = cr.r.UnreadByte() + } + cr.state = stateNormal + } + + case stateLineComment: + if b == '\n' || b == '\r' { + p[n] = b + n++ + cr.state = stateNormal + } + // Otherwise, consume and discard + + case stateBlockComment: + if b == '*' { + cr.state = stateMaybeBlockEnd + } + // Otherwise, consume and discard + + case stateMaybeBlockEnd: + if b == '/' { + cr.state = stateNormal + } else if b == '*' { + // Stay in stateMaybeBlockEnd (consecutive *'s) + cr.state = stateMaybeBlockEnd + } else { + cr.state = stateBlockComment + } + } + } + return n, nil +} diff --git a/utils/jsoncommentstrip/jsoncommentstrip_test.go b/utils/jsoncommentstrip/jsoncommentstrip_test.go new file mode 100644 index 000000000..21e4bd0b1 --- /dev/null +++ b/utils/jsoncommentstrip/jsoncommentstrip_test.go @@ -0,0 +1,164 @@ +package jsoncommentstrip_test + +import ( + "bytes" + "encoding/json" + "io" + "strings" + "testing" + + "github.com/navidrome/navidrome/utils/jsoncommentstrip" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestJsonCommentStrip(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "JsonCommentStrip Suite") +} + +var _ = Describe("NewReader", func() { + read := func(input string) string { + r := jsoncommentstrip.NewReader(strings.NewReader(input)) + out, err := io.ReadAll(r) + Expect(err).ToNot(HaveOccurred()) + return string(out) + } + + // compact returns the compacted JSON form of s, for readable comparisons. + compact := func(s string) string { + var buf bytes.Buffer + ExpectWithOffset(1, json.Compact(&buf, []byte(s))).To(Succeed()) + return buf.String() + } + + It("passes through JSON without comments unchanged", func() { + input := `{"key": "value", "num": 42}` + Expect(read(input)).To(Equal(input)) + }) + + It("strips single-line comments", func() { + input := `{ + // this is a comment + "key": "value" + }` + Expect(compact(read(input))).To(Equal(compact(`{ + "key": "value" + }`))) + }) + + It("strips single-line comments at end of line", func() { + input := `{ + "key": "value" // inline comment + }` + Expect(compact(read(input))).To(Equal(compact(`{ + "key": "value" + }`))) + }) + + It("strips block comments", func() { + input := `{/* comment */"key": "value"}` + Expect(compact(read(input))).To(Equal(`{"key":"value"}`)) + }) + + It("strips multi-line block comments", func() { + input := `{ + /* this is + a multi-line + comment */ + "key": "value" + }` + Expect(compact(read(input))).To(Equal(compact(`{ + "key": "value" + }`))) + }) + + It("preserves // inside JSON strings", func() { + input := `{"key": "value // not a comment"}` + Expect(read(input)).To(Equal(input)) + }) + + It("preserves /* inside JSON strings", func() { + input := `{"key": "value /* not a comment */"}` + Expect(read(input)).To(Equal(input)) + }) + + It("handles escaped quotes in strings", func() { + input := `{"key": "val\"ue // not a comment"}` + Expect(read(input)).To(Equal(input)) + }) + + It("handles / at end of input as literal", func() { + input := `{"key": "value"}/` + Expect(read(input)).To(Equal(input)) + }) + + It("handles * inside block comment not followed by /", func() { + input := `{/* a * b */"key": "value"}` + Expect(compact(read(input))).To(Equal(`{"key":"value"}`)) + }) + + It("handles empty input", func() { + Expect(read("")).To(Equal("")) + }) + + It("handles mixed comments with real content", func() { + input := `{ + // line comment + "name": "test", /* inline block */ + /* multi + line */ + "value": "hello // world", + "other": 123 // trailing + }` + Expect(compact(read(input))).To(Equal(compact(`{ + "name": "test", + "value": "hello // world", + "other": 123 + }`))) + }) + + It("handles consecutive slashes that are not comments", func() { + input := `{"path": "/a/b"}` + Expect(read(input)).To(Equal(input)) + }) + + It("handles block comment at end of input", func() { + input := `{"key": "value"}/* comment */` + Expect(compact(read(input))).To(Equal(`{"key":"value"}`)) + }) + + It("strips comment with windows-style line endings", func() { + input := "{\r\n// comment\r\n\"key\": \"value\"\r\n}" + Expect(compact(read(input))).To(Equal(compact(`{"key": "value"}`))) + }) + + It("strips line comments with mixed line endings", func() { + // From original library: // comments with both \n and \r\n, including multiple on same line + input := "{\n\"one\": 1, // test //\n\"two\": 2, //test //\r\n\"string\": \"value\"\n//test\n}" + expected := "{\n\"one\": 1, \n\"two\": 2, \r\n\"string\": \"value\"\n\n}" + Expect(read(input)).To(Equal(expected)) + }) + + It("strips line comment at start of JSON", func() { + // From original library: // comment as first thing in JSON + input := "{// woot\n\"one\": 1, // test //\n\"two\": 2, //test //\r\n\"string\": \"value\"\n//test\n}" + expected := "{\n\"one\": 1, \n\"two\": 2, \r\n\"string\": \"value\"\n\n}" + Expect(read(input)).To(Equal(expected)) + }) + + It("strips block comments with mixed line endings inside", func() { + // From original library: block comment containing \r\n + input := "{/* multi\nline\r\ncomment */\"one\":1}" + expected := "{\"one\":1}" + Expect(read(input)).To(Equal(expected)) + }) + + It("handles complex mix of escaped quotes, comments, and strings", func() { + // From original library TestQuotationEscape: escaped quote inside string followed by + // comment-like chars, then real comments of both types + input := "{/* multi\nline\r\ncomment */\"one\": \"a value \\\" // /*woot\"/* m\nl *///woot\r\n}" + expected := "{\"one\": \"a value \\\" // /*woot\"\r\n}" + Expect(read(input)).To(Equal(expected)) + }) +}) From ba3974ee59f2df336df44d39865e496496cb2c39 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sat, 14 Mar 2026 09:59:52 -0400 Subject: [PATCH 15/81] refactor(shellquote): replace go-shellquote with custom shell quoting implementation --- core/playback/mpv/mpv.go | 2 +- core/playback/mpv/mpv_test.go | 2 +- utils/shellquote/shellquote.go | 115 ++++++++++++++++ utils/shellquote/shellquote_test.go | 200 ++++++++++++++++++++++++++++ 4 files changed, 317 insertions(+), 2 deletions(-) create mode 100644 utils/shellquote/shellquote.go create mode 100644 utils/shellquote/shellquote_test.go diff --git a/core/playback/mpv/mpv.go b/core/playback/mpv/mpv.go index f356a1410..035e18dd5 100644 --- a/core/playback/mpv/mpv.go +++ b/core/playback/mpv/mpv.go @@ -10,9 +10,9 @@ import ( "strings" "sync" - "github.com/kballard/go-shellquote" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/utils/shellquote" ) func start(ctx context.Context, args []string) (Executor, error) { diff --git a/core/playback/mpv/mpv_test.go b/core/playback/mpv/mpv_test.go index 20c02501b..b1f2435a3 100644 --- a/core/playback/mpv/mpv_test.go +++ b/core/playback/mpv/mpv_test.go @@ -188,7 +188,7 @@ var _ = Describe("MPV", func() { It("returns empty slice for empty template", func() { args := createMPVCommand("auto", "/music/test.mp3", "/tmp/socket") - Expect(args).To(Equal([]string{})) + Expect(args).To(BeEmpty()) }) }) }) diff --git a/utils/shellquote/shellquote.go b/utils/shellquote/shellquote.go new file mode 100644 index 000000000..685e3c2a7 --- /dev/null +++ b/utils/shellquote/shellquote.go @@ -0,0 +1,115 @@ +package shellquote + +import ( + "errors" + "strings" +) + +var ( + ErrUnterminatedSingleQuote = errors.New("unterminated single-quoted string") + ErrUnterminatedDoubleQuote = errors.New("unterminated double-quoted string") + ErrUnterminatedEscape = errors.New("unterminated backslash-escape") +) + +type state int + +const ( + stateUnquoted state = iota + stateSingleQuoted + stateDoubleQuoted +) + +// Split splits a string into words following POSIX-like shell quoting rules. +// It handles single quotes, double quotes, and backslash escapes. +func Split(input string) ([]string, error) { + var words []string + var word strings.Builder + inWord := false + parseState := stateUnquoted + + i := 0 + for i < len(input) { + ch := input[i] + + switch parseState { + case stateUnquoted: + switch { + case ch == '\\': + if i+1 >= len(input) { + return nil, ErrUnterminatedEscape + } + if input[i+1] == '\n' { + // Line continuation: skip both backslash and newline + i += 2 + continue + } + i++ + word.WriteByte(input[i]) + inWord = true + case ch == '\'': + parseState = stateSingleQuoted + inWord = true + case ch == '"': + parseState = stateDoubleQuoted + inWord = true + case ch == ' ' || ch == '\t' || ch == '\n': + if inWord { + words = append(words, word.String()) + word.Reset() + inWord = false + } + default: + word.WriteByte(ch) + inWord = true + } + + case stateSingleQuoted: + if ch == '\'' { + parseState = stateUnquoted + } else { + word.WriteByte(ch) + } + + case stateDoubleQuoted: + switch { + case ch == '"': + parseState = stateUnquoted + case ch == '\\': + if i+1 >= len(input) { + return nil, ErrUnterminatedEscape + } + next := input[i+1] + // In double quotes, backslash only escapes: $ ` " \n \ + if next == '$' || next == '`' || next == '"' || next == '\n' || next == '\\' { + if next == '\n' { + // Line continuation: skip both backslash and newline + i += 2 + continue + } + i++ + word.WriteByte(next) + } else { + // Backslash is literal for other characters + word.WriteByte(ch) + } + default: + word.WriteByte(ch) + } + } + + i++ + } + + switch parseState { + case stateSingleQuoted: + return nil, ErrUnterminatedSingleQuote + case stateDoubleQuoted: + return nil, ErrUnterminatedDoubleQuote + } + + if inWord { + words = append(words, word.String()) + } + + return words, nil +} diff --git a/utils/shellquote/shellquote_test.go b/utils/shellquote/shellquote_test.go new file mode 100644 index 000000000..889b83a5f --- /dev/null +++ b/utils/shellquote/shellquote_test.go @@ -0,0 +1,200 @@ +package shellquote_test + +import ( + "testing" + + "github.com/navidrome/navidrome/utils/shellquote" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestShellquote(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Shellquote Suite") +} + +var _ = Describe("Split", func() { + It("splits simple space-separated words", func() { + words, err := shellquote.Split("a b c") + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(Equal([]string{"a", "b", "c"})) + }) + + It("handles multiple spaces between words", func() { + words, err := shellquote.Split("a b") + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(Equal([]string{"a", "b"})) + }) + + It("handles single-quoted strings", func() { + words, err := shellquote.Split("'hello world'") + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(Equal([]string{"hello world"})) + }) + + It("handles double-quoted strings", func() { + words, err := shellquote.Split(`"hello world"`) + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(Equal([]string{"hello world"})) + }) + + It("handles backslash escapes in unquoted mode", func() { + words, err := shellquote.Split(`hello\ world`) + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(Equal([]string{"hello world"})) + }) + + It("handles escaped quotes inside double quotes", func() { + words, err := shellquote.Split(`"hello \" world"`) + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(Equal([]string{`hello " world`})) + }) + + It("handles mixed quoting in a single argument", func() { + words, err := shellquote.Split("he'llo wo'rld") + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(Equal([]string{"hello world"})) + }) + + It("returns empty slice for empty input", func() { + words, err := shellquote.Split("") + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(BeEmpty()) + }) + + It("returns empty slice for whitespace-only input", func() { + words, err := shellquote.Split(" ") + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(BeEmpty()) + }) + + It("returns error for unterminated single quote", func() { + _, err := shellquote.Split("'hello") + Expect(err).To(MatchError(shellquote.ErrUnterminatedSingleQuote)) + }) + + It("returns error for unterminated double quote", func() { + _, err := shellquote.Split(`"hello`) + Expect(err).To(MatchError(shellquote.ErrUnterminatedDoubleQuote)) + }) + + It("returns error for unterminated escape", func() { + _, err := shellquote.Split(`hello\`) + Expect(err).To(MatchError(shellquote.ErrUnterminatedEscape)) + }) + + It("handles tabs and newlines as delimiters", func() { + words, err := shellquote.Split("a\tb\nc") + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(Equal([]string{"a", "b", "c"})) + }) + + It("parses the default MPV command template", func() { + words, err := shellquote.Split("mpv --audio-device=%d --no-audio-display --pause %f --input-ipc-server=%s") + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(HaveLen(6)) + Expect(words).To(Equal([]string{ + "mpv", + "--audio-device=%d", + "--no-audio-display", + "--pause", + "%f", + "--input-ipc-server=%s", + })) + }) + + It("preserves spaces in quoted paths", func() { + words, err := shellquote.Split(`--ao-pcm-file="/audio/my folder/file"`) + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(Equal([]string{`--ao-pcm-file=/audio/my folder/file`})) + }) + + It("handles backslash in double quotes for special chars", func() { + words, err := shellquote.Split(`"hello\\world"`) + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(Equal([]string{`hello\world`})) + }) + + It("preserves backslash in double quotes for non-special chars", func() { + words, err := shellquote.Split(`"hello\nworld"`) + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(Equal([]string{`hello\nworld`})) + }) + + It("handles escaped newline in double quotes", func() { + words, err := shellquote.Split("\"hello\\\nworld\"") + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(Equal([]string{"helloworld"})) + }) + + // Cases from original go-shellquote test suite + It("handles shell glob characters as literals", func() { + words, err := shellquote.Split("glob* test?") + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(Equal([]string{"glob*", "test?"})) + }) + + It("handles backslash-escaped special characters", func() { + words, err := shellquote.Split("don\\'t you know the dewey decimal system\\?") + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(Equal([]string{"don't", "you", "know", "the", "dewey", "decimal", "system?"})) + }) + + It("handles single-quote escape idiom", func() { + // Shell idiom: end single-quote, escaped literal quote, start single-quote again + words, err := shellquote.Split("'don'\\''t you know the dewey decimal system?'") + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(Equal([]string{"don't you know the dewey decimal system?"})) + }) + + It("handles empty string argument via quotes", func() { + words, err := shellquote.Split("one '' two") + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(Equal([]string{"one", "", "two"})) + }) + + It("handles backslash-newline joining words in unquoted mode", func() { + words, err := shellquote.Split("text with\\\na backslash-escaped newline") + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(Equal([]string{"text", "witha", "backslash-escaped", "newline"})) + }) + + It("handles quoted newline inside double quotes", func() { + words, err := shellquote.Split("text \"with\na\" quoted newline") + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(Equal([]string{"text", "with\na", "quoted", "newline"})) + }) + + It("handles complex double-quoted escapes with backslash-newline", func() { + words, err := shellquote.Split("\"quoted\\d\\\\\\\" text with\\\na backslash-escaped newline\"") + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(Equal([]string{"quoted\\d\\\" text witha backslash-escaped newline"})) + }) + + It("handles backslash-newline between words", func() { + words, err := shellquote.Split("text with an escaped \\\n newline in the middle") + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(Equal([]string{"text", "with", "an", "escaped", "newline", "in", "the", "middle"})) + }) + + It("handles double-quoted substring concatenation", func() { + words, err := shellquote.Split(`foo"bar"baz`) + Expect(err).ToNot(HaveOccurred()) + Expect(words).To(Equal([]string{"foobarbaz"})) + }) + + It("returns error for unterminated quote after escape idiom", func() { + _, err := shellquote.Split("'test'\\''ing") + Expect(err).To(MatchError(shellquote.ErrUnterminatedSingleQuote)) + }) + + It("returns error for unterminated double quote with single quote inside", func() { + _, err := shellquote.Split("\"foo'bar") + Expect(err).To(MatchError(shellquote.ErrUnterminatedDoubleQuote)) + }) + + It("returns error for unterminated escape with leading whitespace", func() { + _, err := shellquote.Split(" \\") + Expect(err).To(MatchError(shellquote.ErrUnterminatedEscape)) + }) +}) From c63346de040cfc26222b5decdba8f96b04e6472f Mon Sep 17 00:00:00 2001 From: Deluan Date: Sat, 14 Mar 2026 09:59:52 -0400 Subject: [PATCH 16/81] chore: run go mod tidy after dependency replacements --- go.mod | 5 ++--- go.sum | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 33333ca33..28fae78e3 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ replace go.senan.xyz/taglib => github.com/deluan/go-taglib v0.0.0-20260307161927 require ( github.com/Masterminds/squirrel v1.5.4 - github.com/RaveNoX/go-jsoncommentstrip v1.0.0 github.com/andybalholm/cascadia v1.3.3 github.com/bmatcuk/doublestar/v4 v4.10.0 github.com/bradleyjkemp/cupaloy/v2 v2.8.0 @@ -35,10 +34,8 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/jellydator/ttlcache/v3 v3.4.0 github.com/kardianos/service v1.2.4 - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kr/pretty v0.3.1 github.com/lestrrat-go/jwx/v3 v3.0.13 - github.com/maruel/natural v1.3.0 github.com/mattn/go-sqlite3 v1.14.34 github.com/microcosm-cc/bluemonday v1.0.27 github.com/mileusna/useragent v1.3.5 @@ -98,6 +95,7 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/ianlancetaylor/demangle v0.0.0-20251118225945-96ee0021ea0f // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect @@ -108,6 +106,7 @@ require ( github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc/v3 v3.0.4 // indirect github.com/lestrrat-go/option/v2 v2.0.0 // indirect + github.com/maruel/natural v1.3.0 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect diff --git a/go.sum b/go.sum index 30ca76e86..fd02f812e 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,6 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1 github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= -github.com/RaveNoX/go-jsoncommentstrip v1.0.0 h1:t527LHHE3HmiHrq74QMpNPZpGCIJzTx+apLkMKt4HC0= -github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/atombender/go-jsonschema v0.20.0 h1:AHg0LeI0HcjQ686ALwUNqVJjNRcSXpIR6U+wC2J0aFY= From 549b812633e079a1aa9c932041d7025739c593c0 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sat, 14 Mar 2026 20:36:57 -0400 Subject: [PATCH 17/81] fix(ui): prevent duplicate getCoverArt requests on artist page useMediaQuery defaults to false on the first render (SSR compat), causing MobileArtistDetails to briefly render on desktop. Its CSS background-image triggered a full-size image fetch before the component switched to DesktopArtistDetails, which fetched again. Pass noSsr: true so the media query evaluates synchronously, and cap the mobile background image to 800px. --- ui/src/artist/ArtistShow.jsx | 4 +++- ui/src/artist/MobileArtistDetails.jsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ui/src/artist/ArtistShow.jsx b/ui/src/artist/ArtistShow.jsx index ba8586d06..935b0bab7 100644 --- a/ui/src/artist/ArtistShow.jsx +++ b/ui/src/artist/ArtistShow.jsx @@ -50,7 +50,9 @@ const useStyles = makeStyles( const ArtistDetails = (props) => { const record = useRecordContext(props) - const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('sm')) + const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('sm'), { + noSsr: true, + }) const [artistInfo, setArtistInfo] = useState() const biography = artistInfo?.biography || record.biography diff --git a/ui/src/artist/MobileArtistDetails.jsx b/ui/src/artist/MobileArtistDetails.jsx index e8c044d66..4947a4634 100644 --- a/ui/src/artist/MobileArtistDetails.jsx +++ b/ui/src/artist/MobileArtistDetails.jsx @@ -83,7 +83,7 @@ const useStyles = makeStyles( ) const MobileArtistDetails = ({ artistInfo, biography, record }) => { - const img = subsonic.getCoverArtUrl(record) + const img = subsonic.getCoverArtUrl(record, 800) const [expanded, setExpanded] = useState(false) const classes = useStyles({ img, expanded }) const title = record.name From 197d357f02c53d0c74c3218c6dd7c59fd13a69b5 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sat, 14 Mar 2026 21:47:26 -0400 Subject: [PATCH 18/81] fix(ui): prevent mobile touch events from triggering playback after lightbox close Signed-off-by: Deluan --- ui/src/common/SongDatagrid.jsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ui/src/common/SongDatagrid.jsx b/ui/src/common/SongDatagrid.jsx index fbf2b9e31..d2c98bbe7 100644 --- a/ui/src/common/SongDatagrid.jsx +++ b/ui/src/common/SongDatagrid.jsx @@ -85,7 +85,14 @@ const DiscSubtitleRow = forwardRef( const classes = useStyles({ isDesktop }) const [imageError, setImageError] = useState(false) const [isLightboxOpen, setLightboxOpen] = useState(false) + const lightboxClosedAt = React.useRef(0) const handlePlaySubset = (discNumber) => () => { + // Ignore clicks shortly after the lightbox was closed to prevent + // mobile touch events from "falling through" the overlay and + // triggering playback. + if (Date.now() - lightboxClosedAt.current < 400) { + return + } onClick(discNumber) } @@ -112,7 +119,10 @@ const DiscSubtitleRow = forwardRef( [imageError], ) - const handleCloseLightbox = useCallback(() => setLightboxOpen(false), []) + const handleCloseLightbox = useCallback(() => { + lightboxClosedAt.current = Date.now() + setLightboxOpen(false) + }, []) const subtitle = record.discSubtitle ? record.discSubtitle From 6b8fcc37c68f83cf4d7c2b7798e6df079e669a3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Sun, 15 Mar 2026 00:12:58 -0400 Subject: [PATCH 19/81] fix(share): add ownership checks to Delete and Update (#5189) * test(share): add failing tests for Delete ownership checks * fix(share): add ownership check to Delete * test(share): add failing tests for Update ownership checks * fix(share): add ownership check to Update * refactor(share): extract checkOwnership helper with lightweight query - Deduplicate ownership check from Delete and Update into a single helper - Use a minimal single-column SELECT instead of Get (avoids loadMedia overhead) - Use positive bypass form (IsAdmin || invalidUserId) matching codebase convention * fix(share): convert model.ErrNotFound to rest.ErrNotFound in checkOwnership Ensure consistent 404 responses when a nonexistent share ID is passed to Delete or Update, by handling the conversion in checkOwnership rather than relying on the subsequent write operation. --- persistence/share_repository.go | 30 +++++++++- persistence/share_repository_test.go | 88 ++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/persistence/share_repository.go b/persistence/share_repository.go index 9343e3e77..415109640 100644 --- a/persistence/share_repository.go +++ b/persistence/share_repository.go @@ -30,7 +30,33 @@ func NewShareRepository(ctx context.Context, db dbx.Builder) model.ShareReposito return r } +// TODO: Ownership checks should be moved to the service layer (core/share.go) +func (r *shareRepository) checkOwnership(id string) error { + usr := loggedUser(r.ctx) + if usr.IsAdmin || usr.ID == invalidUserId { + return nil + } + sel := r.newSelect().Columns("user_id").Where(Eq{"id": id}) + var share struct { + UserID string `db:"user_id"` + } + err := r.queryOne(sel, &share) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return rest.ErrNotFound + } + return err + } + if share.UserID != usr.ID { + return rest.ErrPermissionDenied + } + return nil +} + func (r *shareRepository) Delete(id string) error { + if err := r.checkOwnership(id); err != nil { + return err + } err := r.delete(Eq{"id": id}) if errors.Is(err, model.ErrNotFound) { return rest.ErrNotFound @@ -140,7 +166,9 @@ func sortByIdPosition(mfs model.MediaFiles, ids []string) model.MediaFiles { func (r *shareRepository) Update(id string, entity any, cols ...string) error { s := entity.(*model.Share) - // TODO Validate record + if err := r.checkOwnership(id); err != nil { + return err + } s.ID = id s.UpdatedAt = time.Now() cols = append(cols, "updated_at") diff --git a/persistence/share_repository_test.go b/persistence/share_repository_test.go index 96fd0c2bc..6988f323f 100644 --- a/persistence/share_repository_test.go +++ b/persistence/share_repository_test.go @@ -4,6 +4,7 @@ import ( "context" "time" + "github.com/deluan/rest" "github.com/navidrome/navidrome/conf/configtest" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" @@ -130,4 +131,91 @@ var _ = Describe("ShareRepository", func() { Expect(share.Albums).To(BeEmpty()) }) }) + + Describe("Ownership Checks", func() { + var ownerUser = model.User{ID: "2222", UserName: "regular-user"} + var otherUser = model.User{ID: "3333", UserName: "third-user"} + + insertShare := func(shareID, userID string) { + _, err := GetDBXBuilder().NewQuery(` + INSERT INTO share (id, user_id, description, resource_type, resource_ids, created_at, updated_at) + VALUES ({:id}, {:user}, {:desc}, {:type}, {:ids}, {:created}, {:updated}) + `).Bind(map[string]any{ + "id": shareID, + "user": userID, + "desc": "Test Share", + "type": "media_file", + "ids": "1001", + "created": time.Now(), + "updated": time.Now(), + }).Execute() + Expect(err).ToNot(HaveOccurred()) + } + + Describe("Delete", func() { + It("allows a non-admin user to delete their own share", func() { + insertShare("own-share-del", ownerUser.ID) + ctx := request.WithUser(log.NewContext(context.TODO()), ownerUser) + repo := NewShareRepository(ctx, GetDBXBuilder()) + err := repo.(rest.Persistable).Delete("own-share-del") + Expect(err).ToNot(HaveOccurred()) + }) + + It("denies a non-admin user from deleting another user's share", func() { + insertShare("other-share-del", ownerUser.ID) + ctx := request.WithUser(log.NewContext(context.TODO()), otherUser) + repo := NewShareRepository(ctx, GetDBXBuilder()) + err := repo.(rest.Persistable).Delete("other-share-del") + Expect(err).To(Equal(rest.ErrPermissionDenied)) + }) + + It("allows an admin to delete any user's share", func() { + insertShare("admin-del-share", ownerUser.ID) + ctx := request.WithUser(log.NewContext(context.TODO()), adminUser) + repo := NewShareRepository(ctx, GetDBXBuilder()) + err := repo.(rest.Persistable).Delete("admin-del-share") + Expect(err).ToNot(HaveOccurred()) + }) + + It("allows headless context (no user) to delete a share", func() { + insertShare("headless-del-share", ownerUser.ID) + repo := NewShareRepository(context.Background(), GetDBXBuilder()) + err := repo.(rest.Persistable).Delete("headless-del-share") + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Describe("Update", func() { + It("allows a non-admin user to update their own share", func() { + insertShare("own-share-upd", ownerUser.ID) + ctx := request.WithUser(log.NewContext(context.TODO()), ownerUser) + repo := NewShareRepository(ctx, GetDBXBuilder()) + err := repo.(rest.Persistable).Update("own-share-upd", &model.Share{Description: "Updated"}, "description") + Expect(err).ToNot(HaveOccurred()) + }) + + It("denies a non-admin user from updating another user's share", func() { + insertShare("other-share-upd", ownerUser.ID) + ctx := request.WithUser(log.NewContext(context.TODO()), otherUser) + repo := NewShareRepository(ctx, GetDBXBuilder()) + err := repo.(rest.Persistable).Update("other-share-upd", &model.Share{Description: "Hacked"}, "description") + Expect(err).To(Equal(rest.ErrPermissionDenied)) + }) + + It("allows an admin to update any user's share", func() { + insertShare("admin-upd-share", ownerUser.ID) + ctx := request.WithUser(log.NewContext(context.TODO()), adminUser) + repo := NewShareRepository(ctx, GetDBXBuilder()) + err := repo.(rest.Persistable).Update("admin-upd-share", &model.Share{Description: "Admin Updated"}, "description") + Expect(err).ToNot(HaveOccurred()) + }) + + It("allows headless context (no user) to update a share", func() { + insertShare("headless-upd-share", ownerUser.ID) + repo := NewShareRepository(context.Background(), GetDBXBuilder()) + err := repo.(rest.Persistable).Update("headless-upd-share", &model.Share{Description: "Headless"}, "description") + Expect(err).ToNot(HaveOccurred()) + }) + }) + }) }) From 69e7d163fc00d8a723ac6f3072c6fdcbfffff1c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Sun, 15 Mar 2026 13:18:54 -0400 Subject: [PATCH 20/81] remove built-in Spotify integration (#5197) * refactor: remove built-in Spotify integration Remove the Spotify adapter and all related configuration, replacing the built-in integration with the plugin system. This deletes the adapters/spotify package, removes Spotify config options (ID/Secret), updates the default agents list from "deezer,lastfm,spotify" to "deezer,lastfm", and cleans up all references across configuration, metrics, logging, artwork caching, and documentation. Users with Spotify config options will now see a warning that the options are no longer available. * feat: add ListenBrainz to list of default agents Signed-off-by: Deluan --------- Signed-off-by: Deluan --- adapters/spotify/client.go | 116 --------------------- adapters/spotify/client_test.go | 131 ------------------------ adapters/spotify/responses.go | 30 ------ adapters/spotify/responses_test.go | 48 --------- adapters/spotify/spotify.go | 96 ----------------- adapters/spotify/spotify_suite_test.go | 17 --- cmd/root.go | 1 - cmd/wire_gen.go | 1 - conf/configuration.go | 32 ++++-- core/agents/README.md | 2 +- core/artwork/reader_artist.go | 2 +- core/external/provider_topsongs_test.go | 1 - core/metrics/insights.go | 1 - core/metrics/insights/data.go | 1 - log/log.go | 1 - plugins/examples/wikimedia/README.md | 2 +- plugins/manifest-schema.json | 4 +- plugins/manifest_gen.go | 4 +- plugins/manifest_test.go | 4 +- server/initial_setup.go | 6 -- server/nativeapi/config.go | 4 +- server/nativeapi/config_test.go | 8 -- 22 files changed, 32 insertions(+), 480 deletions(-) delete mode 100644 adapters/spotify/client.go delete mode 100644 adapters/spotify/client_test.go delete mode 100644 adapters/spotify/responses.go delete mode 100644 adapters/spotify/responses_test.go delete mode 100644 adapters/spotify/spotify.go delete mode 100644 adapters/spotify/spotify_suite_test.go diff --git a/adapters/spotify/client.go b/adapters/spotify/client.go deleted file mode 100644 index 975175930..000000000 --- a/adapters/spotify/client.go +++ /dev/null @@ -1,116 +0,0 @@ -package spotify - -import ( - "context" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "strings" - - "github.com/navidrome/navidrome/log" -) - -const apiBaseUrl = "https://api.spotify.com/v1/" - -var ( - ErrNotFound = errors.New("spotify: not found") -) - -type httpDoer interface { - Do(req *http.Request) (*http.Response, error) -} - -func newClient(id, secret string, hc httpDoer) *client { - return &client{id, secret, hc} -} - -type client struct { - id string - secret string - hc httpDoer -} - -func (c *client) searchArtists(ctx context.Context, name string, limit int) ([]Artist, error) { - token, err := c.authorize(ctx) - if err != nil { - return nil, err - } - - params := url.Values{} - params.Add("type", "artist") - params.Add("q", name) - params.Add("offset", "0") - params.Add("limit", strconv.Itoa(limit)) - req, _ := http.NewRequestWithContext(ctx, "GET", apiBaseUrl+"search", nil) - req.URL.RawQuery = params.Encode() - req.Header.Add("Authorization", "Bearer "+token) - - var results SearchResults - err = c.makeRequest(req, &results) - if err != nil { - return nil, err - } - - if len(results.Artists.Items) == 0 { - return nil, ErrNotFound - } - return results.Artists.Items, err -} - -func (c *client) authorize(ctx context.Context) (string, error) { - payload := url.Values{} - payload.Add("grant_type", "client_credentials") - - encodePayload := payload.Encode() - req, _ := http.NewRequestWithContext(ctx, "POST", "https://accounts.spotify.com/api/token", strings.NewReader(encodePayload)) - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Content-Length", strconv.Itoa(len(encodePayload))) - auth := c.id + ":" + c.secret - req.Header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth))) - - response := map[string]any{} - err := c.makeRequest(req, &response) - if err != nil { - return "", err - } - - if v, ok := response["access_token"]; ok { - return v.(string), nil - } - log.Error(ctx, "Invalid spotify response", "resp", response) - return "", errors.New("invalid response") -} - -func (c *client) makeRequest(req *http.Request, response any) error { - log.Trace(req.Context(), fmt.Sprintf("Sending Spotify %s request", req.Method), "url", req.URL) - resp, err := c.hc.Do(req) - if err != nil { - return err - } - - defer resp.Body.Close() - data, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - - if resp.StatusCode != 200 { - return c.parseError(data) - } - - return json.Unmarshal(data, response) -} - -func (c *client) parseError(data []byte) error { - var e Error - err := json.Unmarshal(data, &e) - if err != nil { - return err - } - return fmt.Errorf("spotify error(%s): %s", e.Code, e.Message) -} diff --git a/adapters/spotify/client_test.go b/adapters/spotify/client_test.go deleted file mode 100644 index 2782d2122..000000000 --- a/adapters/spotify/client_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package spotify - -import ( - "bytes" - "context" - "io" - "net/http" - "os" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("client", func() { - var httpClient *fakeHttpClient - var client *client - - BeforeEach(func() { - httpClient = &fakeHttpClient{} - client = newClient("SPOTIFY_ID", "SPOTIFY_SECRET", httpClient) - }) - - Describe("ArtistImages", func() { - It("returns artist images from a successful request", func() { - f, _ := os.Open("tests/fixtures/spotify.search.artist.json") - httpClient.mock("https://api.spotify.com/v1/search", http.Response{Body: f, StatusCode: 200}) - httpClient.mock("https://accounts.spotify.com/api/token", http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(`{"access_token": "NEW_ACCESS_TOKEN","token_type": "Bearer","expires_in": 3600}`)), - }) - - artists, err := client.searchArtists(context.TODO(), "U2", 10) - Expect(err).To(BeNil()) - Expect(artists).To(HaveLen(20)) - Expect(artists[0].Popularity).To(Equal(82)) - - images := artists[0].Images - Expect(images).To(HaveLen(3)) - Expect(images[0].Width).To(Equal(640)) - Expect(images[1].Width).To(Equal(320)) - Expect(images[2].Width).To(Equal(160)) - }) - - It("fails if artist was not found", func() { - httpClient.mock("https://api.spotify.com/v1/search", http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(`{ - "artists" : { - "href" : "https://api.spotify.com/v1/search?query=dasdasdas%2Cdna&type=artist&offset=0&limit=20", - "items" : [ ], "limit" : 20, "next" : null, "offset" : 0, "previous" : null, "total" : 0 - }}`)), - }) - httpClient.mock("https://accounts.spotify.com/api/token", http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(`{"access_token": "NEW_ACCESS_TOKEN","token_type": "Bearer","expires_in": 3600}`)), - }) - - _, err := client.searchArtists(context.TODO(), "U2", 10) - Expect(err).To(MatchError(ErrNotFound)) - }) - - It("fails if not able to authorize", func() { - f, _ := os.Open("tests/fixtures/spotify.search.artist.json") - httpClient.mock("https://api.spotify.com/v1/search", http.Response{Body: f, StatusCode: 200}) - httpClient.mock("https://accounts.spotify.com/api/token", http.Response{ - StatusCode: 400, - Body: io.NopCloser(bytes.NewBufferString(`{"error":"invalid_client","error_description":"Invalid client"}`)), - }) - - _, err := client.searchArtists(context.TODO(), "U2", 10) - Expect(err).To(MatchError("spotify error(invalid_client): Invalid client")) - }) - }) - - Describe("authorize", func() { - It("returns an access_token on successful authorization", func() { - httpClient.mock("https://accounts.spotify.com/api/token", http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(`{"access_token": "NEW_ACCESS_TOKEN","token_type": "Bearer","expires_in": 3600}`)), - }) - - token, err := client.authorize(context.TODO()) - Expect(err).To(BeNil()) - Expect(token).To(Equal("NEW_ACCESS_TOKEN")) - auth := httpClient.lastRequest.Header.Get("Authorization") - Expect(auth).To(Equal("Basic U1BPVElGWV9JRDpTUE9USUZZX1NFQ1JFVA==")) - }) - - It("fails on unsuccessful authorization", func() { - httpClient.mock("https://accounts.spotify.com/api/token", http.Response{ - StatusCode: 400, - Body: io.NopCloser(bytes.NewBufferString(`{"error":"invalid_client","error_description":"Invalid client"}`)), - }) - - _, err := client.authorize(context.TODO()) - Expect(err).To(MatchError("spotify error(invalid_client): Invalid client")) - }) - - It("fails on invalid JSON response", func() { - httpClient.mock("https://accounts.spotify.com/api/token", http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(`{NOT_VALID}`)), - }) - - _, err := client.authorize(context.TODO()) - Expect(err).To(MatchError("invalid character 'N' looking for beginning of object key string")) - }) - }) -}) - -type fakeHttpClient struct { - responses map[string]*http.Response - lastRequest *http.Request -} - -func (c *fakeHttpClient) mock(url string, response http.Response) { - if c.responses == nil { - c.responses = make(map[string]*http.Response) - } - c.responses[url] = &response -} - -func (c *fakeHttpClient) Do(req *http.Request) (*http.Response, error) { - c.lastRequest = req - u := req.URL - u.RawQuery = "" - if resp, ok := c.responses[u.String()]; ok { - return resp, nil - } - panic("URL not mocked: " + u.String()) -} diff --git a/adapters/spotify/responses.go b/adapters/spotify/responses.go deleted file mode 100644 index 21166bf74..000000000 --- a/adapters/spotify/responses.go +++ /dev/null @@ -1,30 +0,0 @@ -package spotify - -type SearchResults struct { - Artists ArtistsResult `json:"artists"` -} - -type ArtistsResult struct { - HRef string `json:"href"` - Items []Artist `json:"items"` -} - -type Artist struct { - Genres []string `json:"genres"` - HRef string `json:"href"` - ID string `json:"id"` - Popularity int `json:"popularity"` - Images []Image `json:"images"` - Name string `json:"name"` -} - -type Image struct { - URL string `json:"url"` - Width int `json:"width"` - Height int `json:"height"` -} - -type Error struct { - Code string `json:"error"` - Message string `json:"error_description"` -} diff --git a/adapters/spotify/responses_test.go b/adapters/spotify/responses_test.go deleted file mode 100644 index 704119816..000000000 --- a/adapters/spotify/responses_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package spotify - -import ( - "encoding/json" - "os" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("Responses", func() { - Describe("Search type=artist", func() { - It("parses the artist search result correctly ", func() { - var resp SearchResults - body, _ := os.ReadFile("tests/fixtures/spotify.search.artist.json") - err := json.Unmarshal(body, &resp) - Expect(err).To(BeNil()) - - Expect(resp.Artists.Items).To(HaveLen(20)) - u2 := resp.Artists.Items[0] - Expect(u2.Name).To(Equal("U2")) - Expect(u2.Genres).To(ContainElements("irish rock", "permanent wave", "rock")) - Expect(u2.ID).To(Equal("51Blml2LZPmy7TTiAg47vQ")) - Expect(u2.HRef).To(Equal("https://api.spotify.com/v1/artists/51Blml2LZPmy7TTiAg47vQ")) - Expect(u2.Images[0].URL).To(Equal("https://i.scdn.co/image/e22d5c0c8139b8439440a69854ed66efae91112d")) - Expect(u2.Images[0].Width).To(Equal(640)) - Expect(u2.Images[0].Height).To(Equal(640)) - Expect(u2.Images[1].URL).To(Equal("https://i.scdn.co/image/40d6c5c14355cfc127b70da221233315497ec91d")) - Expect(u2.Images[1].Width).To(Equal(320)) - Expect(u2.Images[1].Height).To(Equal(320)) - Expect(u2.Images[2].URL).To(Equal("https://i.scdn.co/image/7293d6752ae8a64e34adee5086858e408185b534")) - Expect(u2.Images[2].Width).To(Equal(160)) - Expect(u2.Images[2].Height).To(Equal(160)) - }) - }) - - Describe("Error", func() { - It("parses the error response correctly", func() { - var errorResp Error - body := []byte(`{"error":"invalid_client","error_description":"Invalid client"}`) - err := json.Unmarshal(body, &errorResp) - Expect(err).To(BeNil()) - - Expect(errorResp.Code).To(Equal("invalid_client")) - Expect(errorResp.Message).To(Equal("Invalid client")) - }) - }) -}) diff --git a/adapters/spotify/spotify.go b/adapters/spotify/spotify.go deleted file mode 100644 index 633c32984..000000000 --- a/adapters/spotify/spotify.go +++ /dev/null @@ -1,96 +0,0 @@ -package spotify - -import ( - "context" - "errors" - "fmt" - "net/http" - "sort" - "strings" - - "github.com/navidrome/navidrome/conf" - "github.com/navidrome/navidrome/consts" - "github.com/navidrome/navidrome/core/agents" - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/model" - "github.com/navidrome/navidrome/utils/cache" - "github.com/xrash/smetrics" -) - -const spotifyAgentName = "spotify" - -type spotifyAgent struct { - ds model.DataStore - id string - secret string - client *client -} - -func spotifyConstructor(ds model.DataStore) agents.Interface { - if conf.Server.Spotify.ID == "" || conf.Server.Spotify.Secret == "" { - return nil - } - l := &spotifyAgent{ - ds: ds, - id: conf.Server.Spotify.ID, - secret: conf.Server.Spotify.Secret, - } - hc := &http.Client{ - Timeout: consts.DefaultHttpClientTimeOut, - } - chc := cache.NewHTTPClient(hc, consts.DefaultHttpClientTimeOut) - l.client = newClient(l.id, l.secret, chc) - return l -} - -func (s *spotifyAgent) AgentName() string { - return spotifyAgentName -} - -func (s *spotifyAgent) GetArtistImages(ctx context.Context, id, name, mbid string) ([]agents.ExternalImage, error) { - a, err := s.searchArtist(ctx, name) - if err != nil { - if errors.Is(err, model.ErrNotFound) { - log.Warn(ctx, "Artist not found in Spotify", "artist", name) - } else { - log.Error(ctx, "Error calling Spotify", "artist", name, err) - } - return nil, err - } - - var res []agents.ExternalImage - for _, img := range a.Images { - res = append(res, agents.ExternalImage{ - URL: img.URL, - Size: img.Width, - }) - } - return res, nil -} - -func (s *spotifyAgent) searchArtist(ctx context.Context, name string) (*Artist, error) { - artists, err := s.client.searchArtists(ctx, name, 40) - if err != nil || len(artists) == 0 { - return nil, model.ErrNotFound - } - name = strings.ToLower(name) - - // Sort results, prioritizing artists with images, with similar names and with high popularity, in this order - sort.Slice(artists, func(i, j int) bool { - ai := fmt.Sprintf("%-5t-%03d-%04d", len(artists[i].Images) == 0, smetrics.WagnerFischer(name, strings.ToLower(artists[i].Name), 1, 1, 2), 1000-artists[i].Popularity) - aj := fmt.Sprintf("%-5t-%03d-%04d", len(artists[j].Images) == 0, smetrics.WagnerFischer(name, strings.ToLower(artists[j].Name), 1, 1, 2), 1000-artists[j].Popularity) - return ai < aj - }) - - // If the first one has the same name, that's the one - if strings.ToLower(artists[0].Name) != name { - return nil, model.ErrNotFound - } - return &artists[0], err -} - -func init() { - conf.AddHook(func() { - agents.Register(spotifyAgentName, spotifyConstructor) - }) -} diff --git a/adapters/spotify/spotify_suite_test.go b/adapters/spotify/spotify_suite_test.go deleted file mode 100644 index 275b05e73..000000000 --- a/adapters/spotify/spotify_suite_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package spotify - -import ( - "testing" - - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/tests" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestSpotify(t *testing.T) { - tests.Init(t, false) - log.SetLevel(log.LevelFatal) - RegisterFailHandler(Fail) - RunSpecs(t, "Spotify Test Suite") -} diff --git a/cmd/root.go b/cmd/root.go index ff9a574ee..5fdb591ff 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -27,7 +27,6 @@ import ( _ "github.com/navidrome/navidrome/adapters/gotaglib" _ "github.com/navidrome/navidrome/adapters/lastfm" _ "github.com/navidrome/navidrome/adapters/listenbrainz" - _ "github.com/navidrome/navidrome/adapters/spotify" _ "github.com/navidrome/navidrome/adapters/taglib" ) diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index d045c12ec..c2f6edaf8 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -39,7 +39,6 @@ import ( _ "github.com/navidrome/navidrome/adapters/gotaglib" _ "github.com/navidrome/navidrome/adapters/lastfm" _ "github.com/navidrome/navidrome/adapters/listenbrainz" - _ "github.com/navidrome/navidrome/adapters/spotify" _ "github.com/navidrome/navidrome/adapters/taglib" ) diff --git a/conf/configuration.go b/conf/configuration.go index dc5fd1b59..986ab7a5a 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -105,7 +105,6 @@ type configOptions struct { Inspect inspectOptions `json:",omitzero"` Subsonic subsonicOptions `json:",omitzero"` LastFM lastfmOptions `json:",omitzero"` - Spotify spotifyOptions `json:",omitzero"` Deezer deezerOptions `json:",omitzero"` ListenBrainz listenBrainzOptions `json:",omitzero"` EnableScrobbleHistory bool @@ -187,11 +186,6 @@ type lastfmOptions struct { Languages []string // Computed from Language, split by comma } -type spotifyOptions struct { - ID string - Secret string //nolint:gosec -} - type deezerOptions struct { Enabled bool Language string @@ -411,6 +405,7 @@ func Load(noConfigDump bool) { // Parse Deezer.Language into Languages slice (comma-separated, with fallback to DefaultInfoLanguage) Server.Deezer.Languages = parseLanguages(Server.Deezer.Language) + // Deprecated options logDeprecatedOptions("Scanner.GenreSeparators", "") logDeprecatedOptions("Scanner.GroupAlbumReleases", "") logDeprecatedOptions("DevEnableBufferedScrobble", "") // Deprecated: Buffered scrobbling is now always enabled and this option is ignored @@ -420,6 +415,9 @@ func Load(noConfigDump bool) { logDeprecatedOptions("HTTPSecurityHeaders.CustomFrameOptionsValue", "HTTPHeaders.FrameOptions") logDeprecatedOptions("CoverJpegQuality", "CoverArtQuality") + // Removed options + logRemovedOptions("Spotify.ID", "Spotify.Secret") + // Call init hooks for _, hook := range hooks { hook() @@ -444,6 +442,23 @@ func logDeprecatedOptions(oldName, newName string) { } } +// logRemovedOptions checks if the option is set, and if yes, outputs a warning message saying the option is +// not available anymore +func logRemovedOptions(options ...string) { + for _, option := range options { + envVar := "ND_" + strings.ToUpper(strings.ReplaceAll(option, ".", "_")) + logWarning := func(option string) { + log.Warn(fmt.Sprintf("Option '%s' is not available anymore and will be ignored. Please remove it from your config", option)) + } + if viper.InConfig(option) { + logWarning(option) + } + if os.Getenv(envVar) != "" { + logWarning(envVar) + } + } +} + // mapDeprecatedOption is used to provide backwards compatibility for deprecated options. It should be called after // the config has been read by viper, but before unmarshalling it into the Config struct. func mapDeprecatedOption(legacyName, newName string) { @@ -482,7 +497,6 @@ func disableExternalServices() { Server.EnableInsightsCollector = false Server.EnableM3UExternalAlbumArt = false Server.LastFM.Enabled = false - Server.Spotify.ID = "" Server.Deezer.Enabled = false Server.ListenBrainz.Enabled = false Server.Agents = "" @@ -712,14 +726,12 @@ func setViperDefaults() { viper.SetDefault("subsonic.enableaveragerating", true) viper.SetDefault("subsonic.legacyclients", "DSub") viper.SetDefault("subsonic.minimalclients", "SubMusic") - viper.SetDefault("agents", "deezer,lastfm,spotify") + viper.SetDefault("agents", "deezer,lastfm,listenbrainz") viper.SetDefault("lastfm.enabled", true) viper.SetDefault("lastfm.language", consts.DefaultInfoLanguage) viper.SetDefault("lastfm.apikey", "") viper.SetDefault("lastfm.secret", "") viper.SetDefault("lastfm.scrobblefirstartistonly", false) - viper.SetDefault("spotify.id", "") - viper.SetDefault("spotify.secret", "") viper.SetDefault("deezer.enabled", true) viper.SetDefault("deezer.language", consts.DefaultInfoLanguage) viper.SetDefault("listenbrainz.enabled", true) diff --git a/core/agents/README.md b/core/agents/README.md index 1a3a8e96e..cce62889c 100644 --- a/core/agents/README.md +++ b/core/agents/README.md @@ -7,6 +7,6 @@ A new agent must comply with these simple implementation rules: 2) Implement one or more of the `*Retriever()` interfaces. That's where the agent's logic resides. 3) Register itself (in its `init()` function). -For an agent to be used it needs to be listed in the `Agents` config option (default is `"lastfm,spotify"`). The order dictates the priority of the agents +For an agent to be used it needs to be listed in the `Agents` config option (default is `"deezer,lastfm"`). The order dictates the priority of the agents For a simple Agent example, look at the [local_agent](local_agent.go) agent source code. diff --git a/core/artwork/reader_artist.go b/core/artwork/reader_artist.go index 9905039be..990942d87 100644 --- a/core/artwork/reader_artist.go +++ b/core/artwork/reader_artist.go @@ -79,7 +79,7 @@ func newArtistArtworkReader(ctx context.Context, artwork *artwork, artID model.A } func (a *artistReader) Key() string { - hash := md5.Sum([]byte(conf.Server.Agents + conf.Server.Spotify.ID)) + hash := md5.Sum([]byte(conf.Server.Agents)) return fmt.Sprintf( "%s.%t.%x", a.cacheKey.Key(), diff --git a/core/external/provider_topsongs_test.go b/core/external/provider_topsongs_test.go index 0e513aa37..b73c8ab3e 100644 --- a/core/external/provider_topsongs_test.go +++ b/core/external/provider_topsongs_test.go @@ -6,7 +6,6 @@ import ( _ "github.com/navidrome/navidrome/adapters/lastfm" _ "github.com/navidrome/navidrome/adapters/listenbrainz" - _ "github.com/navidrome/navidrome/adapters/spotify" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf/configtest" "github.com/navidrome/navidrome/core/agents" diff --git a/core/metrics/insights.go b/core/metrics/insights.go index f059d739a..5f3f491ea 100644 --- a/core/metrics/insights.go +++ b/core/metrics/insights.go @@ -199,7 +199,6 @@ var staticData = sync.OnceValue(func() insights.Data { data.Config.EnableSharing = conf.Server.EnableSharing data.Config.EnableStarRating = conf.Server.EnableStarRating data.Config.EnableLastFM = conf.Server.LastFM.Enabled && conf.Server.LastFM.ApiKey != "" && conf.Server.LastFM.Secret != "" - data.Config.EnableSpotify = conf.Server.Spotify.ID != "" && conf.Server.Spotify.Secret != "" data.Config.EnableListenBrainz = conf.Server.ListenBrainz.Enabled data.Config.EnableDeezer = conf.Server.Deezer.Enabled data.Config.EnableMediaFileCoverArt = conf.Server.EnableMediaFileCoverArt diff --git a/core/metrics/insights/data.go b/core/metrics/insights/data.go index 5580d895d..27186e020 100644 --- a/core/metrics/insights/data.go +++ b/core/metrics/insights/data.go @@ -61,7 +61,6 @@ type Data struct { EnableListenBrainz bool `json:"enableListenBrainz,omitempty"` EnableDeezer bool `json:"enableDeezer,omitempty"` EnableMediaFileCoverArt bool `json:"enableMediaFileCoverArt,omitempty"` - EnableSpotify bool `json:"enableSpotify,omitempty"` EnableJukebox bool `json:"enableJukebox,omitempty"` EnablePrometheus bool `json:"enablePrometheus,omitempty"` EnableCoverAnimation bool `json:"enableCoverAnimation,omitempty"` diff --git a/log/log.go b/log/log.go index 7d294792b..ef38484c8 100644 --- a/log/log.go +++ b/log/log.go @@ -27,7 +27,6 @@ var redacted = &Hook{ // Keys from the config "(ApiKey:\")[\\w]*", "(Secret:\")[\\w]*", - "(Spotify.*ID:\")[\\w]*", "(PasswordEncryptionKey:[\\s]*\")[^\"]*", "(UserHeader:[\\s]*\")[^\"]*", "(TrustedSources:[\\s]*\")[^\"]*", diff --git a/plugins/examples/wikimedia/README.md b/plugins/examples/wikimedia/README.md index e4833cff6..181055c69 100644 --- a/plugins/examples/wikimedia/README.md +++ b/plugins/examples/wikimedia/README.md @@ -63,7 +63,7 @@ Folder = "/path/to/navidrome/plugins" Add the plugin to your agents list: ```toml -Agents = "lastfm,spotify,wikimedia" +Agents = "lastfm,wikimedia" ``` ## Testing with Extism CLI diff --git a/plugins/manifest-schema.json b/plugins/manifest-schema.json index 8daf88ccf..c15a3bf3d 100644 --- a/plugins/manifest-schema.json +++ b/plugins/manifest-schema.json @@ -149,7 +149,7 @@ }, "requiredHosts": { "type": "array", - "description": "List of required host patterns for HTTP requests (e.g., 'api.example.com', '*.spotify.com')", + "description": "List of required host patterns for HTTP requests (e.g., 'api.example.com', '*.musicbrainz.org')", "items": { "type": "string" } @@ -189,7 +189,7 @@ }, "requiredHosts": { "type": "array", - "description": "List of required host patterns for WebSocket connections (e.g., 'api.example.com', '*.spotify.com')", + "description": "List of required host patterns for WebSocket connections (e.g., 'api.example.com', '*.musicbrainz.org')", "items": { "type": "string" } diff --git a/plugins/manifest_gen.go b/plugins/manifest_gen.go index a565ed3d1..efe93e05f 100644 --- a/plugins/manifest_gen.go +++ b/plugins/manifest_gen.go @@ -57,7 +57,7 @@ type HTTPPermission struct { Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"` // List of required host patterns for HTTP requests (e.g., 'api.example.com', - // '*.spotify.com') + // '*.musicbrainz.org') RequiredHosts []string `json:"requiredHosts,omitempty" yaml:"requiredHosts,omitempty" mapstructure:"requiredHosts,omitempty"` } @@ -251,6 +251,6 @@ type WebSocketPermission struct { Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"` // List of required host patterns for WebSocket connections (e.g., - // 'api.example.com', '*.spotify.com') + // 'api.example.com', '*.musicbrainz.org') RequiredHosts []string `json:"requiredHosts,omitempty" yaml:"requiredHosts,omitempty" mapstructure:"requiredHosts,omitempty"` } diff --git a/plugins/manifest_test.go b/plugins/manifest_test.go index de15324ab..c45a480eb 100644 --- a/plugins/manifest_test.go +++ b/plugins/manifest_test.go @@ -19,7 +19,7 @@ var _ = Describe("Manifest", func() { "permissions": { "http": { "reason": "Fetch metadata", - "requiredHosts": ["api.example.com", "*.spotify.com"] + "requiredHosts": ["api.example.com", "*.musicbrainz.org"] } } }`) @@ -34,7 +34,7 @@ var _ = Describe("Manifest", func() { Expect(*m.Website).To(Equal("https://example.com")) Expect(m.Permissions.Http).ToNot(BeNil()) Expect(*m.Permissions.Http.Reason).To(Equal("Fetch metadata")) - Expect(m.Permissions.Http.RequiredHosts).To(ContainElements("api.example.com", "*.spotify.com")) + Expect(m.Permissions.Http.RequiredHosts).To(ContainElements("api.example.com", "*.musicbrainz.org")) }) It("parses a minimal manifest", func() { diff --git a/server/initial_setup.go b/server/initial_setup.go index ebfdad47a..d50f25958 100644 --- a/server/initial_setup.go +++ b/server/initial_setup.go @@ -91,11 +91,5 @@ func checkExternalCredentials() { } else { log.Debug("ListenBrainz integration is ENABLED", "ListenBrainz.BaseURL", conf.Server.ListenBrainz.BaseURL) } - - if conf.Server.Spotify.ID == "" || conf.Server.Spotify.Secret == "" { - log.Info("Spotify integration is not enabled: missing ID/Secret") - } else { - log.Debug("Spotify integration is ENABLED") - } } } diff --git a/server/nativeapi/config.go b/server/nativeapi/config.go index 086e8d3c1..02626a4ee 100644 --- a/server/nativeapi/config.go +++ b/server/nativeapi/config.go @@ -16,13 +16,11 @@ import ( // using partial masking (first and last character visible, middle replaced with *). // For values with 7+ characters: "secretvalue123" becomes "s***********3" // For values with <7 characters: "short" becomes "****" -// Add field paths using dot notation (e.g., "LastFM.ApiKey", "Spotify.Secret") +// Add field paths using dot notation (e.g., "LastFM.ApiKey") var sensitiveFieldsPartialMask = []string{ "LastFM.ApiKey", "LastFM.Secret", "Prometheus.MetricsPath", - "Spotify.ID", - "Spotify.Secret", "DevAutoLoginUsername", } diff --git a/server/nativeapi/config_test.go b/server/nativeapi/config_test.go index 546dd4f12..d7368cabf 100644 --- a/server/nativeapi/config_test.go +++ b/server/nativeapi/config_test.go @@ -78,7 +78,6 @@ var _ = Describe("Config API", func() { It("redacts sensitive fields", func() { conf.Server.LastFM.ApiKey = "secretapikey123" - conf.Server.Spotify.Secret = "spotifysecret456" conf.Server.PasswordEncryptionKey = "encryptionkey789" conf.Server.DevAutoCreateAdminPassword = "adminpassword123" conf.Server.Prometheus.Password = "prometheuspass" @@ -97,11 +96,6 @@ var _ = Describe("Config API", func() { Expect(ok).To(BeTrue()) Expect(lastfm["ApiKey"]).To(Equal("s*************3")) - // Check Spotify.Secret (partially masked) - spotify, ok := resp.Config["Spotify"].(map[string]any) - Expect(ok).To(BeTrue()) - Expect(spotify["Secret"]).To(Equal("s**************6")) - // Check PasswordEncryptionKey (fully masked) Expect(resp.Config["PasswordEncryptionKey"]).To(Equal("****")) @@ -172,7 +166,6 @@ var _ = Describe("Config API", func() { var _ = Describe("redactValue function", func() { It("partially masks long sensitive values", func() { Expect(redactValue("LastFM.ApiKey", "ba46f0e84a")).To(Equal("b********a")) - Expect(redactValue("Spotify.Secret", "verylongsecret123")).To(Equal("v***************3")) }) It("fully masks long sensitive values that should be completely hidden", func() { @@ -183,7 +176,6 @@ var _ = Describe("redactValue function", func() { It("fully masks short sensitive values", func() { Expect(redactValue("LastFM.Secret", "short")).To(Equal("****")) - Expect(redactValue("Spotify.ID", "abc")).To(Equal("****")) Expect(redactValue("PasswordEncryptionKey", "12345")).To(Equal("****")) Expect(redactValue("DevAutoCreateAdminPassword", "short")).To(Equal("****")) Expect(redactValue("Prometheus.Password", "short")).To(Equal("****")) From a887521d7a27ec626c04c557e9591a9f2e3819d1 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sun, 15 Mar 2026 13:36:26 -0400 Subject: [PATCH 21/81] fix(subsonic): always include mandatory title field in Child responses Removed `omitempty` from the `Title` struct tag in the `Child` response type. The Subsonic/OpenSubsonic API spec requires `title` to be a mandatory field, but songs with empty titles caused the field to be omitted entirely, crashing clients like Symfonium during sync. Ref: https://support.symfonium.app/t/app-gets-stuck-on-syncing-large-database/13004/8 --- server/subsonic/helpers_test.go | 8 ++++++++ .../Responses AlbumList with OS data should match .JSON | 1 + .../Responses AlbumList with OS data should match .XML | 2 +- .../Responses Child with data should match .JSON | 1 + .../Responses Child with data should match .XML | 2 +- .../Responses Child without data should match .JSON | 3 ++- .../Responses Child without data should match .XML | 2 +- ...ses Child without data should match OpenSubsonic .JSON | 1 + ...nses Child without data should match OpenSubsonic .XML | 2 +- server/subsonic/responses/responses.go | 2 +- 10 files changed, 18 insertions(+), 6 deletions(-) diff --git a/server/subsonic/helpers_test.go b/server/subsonic/helpers_test.go index d20ca89ad..4eb756b98 100644 --- a/server/subsonic/helpers_test.go +++ b/server/subsonic/helpers_test.go @@ -309,6 +309,14 @@ var _ = Describe("helpers", func() { Expect(child.Artist).To(Equal("Test Artist")) }) }) + + Context("when MediaFile has an empty title", func() { + It("still includes the title field in the response", func() { + mf.Title = "" + child := childFromMediaFile(ctx, mf) + Expect(child.Title).To(Equal("")) + }) + }) }) Describe("osChildFromMediaFile", func() { diff --git a/server/subsonic/responses/.snapshots/Responses AlbumList with OS data should match .JSON b/server/subsonic/responses/.snapshots/Responses AlbumList with OS data should match .JSON index a50603bf9..8491a577b 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumList with OS data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses AlbumList with OS data should match .JSON @@ -9,6 +9,7 @@ { "id": "1", "isDir": false, + "title": "", "bpm": 0, "comment": "", "sortName": "sort name", diff --git a/server/subsonic/responses/.snapshots/Responses AlbumList with OS data should match .XML b/server/subsonic/responses/.snapshots/Responses AlbumList with OS data should match .XML index 45b7033f3..5d9e83f96 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumList with OS data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses AlbumList with OS data should match .XML @@ -1,6 +1,6 @@ - + mood1 diff --git a/server/subsonic/responses/.snapshots/Responses Child with data should match .JSON b/server/subsonic/responses/.snapshots/Responses Child with data should match .JSON index 448a84d6a..d20a6d48c 100644 --- a/server/subsonic/responses/.snapshots/Responses Child with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Child with data should match .JSON @@ -115,6 +115,7 @@ { "id": "", "isDir": false, + "title": "", "bpm": 0, "comment": "", "sortName": "", diff --git a/server/subsonic/responses/.snapshots/Responses Child with data should match .XML b/server/subsonic/responses/.snapshots/Responses Child with data should match .XML index 2de1efbfb..1d307b0b9 100644 --- a/server/subsonic/responses/.snapshots/Responses Child with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Child with data should match .XML @@ -25,7 +25,7 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses Child without data should match .JSON b/server/subsonic/responses/.snapshots/Responses Child without data should match .JSON index 2d0995831..b66a2bdea 100644 --- a/server/subsonic/responses/.snapshots/Responses Child without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Child without data should match .JSON @@ -8,7 +8,8 @@ "child": [ { "id": "1", - "isDir": false + "isDir": false, + "title": "" } ], "id": "", diff --git a/server/subsonic/responses/.snapshots/Responses Child without data should match .XML b/server/subsonic/responses/.snapshots/Responses Child without data should match .XML index 3e5a1cf13..d64d526d6 100644 --- a/server/subsonic/responses/.snapshots/Responses Child without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Child without data should match .XML @@ -1,5 +1,5 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses Child without data should match OpenSubsonic .JSON b/server/subsonic/responses/.snapshots/Responses Child without data should match OpenSubsonic .JSON index c3ccc6cd0..25284295e 100644 --- a/server/subsonic/responses/.snapshots/Responses Child without data should match OpenSubsonic .JSON +++ b/server/subsonic/responses/.snapshots/Responses Child without data should match OpenSubsonic .JSON @@ -9,6 +9,7 @@ { "id": "1", "isDir": false, + "title": "", "bpm": 0, "comment": "", "sortName": "", diff --git a/server/subsonic/responses/.snapshots/Responses Child without data should match OpenSubsonic .XML b/server/subsonic/responses/.snapshots/Responses Child without data should match OpenSubsonic .XML index 3e5a1cf13..d64d526d6 100644 --- a/server/subsonic/responses/.snapshots/Responses Child without data should match OpenSubsonic .XML +++ b/server/subsonic/responses/.snapshots/Responses Child without data should match OpenSubsonic .XML @@ -1,5 +1,5 @@ - + diff --git a/server/subsonic/responses/responses.go b/server/subsonic/responses/responses.go index c550ce61e..b70f3b128 100644 --- a/server/subsonic/responses/responses.go +++ b/server/subsonic/responses/responses.go @@ -135,7 +135,7 @@ type Child struct { Id string `xml:"id,attr" json:"id"` Parent string `xml:"parent,attr,omitempty" json:"parent,omitempty"` IsDir bool `xml:"isDir,attr" json:"isDir"` - Title string `xml:"title,attr,omitempty" json:"title,omitempty"` + Title string `xml:"title,attr" json:"title"` Name string `xml:"name,attr,omitempty" json:"name,omitempty"` Album string `xml:"album,attr,omitempty" json:"album,omitempty"` Artist string `xml:"artist,attr,omitempty" json:"artist,omitempty"` From c42570446b80d019840c59ba962da28c781cc76d Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 15 Mar 2026 14:00:21 -0400 Subject: [PATCH 22/81] fix(ui): allow DefaultTheme "Auto" from config (#5190) * fix(ui): allow DefaultTheme "Auto" from config When DefaultTheme is set to "Auto" in the server config, the defaultTheme() function in themeReducer now returns AUTO_THEME_ID instead of falling through to the DarkTheme fallback. This allows useCurrentTheme to correctly read prefers-color-scheme and select Light or Dark theme automatically for new/incognito users. Adds themeReducer unit tests covering Auto, named-theme, and unrecognized-value fallback paths. * chore: format Signed-off-by: Deluan --------- Signed-off-by: Deluan Co-authored-by: Deluan --- ui/src/consts.js | 2 ++ ui/src/reducers/themeReducer.js | 4 ++++ ui/src/reducers/themeReducer.test.js | 32 ++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 ui/src/reducers/themeReducer.test.js diff --git a/ui/src/consts.js b/ui/src/consts.js index e3446c2fe..30731a080 100644 --- a/ui/src/consts.js +++ b/ui/src/consts.js @@ -7,6 +7,8 @@ export const M3U_MIME_TYPE = 'audio/x-mpegurl' export const AUTO_THEME_ID = 'AUTO_THEME_ID' +export const AUTO_THEME_CONFIG_VALUE = 'Auto' + export const DraggableTypes = { SONG: 'song', ALBUM: 'album', diff --git a/ui/src/reducers/themeReducer.js b/ui/src/reducers/themeReducer.js index 2a5d5bac6..16d5fa87b 100644 --- a/ui/src/reducers/themeReducer.js +++ b/ui/src/reducers/themeReducer.js @@ -1,8 +1,12 @@ import { CHANGE_THEME } from '../actions' +import { AUTO_THEME_ID, AUTO_THEME_CONFIG_VALUE } from '../consts' import config from '../config' import themes from '../themes' const defaultTheme = () => { + if (config.defaultTheme === AUTO_THEME_CONFIG_VALUE) { + return AUTO_THEME_ID + } return ( Object.keys(themes).find( (t) => themes[t].themeName === config.defaultTheme, diff --git a/ui/src/reducers/themeReducer.test.js b/ui/src/reducers/themeReducer.test.js new file mode 100644 index 000000000..2a66ea851 --- /dev/null +++ b/ui/src/reducers/themeReducer.test.js @@ -0,0 +1,32 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { AUTO_THEME_ID, AUTO_THEME_CONFIG_VALUE } from '../consts' + +describe('themeReducer', () => { + beforeEach(() => { + vi.resetModules() + }) + + it.each([ + { + configTheme: AUTO_THEME_CONFIG_VALUE, + expected: AUTO_THEME_ID, + description: 'is "Auto"', + }, + { configTheme: 'Dark', expected: 'DarkTheme', description: 'is "Dark"' }, + { + configTheme: 'NonExistent', + expected: 'DarkTheme', + description: 'is unrecognized', + }, + ])( + 'returns $expected when defaultTheme config $description', + async ({ configTheme, expected }) => { + vi.doMock('../config', () => ({ + default: { defaultTheme: configTheme }, + })) + const { themeReducer } = await import('./themeReducer') + const result = themeReducer(undefined, { type: 'UNKNOWN' }) + expect(result).toBe(expected) + }, + ) +}) From aa9391199182bb28fa4ffa222ec2a5fa02b77ee9 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 15 Mar 2026 14:14:05 -0400 Subject: [PATCH 23/81] feat(server): add syslog priority prefixes for systemd-journald (#5192) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: add syslog priority prefixes for systemd-journald When running under systemd, all log messages were assigned priority 3 (error) by journald because navidrome wrote plain text to stderr without syslog priority prefixes. Add a journalFormatter that wraps the existing logrus formatter and prepends syslog priority prefixes (RFC 5424) to each log line. The formatter is automatically enabled when the JOURNAL_STREAM environment variable is set (indicating the process is managed by systemd). Priority mapping: - Fatal/Panic → <2>/<0> (crit/emerg) - Error → <3> (err) - Warn → <4> (warning) - Info → <6> (info) - Debug/Trace → <7> (debug) Fixes #5142 * test: refactor journalFormatter tests to use Ginkgo and DescribeTable Signed-off-by: Deluan --------- Signed-off-by: Deluan Co-authored-by: Deluan --- conf/configuration.go | 4 ++++ log/journal.go | 41 +++++++++++++++++++++++++++++++++++++++++ log/journal_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ log/log.go | 9 +++++++++ 4 files changed, 95 insertions(+) create mode 100644 log/journal.go create mode 100644 log/journal_test.go diff --git a/conf/configuration.go b/conf/configuration.go index 986ab7a5a..dacd42539 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -340,6 +340,10 @@ func Load(noConfigDump bool) { os.Exit(1) } log.SetOutput(out) + } else if os.Getenv("JOURNAL_STREAM") != "" { + // When running under systemd, prepend syslog priority prefixes so + // journald assigns the correct severity to each log line. + log.EnableJournalFormat() } log.SetLevelString(Server.LogLevel) diff --git a/log/journal.go b/log/journal.go new file mode 100644 index 000000000..f1c17d2e7 --- /dev/null +++ b/log/journal.go @@ -0,0 +1,41 @@ +package log + +import ( + "fmt" + + "github.com/sirupsen/logrus" +) + +// journalFormatter wraps a logrus.Formatter and prepends a syslog priority +// prefix () to each log line. When stderr is captured by systemd-journald, +// this prefix tells journald the correct severity for each message. +// +// See https://www.freedesktop.org/software/systemd/man/sd-daemon.html +type journalFormatter struct { + inner logrus.Formatter +} + +// levelToPriority maps logrus levels to syslog priority values. +// The mapping follows RFC 5424 severity levels. +var levelToPriority = map[logrus.Level]int{ + logrus.PanicLevel: 0, // emerg + logrus.FatalLevel: 2, // crit + logrus.ErrorLevel: 3, // err + logrus.WarnLevel: 4, // warning + logrus.InfoLevel: 6, // info + logrus.DebugLevel: 7, // debug + logrus.TraceLevel: 7, // debug +} + +func (f *journalFormatter) Format(entry *logrus.Entry) ([]byte, error) { + formatted, err := f.inner.Format(entry) + if err != nil { + return formatted, err + } + priority, ok := levelToPriority[entry.Level] + if !ok { + priority = 6 // default to info for unknown levels + } + prefix := []byte(fmt.Sprintf("<%d>", priority)) + return append(prefix, formatted...), nil +} diff --git a/log/journal_test.go b/log/journal_test.go new file mode 100644 index 000000000..770f12b6d --- /dev/null +++ b/log/journal_test.go @@ -0,0 +1,41 @@ +package log + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/sirupsen/logrus" +) + +var _ = Describe("journalFormatter", func() { + var formatter *journalFormatter + + BeforeEach(func() { + inner := &logrus.TextFormatter{ + DisableTimestamp: true, + DisableColors: true, + } + formatter = &journalFormatter{inner: inner} + }) + + DescribeTable("prefixes log lines with syslog priority", + func(level logrus.Level, expectedPrefix string) { + entry := &logrus.Entry{ + Logger: logrus.New(), + Level: level, + Message: "test message", + Data: logrus.Fields{}, + } + out, err := formatter.Format(entry) + Expect(err).ToNot(HaveOccurred()) + Expect(string(out)).To(HavePrefix(expectedPrefix)) + }, + Entry("error", logrus.ErrorLevel, "<3>"), + Entry("warning", logrus.WarnLevel, "<4>"), + Entry("info", logrus.InfoLevel, "<6>"), + Entry("debug", logrus.DebugLevel, "<7>"), + Entry("trace", logrus.TraceLevel, "<7>"), + Entry("fatal", logrus.FatalLevel, "<2>"), + Entry("panic", logrus.PanicLevel, "<0>"), + Entry("unknown level defaults to info", logrus.Level(99), "<6>"), + ) +}) diff --git a/log/log.go b/log/log.go index ef38484c8..2764d80e5 100644 --- a/log/log.go +++ b/log/log.go @@ -145,6 +145,15 @@ func SetOutput(w io.Writer) { defaultLogger.SetOutput(w) } +// EnableJournalFormat wraps the current logger formatter with syslog +// priority prefixes for systemd-journald. Only call this when output +// goes to stderr and JOURNAL_STREAM is set. +func EnableJournalFormat() { + loggerMu.Lock() + defer loggerMu.Unlock() + defaultLogger.Formatter = &journalFormatter{inner: defaultLogger.Formatter} +} + // Redact applies redaction to a single string func Redact(msg string) string { r, _ := redacted.redact(msg) From 36aea8a11fbccc2efca56f5ecd88545059ec90ac Mon Sep 17 00:00:00 2001 From: Thiago Sfredo Date: Sun, 15 Mar 2026 15:55:55 -0300 Subject: [PATCH 24/81] feat(ui): add tooltips for long playlist and album names - 5068 (#5070) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * style(ui): add tooltips for long playlist and album names - 5068 Signed-off-by: Thiago Sfreddo * fix dnd and improve performance Signed-off-by: Thiago Sfreddo * lint fixes Signed-off-by: Thiago Sfreddo * fix(ui): update tooltip styles for improved visibility and consistency Signed-off-by: Deluan * fix(ui): add overflow tooltip to playlist name for better visibility Signed-off-by: Deluan * refactor(ui): simplify OverflowTooltip and improve render performance - Inline styles from useMenuTooltipStyles into OverflowTooltip (single consumer) - Use MUI named colors (grey[700]/grey[300] with alpha) instead of raw rgba - Stabilize ref callback with useCallback to avoid unnecessary ref churn - Memoize Tooltip classes and hoist TransitionProps to module level - Fix useLayoutEffect dependency: observe DOM size, not title string --------- Signed-off-by: Thiago Sfreddo Signed-off-by: Deluan Co-authored-by: Deluan Quintão --- ui/src/album/AlbumGridView.jsx | 11 +++- ui/src/common/OverflowTooltip.jsx | 90 +++++++++++++++++++++++++++++ ui/src/common/index.js | 1 + ui/src/layout/PlaylistsSubMenu.jsx | 10 ++-- ui/src/playlist/PlaylistDetails.jsx | 15 +++-- 5 files changed, 115 insertions(+), 12 deletions(-) create mode 100644 ui/src/common/OverflowTooltip.jsx diff --git a/ui/src/album/AlbumGridView.jsx b/ui/src/album/AlbumGridView.jsx index 58732bbde..b7db39730 100644 --- a/ui/src/album/AlbumGridView.jsx +++ b/ui/src/album/AlbumGridView.jsx @@ -13,7 +13,12 @@ import { linkToRecord, useListContext, Loading } from 'react-admin' import { withContentRect } from 'react-measure' import { useDrag } from 'react-dnd' import subsonic from '../subsonic' -import { AlbumContextMenu, PlayButton, ArtistLinkField } from '../common' +import { + AlbumContextMenu, + PlayButton, + ArtistLinkField, + OverflowTooltip, +} from '../common' import { DraggableTypes } from '../consts' import clsx from 'clsx' import { AlbumDatesField } from './AlbumDatesField.jsx' @@ -198,7 +203,9 @@ const AlbumGridTile = ({ showArtist, record, basePath, ...props }) => { to={linkToRecord(basePath, record.id, 'show')} > - {record.name} + + {record.name} + {record.tags && record.tags['albumversion'] && ( {record.tags['albumversion']} diff --git a/ui/src/common/OverflowTooltip.jsx b/ui/src/common/OverflowTooltip.jsx new file mode 100644 index 000000000..c000bd9f0 --- /dev/null +++ b/ui/src/common/OverflowTooltip.jsx @@ -0,0 +1,90 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Tooltip } from '@material-ui/core' +import { makeStyles, alpha } from '@material-ui/core/styles' +import grey from '@material-ui/core/colors/grey' + +const useStyles = makeStyles( + (theme) => ({ + tooltip: { + backgroundColor: + theme.palette.type === 'dark' + ? alpha(grey[700], 0.92) + : alpha(grey[300], 0.92), + color: + theme.palette.type === 'dark' + ? theme.palette.common.white + : theme.palette.common.black, + borderRadius: theme.shape.borderRadius, + ...theme.typography.body2, + padding: theme.spacing(0.5, 1), + maxWidth: 300, + }, + }), + { name: 'NDOverflowTooltip' }, +) + +const transitionProps = { timeout: 0 } + +export const OverflowTooltip = ({ + children, + title, + placement = 'bottom-start', +}) => { + const classes = useStyles() + const textRef = React.useRef(null) + const [isOverflowing, setIsOverflowing] = React.useState(false) + const tooltipClasses = React.useMemo( + () => ({ tooltip: classes.tooltip }), + [classes.tooltip], + ) + + React.useLayoutEffect(() => { + const el = textRef.current + if (!el) return + + const checkOverflow = () => { + setIsOverflowing(el.scrollWidth > el.clientWidth) + } + + const resizeObserver = new ResizeObserver(checkOverflow) + resizeObserver.observe(el) + + checkOverflow() + + return () => resizeObserver.disconnect() + }, []) + + const mergedRef = React.useCallback( + (el) => { + textRef.current = el + + const { ref } = children + if (typeof ref === 'function') { + ref(el) + } else if (ref && typeof ref === 'object') { + ref.current = el + } + }, + [children], + ) + + return ( + + {React.cloneElement(children, { ref: mergedRef })} + + ) +} + +OverflowTooltip.propTypes = { + children: PropTypes.element.isRequired, + title: PropTypes.string.isRequired, + placement: PropTypes.string, +} diff --git a/ui/src/common/index.js b/ui/src/common/index.js index 356225680..a8dc354cb 100644 --- a/ui/src/common/index.js +++ b/ui/src/common/index.js @@ -41,4 +41,5 @@ export * from './formatRange.js' export * from './playlistUtils.js' export * from './PathField.jsx' export * from './ParticipantsInfo' +export * from './OverflowTooltip' export * from './useSearchRefocus' diff --git a/ui/src/layout/PlaylistsSubMenu.jsx b/ui/src/layout/PlaylistsSubMenu.jsx index a9f70b875..b94bebf86 100644 --- a/ui/src/layout/PlaylistsSubMenu.jsx +++ b/ui/src/layout/PlaylistsSubMenu.jsx @@ -12,7 +12,7 @@ import QueueMusicOutlinedIcon from '@material-ui/icons/QueueMusicOutlined' import { BiCog } from 'react-icons/bi' import { useDrop } from 'react-dnd' import SubMenu from './SubMenu' -import { canChangeTracks } from '../common' +import { canChangeTracks, OverflowTooltip } from '../common' import { DraggableTypes } from '../consts' import config from '../config' @@ -39,9 +39,11 @@ const PlaylistMenuItemLink = ({ pls, sidebarIsOpen }) => { - {pls.name} - + + + {pls.name} + + } sidebarIsOpen={sidebarIsOpen} dense={false} diff --git a/ui/src/playlist/PlaylistDetails.jsx b/ui/src/playlist/PlaylistDetails.jsx index eefed83b4..aeddf0d42 100644 --- a/ui/src/playlist/PlaylistDetails.jsx +++ b/ui/src/playlist/PlaylistDetails.jsx @@ -19,6 +19,7 @@ import { DurationField, SizeField, isWritable, + OverflowTooltip, } from '../common' import config from '../config' import subsonic from '../subsonic' @@ -274,12 +275,14 @@ const PlaylistDetails = (props) => {
- - {record.name || translate('ra.page.loading')} - + + + {record.name || translate('ra.page.loading')} + + {record.songCount ? ( From be06196168f267510ab8b24f59f0a762804732de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Sun, 15 Mar 2026 20:44:59 -0400 Subject: [PATCH 25/81] fix(ui): update Bulgarian, Catalan, Danish, German, Greek, Spanish, Finnish, French, Galician, Russian, Slovenian, Swedish, Thai, Chinese (traditional) translations from POEditor (#5044) Co-authored-by: navidrome-bot --- resources/i18n/bg.json | 284 +++++++++++++++++++----------------- resources/i18n/ca.json | 22 ++- resources/i18n/da.json | 21 ++- resources/i18n/de.json | 22 ++- resources/i18n/el.json | 22 ++- resources/i18n/es.json | 22 ++- resources/i18n/fi.json | 22 ++- resources/i18n/fr.json | 22 ++- resources/i18n/gl.json | 22 ++- resources/i18n/ru.json | 22 ++- resources/i18n/sl.json | 28 ++-- resources/i18n/sv.json | 22 ++- resources/i18n/th.json | 26 +++- resources/i18n/zh-Hant.json | 166 +++++++++++---------- 14 files changed, 430 insertions(+), 293 deletions(-) diff --git a/resources/i18n/bg.json b/resources/i18n/bg.json index bce5a3a6e..626657129 100644 --- a/resources/i18n/bg.json +++ b/resources/i18n/bg.json @@ -31,13 +31,14 @@ "mood": "Настроение", "participants": "Допълнителни участници", "tags": "Допълнителни етикети", - "mappedTags": "", - "rawTags": "", + "mappedTags": "Картирани тагове", + "rawTags": "Сурови тагове", "bitDepth": "Битова дълбочина", - "sampleRate": "", + "sampleRate": "Честота на семплиране", "missing": "Липсва", - "libraryName": "", - "composer": "" + "libraryName": "Библиотека", + "composer": "Композитор", + "disc": "" }, "actions": { "addToQueue": "Пусни по-късно", @@ -47,8 +48,8 @@ "download": "Свали", "playNext": "Следваща", "info": "Информация", - "showInPlaylist": "", - "instantMix": "" + "showInPlaylist": "Показване в плейлиста", + "instantMix": "Незабавен микс" } }, "album": { @@ -80,7 +81,7 @@ "mood": "Настроение", "date": "Дата на запис", "missing": "Липсва", - "libraryName": "" + "libraryName": "Библиотека" }, "actions": { "playAll": "Пусни", @@ -129,12 +130,12 @@ "remixer": "Ремиксер |||| Ремиксери", "djmixer": "DJ миксер |||| DJ миксери", "performer": "Изпълнител |||| Изпълнители", - "maincredit": "" + "maincredit": "Изпълнител на албума или изпълнител |||| Изпълнители на албума или изпълнители" }, "actions": { - "shuffle": "", - "radio": "", - "topSongs": "" + "shuffle": "Разбъркване", + "radio": "Радио", + "topSongs": "Топ песни" } }, "user": { @@ -152,11 +153,11 @@ "newPassword": "Нова парола", "token": "Токен", "lastAccessAt": "Последен достъп", - "libraries": "" + "libraries": "Библиотеки" }, "helperTexts": { "name": "Промените в името ще бъдат отразени при следващото влизане", - "libraries": "" + "libraries": "Изберете конкретни библиотеки за този потребител или оставете празно, за да използвате библиотеки по подразбиране" }, "notifications": { "created": "Потребителят е създаден", @@ -166,11 +167,11 @@ "message": { "listenBrainzToken": "Въведете Вашия токен за ListenBrainz.", "clickHereForToken": "Кликнете тук, за да получите Вашия токен", - "selectAllLibraries": "", - "adminAutoLibraries": "" + "selectAllLibraries": "Изберете всички библиотеки", + "adminAutoLibraries": "Администраторите автоматично получават достъп до всички библиотеки" }, "validation": { - "librariesRequired": "" + "librariesRequired": "Трябва да бъде избрана поне една библиотека за потребители без администраторски права" } }, "player": { @@ -215,16 +216,22 @@ "export": "Експорт", "makePublic": "Направи публичен", "makePrivate": "Направи личен", - "saveQueue": "", - "searchOrCreate": "", - "pressEnterToCreate": "", - "removeFromSelection": "" + "saveQueue": "Запазване на опашката в плейлист", + "searchOrCreate": "Търсете в плейлисти или пишете, за да създадете нови...", + "pressEnterToCreate": "Натиснете Enter, за да създадете нов плейлист", + "removeFromSelection": "Премахване от селекцията", + "uploadCover": "", + "removeCover": "" }, "message": { "duplicate_song": "Добави дублирани песни", "song_exist": "Към плейлиста се добавят дублиращи. Желаете ли да ги добавите или предпочитате да ги пропуснете?", - "noPlaylistsFound": "", - "noPlaylists": "" + "noPlaylistsFound": "Няма намерени плейлисти", + "noPlaylists": "Няма налични плейлисти", + "coverUploaded": "", + "coverRemoved": "", + "coverUploadError": "", + "coverRemoveError": "" } }, "radio": { @@ -263,7 +270,7 @@ "path": "Път", "size": "Размер", "updatedAt": "Изчезнал на", - "libraryName": "" + "libraryName": "Библиотека" }, "actions": { "remove": "Премахни", @@ -275,134 +282,136 @@ "empty": "Няма липсващи файлове" }, "library": { - "name": "", + "name": "Библиотека |||| Библиотеки", "fields": { - "name": "", - "path": "", - "remotePath": "", - "lastScanAt": "", - "songCount": "", - "albumCount": "", - "artistCount": "", - "totalSongs": "", - "totalAlbums": "", - "totalArtists": "", - "totalFolders": "", - "totalFiles": "", - "totalMissingFiles": "", - "totalSize": "", - "totalDuration": "", - "defaultNewUsers": "", - "createdAt": "", - "updatedAt": "" + "name": "Име", + "path": "Път", + "remotePath": "Отдалечен път", + "lastScanAt": "Последно сканиране", + "songCount": "Песни", + "albumCount": "Албуми", + "artistCount": "Изпълнители", + "totalSongs": "Песни", + "totalAlbums": "Албуми", + "totalArtists": "Изпълнители", + "totalFolders": "Папки", + "totalFiles": "Файлове", + "totalMissingFiles": "Липсващи файлове", + "totalSize": "Общ размер", + "totalDuration": "Продължителност", + "defaultNewUsers": "По подразбиране за нови потребители", + "createdAt": "Създаден", + "updatedAt": "Актуализиран" }, "sections": { - "basic": "", - "statistics": "" + "basic": "Основна информация", + "statistics": "Статистика" }, "actions": { - "scan": "", - "manageUsers": "", - "viewDetails": "", + "scan": "Сканирай библиотеката", + "manageUsers": "Управление на потребителския достъп", + "viewDetails": "Преглед на подробности", "quickScan": "Quick Scan", - "fullScan": "" + "fullScan": "Пълно сканиране" }, "notifications": { - "created": "", - "updated": "", - "deleted": "", - "scanStarted": "", - "scanCompleted": "", - "quickScanStarted": "", - "fullScanStarted": "", - "scanError": "" + "created": "Библиотеката е създадена успешно", + "updated": "Библиотеката е актуализирана успешно", + "deleted": "Библиотеката е изтрита успешно", + "scanStarted": "Сканирането на библиотеката започна", + "scanCompleted": "Сканирането на библиотеката е завършено", + "quickScanStarted": "Бързото сканиране започна", + "fullScanStarted": "Пълното сканиране започна", + "scanError": "Грешка при стартиране на сканирането. Проверете лог файловете" }, "validation": { - "nameRequired": "", - "pathRequired": "", - "pathNotDirectory": "", - "pathNotFound": "", - "pathNotAccessible": "", - "pathInvalid": "" + "nameRequired": "Името на библиотеката е задължително", + "pathRequired": "Пътят към библиотеката е задължителен", + "pathNotDirectory": "Пътят до библиотеката трябва да е директория", + "pathNotFound": "Пътят към библиотеката не е намерен", + "pathNotAccessible": "Пътят до библиотеката не е достъпен", + "pathInvalid": "Невалиден път към библиотеката" }, "messages": { - "deleteConfirm": "", - "scanInProgress": "", - "noLibrariesAssigned": "" + "deleteConfirm": "Сигурни ли сте, че желаете да изтриете тази библиотека? Това ще премахне всички свързани данни и потребителски достъп.", + "scanInProgress": "Сканирането е в ход...", + "noLibrariesAssigned": "Няма библиотеки, присвоени на този потребител" } }, "plugin": { - "name": "", + "name": "Плъгин |||| Плъгини", "fields": { - "id": "", - "name": "", - "description": "", - "version": "", - "author": "", - "website": "", - "permissions": "", - "enabled": "", - "status": "", - "path": "", - "lastError": "", - "hasError": "", - "updatedAt": "", - "createdAt": "", - "configKey": "", - "configValue": "", - "allUsers": "", - "selectedUsers": "", - "allLibraries": "", - "selectedLibraries": "" + "id": "ID номер", + "name": "Име", + "description": "Описание", + "version": "Версия", + "author": "Автор", + "website": "Уебсайт", + "permissions": "Разрешения", + "enabled": "Активирано", + "status": "Статус", + "path": "Път", + "lastError": "Грешка", + "hasError": "Грешка", + "updatedAt": "Актуализирано", + "createdAt": "Инсталирано", + "configKey": "Ключ", + "configValue": "Стойност", + "allUsers": "Разрешаване на всички потребители", + "selectedUsers": "Избрани потребители", + "allLibraries": "Разрешаване на всички библиотеки", + "selectedLibraries": "Избрани библиотеки", + "allowWriteAccess": "" }, "sections": { - "status": "", - "info": "", - "configuration": "", - "manifest": "", - "usersPermission": "", - "libraryPermission": "" + "status": "Статус", + "info": "Информация за плъгина", + "configuration": "Конфигурация", + "manifest": "Манифест", + "usersPermission": "Права за потребители", + "libraryPermission": "Права за библиотека" }, "status": { - "enabled": "", - "disabled": "" + "enabled": "Активирано", + "disabled": "Деактивирано" }, "actions": { - "enable": "", - "disable": "", - "disabledDueToError": "", - "disabledUsersRequired": "", - "disabledLibrariesRequired": "", - "addConfig": "", - "rescan": "" + "enable": "Активирай", + "disable": "Деактивирай", + "disabledDueToError": "Поправете грешката преди активиране", + "disabledUsersRequired": "Изберете потребители преди активиране", + "disabledLibrariesRequired": "Изберете библиотеки преди активиране", + "addConfig": "Добавяне на конфигурация", + "rescan": "Повторно сканиране" }, "notifications": { - "enabled": "", - "disabled": "", - "updated": "", - "error": "" + "enabled": "Плъгинът е активиран", + "disabled": "Плъгинът е деактивиран", + "updated": "Плъгинът е актуализиран", + "error": "Грешка при актуализиране на плъгина" }, "validation": { - "invalidJson": "" + "invalidJson": "Конфигурацията трябва да е валиден JSON" }, "messages": { - "configHelp": "", - "clickPermissions": "", - "noConfig": "", - "allUsersHelp": "", - "noUsers": "", - "permissionReason": "", - "usersRequired": "", - "allLibrariesHelp": "", - "noLibraries": "", - "librariesRequired": "", - "requiredHosts": "", - "configValidationError": "", - "schemaRenderError": "" + "configHelp": "Конфигурирайте плъгина, използвайки двойки ключ-стойност. Оставете празно, ако плъгинът не изисква конфигурация.", + "clickPermissions": "Кликнете върху разрешение за подробности", + "noConfig": "Няма зададена конфигурация", + "allUsersHelp": "Когато е активиран, плъгинът ще има достъп до всички потребители, включително тези, създадени в бъдеще.", + "noUsers": "Няма избрани потребители", + "permissionReason": "Причина", + "usersRequired": "Този плъгин изисква достъп до потребителска информация. Изберете до кои потребители плъгинът може да има достъп или активирайте „Разрешаване на всички потребители“.", + "allLibrariesHelp": "Когато е активиран, плъгинът ще има достъп до всички библиотеки, включително тези, създадени в бъдеще.", + "noLibraries": "Няма избрани библиотеки", + "librariesRequired": "Този плъгин изисква достъп до информация за библиотеката. Изберете до кои библиотеки плъгинът може да има достъп или активирайте „Разрешаване на всички библиотеки“.", + "requiredHosts": "Необходими хостове", + "configValidationError": "Валидирането на конфигурацията не бе успешно:", + "schemaRenderError": "Не може да се изобрази формята за конфигурация. Схемата на плъгина може да е невалидна.", + "allowWriteAccessHelp": "" }, "placeholders": { - "configKey": "", - "configValue": "" + "configKey": "ключ", + "configValue": "стойност" } } }, @@ -586,9 +595,9 @@ "remove_missing_content": "Сигурни ли сте, че желаете да премахнете избраните липсващи файлове от базата данни? Това ще премахне завинаги всички препратки към тях, включително броя на възпроизвежданията и оценките им.", "remove_all_missing_title": "Премахни всички липсващи файлове", "remove_all_missing_content": "Сигурни ли сте, че желаете да премахнете всички липсващи файлове от базата данни? Това ще премахне завинаги всички препратки към тях, включително броя на възпроизвежданията и оценките им.", - "noSimilarSongsFound": "", - "noTopSongsFound": "", - "startingInstantMix": "" + "noSimilarSongsFound": "Не са намерени подобни песни", + "noTopSongsFound": "Няма намерени топ песни", + "startingInstantMix": "Зареждане на незабавен микс..." }, "menu": { "library": "Библиотека", @@ -619,10 +628,10 @@ "playlists": "Плейлисти", "sharedPlaylists": "Споделени плейлисти", "librarySelector": { - "allLibraries": "", - "multipleLibraries": "", - "selectLibraries": "", - "none": "" + "allLibraries": "Всички библиотеки (%{count})", + "multipleLibraries": "%{selected} от %{total} библиотеки", + "selectLibraries": "Изберете библиотеки", + "none": "Няма" } }, "player": { @@ -655,7 +664,7 @@ "homepage": "Начална страница", "source": "Програмен код", "featureRequests": "Заявете функционалност", - "lastInsightsCollection": "", + "lastInsightsCollection": "Последна колекция от анализи", "insights": { "disabled": "Деактивиран", "waiting": "Изчакване" @@ -669,12 +678,13 @@ "configName": "Име на конфигурация", "environmentVariable": "Променлива на средата", "currentValue": "Текуща стойност", - "configurationFile": "", + "configurationFile": "Конфигурационен файл", "exportToml": "Експортиране на конфигурация (TOML)", "exportSuccess": "Конфигурация, експортирана в клипборда във формат TOML", "exportFailed": "Неуспешно копиране на конфигурация", - "devFlagsHeader": "", - "devFlagsComment": "" + "devFlagsHeader": "Флагове за разработка (подлежащи на промяна/премахване)", + "devFlagsComment": "Това са експериментални настройки и е възможно да бъдат премахнати в бъдещи версии.", + "downloadToml": "Изтегляне на конфигурация (TOML)" } }, "activity": { @@ -687,7 +697,7 @@ "scanType": "Последно сканиране", "status": "Грешка при сканиране", "elapsedTime": "Изминало време", - "selectiveScan": "" + "selectiveScan": "Селективен" }, "help": { "title": "Бързи клавиши на Navidrome", @@ -704,8 +714,8 @@ } }, "nowPlaying": { - "title": "", - "empty": "", - "minutesAgo": "" + "title": "Сега свири", + "empty": "Нищо не се възпроизвежда", + "minutesAgo": "преди %{smart_count} минута |||| преди %{smart_count} минути" } } \ No newline at end of file diff --git a/resources/i18n/ca.json b/resources/i18n/ca.json index 264a76639..079c130d1 100644 --- a/resources/i18n/ca.json +++ b/resources/i18n/ca.json @@ -37,7 +37,8 @@ "sampleRate": "Freqüencia de mostreig", "missing": "Desaparegut", "libraryName": "Biblioteca", - "composer": "Compositor" + "composer": "Compositor", + "disc": "" }, "actions": { "addToQueue": "Reprodueix després", @@ -218,13 +219,19 @@ "saveQueue": "Desar la cua a una llista", "searchOrCreate": "Cerca llistes o escriu per crear-ne de noves...", "pressEnterToCreate": "Prem Retorn per crear una nova llista", - "removeFromSelection": "Elimina de la selecció" + "removeFromSelection": "Elimina de la selecció", + "uploadCover": "", + "removeCover": "" }, "message": { "duplicate_song": "Afegeix cançons duplicades", "song_exist": "Heu afegit duplicats a la llista. Voleu afegir-los o ignorar-los?", "noPlaylistsFound": "No s'ha trobat cap llista", - "noPlaylists": "No hi ha cap llista disponible" + "noPlaylists": "No hi ha cap llista disponible", + "coverUploaded": "", + "coverRemoved": "", + "coverUploadError": "", + "coverRemoveError": "" } }, "radio": { @@ -353,7 +360,8 @@ "allUsers": "Permet tots els usuaris", "selectedUsers": "Usuaris seleccionats", "allLibraries": "Permet totes les llibreries", - "selectedLibraries": "Biblioteques seleccionades" + "selectedLibraries": "Biblioteques seleccionades", + "allowWriteAccess": "" }, "sections": { "status": "Estat", @@ -398,7 +406,8 @@ "librariesRequired": "Aquest controlador necessita accedir a la informació de la biblioteca. Selecciona a quines biblioteques pot accedir o activa «Permet totes les biblioteques».", "requiredHosts": "Hosts requerits", "configValidationError": "Ha fallat la validació de la configuració:", - "schemaRenderError": "No s'ha pogut renderitzar el formulari de configuració. És possible que l'esquema del controlador sigui invàlid." + "schemaRenderError": "No s'ha pogut renderitzar el formulari de configuració. És possible que l'esquema del controlador sigui invàlid.", + "allowWriteAccessHelp": "" }, "placeholders": { "configKey": "clau", @@ -674,7 +683,8 @@ "exportSuccess": "Configuració exportada al porta-retalls en format TOML", "exportFailed": "La còpia de la configuració ha fallat", "devFlagsHeader": "Indicadors de desenvolupament (subjecte a canvis o eliminació)", - "devFlagsComment": "Aquests paràmetres són experimentals i és possible que s'eliminin en versions futures" + "devFlagsComment": "Aquests paràmetres són experimentals i és possible que s'eliminin en versions futures", + "downloadToml": "Descarrega la configuració (TOML)" } }, "activity": { diff --git a/resources/i18n/da.json b/resources/i18n/da.json index 01d0856d6..a7c090cca 100644 --- a/resources/i18n/da.json +++ b/resources/i18n/da.json @@ -37,7 +37,8 @@ "sampleRate": "Samplingfrekvens", "missing": "Manglende", "libraryName": "Bibliotek", - "composer": "Komponist" + "composer": "Komponist", + "disc": "" }, "actions": { "addToQueue": "Afspil senere", @@ -218,13 +219,19 @@ "saveQueue": "Gem kø på afspilningsliste", "searchOrCreate": "Søg i afspilningslister eller skriv for at oprette nye...", "pressEnterToCreate": "Tryk Enter for at oprette en ny afspilningsliste", - "removeFromSelection": "Fjern fra valg" + "removeFromSelection": "Fjern fra valg", + "uploadCover": "", + "removeCover": "" }, "message": { "duplicate_song": "Tilføj dubletter af sange", "song_exist": "Der føjes dubletter til playlisten", "noPlaylistsFound": "Ingen playlister fundet", - "noPlaylists": "Ingen tilgængelige playlister" + "noPlaylists": "Ingen tilgængelige playlister", + "coverUploaded": "", + "coverRemoved": "", + "coverUploadError": "", + "coverRemoveError": "" } }, "radio": { @@ -353,7 +360,8 @@ "allUsers": "Tillad alle brugere", "selectedUsers": "Valgte brugere", "allLibraries": "Tillad alle biblioteker", - "selectedLibraries": "Valgte biblioteker" + "selectedLibraries": "Valgte biblioteker", + "allowWriteAccess": "" }, "sections": { "status": "Status", @@ -398,7 +406,8 @@ "librariesRequired": "Dette plugin kræver adgang til biblioteksoplysninger. Vælg hvilke biblioteker pluginet kan tilgå, eller aktivér 'Tillad alle biblioteker'.", "requiredHosts": "Påkrævede hosts", "configValidationError": "Konfigurationsvalidering mislykkedes:", - "schemaRenderError": "Kan ikke vise konfigurationsformularen. Pluginets skema er muligvis ugyldigt." + "schemaRenderError": "Kan ikke vise konfigurationsformularen. Pluginets skema er muligvis ugyldigt.", + "allowWriteAccessHelp": "" }, "placeholders": { "configKey": "nøgle", @@ -675,7 +684,7 @@ "exportFailed": "Kunne ikke kopiere konfigurationen", "devFlagsHeader": "Udviklingsflagget (med forbehold for ændring/fjernelse)", "devFlagsComment": "Disse er eksperimental-indstillinger og kan blive fjernet i fremtidige udgaver", - "downloadToml": "" + "downloadToml": "Download konfigurationen (TOML)" } }, "activity": { diff --git a/resources/i18n/de.json b/resources/i18n/de.json index 568c65c51..6ba8a14eb 100644 --- a/resources/i18n/de.json +++ b/resources/i18n/de.json @@ -37,7 +37,8 @@ "sampleRate": "Samplerate", "missing": "Fehlend", "libraryName": "Bibliothek", - "composer": "Komponist" + "composer": "Komponist", + "disc": "" }, "actions": { "addToQueue": "Später abspielen", @@ -218,13 +219,19 @@ "saveQueue": "Warteschlange in Wiedergabeliste speichern", "searchOrCreate": "Wiedergabeliste suchen oder neue erstellen...", "pressEnterToCreate": "Enter drücken um neue Wiedergabeliste zu erstellen", - "removeFromSelection": "Von Auswahl entfernen" + "removeFromSelection": "Von Auswahl entfernen", + "uploadCover": "Cover hochladen", + "removeCover": "Cover entfernen" }, "message": { "duplicate_song": "Duplikate hinzufügen", "song_exist": "Manche Titel sind bereits in der Playlist. Möchtest du sie trotzdem hinzufügen oder überspringen?", "noPlaylistsFound": "Keine Wiedergabeliste gefunden", - "noPlaylists": "Keine Wiedergabelisten vorhanden" + "noPlaylists": "Keine Wiedergabelisten vorhanden", + "coverUploaded": "Cover aktualisiert", + "coverRemoved": "Cover entfernt", + "coverUploadError": "Fehler beim Hochladen des Covers", + "coverRemoveError": "Fehler beim Entfernen des Covers" } }, "radio": { @@ -353,7 +360,8 @@ "allUsers": "Alle Benutzer", "selectedUsers": "Ausgewählte Benutzer", "allLibraries": "Alle Bibliotheken", - "selectedLibraries": "Ausgewählte Bibliotheken" + "selectedLibraries": "Ausgewählte Bibliotheken", + "allowWriteAccess": "Schreibzugriff erlauben" }, "sections": { "status": "Status", @@ -398,7 +406,8 @@ "librariesRequired": "Dieses Plugin benötigt Zugriff auf Bibliotheken. Wähle aus, auf welche Bibliotheken das Plugin zugreifen darf oder wähle 'Alle Bibliotheken'.", "requiredHosts": "Benötigte Hosts", "configValidationError": "Validierung der Konfiguration fehlgeschlagen:", - "schemaRenderError": "Rendern der Konfiguration fehlgeschlagen. Das Schema das Plugins ist eventuell nicht korrekt." + "schemaRenderError": "Rendern der Konfiguration fehlgeschlagen. Das Schema das Plugins ist eventuell nicht korrekt.", + "allowWriteAccessHelp": "Wenn aktiviert, kann das Plugin Dateien in den Bibliotheken verändern. Als Standard haben Plugins nur Lesezugriff." }, "placeholders": { "configKey": "Schlüssel", @@ -674,7 +683,8 @@ "exportSuccess": "Konfiguration im TOML Format in die Zwischenablage kopiert", "exportFailed": "Fehler beim Kopieren der Konfiguration", "devFlagsHeader": "Entwicklungseinstellungen (können sich ändern)", - "devFlagsComment": "Experimentelle Einstellungen, die eventuell in Zukunft entfernt oder geändert werden" + "devFlagsComment": "Experimentelle Einstellungen, die eventuell in Zukunft entfernt oder geändert werden", + "downloadToml": "Konfiguration Herunterladen (TOML)" } }, "activity": { diff --git a/resources/i18n/el.json b/resources/i18n/el.json index 02d0b06c4..e5d983d6f 100644 --- a/resources/i18n/el.json +++ b/resources/i18n/el.json @@ -37,7 +37,8 @@ "sampleRate": "Ποσοστό δειγματοληψίας", "missing": "Απών", "libraryName": "Βιβλιοθήκη", - "composer": "Συνθέτης" + "composer": "Συνθέτης", + "disc": "" }, "actions": { "addToQueue": "Αναπαραγωγη Μετα", @@ -218,13 +219,19 @@ "saveQueue": "Αποθήκευση ουράς στη λίστα αναπαραγωγής", "searchOrCreate": "Αναζητήστε λίστες αναπαραγωγής ή πληκτρολογήστε για να δημιουργήσετε νέες...", "pressEnterToCreate": "Πατήστε Enter για να δημιουργήσετε νέα λίστα αναπαραγωγής", - "removeFromSelection": "Αφαίρεση από την επιλογή" + "removeFromSelection": "Αφαίρεση από την επιλογή", + "uploadCover": "", + "removeCover": "" }, "message": { "duplicate_song": "Προσθήκη διπλοεγγραφών τραγουδιών", "song_exist": "Υπάρχουν διπλοεγγραφές στην λίστα αναπαραγωγής. Θέλετε να προστεθούν οι διπλοεγγραφές ή να τις παραβλέψετε?", "noPlaylistsFound": "Δεν βρέθηκαν λίστες αναπαραγωγής", - "noPlaylists": "Δεν υπάρχουν διαθέσιμες λίστες αναπαραγωγής" + "noPlaylists": "Δεν υπάρχουν διαθέσιμες λίστες αναπαραγωγής", + "coverUploaded": "", + "coverRemoved": "", + "coverUploadError": "", + "coverRemoveError": "" } }, "radio": { @@ -353,7 +360,8 @@ "allUsers": "Επιτρέψτε όλους τους χρήστες", "selectedUsers": "Επιλογή χρηστών", "allLibraries": "Επιτρέψτε όλες τις βιβλιοθήκες", - "selectedLibraries": "Επιλεγμένες βιβλιοθήκες" + "selectedLibraries": "Επιλεγμένες βιβλιοθήκες", + "allowWriteAccess": "" }, "sections": { "status": "Κατάσταση", @@ -398,7 +406,8 @@ "librariesRequired": "Αυτό το πρόσθετο απαιτεί πρόσβαση στις πληροφορίες βιβλιοθήκης. Επιλέξτε σε ποιές βιβλιοθήκες μπορεί να έχει πρόσβαση το πρόσθετο, ή ενεργοποιήστε το 'Επιτρέψτε όλες τις βιβλιοθήκες'", "requiredHosts": "Απαιτούμενοι hosts", "configValidationError": "Η επικύρωση διαμόρφωσης απέτυχε:", - "schemaRenderError": "Δεν είναι δυνατή η απόδοση της φόρμας διαμόρφωσης. Το σχήμα της προσθήκης ενδέχεται να μην είναι έγκυρο." + "schemaRenderError": "Δεν είναι δυνατή η απόδοση της φόρμας διαμόρφωσης. Το σχήμα της προσθήκης ενδέχεται να μην είναι έγκυρο.", + "allowWriteAccessHelp": "" }, "placeholders": { "configKey": "κλειδί", @@ -674,7 +683,8 @@ "exportSuccess": "Η διαμόρφωση εξήχθη στο πρόχειρο σε μορφή TOML", "exportFailed": "Η αντιγραφή της διαμόρφωσης απέτυχε", "devFlagsHeader": "Σημαίες Ανάπτυξης (υπόκειται σε αλλαγές / αφαίρεση)", - "devFlagsComment": "Αυτές είναι πειραματικές ρυθμίσεις και ενδέχεται να καταργηθούν σε μελλοντικές εκδόσεις" + "devFlagsComment": "Αυτές είναι πειραματικές ρυθμίσεις και ενδέχεται να καταργηθούν σε μελλοντικές εκδόσεις", + "downloadToml": "Λήψη διαμόρφωσης (TOML)" } }, "activity": { diff --git a/resources/i18n/es.json b/resources/i18n/es.json index 38c1379c9..ef246a0e6 100644 --- a/resources/i18n/es.json +++ b/resources/i18n/es.json @@ -37,7 +37,8 @@ "sampleRate": "Frecuencia de muestreo", "missing": "Faltante", "libraryName": "Biblioteca", - "composer": "Compositor" + "composer": "Compositor", + "disc": "" }, "actions": { "addToQueue": "Reproducir después", @@ -218,13 +219,19 @@ "saveQueue": "Guardar la fila de reproducción en una playlist", "searchOrCreate": "Buscar listas de reproducción o escribe para crear una nueva…", "pressEnterToCreate": "Pulsa Enter para crear una nueva lista de reproducción", - "removeFromSelection": "Quitar de la selección" + "removeFromSelection": "Quitar de la selección", + "uploadCover": "", + "removeCover": "" }, "message": { "duplicate_song": "Algunas de las canciones seleccionadas están presentes en la playlist", "song_exist": "Se están agregando duplicados a la playlist. ¿Quieres agregar los duplicados o omitirlos?", "noPlaylistsFound": "No se encontraron listas de reproducción", - "noPlaylists": "No hay listas de reproducción disponibles" + "noPlaylists": "No hay listas de reproducción disponibles", + "coverUploaded": "", + "coverRemoved": "", + "coverUploadError": "", + "coverRemoveError": "" } }, "radio": { @@ -353,7 +360,8 @@ "allUsers": "Permitir todos los usuarios", "selectedUsers": "Usuarios seleccionados", "allLibraries": "Permitir todas las bibliotecas", - "selectedLibraries": "Bibliotecas seleccionadas" + "selectedLibraries": "Bibliotecas seleccionadas", + "allowWriteAccess": "" }, "sections": { "status": "Estado", @@ -398,7 +406,8 @@ "librariesRequired": "Este plugin requiere acceso a la información de las bibliotecas. Selecciona a qué bibliotecas puede acceder el plugin, o activa 'Permitir todas las bibliotecas'.", "requiredHosts": "Hosts requeridos", "configValidationError": "La validación de la configuración falló:", - "schemaRenderError": "No se pudo renderizar el formulario de configuración. Es posible que el esquema del complemento no sea válido." + "schemaRenderError": "No se pudo renderizar el formulario de configuración. Es posible que el esquema del complemento no sea válido.", + "allowWriteAccessHelp": "" }, "placeholders": { "configKey": "clave", @@ -674,7 +683,8 @@ "exportSuccess": "Configuración exportada al portapapeles en formato TOML", "exportFailed": "Error al copiar la configuración", "devFlagsHeader": "Indicadores de desarrollo (sujetos a cambios o eliminación)", - "devFlagsComment": "Estas son configuraciones experimentales y pueden eliminarse en versiones futuras" + "devFlagsComment": "Estas son configuraciones experimentales y pueden eliminarse en versiones futuras", + "downloadToml": "Descargar la configuración (TOML)" } }, "activity": { diff --git a/resources/i18n/fi.json b/resources/i18n/fi.json index 0d260fb44..ea5d22f4c 100644 --- a/resources/i18n/fi.json +++ b/resources/i18n/fi.json @@ -37,7 +37,8 @@ "sampleRate": "Näytteenottotaajuus", "missing": "Puuttuva", "libraryName": "Kirjasto", - "composer": "Säveltäjä" + "composer": "Säveltäjä", + "disc": "" }, "actions": { "addToQueue": "Lisää jonoon", @@ -218,13 +219,19 @@ "saveQueue": "Tallenna jono soittolistaan", "searchOrCreate": "Etsi soittolistoja tai kirjoita luodaksesi uuden...", "pressEnterToCreate": "Paina Enter luodaksesi uuden soittolistan", - "removeFromSelection": "Poista valinnasta" + "removeFromSelection": "Poista valinnasta", + "uploadCover": "Lataa kansikuva", + "removeCover": "Poista kansikuva" }, "message": { "duplicate_song": "Lisää olemassa oleva kappale", "song_exist": "Olet lisäämässä soittolistalla jo olevaa kappaletta. Haluatko lisätä saman kappaleen vai ohittaa sen?", "noPlaylistsFound": "Soittolistoja ei löytynyt", - "noPlaylists": "Soittolistoja ei ole saatavilla" + "noPlaylists": "Soittolistoja ei ole saatavilla", + "coverUploaded": "Kansikuva päivitetty", + "coverRemoved": "Kansikuva poistettu", + "coverUploadError": "Virhe ladattaessa kansikuvaa", + "coverRemoveError": "Virhe poistettaessa kansikuvaa" } }, "radio": { @@ -353,7 +360,8 @@ "allUsers": "Salli kaikki käyttäjät", "selectedUsers": "Valitut käyttäjät", "allLibraries": "Salli kaikki kirjastot", - "selectedLibraries": "Valitut kirjastot" + "selectedLibraries": "Valitut kirjastot", + "allowWriteAccess": "Salli kirjoitusoikeus" }, "sections": { "status": "Tila", @@ -398,7 +406,8 @@ "librariesRequired": "Tämä laajennus vaatii pääsyn kirjastotietoihin. Valitse, mihin kirjastoihin laajennus voi käyttää, tai ota käyttöön 'Salli kaikki kirjastot'.", "requiredHosts": "Vaaditut palvelimet", "configValidationError": "Määrityksen validointi epäonnistui:", - "schemaRenderError": "Konfiguraatiolomaketta ei voi näyttää. Lisäosan skeema saattaa olla virheellinen." + "schemaRenderError": "Konfiguraatiolomaketta ei voi näyttää. Lisäosan skeema saattaa olla virheellinen.", + "allowWriteAccessHelp": "Kun otettu käyttöön, liitännäinen voi muokata tiedostoja kirjastohakemistoissa. Oletuksena liitännäisillä on vain luku -oikeus." }, "placeholders": { "configKey": "avain", @@ -674,7 +683,8 @@ "exportSuccess": "Määritykset viety leikepöydälle TOML-muodossa", "exportFailed": "Määritysten kopiointi epäonnistui", "devFlagsHeader": "Kehitysliput (voivat muuttua/poistua)", - "devFlagsComment": "Nämä ovat kokeellisia asetuksia ja ne voidaan poistaa tulevissa versioissa" + "devFlagsComment": "Nämä ovat kokeellisia asetuksia ja ne voidaan poistaa tulevissa versioissa", + "downloadToml": "Lataa määritykset (TOML)" } }, "activity": { diff --git a/resources/i18n/fr.json b/resources/i18n/fr.json index 66bd454cc..57af0b8cd 100644 --- a/resources/i18n/fr.json +++ b/resources/i18n/fr.json @@ -37,7 +37,8 @@ "sampleRate": "Fréquence d'échantillonnage", "missing": "Manquant", "libraryName": "Bibliothèque", - "composer": "Compositeur·e" + "composer": "Compositeur·e", + "disc": "" }, "actions": { "addToQueue": "Ajouter à la file", @@ -218,13 +219,19 @@ "saveQueue": "Sauvegarder la file de lecture dans la playlist", "searchOrCreate": "Chercher ou créer une nouvelle playlist...", "pressEnterToCreate": "Appuyer sur entrée pour créer une nouvelle playlist", - "removeFromSelection": "Supprimer de la sélection" + "removeFromSelection": "Supprimer de la sélection", + "uploadCover": "", + "removeCover": "" }, "message": { "duplicate_song": "Ajouter les titres déjà présents dans la playlist", "song_exist": "Certains des titres sélectionnés font déjà partie de la playlist. Voulez-vous les ajouter ou les ignorer ?", "noPlaylistsFound": "Aucune playlist trouvée", - "noPlaylists": "Aucune playlist disponible" + "noPlaylists": "Aucune playlist disponible", + "coverUploaded": "", + "coverRemoved": "", + "coverUploadError": "", + "coverRemoveError": "" } }, "radio": { @@ -353,7 +360,8 @@ "allUsers": "Autoriser tous les utilisateur·rices", "selectedUsers": "Utilisateur·rices sélectionné.e.s", "allLibraries": "Autoriser toutes les bibliothèques", - "selectedLibraries": "Bibliothèques sélectionnées" + "selectedLibraries": "Bibliothèques sélectionnées", + "allowWriteAccess": "" }, "sections": { "status": "Statut", @@ -398,7 +406,8 @@ "librariesRequired": "Cette extension nécessite l'accès aux information de la bibliothèque. Sélectionnez à quelles bibliothèque cette extension a accès, ou sélectionnez 'Autoriser toutes les bibliothèques'.", "requiredHosts": "Hôtes requis", "configValidationError": "Erreur lors de la validation de la configuration", - "schemaRenderError": "Impossible de processer la configuration. Le schéma de l'extension n'est peut-être pas valide." + "schemaRenderError": "Impossible de processer la configuration. Le schéma de l'extension n'est peut-être pas valide.", + "allowWriteAccessHelp": "" }, "placeholders": { "configKey": "clef", @@ -674,7 +683,8 @@ "exportSuccess": "La configuration a été copiée vers le presse-papier au format TOML", "exportFailed": "Une erreur est survenue en copiant la configuration", "devFlagsHeader": "Options de développement (peuvent être amenés à changer / être supprimés)", - "devFlagsComment": "Ces paramètres sont expérimentaux et peuvent être amenés à changer dans le futur" + "devFlagsComment": "Ces paramètres sont expérimentaux et peuvent être amenés à changer dans le futur", + "downloadToml": "Télécharger la configuration (TOML)" } }, "activity": { diff --git a/resources/i18n/gl.json b/resources/i18n/gl.json index 32d0d919f..c10560912 100644 --- a/resources/i18n/gl.json +++ b/resources/i18n/gl.json @@ -37,7 +37,8 @@ "sampleRate": "Taxa de mostra", "missing": "Falta", "libraryName": "Biblioteca", - "composer": "Composición" + "composer": "Composición", + "disc": "" }, "actions": { "addToQueue": "Ao final da cola", @@ -218,13 +219,19 @@ "saveQueue": "Salvar a Cola como Lista de reprodución", "searchOrCreate": "Buscar listas ou escribe para crear nova…", "pressEnterToCreate": "Preme Enter para crear nova lista", - "removeFromSelection": "Retirar da selección" + "removeFromSelection": "Retirar da selección", + "uploadCover": "", + "removeCover": "" }, "message": { "duplicate_song": "Engadir cancións duplicadas", "song_exist": "Hai duplicadas que serán engadidas á lista de reprodución. Desexas engadir as duplicadas ou omitilas?", "noPlaylistsFound": "Sen listas de reprodución", - "noPlaylists": "Sen listas dispoñibles" + "noPlaylists": "Sen listas dispoñibles", + "coverUploaded": "", + "coverRemoved": "", + "coverUploadError": "", + "coverRemoveError": "" } }, "radio": { @@ -353,7 +360,8 @@ "allUsers": "Para todas as usuarias", "selectedUsers": "Usuarias seleccionadas", "allLibraries": "Permitir todas as bibliotecas", - "selectedLibraries": "Selecciona bibliotecas" + "selectedLibraries": "Selecciona bibliotecas", + "allowWriteAccess": "" }, "sections": { "status": "Estado", @@ -398,7 +406,8 @@ "librariesRequired": "O complemento precisa acceso á información sobre a biblioteca. Selecciona as bibliotecas ás que pode acceder, ou activa 'Todas as bibliotecas'.", "requiredHosts": "Servidores requeridos", "configValidationError": "Fallou a comprobación da configuración:", - "schemaRenderError": "Non se puido aplicar a configuración. O esquema do complemento podería non ser válido." + "schemaRenderError": "Non se puido aplicar a configuración. O esquema do complemento podería non ser válido.", + "allowWriteAccessHelp": "" }, "placeholders": { "configKey": "clave", @@ -674,7 +683,8 @@ "exportSuccess": "Configuración exportada ao portapapeis no formato TOML", "exportFailed": "Fallou a copia da configuración", "devFlagsHeader": "Configuracións de Desenvolvemento (suxeitas a cambio/retirada)", - "devFlagsComment": "Son axustes experimentais e poden retirarse en futuras versións" + "devFlagsComment": "Son axustes experimentais e poden retirarse en futuras versións", + "downloadToml": "Descargar configuración (TOML)" } }, "activity": { diff --git a/resources/i18n/ru.json b/resources/i18n/ru.json index 5b20c3e19..6f0439a38 100644 --- a/resources/i18n/ru.json +++ b/resources/i18n/ru.json @@ -37,7 +37,8 @@ "sampleRate": "Частота дискретизации (Hz)", "missing": "Поле отсутствует", "libraryName": "Библиотека", - "composer": "Композитор" + "composer": "Композитор", + "disc": "" }, "actions": { "addToQueue": "В очередь", @@ -218,13 +219,19 @@ "saveQueue": "Сохранить очередь в плейлист", "searchOrCreate": "Поиск плейлистов или введите текст для создания новых...", "pressEnterToCreate": "Нажмите Enter, чтобы создать новый список воспроизведения", - "removeFromSelection": "Удалить из списка выделенных" + "removeFromSelection": "Удалить из списка выделенных", + "uploadCover": "", + "removeCover": "" }, "message": { "duplicate_song": "Повторяющиеся треки", "song_exist": "Некоторые треки уже есть в плейлисте. Вы хотите добавить их или пропустить?", "noPlaylistsFound": "Плейлисты не найдены", - "noPlaylists": "Нет доступных плейлистов" + "noPlaylists": "Нет доступных плейлистов", + "coverUploaded": "", + "coverRemoved": "", + "coverUploadError": "", + "coverRemoveError": "" } }, "radio": { @@ -353,7 +360,8 @@ "allUsers": "Разрешить всем пользователям", "selectedUsers": "Выбранные пользователи", "allLibraries": "Разрешить доступ ко всем библиотекам", - "selectedLibraries": "Избранные библиотеки" + "selectedLibraries": "Избранные библиотеки", + "allowWriteAccess": "" }, "sections": { "status": "Статус", @@ -398,7 +406,8 @@ "librariesRequired": "Этому плагину требуется доступ к библиотечной информации. Выберите, к каким библиотекам плагин может получить доступ, или включите \"Разрешить все библиотеки\".", "requiredHosts": "Необходимые хосты", "configValidationError": "Проверка конфигурации завершилась неудачей:", - "schemaRenderError": "Не удалось отобразить форму конфигурации. Возможно, схема плагина недействительна." + "schemaRenderError": "Не удалось отобразить форму конфигурации. Возможно, схема плагина недействительна.", + "allowWriteAccessHelp": "" }, "placeholders": { "configKey": "ключ", @@ -674,7 +683,8 @@ "exportSuccess": "Конфигурация экспортирована в буфер обмена в формате TOML", "exportFailed": "Не удалось скопировать конфигурацию", "devFlagsHeader": "Флаги разработки (могут быть изменены/удалены)", - "devFlagsComment": "Это экспериментальные настройки, которые могут быть удалены в будущих версиях." + "devFlagsComment": "Это экспериментальные настройки, которые могут быть удалены в будущих версиях.", + "downloadToml": "Скачать конфигурацию (TOML)" } }, "activity": { diff --git a/resources/i18n/sl.json b/resources/i18n/sl.json index f499d6ad5..2ea1cad49 100644 --- a/resources/i18n/sl.json +++ b/resources/i18n/sl.json @@ -37,7 +37,8 @@ "sampleRate": "Frekvenca vzorčenja", "missing": "Manjka", "libraryName": "Knjižnica", - "composer": "Skladatelj" + "composer": "Skladatelj", + "disc": "" }, "actions": { "addToQueue": "Predvajaj kasneje", @@ -48,7 +49,7 @@ "playNext": "Naslednji", "info": "Več informacij", "showInPlaylist": "Prikaži na seznamu predvajanja", - "instantMix": "" + "instantMix": "Instant Mix" } }, "album": { @@ -218,13 +219,19 @@ "saveQueue": "Shrani čakalno vrsto na seznam predvajanja", "searchOrCreate": "Iščite po seznamih predvajanja ali vnesite besedilo, da ustvarite nove ...", "pressEnterToCreate": "Pritisnite Enter za ustvarjanje novega seznama predvajanja", - "removeFromSelection": "Odstrani iz izbora" + "removeFromSelection": "Odstrani iz izbora", + "uploadCover": "", + "removeCover": "" }, "message": { "duplicate_song": "Dodaj podvojene pesmi", "song_exist": "Seznamu predvajanja boste dodali duplikate. Jih želite dodati ali izpustiti?", "noPlaylistsFound": "Ni najdenih seznamov predvajanja", - "noPlaylists": "Ni na voljo seznamov predvajanja" + "noPlaylists": "Ni na voljo seznamov predvajanja", + "coverUploaded": "", + "coverRemoved": "", + "coverUploadError": "", + "coverRemoveError": "" } }, "radio": { @@ -353,7 +360,8 @@ "allUsers": "Dovoli vsem uporabnikom", "selectedUsers": "Izbrani uporabniki", "allLibraries": "Dovoli vse knjižnice", - "selectedLibraries": "Izbrane knjižnice" + "selectedLibraries": "Izbrane knjižnice", + "allowWriteAccess": "" }, "sections": { "status": "Status", @@ -397,8 +405,9 @@ "noLibraries": "Ni izbranih knjižnic", "librariesRequired": "Vtičnik zahteva dostop do knjižnih informacij. Izberi do katerih knjižnic lahko dostopa, ali vključi dostop do vseh knjižnic.", "requiredHosts": "Zahtevani gostitelji", - "configValidationError": "", - "schemaRenderError": "" + "configValidationError": "Validacija konfiguracije neuspešna:", + "schemaRenderError": "Konfiguracijskega obrazca ni mogoče upodobiti. Shema vtičnika je morda neveljavna.", + "allowWriteAccessHelp": "" }, "placeholders": { "configKey": "ključ", @@ -588,7 +597,7 @@ "remove_all_missing_content": "Ste prepričani, da želite odstraniti vse manjkajoče datoteke iz baze? Trajno boste odstranili vse reference nanje, vključno s številom predvajanj in ocenami.", "noSimilarSongsFound": "Ni najdenih podobnih pesmi", "noTopSongsFound": "Ni najdenih najboljših pesmi", - "startingInstantMix": "" + "startingInstantMix": "Nalaganje Instant Mix..." }, "menu": { "library": "Knjižnica", @@ -674,7 +683,8 @@ "exportSuccess": "Konfiguracija izvožena v odložišče v formatu TOML", "exportFailed": "Kopiranje konfiguracije ni uspelo", "devFlagsHeader": "Razvojne zastavice (lahko se spremenijo/odstranijo)", - "devFlagsComment": "To so eksperimentalne nastavitve in bodo morda odstranjene v prihodnjih različicah" + "devFlagsComment": "To so eksperimentalne nastavitve in bodo morda odstranjene v prihodnjih različicah", + "downloadToml": "Naloži konfiguracijo (TOML)" } }, "activity": { diff --git a/resources/i18n/sv.json b/resources/i18n/sv.json index 5896b4ed9..b792943d0 100644 --- a/resources/i18n/sv.json +++ b/resources/i18n/sv.json @@ -37,7 +37,8 @@ "sampleRate": "Samplingsfrekvens", "missing": "Saknade", "libraryName": "Bibliotek", - "composer": "Kompositör" + "composer": "Kompositör", + "disc": "" }, "actions": { "addToQueue": "Lägg till i kön", @@ -218,13 +219,19 @@ "saveQueue": "Spara kö till spellista", "searchOrCreate": "Sök spellista eller skapa ny...", "pressEnterToCreate": "Tryck Enter för att skapa ny spellista", - "removeFromSelection": "Ta bort från urval" + "removeFromSelection": "Ta bort från urval", + "uploadCover": "", + "removeCover": "" }, "message": { "duplicate_song": "Lägg till dubletter", "song_exist": "Vissa låtar finns redan i spellistan. Vill du lägga till dubbletterna eller hoppa över dem?", "noPlaylistsFound": "Hittade inga spellistor", - "noPlaylists": "Inga spellistor tillgängliga" + "noPlaylists": "Inga spellistor tillgängliga", + "coverUploaded": "", + "coverRemoved": "", + "coverUploadError": "", + "coverRemoveError": "" } }, "radio": { @@ -353,7 +360,8 @@ "allUsers": "Tillåt alla användare", "selectedUsers": "Valda användare", "allLibraries": "Tillåt alla bibliotek", - "selectedLibraries": "Valda bibliotek" + "selectedLibraries": "Valda bibliotek", + "allowWriteAccess": "" }, "sections": { "status": "Status", @@ -398,7 +406,8 @@ "librariesRequired": "Detta tillägg kräver tillgång till biblioteksinformation. Välj vilka bibliotek tillägget kan komma åt eller aktivera 'Tillåt alla bibliotek'.", "requiredHosts": "Krävda värdar", "configValidationError": "Validering av konfigurationen misslyckades:", - "schemaRenderError": "Kunde inte rendera konfigurationsformuläret. Tilläggets schema kan vara ogiltigt." + "schemaRenderError": "Kunde inte rendera konfigurationsformuläret. Tilläggets schema kan vara ogiltigt.", + "allowWriteAccessHelp": "" }, "placeholders": { "configKey": "nyckel", @@ -674,7 +683,8 @@ "exportSuccess": "Inställningarna kopierade till urklippet i TOML-format", "exportFailed": "Kopiering av inställningarna misslyckades", "devFlagsHeader": "Utvecklingsflaggor (kan ändras eller tas bort)", - "devFlagsComment": "Dessa inställningar är experimentella och kan tas bort i framtida versioner" + "devFlagsComment": "Dessa inställningar är experimentella och kan tas bort i framtida versioner", + "downloadToml": "Ladda ner konfiguration (TOML)" } }, "activity": { diff --git a/resources/i18n/th.json b/resources/i18n/th.json index 45a5e5f34..d3d64bdbf 100644 --- a/resources/i18n/th.json +++ b/resources/i18n/th.json @@ -37,7 +37,8 @@ "sampleRate": "แซมเปิ้ลเรต", "missing": "หายไป", "libraryName": "ห้องสมุด", - "composer": "ผู้แต่ง" + "composer": "ผู้แต่ง", + "disc": "" }, "actions": { "addToQueue": "เพิ่มในคิว", @@ -48,7 +49,7 @@ "playNext": "เล่นถัดไป", "info": "ดูรายละเอียด", "showInPlaylist": "แสดงในเพลย์ลิสต์", - "instantMix": "" + "instantMix": "อินสแตนต์ มิก" } }, "album": { @@ -218,13 +219,19 @@ "saveQueue": "บันทึกคิวลงเพลย์ลิสต์", "searchOrCreate": "ค้นหาเพลย์ลิสต์หรือพิมพ์เพื่อสร้างใหม่", "pressEnterToCreate": "กด Enter เพื่อสร้างเพลย์ลิสต์", - "removeFromSelection": "เอาออกจากที่เลือกไว้" + "removeFromSelection": "เอาออกจากที่เลือกไว้", + "uploadCover": "", + "removeCover": "" }, "message": { "duplicate_song": "เพิ่มเพลงซ้ำ", "song_exist": "เพิ่มเพลงซ้ำกันในเพลย์ลิสต์ คุณจะเพิ่มเพลงต่อหรือข้าม", "noPlaylistsFound": "ไม่พบเพลย์ลิสต์", - "noPlaylists": "ไม่มีเพลย์ลิสต์อยู่" + "noPlaylists": "ไม่มีเพลย์ลิสต์อยู่", + "coverUploaded": "", + "coverRemoved": "", + "coverUploadError": "", + "coverRemoveError": "" } }, "radio": { @@ -353,7 +360,8 @@ "allUsers": "อนุญาติผู้ใช้ทั้งหมด", "selectedUsers": "ผู้ใช้ถูกเลือก", "allLibraries": "อนุญาติห้องสมุดเพลงทั้งหมด", - "selectedLibraries": "ห้องสมุดเพลงถูกเลือก" + "selectedLibraries": "ห้องสมุดเพลงถูกเลือก", + "allowWriteAccess": "" }, "sections": { "status": "สถานะ", @@ -398,7 +406,8 @@ "librariesRequired": "ปลั๊กอินนี้ต้องการเข้าถึงข้อมูลห้องสมุดเพลง เลือกห้องสมุดเพลงที่ต้องการให้ปลั๊กอินเข้าถึงหรือเปิดใช้งานกับห้องสมุดเพลงทั้งหมด", "requiredHosts": "ต้องการ Host", "configValidationError": "การตั้งค่าเกิดความผิดพลาด", - "schemaRenderError": "ไม่สามารถแสดงหน้าจอการตั้งค่า อาจเกิดจากความผิดพลาดจากปลั๊กอิน" + "schemaRenderError": "ไม่สามารถแสดงหน้าจอการตั้งค่า อาจเกิดจากความผิดพลาดจากปลั๊กอิน", + "allowWriteAccessHelp": "" }, "placeholders": { "configKey": "คีย์", @@ -588,7 +597,7 @@ "remove_all_missing_content": "คุณแน่ใจว่าจะเอารายการไฟล์ที่หายไปออกจากดาต้าเบส นี่จะเป็นการลบข้อมูลอ้างอิงทั้งหมดของไฟล์ออกอย่างถาวร", "noSimilarSongsFound": "ไม่มีเพลงคล้ายกัน", "noTopSongsFound": "ไม่พบเพลงยอดนิยม", - "startingInstantMix": "" + "startingInstantMix": "กำลังโหลดอินสแตนท์ มิก..." }, "menu": { "library": "ห้องสมุดเพลง", @@ -674,7 +683,8 @@ "exportSuccess": "นำออกการตั้งค่าไปยังคลิปบอร์ดในรูปแบบ TOML แล้ว", "exportFailed": "คัดลอกการตั้งค่าล้มเหลว", "devFlagsHeader": "ปักธงการพัฒนา (อาจมีการเปลี่ยน/เอาออก)", - "devFlagsComment": "การตั้งค่านี้อยู่ในช่วงทดลองและอาจจะมีการเอาออกในเวอร์ชั่นหลัง" + "devFlagsComment": "การตั้งค่านี้อยู่ในช่วงทดลองและอาจจะมีการเอาออกในเวอร์ชั่นหลัง", + "downloadToml": "ดาวน์โหลดการตั้งค่า (TOML)" } }, "activity": { diff --git a/resources/i18n/zh-Hant.json b/resources/i18n/zh-Hant.json index 1bb59a8b1..aca2e4742 100644 --- a/resources/i18n/zh-Hant.json +++ b/resources/i18n/zh-Hant.json @@ -10,19 +10,14 @@ "playCount": "播放次數", "title": "標題", "artist": "藝人", - "composer": "作曲者", "album": "專輯", "path": "檔案路徑", - "libraryName": "媒體庫", "genre": "曲風", "compilation": "合輯", "year": "發行年份", "size": "檔案大小", "updatedAt": "更新於", "bitRate": "位元率", - "bitDepth": "位元深度", - "sampleRate": "取樣率", - "channels": "聲道", "discSubtitle": "光碟副標題", "starred": "收藏", "comment": "註解", @@ -30,6 +25,7 @@ "quality": "品質", "bpm": "BPM", "playDate": "上次播放", + "channels": "聲道", "createdAt": "建立於", "grouping": "分組", "mood": "情緒", @@ -37,17 +33,22 @@ "tags": "額外標籤", "mappedTags": "分類後標籤", "rawTags": "原始標籤", - "missing": "遺失" + "bitDepth": "位元深度", + "sampleRate": "取樣率", + "missing": "遺失", + "libraryName": "媒體庫", + "composer": "作曲者", + "disc": "" }, "actions": { "addToQueue": "加入至播放佇列", "playNow": "立即播放", "addToPlaylist": "加入至播放清單", - "showInPlaylist": "在播放清單中顯示", "shuffleAll": "全部隨機播放", "download": "下載", "playNext": "下一首播放", "info": "取得資訊", + "showInPlaylist": "在播放清單中顯示", "instantMix": "即時混音" } }, @@ -59,38 +60,38 @@ "duration": "長度", "songCount": "歌曲數", "playCount": "播放次數", - "size": "檔案大小", "name": "名稱", - "libraryName": "媒體庫", "genre": "曲風", "compilation": "合輯", "year": "發行年份", - "date": "錄製日期", - "originalDate": "原始日期", - "releaseDate": "發行日期", - "releases": "發行", - "released": "已發行", "updatedAt": "更新於", "comment": "註解", "rating": "評分", "createdAt": "建立於", + "size": "檔案大小", + "originalDate": "原始日期", + "releaseDate": "發行日期", + "releases": "發行", + "released": "已發行", "recordLabel": "唱片公司", "catalogNum": "目錄編號", "releaseType": "發行類型", "grouping": "分組", "media": "媒體類型", "mood": "情緒", - "missing": "遺失" + "date": "錄製日期", + "missing": "遺失", + "libraryName": "媒體庫" }, "actions": { "playAll": "播放全部", "playNext": "下一首播放", "addToQueue": "加入至播放佇列", - "share": "分享", "shuffle": "隨機播放", "addToPlaylist": "加入至播放清單", "download": "下載", - "info": "取得資訊" + "info": "取得資訊", + "share": "分享" }, "lists": { "all": "所有", @@ -108,10 +109,10 @@ "name": "名稱", "albumCount": "專輯數", "songCount": "歌曲數", - "size": "檔案大小", "playCount": "播放次數", "rating": "評分", "genre": "曲風", + "size": "檔案大小", "role": "參與角色", "missing": "遺失" }, @@ -132,9 +133,9 @@ "maincredit": "專輯藝人或藝人 |||| 專輯藝人或藝人" }, "actions": { - "topSongs": "熱門歌曲", "shuffle": "隨機播放", - "radio": "電台" + "radio": "電台", + "topSongs": "熱門歌曲" } }, "user": { @@ -143,7 +144,6 @@ "userName": "使用者名稱", "isAdmin": "管理員", "lastLoginAt": "上次登入", - "lastAccessAt": "上次存取", "updatedAt": "更新於", "name": "名稱", "password": "密碼", @@ -152,6 +152,7 @@ "currentPassword": "目前密碼", "newPassword": "新密碼", "token": "權杖", + "lastAccessAt": "上次存取", "libraries": "媒體庫" }, "helperTexts": { @@ -163,14 +164,14 @@ "updated": "使用者已更新", "deleted": "使用者已刪除" }, - "validation": { - "librariesRequired": "非管理員使用者必須至少選擇一個媒體庫" - }, "message": { "listenBrainzToken": "輸入您的 ListenBrainz 使用者權杖", "clickHereForToken": "點擊此處來獲得您的 ListenBrainz 權杖", "selectAllLibraries": "選取全部媒體庫", "adminAutoLibraries": "管理員預設可存取所有媒體庫" + }, + "validation": { + "librariesRequired": "非管理員使用者必須至少選擇一個媒體庫" } }, "player": { @@ -213,18 +214,24 @@ "selectPlaylist": "選取播放清單:", "addNewPlaylist": "建立「%{name}」", "export": "匯出", - "saveQueue": "將播放佇列儲存到播放清單", "makePublic": "設為公開", "makePrivate": "設為私人", + "saveQueue": "將播放佇列儲存到播放清單", "searchOrCreate": "搜尋播放清單,或輸入名稱來新建…", "pressEnterToCreate": "按 Enter 鍵建立新的播放清單", - "removeFromSelection": "移除選取項目" + "removeFromSelection": "移除選取項目", + "uploadCover": "上傳封面", + "removeCover": "移除封面" }, "message": { "duplicate_song": "加入重複的歌曲", "song_exist": "有重複歌曲正要加入播放清單,您要加入或略過重複歌曲?", "noPlaylistsFound": "找不到播放清單", - "noPlaylists": "暫無播放清單" + "noPlaylists": "暫無播放清單", + "coverUploaded": "已更新封面圖", + "coverRemoved": "已移除封面圖", + "coverUploadError": "上傳封面圖時發生錯誤", + "coverRemoveError": "移除封面圖時發生錯誤" } }, "radio": { @@ -246,7 +253,6 @@ "username": "分享者", "url": "網址", "description": "描述", - "downloadable": "允許下載?", "contents": "內容", "expiresAt": "過期時間", "lastVisitedAt": "上次造訪時間", @@ -254,19 +260,17 @@ "format": "格式", "maxBitRate": "最大位元率", "updatedAt": "更新於", - "createdAt": "建立於" - }, - "notifications": {}, - "actions": {} + "createdAt": "建立於", + "downloadable": "允許下載?" + } }, "missing": { "name": "遺失檔案 |||| 遺失檔案", - "empty": "無遺失檔案", "fields": { "path": "路徑", "size": "檔案大小", - "libraryName": "媒體庫", - "updatedAt": "遺失於" + "updatedAt": "遺失於", + "libraryName": "媒體庫" }, "actions": { "remove": "刪除", @@ -274,7 +278,8 @@ }, "notifications": { "removed": "遺失檔案已刪除" - } + }, + "empty": "無遺失檔案" }, "library": { "name": "媒體庫 |||| 媒體庫", @@ -304,20 +309,20 @@ }, "actions": { "scan": "掃描媒體庫", - "quickScan": "快速掃描", - "fullScan": "完整掃描", "manageUsers": "管理使用者權限", - "viewDetails": "查看詳細資料" + "viewDetails": "查看詳細資料", + "quickScan": "快速掃描", + "fullScan": "完整掃描" }, "notifications": { "created": "成功建立媒體庫", "updated": "成功更新媒體庫", "deleted": "成功刪除媒體庫", "scanStarted": "開始掃描媒體庫", + "scanCompleted": "媒體庫掃描完成", "quickScanStarted": "快速掃描已開始", "fullScanStarted": "完整掃描已開始", - "scanError": "掃描啟動失敗,請檢查日誌", - "scanCompleted": "媒體庫掃描完成" + "scanError": "掃描啟動失敗,請檢查日誌" }, "validation": { "nameRequired": "請輸入媒體庫名稱", @@ -355,7 +360,8 @@ "allUsers": "允許所有使用者", "selectedUsers": "選定的使用者", "allLibraries": "允許所有媒體庫", - "selectedLibraries": "選定的媒體庫" + "selectedLibraries": "選定的媒體庫", + "allowWriteAccess": "允許寫入權限" }, "sections": { "status": "狀態", @@ -389,8 +395,6 @@ }, "messages": { "configHelp": "使用鍵值對設定插件。若插件無需設定則留空。", - "configValidationError": "設定驗證失敗:", - "schemaRenderError": "無法顯示設定表單。插件的 schema 可能無效。", "clickPermissions": "點擊權限以查看詳細資訊", "noConfig": "無設定", "allUsersHelp": "啟用後,插件將可存取所有使用者,包含未來建立的使用者。", @@ -400,7 +404,10 @@ "allLibrariesHelp": "啟用後,插件將可存取所有媒體庫,包含未來建立的媒體庫。", "noLibraries": "未選擇媒體庫", "librariesRequired": "此插件需要存取媒體庫資訊。請選擇插件可存取的媒體庫,或啟用「允許所有媒體庫」。", - "requiredHosts": "必要的 Hosts" + "requiredHosts": "必要的 Hosts", + "configValidationError": "設定驗證失敗:", + "schemaRenderError": "無法顯示設定表單。插件的 schema 可能無效。", + "allowWriteAccessHelp": "啟用後,插件可以修改媒體庫目錄中的檔案。 預設情況下,插件具有唯讀權限。" }, "placeholders": { "configKey": "鍵", @@ -443,7 +450,6 @@ "add": "加入", "back": "返回", "bulk_actions": "選中 1 項 |||| 選中 %{smart_count} 項", - "bulk_actions_mobile": "1 |||| %{smart_count}", "cancel": "取消", "clear_input_value": "清除", "clone": "複製", @@ -467,6 +473,7 @@ "close_menu": "關閉選單", "unselect": "取消選取", "skip": "略過", + "bulk_actions_mobile": "1 |||| %{smart_count}", "share": "分享", "download": "下載" }, @@ -558,48 +565,42 @@ "transcodingDisabled": "出於安全原因,已禁用了從 Web 介面更改參數。要更改(編輯或新增)轉碼選項,請在啟用 %{config} 設定選項的情況下重新啟動伺服器。", "transcodingEnabled": "Navidrome 目前與 %{config} 一起使用,因此可以透過 Web 介面從轉碼設定中執行系統命令。出於安全考慮,我們建議停用此功能,並僅在設定轉碼選項時啟用。", "songsAddedToPlaylist": "已加入一首歌到播放清單 |||| 已新增 %{smart_count} 首歌到播放清單", - "noSimilarSongsFound": "找不到相似歌曲", - "startingInstantMix": "正在載入即時混音...", - "noTopSongsFound": "找不到熱門歌曲", "noPlaylistsAvailable": "沒有可用的播放清單", "delete_user_title": "刪除使用者「%{name}」", "delete_user_content": "您確定要刪除此使用者及其所有資料(包括播放清單和偏好設定)嗎?", - "remove_missing_title": "刪除遺失檔案", - "remove_missing_content": "您確定要從媒體庫中刪除所選的遺失的檔案嗎?這將永久刪除它們的所有相關資訊,包括其播放次數和評分。", - "remove_all_missing_title": "刪除所有遺失檔案", - "remove_all_missing_content": "您確定要從媒體庫中刪除所有遺失的檔案嗎?這將永久刪除它們的所有相關資訊,包括它們的播放次數和評分。", "notifications_blocked": "您已在瀏覽器設定中封鎖了此網站的通知", "notifications_not_available": "此瀏覽器不支援桌面通知,或您並非透過 HTTPS 存取 Navidrome", "lastfmLinkSuccess": "已成功連接 Last.fm 並開啟音樂記錄", "lastfmLinkFailure": "無法連接 Last.fm", "lastfmUnlinkSuccess": "已取消與 Last.fm 的連接並停用音樂記錄", "lastfmUnlinkFailure": "無法取消與 Last.fm 的連接", - "listenBrainzLinkSuccess": "已成功以 %{user} 的身份連接 ListenBrainz 並開啟音樂記錄", - "listenBrainzLinkFailure": "無法連接 ListenBrainz:%{error}", - "listenBrainzUnlinkSuccess": "已取消與 ListenBrainz 的連接並停用音樂記錄", - "listenBrainzUnlinkFailure": "無法取消與 ListenBrainz 的連接", "openIn": { "lastfm": "在 Last.fm 中開啟", "musicbrainz": "在 MusicBrainz 中開啟" }, "lastfmLink": "查看更多…", + "listenBrainzLinkSuccess": "已成功以 %{user} 的身份連接 ListenBrainz 並開啟音樂記錄", + "listenBrainzLinkFailure": "無法連接 ListenBrainz:%{error}", + "listenBrainzUnlinkSuccess": "已取消與 ListenBrainz 的連接並停用音樂記錄", + "listenBrainzUnlinkFailure": "無法取消與 ListenBrainz 的連接", + "downloadOriginalFormat": "下載原始格式", "shareOriginalFormat": "分享原始格式", "shareDialogTitle": "分享 %{resource} '%{name}'", "shareBatchDialogTitle": "分享 1 個%{resource} |||| 分享 %{smart_count} 個%{resource}", - "shareCopyToClipboard": "複製到剪貼簿:Ctrl+C, Enter", "shareSuccess": "分享成功,連結已複製到剪貼簿:%{url}", "shareFailure": "分享連結複製失敗:%{url}", "downloadDialogTitle": "下載 %{resource} '%{name}' (%{size})", - "downloadOriginalFormat": "下載原始格式" + "shareCopyToClipboard": "複製到剪貼簿:Ctrl+C, Enter", + "remove_missing_title": "刪除遺失檔案", + "remove_missing_content": "您確定要從媒體庫中刪除所選的遺失的檔案嗎?這將永久刪除它們的所有相關資訊,包括其播放次數和評分。", + "remove_all_missing_title": "刪除所有遺失檔案", + "remove_all_missing_content": "您確定要從媒體庫中刪除所有遺失的檔案嗎?這將永久刪除它們的所有相關資訊,包括它們的播放次數和評分。", + "noSimilarSongsFound": "找不到相似歌曲", + "noTopSongsFound": "找不到熱門歌曲", + "startingInstantMix": "正在載入即時混音..." }, "menu": { "library": "媒體庫", - "librarySelector": { - "allLibraries": "所有媒體庫 (%{count})", - "multipleLibraries": "已選 %{selected} 共 %{total} 媒體庫", - "selectLibraries": "選取媒體庫", - "none": "無" - }, "settings": "設定", "version": "版本", "theme": "主題", @@ -610,7 +611,6 @@ "language": "語言", "defaultView": "預設畫面", "desktop_notifications": "桌面通知", - "lastfmNotConfigured": "Last.fm API 金鑰未設定", "lastfmScrobbling": "啟用 Last.fm 音樂記錄", "listenBrainzScrobbling": "啟用 ListenBrainz 音樂記錄", "replaygain": "重播增益模式", @@ -619,13 +619,20 @@ "none": "無", "album": "專輯增益", "track": "曲目增益" - } + }, + "lastfmNotConfigured": "Last.fm API 金鑰未設定" } }, "albumList": "專輯", + "about": "關於", "playlists": "播放清單", "sharedPlaylists": "分享的播放清單", - "about": "關於" + "librarySelector": { + "allLibraries": "所有媒體庫 (%{count})", + "multipleLibraries": "已選 %{selected} 共 %{total} 媒體庫", + "selectLibraries": "選取媒體庫", + "none": "無" + } }, "player": { "playListsText": "播放佇列", @@ -676,7 +683,8 @@ "exportSuccess": "設定已以 TOML 格式匯出至剪貼簿", "exportFailed": "設定複製失敗", "devFlagsHeader": "開發旗標(可能會更改/刪除)", - "devFlagsComment": "這些是實驗性設定,可能會在未來版本中刪除" + "devFlagsComment": "這些是實驗性設定,可能會在未來版本中刪除", + "downloadToml": "下載設定檔 (TOML)" } }, "activity": { @@ -684,17 +692,12 @@ "totalScanned": "已掃描的資料夾總數", "quickScan": "快速掃描", "fullScan": "完全掃描", - "selectiveScan": "選擇性掃描", "serverUptime": "伺服器運作時間", "serverDown": "伺服器已離線", "scanType": "掃描類型", "status": "掃描錯誤", - "elapsedTime": "經過時間" - }, - "nowPlaying": { - "title": "正在播放", - "empty": "無播放內容", - "minutesAgo": "1 分鐘前 |||| %{smart_count} 分鐘前" + "elapsedTime": "經過時間", + "selectiveScan": "選擇性掃描" }, "help": { "title": "Navidrome 快捷鍵", @@ -704,10 +707,15 @@ "toggle_play": "播放/暫停", "prev_song": "上一首歌", "next_song": "下一首歌", - "current_song": "前往目前歌曲", "vol_up": "提高音量", "vol_down": "降低音量", - "toggle_love": "新增此歌曲至收藏" + "toggle_love": "新增此歌曲至收藏", + "current_song": "前往目前歌曲" } + }, + "nowPlaying": { + "title": "正在播放", + "empty": "無播放內容", + "minutesAgo": "1 分鐘前 |||| %{smart_count} 分鐘前" } -} +} \ No newline at end of file From ab8a58157a0b1d25ba699e2751c8c961aa9ab4f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Sun, 15 Mar 2026 22:19:55 -0400 Subject: [PATCH 26/81] feat: add artist image uploads and image-folder artwork source (#5198) * feat: add shared ImageUploadService for entity image management * feat: add UploadedImage field and methods to Artist model * feat: add uploaded_image column to artist table * feat: add ArtistImageFolder config option * refactor: wire ImageUploadService and delegate playlist file ops to it Wire ImageUploadService into the DI container and refactor the playlist service to delegate image file operations (SetImage/RemoveImage) to the shared ImageUploadService, removing duplicated file I/O logic. A local ImageUploadService interface is defined in core/playlists to avoid an import cycle between core and core/playlists. * feat: artist artwork reader checks uploaded image first * feat: add image-folder priority source for artist artwork * feat: cache key invalidation for image-folder and uploaded images * refactor: extract shared image upload HTTP helpers * feat: add artist image upload/delete API endpoints * refactor: playlist handlers use shared image upload helpers * feat: add shared ImageUploadOverlay component * feat: add i18n keys for artist image upload * feat: add image upload overlay to artist detail pages * refactor: playlist details uses shared ImageUploadOverlay component * fix: add gosec nolint directive for ParseMultipartForm * refactor: deduplicate image upload code and optimize dir scanning - Remove dead ImageFilename methods from Artist and Playlist models (production code uses core.imageFilename exclusively) - Extract shared uploadedImagePath helper in model/image.go - Extract findImageInArtistFolder to deduplicate dir-scanning logic between fromArtistImageFolder and getArtistImageFolderModTime - Fix fileInputRef in useCallback dependency array * fix: include artist UpdatedAt in artwork cache key Without this, uploading or deleting an artist image would not invalidate the cached artwork because the cache key was only based on album folder timestamps, not the artist's own UpdatedAt field. * feat: add Portuguese translations for artist image upload * refactor: use shared i18n keys for cover art upload messages Move cover art upload/remove translations from per-entity sections (artist, playlist) to a shared top-level "message" section, avoiding duplication across entity types and translation files. * refactor: move cover art i18n keys to shared message section for all languages * refactor: simplify image upload code and eliminate redundancies Extracted duplicate image loading/lightbox state logic from DesktopArtistDetails and MobileArtistDetails into a shared useArtistImageState hook. Moved entity type constants to the consts package and replaced raw string literals throughout model, core, and nativeapi packages. Exported model.UploadedImagePath and reused it in core/image_upload.go to consolidate path construction. Cached the ArtistImageFolder lookup result in artistReader to eliminate a redundant os.ReadDir call on every artwork request. Signed-off-by: Deluan * style: fix prettier formatting in ImageUploadOverlay * fix: address code review feedback on image upload error handling - RemoveImage now returns errors instead of swallowing them - Artist handlers distinguish not-found from other DB errors - Defer multipart temp file cleanup after parsing * fix: enforce hard request size limit with MaxBytesReader for image uploads Signed-off-by: Deluan --------- Signed-off-by: Deluan --- cmd/scan.go | 3 +- cmd/wire_gen.go | 14 +- conf/configuration.go | 1 + consts/consts.go | 6 + core/artwork/reader_artist.go | 79 +++++- core/artwork/reader_artist_test.go | 253 ++++++++++++++++++ core/image_upload.go | 71 +++++ core/image_upload_test.go | 99 +++++++ core/playlists/import_test.go | 31 +-- core/playlists/playlists.go | 50 ++-- core/playlists/playlists_test.go | 17 +- core/playlists/rest_adapter_test.go | 3 +- core/wire_providers.go | 2 + ...0260315233131_add_artist_uploaded_image.go | 22 ++ model/artist.go | 8 + model/artist_test.go | 30 +++ model/image.go | 17 ++ model/playlist.go | 18 +- model/playlist_test.go | 22 -- resources/i18n/bg.json | 12 +- resources/i18n/ca.json | 12 +- resources/i18n/da.json | 12 +- resources/i18n/de.json | 20 +- resources/i18n/el.json | 12 +- resources/i18n/es.json | 12 +- resources/i18n/fi.json | 20 +- resources/i18n/fr.json | 12 +- resources/i18n/gl.json | 12 +- resources/i18n/pt-br.json | 14 +- resources/i18n/ru.json | 12 +- resources/i18n/sl.json | 12 +- resources/i18n/sv.json | 12 +- resources/i18n/th.json | 12 +- resources/i18n/zh-Hant.json | 20 +- scanner/controller_test.go | 3 +- scanner/scanner_benchmark_test.go | 3 +- scanner/scanner_multilibrary_test.go | 3 +- scanner/scanner_selective_test.go | 3 +- scanner/scanner_test.go | 3 +- server/e2e/e2e_suite_test.go | 6 +- server/e2e/subsonic_multilibrary_test.go | 3 +- server/nativeapi/artists.go | 72 +++++ server/nativeapi/config_test.go | 2 +- server/nativeapi/image_upload.go | 120 +++++++++ server/nativeapi/library_test.go | 2 +- server/nativeapi/native_api.go | 7 +- server/nativeapi/native_api_song_test.go | 2 +- server/nativeapi/playlists.go | 118 +------- server/nativeapi/playlists_test.go | 2 +- server/nativeapi/plugin_test.go | 2 +- ui/src/artist/DesktopArtistDetails.jsx | 48 ++-- ui/src/artist/MobileArtistDetails.jsx | 48 ++-- ui/src/artist/useArtistImageState.js | 46 ++++ ui/src/common/ImageUploadOverlay.jsx | 139 ++++++++++ ui/src/common/index.js | 1 + ui/src/i18n/en.json | 14 +- ui/src/playlist/PlaylistDetails.jsx | 137 +--------- 57 files changed, 1169 insertions(+), 567 deletions(-) create mode 100644 core/image_upload.go create mode 100644 core/image_upload_test.go create mode 100644 db/migrations/20260315233131_add_artist_uploaded_image.go create mode 100644 model/artist_test.go create mode 100644 model/image.go create mode 100644 server/nativeapi/artists.go create mode 100644 server/nativeapi/image_upload.go create mode 100644 ui/src/artist/useArtistImageState.js create mode 100644 ui/src/common/ImageUploadOverlay.jsx diff --git a/cmd/scan.go b/cmd/scan.go index ffb77b108..d8a563396 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -8,6 +8,7 @@ import ( "os" "strings" + "github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/core/playlists" "github.com/navidrome/navidrome/db" "github.com/navidrome/navidrome/log" @@ -74,7 +75,7 @@ func runScanner(ctx context.Context) { sqlDB := db.Db() defer db.Db().Close() ds := persistence.New(sqlDB) - pls := playlists.NewPlaylists(ds) + pls := playlists.NewPlaylists(ds, core.NewImageUploadService()) // Parse targets from command line or file var scanTargets []model.ScanTarget diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index c2f6edaf8..5b9fd648f 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -63,7 +63,8 @@ func CreateNativeAPIRouter(ctx context.Context) *nativeapi.Router { sqlDB := db.Db() dataStore := persistence.New(sqlDB) share := core.NewShare(dataStore) - playlistsPlaylists := playlists.NewPlaylists(dataStore) + imageUploadService := core.NewImageUploadService() + playlistsPlaylists := playlists.NewPlaylists(dataStore, imageUploadService) insights := metrics.GetInstance(dataStore) fileCache := artwork.GetImageCache() fFmpeg := ffmpeg.New() @@ -79,7 +80,7 @@ func CreateNativeAPIRouter(ctx context.Context) *nativeapi.Router { library := core.NewLibrary(dataStore, modelScanner, watcher, broker, manager) user := core.NewUser(dataStore, manager) maintenance := core.NewMaintenance(dataStore) - router := nativeapi.New(dataStore, share, playlistsPlaylists, insights, library, user, maintenance, manager) + router := nativeapi.New(dataStore, share, playlistsPlaylists, insights, library, user, maintenance, manager, imageUploadService) return router } @@ -100,7 +101,8 @@ func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router { archiver := core.NewArchiver(mediaStreamer, dataStore, share) players := core.NewPlayers(dataStore) cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache) - playlistsPlaylists := playlists.NewPlaylists(dataStore) + imageUploadService := core.NewImageUploadService() + playlistsPlaylists := playlists.NewPlaylists(dataStore, imageUploadService) modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlistsPlaylists, metricsMetrics) playTracker := scrobbler.GetPlayTracker(dataStore, broker, manager) playbackServer := playback.GetInstance(dataStore) @@ -169,7 +171,8 @@ func CreateScanner(ctx context.Context) model.Scanner { provider := external.NewProvider(dataStore, agentsAgents) artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider) cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache) - playlistsPlaylists := playlists.NewPlaylists(dataStore) + imageUploadService := core.NewImageUploadService() + playlistsPlaylists := playlists.NewPlaylists(dataStore, imageUploadService) modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlistsPlaylists, metricsMetrics) return modelScanner } @@ -186,7 +189,8 @@ func CreateScanWatcher(ctx context.Context) scanner.Watcher { provider := external.NewProvider(dataStore, agentsAgents) artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider) cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache) - playlistsPlaylists := playlists.NewPlaylists(dataStore) + imageUploadService := core.NewImageUploadService() + playlistsPlaylists := playlists.NewPlaylists(dataStore, imageUploadService) modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlistsPlaylists, metricsMetrics) watcher := scanner.GetWatcher(dataStore, modelScanner) return watcher diff --git a/conf/configuration.go b/conf/configuration.go index dacd42539..9efa352bd 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -71,6 +71,7 @@ type configOptions struct { CoverArtPriority string CoverArtQuality int ArtistArtPriority string + ArtistImageFolder string DiscArtPriority string LyricsPriority string EnableGravatar bool diff --git a/consts/consts.go b/consts/consts.go index 061aebd7b..9f4387ae6 100644 --- a/consts/consts.go +++ b/consts/consts.go @@ -103,6 +103,12 @@ const ( DefaultCacheCleanUpInterval = 10 * time.Minute ) +// Entity types +const ( + EntityArtist = "artist" + EntityPlaylist = "playlist" +) + const ( AlbumPlayCountModeAbsolute = "absolute" AlbumPlayCountModeNormalized = "normalized" diff --git a/core/artwork/reader_artist.go b/core/artwork/reader_artist.go index 990942d87..96ba08b8f 100644 --- a/core/artwork/reader_artist.go +++ b/core/artwork/reader_artist.go @@ -29,11 +29,12 @@ const ( type artistReader struct { cacheKey - a *artwork - provider external.Provider - artist model.Artist - artistFolder string - imgFiles []string + a *artwork + provider external.Provider + artist model.Artist + artistFolder string + imgFiles []string + imgFolderImgPath string // cached path from ArtistImageFolder lookup } func newArtistArtworkReader(ctx context.Context, artwork *artwork, artID model.ArtworkID, provider external.Provider) (*artistReader, error) { @@ -71,9 +72,20 @@ func newArtistArtworkReader(ctx context.Context, artwork *artwork, artID model.A //a.cacheKey.lastUpdate = ar.ExternalInfoUpdatedAt a.cacheKey.lastUpdate = *imagesUpdatedAt + if ar.UpdatedAt != nil && ar.UpdatedAt.After(a.cacheKey.lastUpdate) { + a.cacheKey.lastUpdate = *ar.UpdatedAt + } if artistFolderLastUpdate.After(a.cacheKey.lastUpdate) { a.cacheKey.lastUpdate = artistFolderLastUpdate } + if conf.Server.ArtistImageFolder != "" && strings.Contains(strings.ToLower(conf.Server.ArtistArtPriority), "image-folder") { + a.imgFolderImgPath = findImageInArtistFolder(conf.Server.ArtistImageFolder, ar.MbzArtistID, ar.Name) + if a.imgFolderImgPath != "" { + if info, err := os.Stat(a.imgFolderImgPath); err == nil && info.ModTime().After(a.cacheKey.lastUpdate) { + a.cacheKey.lastUpdate = info.ModTime() + } + } + } a.cacheKey.artID = artID return a, nil } @@ -93,10 +105,15 @@ func (a *artistReader) LastUpdated() time.Time { } func (a *artistReader) Reader(ctx context.Context) (io.ReadCloser, string, error) { - var ff = a.fromArtistArtPriority(ctx, conf.Server.ArtistArtPriority) + ff := []sourceFunc{a.fromArtistUploadedImage()} + ff = append(ff, a.fromArtistArtPriority(ctx, conf.Server.ArtistArtPriority)...) return selectImageReader(ctx, a.artID, ff...) } +func (a *artistReader) fromArtistUploadedImage() sourceFunc { + return fromLocalFile(a.artist.UploadedImagePath()) +} + func (a *artistReader) fromArtistArtPriority(ctx context.Context, priority string) []sourceFunc { var ff []sourceFunc for pattern := range strings.SplitSeq(strings.ToLower(priority), ",") { @@ -104,6 +121,8 @@ func (a *artistReader) fromArtistArtPriority(ctx context.Context, priority strin switch { case pattern == "external": ff = append(ff, fromArtistExternalSource(ctx, a.artist, a.provider)) + case pattern == "image-folder": + ff = append(ff, a.fromArtistImageFolder(ctx)) case strings.HasPrefix(pattern, "album/"): ff = append(ff, fromExternalFile(ctx, a.imgFiles, strings.TrimPrefix(pattern, "album/"))) default: @@ -196,3 +215,51 @@ func loadArtistFolder(ctx context.Context, ds model.DataStore, albums model.Albu } return folderPath, folders[0].ImagesUpdatedAt, nil } + +func (a *artistReader) fromArtistImageFolder(ctx context.Context) sourceFunc { + return func() (io.ReadCloser, string, error) { + folder := conf.Server.ArtistImageFolder + if folder == "" { + return nil, "", nil + } + // Use cached path from newArtistArtworkReader if available, + // avoiding a second directory scan. + path := a.imgFolderImgPath + if path == "" { + path = findImageInArtistFolder(folder, a.artist.MbzArtistID, a.artist.Name) + } + if path == "" { + return nil, "", fmt.Errorf("no image found for artist %q in %s", a.artist.Name, folder) + } + f, err := os.Open(path) + if err != nil { + return nil, "", err + } + return f, path, nil + } +} + +// findImageInArtistFolder scans a folder for an image file matching the artist's MBID or name +// (case-insensitive). Returns the full path, or empty string if not found. +func findImageInArtistFolder(folder, mbzArtistID, artistName string) string { + entries, err := os.ReadDir(folder) + if err != nil { + return "" + } + for _, candidate := range []string{mbzArtistID, artistName} { + if candidate == "" { + continue + } + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + base := strings.TrimSuffix(name, filepath.Ext(name)) + if strings.EqualFold(base, candidate) && model.IsImageFile(name) { + return filepath.Join(folder, name) + } + } + } + return "" +} diff --git a/core/artwork/reader_artist_test.go b/core/artwork/reader_artist_test.go index 4aa71c9ca..5e2066aeb 100644 --- a/core/artwork/reader_artist_test.go +++ b/core/artwork/reader_artist_test.go @@ -8,6 +8,8 @@ import ( "path/filepath" "time" + "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/conf/configtest" "github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/model" . "github.com/onsi/ginkgo/v2" @@ -413,6 +415,257 @@ var _ = Describe("artistArtworkReader", func() { }) }) }) + + Describe("fromArtistUploadedImage", func() { + var ( + tempDir string + reader *artistReader + ) + + BeforeEach(func() { + DeferCleanup(configtest.SetupConfig()) + tempDir = GinkgoT().TempDir() + conf.Server.DataFolder = tempDir + + // Create the artwork/artist directory + Expect(os.MkdirAll(filepath.Join(tempDir, "artwork", "artist"), 0755)).To(Succeed()) + + reader = &artistReader{} + }) + + When("artist has an uploaded image", func() { + It("returns the uploaded image", func() { + imgPath := filepath.Join(tempDir, "artwork", "artist", "ar-1_test.jpg") + Expect(os.WriteFile(imgPath, []byte("uploaded artist image"), 0600)).To(Succeed()) + + reader.artist = model.Artist{ID: "ar-1", UploadedImage: "ar-1_test.jpg"} + sf := reader.fromArtistUploadedImage() + r, path, err := sf() + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + Expect(path).To(Equal(imgPath)) + + data, err := io.ReadAll(r) + Expect(err).ToNot(HaveOccurred()) + Expect(string(data)).To(Equal("uploaded artist image")) + r.Close() + }) + }) + + When("artist has no uploaded image", func() { + It("returns nil reader (falls through)", func() { + reader.artist = model.Artist{ID: "ar-1"} + sf := reader.fromArtistUploadedImage() + r, path, err := sf() + Expect(err).ToNot(HaveOccurred()) + Expect(r).To(BeNil()) + Expect(path).To(BeEmpty()) + }) + }) + }) + + Describe("fromArtistImageFolder", func() { + var ( + ctx context.Context + tempDir string + ar *artistReader + ) + + BeforeEach(func() { + ctx = context.Background() + DeferCleanup(configtest.SetupConfig()) + tempDir = GinkgoT().TempDir() + ar = &artistReader{} + }) + + When("ArtistImageFolder is not configured", func() { + It("returns nil (skips)", func() { + conf.Server.ArtistImageFolder = "" + ar.artist = model.Artist{Name: "Test Artist"} + sf := ar.fromArtistImageFolder(ctx) + r, path, err := sf() + Expect(err).ToNot(HaveOccurred()) + Expect(r).To(BeNil()) + Expect(path).To(BeEmpty()) + }) + }) + + When("image exists matching MBID", func() { + It("finds the image by MBID", func() { + conf.Server.ArtistImageFolder = tempDir + mbid := "f27ec8db-af05-4f36-916e-3d57f91ecf5e" + imgPath := filepath.Join(tempDir, mbid+".jpg") + Expect(os.WriteFile(imgPath, []byte("mbid image"), 0600)).To(Succeed()) + + ar.artist = model.Artist{Name: "Test Artist", MbzArtistID: mbid} + sf := ar.fromArtistImageFolder(ctx) + r, path, err := sf() + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + Expect(path).To(Equal(imgPath)) + + data, err := io.ReadAll(r) + Expect(err).ToNot(HaveOccurred()) + Expect(string(data)).To(Equal("mbid image")) + r.Close() + }) + }) + + When("MBID match is case-insensitive", func() { + It("finds the image regardless of case", func() { + conf.Server.ArtistImageFolder = tempDir + mbid := "F27EC8DB-AF05-4F36-916E-3D57F91ECF5E" + imgPath := filepath.Join(tempDir, "f27ec8db-af05-4f36-916e-3d57f91ecf5e.png") + Expect(os.WriteFile(imgPath, []byte("mbid case image"), 0600)).To(Succeed()) + + ar.artist = model.Artist{Name: "Test Artist", MbzArtistID: mbid} + sf := ar.fromArtistImageFolder(ctx) + r, path, err := sf() + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + Expect(path).To(Equal(imgPath)) + r.Close() + }) + }) + + When("no MBID file exists but artist name file does", func() { + It("falls back to artist name match", func() { + conf.Server.ArtistImageFolder = tempDir + imgPath := filepath.Join(tempDir, "Test Artist.jpg") + Expect(os.WriteFile(imgPath, []byte("name image"), 0600)).To(Succeed()) + + ar.artist = model.Artist{Name: "Test Artist", MbzArtistID: "nonexistent-mbid"} + sf := ar.fromArtistImageFolder(ctx) + r, path, err := sf() + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + Expect(path).To(Equal(imgPath)) + + data, err := io.ReadAll(r) + Expect(err).ToNot(HaveOccurred()) + Expect(string(data)).To(Equal("name image")) + r.Close() + }) + }) + + When("artist name match is case-insensitive", func() { + It("matches regardless of case", func() { + conf.Server.ArtistImageFolder = tempDir + imgPath := filepath.Join(tempDir, "test artist.jpg") + Expect(os.WriteFile(imgPath, []byte("case insensitive"), 0600)).To(Succeed()) + + ar.artist = model.Artist{Name: "Test Artist"} + sf := ar.fromArtistImageFolder(ctx) + r, path, err := sf() + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + Expect(path).To(Equal(imgPath)) + r.Close() + }) + }) + + When("both MBID and name files exist", func() { + It("prefers MBID over name match", func() { + conf.Server.ArtistImageFolder = tempDir + mbid := "f27ec8db-af05-4f36-916e-3d57f91ecf5e" + mbidPath := filepath.Join(tempDir, mbid+".jpg") + namePath := filepath.Join(tempDir, "Test Artist.jpg") + Expect(os.WriteFile(mbidPath, []byte("mbid image"), 0600)).To(Succeed()) + Expect(os.WriteFile(namePath, []byte("name image"), 0600)).To(Succeed()) + + ar.artist = model.Artist{Name: "Test Artist", MbzArtistID: mbid} + sf := ar.fromArtistImageFolder(ctx) + r, path, err := sf() + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + Expect(path).To(Equal(mbidPath)) + + data, err := io.ReadAll(r) + Expect(err).ToNot(HaveOccurred()) + Expect(string(data)).To(Equal("mbid image")) + r.Close() + }) + }) + + When("no matching image found", func() { + It("returns an error", func() { + conf.Server.ArtistImageFolder = tempDir + // Create an unrelated file + Expect(os.WriteFile(filepath.Join(tempDir, "other.jpg"), []byte("other"), 0600)).To(Succeed()) + + ar.artist = model.Artist{Name: "Test Artist"} + sf := ar.fromArtistImageFolder(ctx) + r, _, err := sf() + Expect(err).To(HaveOccurred()) + Expect(r).To(BeNil()) + Expect(err.Error()).To(ContainSubstring("no image found")) + }) + }) + + When("cached imgFolderImgPath is set", func() { + It("uses cached path instead of scanning", func() { + conf.Server.ArtistImageFolder = tempDir + imgPath := filepath.Join(tempDir, "cached.jpg") + Expect(os.WriteFile(imgPath, []byte("cached image"), 0600)).To(Succeed()) + + ar.artist = model.Artist{Name: "Test Artist"} + ar.imgFolderImgPath = imgPath + sf := ar.fromArtistImageFolder(ctx) + r, path, err := sf() + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + Expect(path).To(Equal(imgPath)) + + data, err := io.ReadAll(r) + Expect(err).ToNot(HaveOccurred()) + Expect(string(data)).To(Equal("cached image")) + r.Close() + }) + }) + }) + + Describe("findImageInArtistFolder", func() { + var tempDir string + + BeforeEach(func() { + tempDir = GinkgoT().TempDir() + }) + + When("matching file exists by MBID", func() { + It("returns the file path", func() { + mbid := "f27ec8db-af05-4f36-916e-3d57f91ecf5e" + imgPath := filepath.Join(tempDir, mbid+".jpg") + Expect(os.WriteFile(imgPath, []byte("image"), 0600)).To(Succeed()) + + path := findImageInArtistFolder(tempDir, mbid, "Test") + Expect(path).To(Equal(imgPath)) + }) + }) + + When("matching file exists by name", func() { + It("returns the file path", func() { + imgPath := filepath.Join(tempDir, "Test Artist.png") + Expect(os.WriteFile(imgPath, []byte("image"), 0600)).To(Succeed()) + + path := findImageInArtistFolder(tempDir, "", "Test Artist") + Expect(path).To(Equal(imgPath)) + }) + }) + + When("no matching file exists", func() { + It("returns empty string", func() { + path := findImageInArtistFolder(tempDir, "", "Unknown Artist") + Expect(path).To(BeEmpty()) + }) + }) + + When("folder does not exist", func() { + It("returns empty string", func() { + path := findImageInArtistFolder("/nonexistent/path", "", "Test") + Expect(path).To(BeEmpty()) + }) + }) + }) }) type fakeFolderRepo struct { diff --git a/core/image_upload.go b/core/image_upload.go new file mode 100644 index 000000000..c2432b647 --- /dev/null +++ b/core/image_upload.go @@ -0,0 +1,71 @@ +package core + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/utils" +) + +type ImageUploadService interface { + SetImage(ctx context.Context, entityType string, entityID string, name string, oldPath string, reader io.Reader, ext string) (filename string, err error) + RemoveImage(ctx context.Context, path string) error +} + +type imageUploadService struct{} + +func NewImageUploadService() ImageUploadService { + return &imageUploadService{} +} + +func (s *imageUploadService) SetImage(ctx context.Context, entityType string, entityID string, name string, oldPath string, reader io.Reader, ext string) (string, error) { + filename := imageFilename(entityID, name, ext) + absPath := model.UploadedImagePath(entityType, filename) + + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + return "", fmt.Errorf("creating image directory: %w", err) + } + + // Remove old image if it exists + if oldPath != "" { + if err := os.Remove(oldPath); err != nil && !os.IsNotExist(err) { + log.Warn(ctx, "Failed to remove old image", "path", oldPath, err) + } + } + + // Save new image + f, err := os.Create(absPath) + if err != nil { + return "", fmt.Errorf("creating image file: %w", err) + } + defer f.Close() + + if _, err := io.Copy(f, reader); err != nil { + return "", fmt.Errorf("writing image file: %w", err) + } + + return filename, nil +} + +func (s *imageUploadService) RemoveImage(ctx context.Context, path string) error { + if path == "" { + return nil + } + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("removing image %q: %w", path, err) + } + return nil +} + +func imageFilename(id, name, ext string) string { + clean := utils.CleanFileName(name) + if clean == "" { + return id + ext + } + return id + "_" + clean + ext +} diff --git a/core/image_upload_test.go b/core/image_upload_test.go new file mode 100644 index 000000000..d13a04775 --- /dev/null +++ b/core/image_upload_test.go @@ -0,0 +1,99 @@ +package core_test + +import ( + "context" + "os" + "path/filepath" + "strings" + + "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/conf/configtest" + "github.com/navidrome/navidrome/consts" + "github.com/navidrome/navidrome/core" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ImageUploadService", func() { + var svc core.ImageUploadService + var tmpDir string + + BeforeEach(func() { + DeferCleanup(configtest.SetupConfig()) + tmpDir = GinkgoT().TempDir() + conf.Server.DataFolder = tmpDir + svc = core.NewImageUploadService() + }) + + Describe("SetImage", func() { + It("creates directory and saves image file", func() { + ctx := context.Background() + reader := strings.NewReader("fake image data") + filename, err := svc.SetImage(ctx, consts.EntityArtist, "ar-1", "Pink Floyd", "", reader, ".jpg") + Expect(err).ToNot(HaveOccurred()) + Expect(filename).To(Equal("ar-1_pink_floyd.jpg")) + + absPath := filepath.Join(tmpDir, "artwork", "artist", "ar-1_pink_floyd.jpg") + data, err := os.ReadFile(absPath) + Expect(err).ToNot(HaveOccurred()) + Expect(string(data)).To(Equal("fake image data")) + }) + + It("falls back to ID-only filename when name cleans to empty", func() { + ctx := context.Background() + reader := strings.NewReader("data") + filename, err := svc.SetImage(ctx, consts.EntityPlaylist, "pl-1", "!!!", "", reader, ".png") + Expect(err).ToNot(HaveOccurred()) + Expect(filename).To(Equal("pl-1.png")) + }) + + It("removes old image when replacing", func() { + ctx := context.Background() + oldDir := filepath.Join(tmpDir, "artwork", "artist") + Expect(os.MkdirAll(oldDir, 0755)).To(Succeed()) + oldFile := filepath.Join(oldDir, "ar-1_old.png") + Expect(os.WriteFile(oldFile, []byte("old"), 0600)).To(Succeed()) + + reader := strings.NewReader("new image") + _, err := svc.SetImage(ctx, consts.EntityArtist, "ar-1", "New Name", oldFile, reader, ".jpg") + Expect(err).ToNot(HaveOccurred()) + Expect(oldFile).ToNot(BeAnExistingFile()) + + newPath := filepath.Join(oldDir, "ar-1_new_name.jpg") + Expect(newPath).To(BeAnExistingFile()) + }) + + It("ignores missing old file without error", func() { + ctx := context.Background() + reader := strings.NewReader("data") + _, err := svc.SetImage(ctx, consts.EntityArtist, "ar-1", "Name", "/nonexistent/path.jpg", reader, ".jpg") + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Describe("RemoveImage", func() { + It("removes the file at the given path", func() { + ctx := context.Background() + dir := filepath.Join(tmpDir, "artwork", "artist") + Expect(os.MkdirAll(dir, 0755)).To(Succeed()) + path := filepath.Join(dir, "ar-1_test.jpg") + Expect(os.WriteFile(path, []byte("img"), 0600)).To(Succeed()) + + err := svc.RemoveImage(ctx, path) + Expect(err).ToNot(HaveOccurred()) + Expect(path).ToNot(BeAnExistingFile()) + }) + + It("succeeds when file does not exist", func() { + ctx := context.Background() + err := svc.RemoveImage(ctx, "/nonexistent/file.jpg") + Expect(err).ToNot(HaveOccurred()) + }) + + It("succeeds with empty path", func() { + ctx := context.Background() + err := svc.RemoveImage(ctx, "") + Expect(err).ToNot(HaveOccurred()) + }) + }) +}) diff --git a/core/playlists/import_test.go b/core/playlists/import_test.go index 5312df95d..a6320bc7e 100644 --- a/core/playlists/import_test.go +++ b/core/playlists/import_test.go @@ -11,6 +11,7 @@ import ( "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf/configtest" + "github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/core/playlists" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/criteria" @@ -42,7 +43,7 @@ var _ = Describe("Playlists - Import", func() { var folder *model.Folder BeforeEach(func() { DeferCleanup(configtest.SetupConfig()) - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) ds.MockedMediaFile = &mockedMediaFileRepo{} libPath, _ := os.Getwd() // Set up library with the actual library path that matches the folder @@ -117,7 +118,7 @@ var _ = Describe("Playlists - Import", func() { mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}}) ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{"test.mp3", "test.ogg"}} - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) plsFolder := &model.Folder{ID: "1", LibraryID: 1, LibraryPath: tmpDir, Path: "", Name: ""} pls, err := ps.ImportFile(ctx, plsFolder, "test.m3u") @@ -135,7 +136,7 @@ var _ = Describe("Playlists - Import", func() { mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}}) ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{"test.mp3"}} - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) plsFolder := &model.Folder{ID: "1", LibraryID: 1, LibraryPath: tmpDir, Path: "", Name: ""} pls, err := ps.ImportFile(ctx, plsFolder, "test.m3u") @@ -154,7 +155,7 @@ var _ = Describe("Playlists - Import", func() { mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}}) ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{"test.mp3"}} - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) plsFolder := &model.Folder{ID: "1", LibraryID: 1, LibraryPath: tmpDir, Path: "", Name: ""} pls, err := ps.ImportFile(ctx, plsFolder, "test.m3u") @@ -173,7 +174,7 @@ var _ = Describe("Playlists - Import", func() { mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}}) ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{"test.mp3"}} - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) plsFolder := &model.Folder{ID: "1", LibraryID: 1, LibraryPath: tmpDir, Path: "", Name: ""} pls, err := ps.ImportFile(ctx, plsFolder, "test.m3u") @@ -190,7 +191,7 @@ var _ = Describe("Playlists - Import", func() { mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}}) ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{"test.mp3"}} - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) plsFolder := &model.Folder{ID: "1", LibraryID: 1, LibraryPath: tmpDir, Path: "", Name: ""} pls, err := ps.ImportFile(ctx, plsFolder, "test.m3u") @@ -207,7 +208,7 @@ var _ = Describe("Playlists - Import", func() { mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}}) ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{"test.mp3"}} - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) plsFolder := &model.Folder{ID: "1", LibraryID: 1, LibraryPath: tmpDir, Path: "", Name: ""} pls, err := ps.ImportFile(ctx, plsFolder, "test.m3u") @@ -224,7 +225,7 @@ var _ = Describe("Playlists - Import", func() { mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}}) ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{"test.mp3"}} - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) plsFolder := &model.Folder{ID: "1", LibraryID: 1, LibraryPath: tmpDir, Path: "", Name: ""} pls, err := ps.ImportFile(ctx, plsFolder, "test.m3u") @@ -242,7 +243,7 @@ var _ = Describe("Playlists - Import", func() { mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}}) ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{"test.mp3"}} - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) plsFolder := &model.Folder{ID: "1", LibraryID: 1, LibraryPath: tmpDir, Path: "", Name: ""} pls, err := ps.ImportFile(ctx, plsFolder, "test.m3u") @@ -256,7 +257,7 @@ var _ = Describe("Playlists - Import", func() { tmpDir := GinkgoT().TempDir() mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}}) ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{"test.mp3"}} - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) m3u := "#EXTALBUMARTURL:https://example.com/new-cover.jpg\ntest.mp3\n" plsFile := filepath.Join(tmpDir, "test.m3u") @@ -283,7 +284,7 @@ var _ = Describe("Playlists - Import", func() { tmpDir := GinkgoT().TempDir() mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}}) ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{"test.mp3"}} - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) m3u := "test.mp3\n" plsFile := filepath.Join(tmpDir, "test.m3u") @@ -358,7 +359,7 @@ var _ = Describe("Playlists - Import", func() { tmpDir := GinkgoT().TempDir() mockLibRepo.SetData([]model.Library{{ID: 1, Path: tmpDir}}) ds.MockedMediaFile = &mockedMediaFileFromListRepo{data: []string{}} - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) // Create the playlist file on disk with the filesystem's normalization form plsFile := tmpDir + "/" + filesystemName + ".m3u" @@ -418,7 +419,7 @@ var _ = Describe("Playlists - Import", func() { "def.mp3", // This is playlists/def.mp3 relative to plsDir }, } - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) }) It("handles relative paths that reference files in other libraries", func() { @@ -574,7 +575,7 @@ var _ = Describe("Playlists - Import", func() { }, } // Recreate playlists service to pick up new mock - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) // Create playlist in music library that references both tracks plsContent := "#PLAYLIST:Same Path Test\nalbum/track.mp3\n../classical/album/track.mp3" @@ -617,7 +618,7 @@ var _ = Describe("Playlists - Import", func() { BeforeEach(func() { repo = &mockedMediaFileFromListRepo{} ds.MockedMediaFile = repo - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) mockLibRepo.SetData([]model.Library{{ID: 1, Path: "/music"}, {ID: 2, Path: "/new"}}) ctx = request.WithUser(ctx, model.User{ID: "123"}) }) diff --git a/core/playlists/playlists.go b/core/playlists/playlists.go index 0649b16a9..a0086cd2d 100644 --- a/core/playlists/playlists.go +++ b/core/playlists/playlists.go @@ -2,7 +2,6 @@ package playlists import ( "context" - "fmt" "io" "os" "path/filepath" @@ -12,6 +11,7 @@ import ( "github.com/bmatcuk/doublestar/v4" "github.com/deluan/rest" "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" @@ -50,12 +50,20 @@ type Playlists interface { TracksRepository(ctx context.Context, playlistId string, refreshSmartPlaylist bool) rest.Repository } -type playlists struct { - ds model.DataStore +// ImageUploadService is a local interface satisfied by core.ImageUploadService. +// Defined here to avoid an import cycle between core and core/playlists. +type ImageUploadService interface { + SetImage(ctx context.Context, entityType string, entityID string, name string, oldPath string, reader io.Reader, ext string) (filename string, err error) + RemoveImage(ctx context.Context, path string) error } -func NewPlaylists(ds model.DataStore) Playlists { - return &playlists{ds: ds} +type playlists struct { + ds model.DataStore + imgUpload ImageUploadService +} + +func NewPlaylists(ds model.DataStore, imgUpload ImageUploadService) Playlists { + return &playlists{ds: ds, imgUpload: imgUpload} } func InPath(folder model.Folder) bool { @@ -288,33 +296,13 @@ func (s *playlists) SetImage(ctx context.Context, playlistID string, reader io.R return err } - filename := pls.ImageFilename(ext) oldPath := pls.UploadedImagePath() - pls.UploadedImage = filename - absPath := pls.UploadedImagePath() - - if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { - return fmt.Errorf("creating playlist images directory: %w", err) - } - - // Remove old image if it exists - if oldPath != "" { - if err := os.Remove(oldPath); err != nil && !os.IsNotExist(err) { - log.Warn(ctx, "Failed to remove old playlist image", "path", oldPath, err) - } - } - - // Save new image - f, err := os.Create(absPath) + filename, err := s.imgUpload.SetImage(ctx, consts.EntityPlaylist, pls.ID, pls.Name, oldPath, reader, ext) if err != nil { - return fmt.Errorf("creating playlist image file: %w", err) - } - defer f.Close() - - if _, err := io.Copy(f, reader); err != nil { - return fmt.Errorf("writing playlist image file: %w", err) + return err } + pls.UploadedImage = filename return s.ds.Playlist(ctx).Put(pls) } @@ -324,10 +312,8 @@ func (s *playlists) RemoveImage(ctx context.Context, playlistID string) error { return err } - if path := pls.UploadedImagePath(); path != "" { - if err := os.Remove(path); err != nil && !os.IsNotExist(err) { - log.Warn(ctx, "Failed to remove playlist image", "path", path, err) - } + if err := s.imgUpload.RemoveImage(ctx, pls.UploadedImagePath()); err != nil { + return err } pls.UploadedImage = "" diff --git a/core/playlists/playlists_test.go b/core/playlists/playlists_test.go index ec73c329a..52d5c88d8 100644 --- a/core/playlists/playlists_test.go +++ b/core/playlists/playlists_test.go @@ -8,6 +8,7 @@ import ( "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf/configtest" + "github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/core/playlists" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/criteria" @@ -41,7 +42,7 @@ var _ = Describe("Playlists", func() { "pls-1": {ID: "pls-1", Name: "My Playlist", OwnerID: "user-1"}, } mockPlsRepo.TracksRepo = mockTracks - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) }) It("allows owner to delete their playlist", func() { @@ -80,7 +81,7 @@ var _ = Describe("Playlists", func() { "pls-smart": {ID: "pls-smart", Name: "Smart", OwnerID: "user-1", Rules: &criteria.Criteria{Expression: criteria.Contains{"title": "test"}}}, } - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) }) It("creates a new playlist with owner set from context", func() { @@ -138,7 +139,7 @@ var _ = Describe("Playlists", func() { Rules: &criteria.Criteria{Expression: criteria.Contains{"title": "test"}}}, } mockPlsRepo.TracksRepo = mockTracks - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) }) It("allows owner to update their playlist", func() { @@ -201,7 +202,7 @@ var _ = Describe("Playlists", func() { "pls-other": {ID: "pls-other", Name: "Other's", OwnerID: "other-user"}, } mockPlsRepo.TracksRepo = mockTracks - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) }) It("allows owner to add tracks", func() { @@ -249,7 +250,7 @@ var _ = Describe("Playlists", func() { Rules: &criteria.Criteria{Expression: criteria.Contains{"title": "test"}}}, } mockPlsRepo.TracksRepo = mockTracks - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) }) It("allows owner to remove tracks", func() { @@ -283,7 +284,7 @@ var _ = Describe("Playlists", func() { Rules: &criteria.Criteria{Expression: criteria.Contains{"title": "test"}}}, } mockPlsRepo.TracksRepo = mockTracks - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) }) It("allows owner to reorder", func() { @@ -312,7 +313,7 @@ var _ = Describe("Playlists", func() { "pls-1": {ID: "pls-1", Name: "My Playlist", OwnerID: "user-1"}, "pls-other": {ID: "pls-other", Name: "Other's", OwnerID: "other-user"}, } - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) }) It("saves image file and updates UploadedImage", func() { @@ -382,7 +383,7 @@ var _ = Describe("Playlists", func() { "pls-empty": {ID: "pls-empty", Name: "No Cover", OwnerID: "user-1"}, "pls-other": {ID: "pls-other", Name: "Other's", OwnerID: "other-user"}, } - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) }) It("removes file and clears UploadedImage", func() { diff --git a/core/playlists/rest_adapter_test.go b/core/playlists/rest_adapter_test.go index 70ca8a9e4..097bc6310 100644 --- a/core/playlists/rest_adapter_test.go +++ b/core/playlists/rest_adapter_test.go @@ -5,6 +5,7 @@ import ( "time" "github.com/deluan/rest" + "github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/core/playlists" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/criteria" @@ -36,7 +37,7 @@ var _ = Describe("REST Adapter", func() { mockPlsRepo.Data = map[string]*model.Playlist{ "pls-1": {ID: "pls-1", Name: "My Playlist", OwnerID: "user-1"}, } - ps = playlists.NewPlaylists(ds) + ps = playlists.NewPlaylists(ds, core.NewImageUploadService()) }) Describe("Save", func() { diff --git a/core/wire_providers.go b/core/wire_providers.go index 153df7262..276d9556a 100644 --- a/core/wire_providers.go +++ b/core/wire_providers.go @@ -23,6 +23,8 @@ var Set = wire.NewSet( NewLibrary, NewUser, NewMaintenance, + NewImageUploadService, + wire.Bind(new(playlists.ImageUploadService), new(ImageUploadService)), stream.NewTranscodeDecider, agents.GetAgents, external.NewProvider, diff --git a/db/migrations/20260315233131_add_artist_uploaded_image.go b/db/migrations/20260315233131_add_artist_uploaded_image.go new file mode 100644 index 000000000..964e346f5 --- /dev/null +++ b/db/migrations/20260315233131_add_artist_uploaded_image.go @@ -0,0 +1,22 @@ +package migrations + +import ( + "context" + "database/sql" + + "github.com/pressly/goose/v3" +) + +func init() { + goose.AddMigrationContext(upAddArtistUploadedImage, downAddArtistUploadedImage) +} + +func upAddArtistUploadedImage(ctx context.Context, tx *sql.Tx) error { + _, err := tx.ExecContext(ctx, `ALTER TABLE artist ADD COLUMN uploaded_image VARCHAR(255) DEFAULT ''`) + return err +} + +func downAddArtistUploadedImage(ctx context.Context, tx *sql.Tx) error { + // This code is executed when the migration is rolled back. + return nil +} diff --git a/model/artist.go b/model/artist.go index 309ee800f..2085f0051 100644 --- a/model/artist.go +++ b/model/artist.go @@ -4,6 +4,8 @@ import ( "maps" "slices" "time" + + "github.com/navidrome/navidrome/consts" ) type Artist struct { @@ -34,6 +36,8 @@ type Artist struct { Missing bool `structs:"missing" json:"missing"` + UploadedImage string `structs:"uploaded_image" json:"uploadedImage,omitempty"` + CreatedAt *time.Time `structs:"created_at" json:"createdAt,omitempty"` UpdatedAt *time.Time `structs:"updated_at" json:"updatedAt,omitempty"` } @@ -58,6 +62,10 @@ func (a Artist) CoverArtID() ArtworkID { return artworkIDFromArtist(a) } +func (a Artist) UploadedImagePath() string { + return UploadedImagePath(consts.EntityArtist, a.UploadedImage) +} + // Roles returns the roles this artist has participated in., based on the Stats field func (a Artist) Roles() []Role { return slices.Collect(maps.Keys(a.Stats)) diff --git a/model/artist_test.go b/model/artist_test.go new file mode 100644 index 000000000..5a24504eb --- /dev/null +++ b/model/artist_test.go @@ -0,0 +1,30 @@ +package model_test + +import ( + "path/filepath" + + "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/conf/configtest" + "github.com/navidrome/navidrome/model" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Artist", func() { + Describe("UploadedImagePath", func() { + BeforeEach(func() { + DeferCleanup(configtest.SetupConfig()) + conf.Server.DataFolder = "/data" + }) + + It("returns empty string when no image uploaded", func() { + a := model.Artist{ID: "ar-1"} + Expect(a.UploadedImagePath()).To(BeEmpty()) + }) + + It("returns full path when image is set", func() { + a := model.Artist{ID: "ar-1", UploadedImage: "ar-1_test.jpg"} + Expect(a.UploadedImagePath()).To(Equal(filepath.Join("/data", "artwork", "artist", "ar-1_test.jpg"))) + }) + }) +}) diff --git a/model/image.go b/model/image.go new file mode 100644 index 000000000..68d8ae64c --- /dev/null +++ b/model/image.go @@ -0,0 +1,17 @@ +package model + +import ( + "path/filepath" + + "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/consts" +) + +// UploadedImagePath returns the absolute filesystem path for a manually uploaded +// entity cover image. Returns empty string if filename is empty. +func UploadedImagePath(entityType, filename string) string { + if filename == "" { + return "" + } + return filepath.Join(conf.Server.DataFolder, consts.ArtworkFolder, entityType, filename) +} diff --git a/model/playlist.go b/model/playlist.go index 5c9052eb7..e2f93993d 100644 --- a/model/playlist.go +++ b/model/playlist.go @@ -1,15 +1,12 @@ package model import ( - "path/filepath" "slices" "strconv" "time" - "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/model/criteria" - "github.com/navidrome/navidrome/utils" ) type Playlist struct { @@ -108,16 +105,6 @@ func (pls *Playlist) AddMediaFiles(mfs MediaFiles) { pls.refreshStats() } -// ImageFilename returns a human-friendly filename for an uploaded playlist cover image. -// Format: _, falling back to if the name cleans to empty. -func (pls Playlist) ImageFilename(ext string) string { - clean := utils.CleanFileName(pls.Name) - if clean == "" { - return pls.ID + ext - } - return pls.ID + "_" + clean + ext -} - func (pls Playlist) CoverArtID() ArtworkID { return artworkIDFromPlaylist(pls) } @@ -127,10 +114,7 @@ func (pls Playlist) CoverArtID() ArtworkID { // This does NOT cover sidecar images or external URLs — those are resolved // by the artwork reader's fallback chain. func (pls Playlist) UploadedImagePath() string { - if pls.UploadedImage == "" { - return "" - } - return filepath.Join(conf.Server.DataFolder, consts.ArtworkFolder, "playlist", pls.UploadedImage) + return UploadedImagePath(consts.EntityPlaylist, pls.UploadedImage) } type Playlists []Playlist diff --git a/model/playlist_test.go b/model/playlist_test.go index 98dd4e978..a54cecd53 100644 --- a/model/playlist_test.go +++ b/model/playlist_test.go @@ -7,28 +7,6 @@ import ( ) var _ = Describe("Playlist", func() { - Describe("ImageFilename", func() { - It("returns ID_cleanname.ext for a normal name", func() { - pls := model.Playlist{ID: "abc123", Name: "My Cool Playlist"} - Expect(pls.ImageFilename(".jpg")).To(Equal("abc123_my_cool_playlist.jpg")) - }) - - It("falls back to ID.ext when name cleans to empty", func() { - pls := model.Playlist{ID: "abc123", Name: "!!!"} - Expect(pls.ImageFilename(".png")).To(Equal("abc123.png")) - }) - - It("falls back to ID.ext for empty name", func() { - pls := model.Playlist{ID: "abc123", Name: ""} - Expect(pls.ImageFilename(".jpg")).To(Equal("abc123.jpg")) - }) - - It("handles names with special characters", func() { - pls := model.Playlist{ID: "x1", Name: "Rock & Roll! (2024)"} - Expect(pls.ImageFilename(".webp")).To(Equal("x1_rock__roll_2024.webp")) - }) - }) - Describe("ToM3U8()", func() { var pls model.Playlist BeforeEach(func() { diff --git a/resources/i18n/bg.json b/resources/i18n/bg.json index 626657129..7a0281f33 100644 --- a/resources/i18n/bg.json +++ b/resources/i18n/bg.json @@ -219,19 +219,13 @@ "saveQueue": "Запазване на опашката в плейлист", "searchOrCreate": "Търсете в плейлисти или пишете, за да създадете нови...", "pressEnterToCreate": "Натиснете Enter, за да създадете нов плейлист", - "removeFromSelection": "Премахване от селекцията", - "uploadCover": "", - "removeCover": "" + "removeFromSelection": "Премахване от селекцията" }, "message": { "duplicate_song": "Добави дублирани песни", "song_exist": "Към плейлиста се добавят дублиращи. Желаете ли да ги добавите или предпочитате да ги пропуснете?", "noPlaylistsFound": "Няма намерени плейлисти", - "noPlaylists": "Няма налични плейлисти", - "coverUploaded": "", - "coverRemoved": "", - "coverUploadError": "", - "coverRemoveError": "" + "noPlaylists": "Няма налични плейлисти" } }, "radio": { @@ -718,4 +712,4 @@ "empty": "Нищо не се възпроизвежда", "minutesAgo": "преди %{smart_count} минута |||| преди %{smart_count} минути" } -} \ No newline at end of file +} diff --git a/resources/i18n/ca.json b/resources/i18n/ca.json index 079c130d1..1ef2ce016 100644 --- a/resources/i18n/ca.json +++ b/resources/i18n/ca.json @@ -219,19 +219,13 @@ "saveQueue": "Desar la cua a una llista", "searchOrCreate": "Cerca llistes o escriu per crear-ne de noves...", "pressEnterToCreate": "Prem Retorn per crear una nova llista", - "removeFromSelection": "Elimina de la selecció", - "uploadCover": "", - "removeCover": "" + "removeFromSelection": "Elimina de la selecció" }, "message": { "duplicate_song": "Afegeix cançons duplicades", "song_exist": "Heu afegit duplicats a la llista. Voleu afegir-los o ignorar-los?", "noPlaylistsFound": "No s'ha trobat cap llista", - "noPlaylists": "No hi ha cap llista disponible", - "coverUploaded": "", - "coverRemoved": "", - "coverUploadError": "", - "coverRemoveError": "" + "noPlaylists": "No hi ha cap llista disponible" } }, "radio": { @@ -718,4 +712,4 @@ "empty": "No s'està reproduint res", "minutesAgo": "Fa %{smart_count} minut |||| Fa %{smart_count} minuts" } -} \ No newline at end of file +} diff --git a/resources/i18n/da.json b/resources/i18n/da.json index a7c090cca..a47b30bbc 100644 --- a/resources/i18n/da.json +++ b/resources/i18n/da.json @@ -219,19 +219,13 @@ "saveQueue": "Gem kø på afspilningsliste", "searchOrCreate": "Søg i afspilningslister eller skriv for at oprette nye...", "pressEnterToCreate": "Tryk Enter for at oprette en ny afspilningsliste", - "removeFromSelection": "Fjern fra valg", - "uploadCover": "", - "removeCover": "" + "removeFromSelection": "Fjern fra valg" }, "message": { "duplicate_song": "Tilføj dubletter af sange", "song_exist": "Der føjes dubletter til playlisten", "noPlaylistsFound": "Ingen playlister fundet", - "noPlaylists": "Ingen tilgængelige playlister", - "coverUploaded": "", - "coverRemoved": "", - "coverUploadError": "", - "coverRemoveError": "" + "noPlaylists": "Ingen tilgængelige playlister" } }, "radio": { @@ -718,4 +712,4 @@ "empty": "Intet afspilles nu", "minutesAgo": "for %{smart_count} minut siden |||| for %{smart_count} minutter siden" } -} \ No newline at end of file +} diff --git a/resources/i18n/de.json b/resources/i18n/de.json index 6ba8a14eb..ab1760ed5 100644 --- a/resources/i18n/de.json +++ b/resources/i18n/de.json @@ -219,19 +219,13 @@ "saveQueue": "Warteschlange in Wiedergabeliste speichern", "searchOrCreate": "Wiedergabeliste suchen oder neue erstellen...", "pressEnterToCreate": "Enter drücken um neue Wiedergabeliste zu erstellen", - "removeFromSelection": "Von Auswahl entfernen", - "uploadCover": "Cover hochladen", - "removeCover": "Cover entfernen" + "removeFromSelection": "Von Auswahl entfernen" }, "message": { "duplicate_song": "Duplikate hinzufügen", "song_exist": "Manche Titel sind bereits in der Playlist. Möchtest du sie trotzdem hinzufügen oder überspringen?", "noPlaylistsFound": "Keine Wiedergabeliste gefunden", - "noPlaylists": "Keine Wiedergabelisten vorhanden", - "coverUploaded": "Cover aktualisiert", - "coverRemoved": "Cover entfernt", - "coverUploadError": "Fehler beim Hochladen des Covers", - "coverRemoveError": "Fehler beim Entfernen des Covers" + "noPlaylists": "Keine Wiedergabelisten vorhanden" } }, "radio": { @@ -597,7 +591,13 @@ "remove_all_missing_content": "Möchtest du wirklich alle Fehlenden Dateien aus der Datenbank entfernen? Alle Referenzen zu den Dateien wie Anzahl Wiedergaben und Bewertungen werden permanent gelöscht.", "noSimilarSongsFound": "Keine ähnlichen Titel gefunden", "noTopSongsFound": "Keine beliebten Titel gefunden", - "startingInstantMix": "Lade Sofort-Mix..." + "startingInstantMix": "Lade Sofort-Mix...", + "uploadCover": "Cover hochladen", + "removeCover": "Cover entfernen", + "coverUploaded": "Cover aktualisiert", + "coverRemoved": "Cover entfernt", + "coverUploadError": "Fehler beim Hochladen des Covers", + "coverRemoveError": "Fehler beim Entfernen des Covers" }, "menu": { "library": "Bibliothek", @@ -718,4 +718,4 @@ "empty": "Keine Wiedergabe", "minutesAgo": "Vor %{smart_count} Minute |||| Vor %{smart_count} Minuten" } -} \ No newline at end of file +} diff --git a/resources/i18n/el.json b/resources/i18n/el.json index e5d983d6f..019d05978 100644 --- a/resources/i18n/el.json +++ b/resources/i18n/el.json @@ -219,19 +219,13 @@ "saveQueue": "Αποθήκευση ουράς στη λίστα αναπαραγωγής", "searchOrCreate": "Αναζητήστε λίστες αναπαραγωγής ή πληκτρολογήστε για να δημιουργήσετε νέες...", "pressEnterToCreate": "Πατήστε Enter για να δημιουργήσετε νέα λίστα αναπαραγωγής", - "removeFromSelection": "Αφαίρεση από την επιλογή", - "uploadCover": "", - "removeCover": "" + "removeFromSelection": "Αφαίρεση από την επιλογή" }, "message": { "duplicate_song": "Προσθήκη διπλοεγγραφών τραγουδιών", "song_exist": "Υπάρχουν διπλοεγγραφές στην λίστα αναπαραγωγής. Θέλετε να προστεθούν οι διπλοεγγραφές ή να τις παραβλέψετε?", "noPlaylistsFound": "Δεν βρέθηκαν λίστες αναπαραγωγής", - "noPlaylists": "Δεν υπάρχουν διαθέσιμες λίστες αναπαραγωγής", - "coverUploaded": "", - "coverRemoved": "", - "coverUploadError": "", - "coverRemoveError": "" + "noPlaylists": "Δεν υπάρχουν διαθέσιμες λίστες αναπαραγωγής" } }, "radio": { @@ -718,4 +712,4 @@ "empty": "Δεν παίζει τίποτα", "minutesAgo": "%{smart_count} λεπτό πριν |||| %{smart_count} λεπτά πριν" } -} \ No newline at end of file +} diff --git a/resources/i18n/es.json b/resources/i18n/es.json index ef246a0e6..29d1a367f 100644 --- a/resources/i18n/es.json +++ b/resources/i18n/es.json @@ -219,19 +219,13 @@ "saveQueue": "Guardar la fila de reproducción en una playlist", "searchOrCreate": "Buscar listas de reproducción o escribe para crear una nueva…", "pressEnterToCreate": "Pulsa Enter para crear una nueva lista de reproducción", - "removeFromSelection": "Quitar de la selección", - "uploadCover": "", - "removeCover": "" + "removeFromSelection": "Quitar de la selección" }, "message": { "duplicate_song": "Algunas de las canciones seleccionadas están presentes en la playlist", "song_exist": "Se están agregando duplicados a la playlist. ¿Quieres agregar los duplicados o omitirlos?", "noPlaylistsFound": "No se encontraron listas de reproducción", - "noPlaylists": "No hay listas de reproducción disponibles", - "coverUploaded": "", - "coverRemoved": "", - "coverUploadError": "", - "coverRemoveError": "" + "noPlaylists": "No hay listas de reproducción disponibles" } }, "radio": { @@ -718,4 +712,4 @@ "empty": "Nada en reproducción", "minutesAgo": "Hace %{smart_count} minuto |||| Hace %{smart_count} minutos" } -} \ No newline at end of file +} diff --git a/resources/i18n/fi.json b/resources/i18n/fi.json index ea5d22f4c..59f353350 100644 --- a/resources/i18n/fi.json +++ b/resources/i18n/fi.json @@ -219,19 +219,13 @@ "saveQueue": "Tallenna jono soittolistaan", "searchOrCreate": "Etsi soittolistoja tai kirjoita luodaksesi uuden...", "pressEnterToCreate": "Paina Enter luodaksesi uuden soittolistan", - "removeFromSelection": "Poista valinnasta", - "uploadCover": "Lataa kansikuva", - "removeCover": "Poista kansikuva" + "removeFromSelection": "Poista valinnasta" }, "message": { "duplicate_song": "Lisää olemassa oleva kappale", "song_exist": "Olet lisäämässä soittolistalla jo olevaa kappaletta. Haluatko lisätä saman kappaleen vai ohittaa sen?", "noPlaylistsFound": "Soittolistoja ei löytynyt", - "noPlaylists": "Soittolistoja ei ole saatavilla", - "coverUploaded": "Kansikuva päivitetty", - "coverRemoved": "Kansikuva poistettu", - "coverUploadError": "Virhe ladattaessa kansikuvaa", - "coverRemoveError": "Virhe poistettaessa kansikuvaa" + "noPlaylists": "Soittolistoja ei ole saatavilla" } }, "radio": { @@ -597,7 +591,13 @@ "remove_all_missing_content": "Haluatko varmasti poistaa kaikki puuttuvat tiedostot tietokannasta? Tämä poistaa pysyvästi kaikki viittaukset niihin, mukaan lukien toistomäärät ja arvostelut.", "noSimilarSongsFound": "Samankaltaisia kappaleita ei löytynyt", "noTopSongsFound": "Suosituimpia kappaleita ei löytynyt", - "startingInstantMix": "Ladataan Pikasekoitus..." + "startingInstantMix": "Ladataan Pikasekoitus...", + "uploadCover": "Lataa kansikuva", + "removeCover": "Poista kansikuva", + "coverUploaded": "Kansikuva päivitetty", + "coverRemoved": "Kansikuva poistettu", + "coverUploadError": "Virhe ladattaessa kansikuvaa", + "coverRemoveError": "Virhe poistettaessa kansikuvaa" }, "menu": { "library": "Kirjasto", @@ -718,4 +718,4 @@ "empty": "Ei soita mitään", "minutesAgo": "%{smart_count} minuutti sitten |||| %{smart_count} minuuttia sitten" } -} \ No newline at end of file +} diff --git a/resources/i18n/fr.json b/resources/i18n/fr.json index 57af0b8cd..891fde03a 100644 --- a/resources/i18n/fr.json +++ b/resources/i18n/fr.json @@ -219,19 +219,13 @@ "saveQueue": "Sauvegarder la file de lecture dans la playlist", "searchOrCreate": "Chercher ou créer une nouvelle playlist...", "pressEnterToCreate": "Appuyer sur entrée pour créer une nouvelle playlist", - "removeFromSelection": "Supprimer de la sélection", - "uploadCover": "", - "removeCover": "" + "removeFromSelection": "Supprimer de la sélection" }, "message": { "duplicate_song": "Ajouter les titres déjà présents dans la playlist", "song_exist": "Certains des titres sélectionnés font déjà partie de la playlist. Voulez-vous les ajouter ou les ignorer ?", "noPlaylistsFound": "Aucune playlist trouvée", - "noPlaylists": "Aucune playlist disponible", - "coverUploaded": "", - "coverRemoved": "", - "coverUploadError": "", - "coverRemoveError": "" + "noPlaylists": "Aucune playlist disponible" } }, "radio": { @@ -718,4 +712,4 @@ "empty": "Aucun titre en cours de lecture", "minutesAgo": "Il y a %{smart_count} minute |||| Il y a %{smart_count} minutes" } -} \ No newline at end of file +} diff --git a/resources/i18n/gl.json b/resources/i18n/gl.json index c10560912..aba22b714 100644 --- a/resources/i18n/gl.json +++ b/resources/i18n/gl.json @@ -219,19 +219,13 @@ "saveQueue": "Salvar a Cola como Lista de reprodución", "searchOrCreate": "Buscar listas ou escribe para crear nova…", "pressEnterToCreate": "Preme Enter para crear nova lista", - "removeFromSelection": "Retirar da selección", - "uploadCover": "", - "removeCover": "" + "removeFromSelection": "Retirar da selección" }, "message": { "duplicate_song": "Engadir cancións duplicadas", "song_exist": "Hai duplicadas que serán engadidas á lista de reprodución. Desexas engadir as duplicadas ou omitilas?", "noPlaylistsFound": "Sen listas de reprodución", - "noPlaylists": "Sen listas dispoñibles", - "coverUploaded": "", - "coverRemoved": "", - "coverUploadError": "", - "coverRemoveError": "" + "noPlaylists": "Sen listas dispoñibles" } }, "radio": { @@ -718,4 +712,4 @@ "empty": "Sen reprodución", "minutesAgo": "hai %{smart_count} minuto |||| hai %{smart_count} minutos" } -} \ No newline at end of file +} diff --git a/resources/i18n/pt-br.json b/resources/i18n/pt-br.json index 6c9e154d1..bc2a5f85e 100644 --- a/resources/i18n/pt-br.json +++ b/resources/i18n/pt-br.json @@ -218,15 +218,9 @@ "saveQueue": "Salvar fila em nova Playlist", "searchOrCreate": "Buscar playlists ou criar nova...", "pressEnterToCreate": "Pressione Enter para criar nova playlist", - "removeFromSelection": "Remover da seleção", - "uploadCover": "Enviar Capa", - "removeCover": "Remover Capa" + "removeFromSelection": "Remover da seleção" }, "message": { - "coverUploaded": "Capa atualizada", - "coverRemoved": "Capa removida", - "coverUploadError": "Erro ao enviar capa", - "coverRemoveError": "Erro ao remover capa", "duplicate_song": "Adicionar músicas duplicadas", "song_exist": "Algumas destas músicas já existem na playlist. Você quer adicionar as duplicadas ou ignorá-las?", "noPlaylistsFound": "Nenhuma playlist encontrada", @@ -560,6 +554,12 @@ } }, "message": { + "uploadCover": "Enviar Capa", + "removeCover": "Remover Capa", + "coverUploaded": "Capa atualizada", + "coverRemoved": "Capa removida", + "coverUploadError": "Erro ao enviar capa", + "coverRemoveError": "Erro ao remover capa", "note": "ATENÇÃO", "transcodingDisabled": "Por questão de segurança, esta tela de configuração está desabilitada. Se você quiser alterar estas configurações, reinicie o servidor com a opção %{config}", "transcodingEnabled": "Navidrome está sendo executado com a opção %{config}. Isto permite que potencialmente se execute comandos do sistema pela interface Web. É recomendado que vc mantenha esta opção desabilitada, e só a habilite quando precisar configurar opções de Conversão", diff --git a/resources/i18n/ru.json b/resources/i18n/ru.json index 6f0439a38..78e7cfa26 100644 --- a/resources/i18n/ru.json +++ b/resources/i18n/ru.json @@ -219,19 +219,13 @@ "saveQueue": "Сохранить очередь в плейлист", "searchOrCreate": "Поиск плейлистов или введите текст для создания новых...", "pressEnterToCreate": "Нажмите Enter, чтобы создать новый список воспроизведения", - "removeFromSelection": "Удалить из списка выделенных", - "uploadCover": "", - "removeCover": "" + "removeFromSelection": "Удалить из списка выделенных" }, "message": { "duplicate_song": "Повторяющиеся треки", "song_exist": "Некоторые треки уже есть в плейлисте. Вы хотите добавить их или пропустить?", "noPlaylistsFound": "Плейлисты не найдены", - "noPlaylists": "Нет доступных плейлистов", - "coverUploaded": "", - "coverRemoved": "", - "coverUploadError": "", - "coverRemoveError": "" + "noPlaylists": "Нет доступных плейлистов" } }, "radio": { @@ -718,4 +712,4 @@ "empty": "Ничего не играет", "minutesAgo": "%{smart_count} минут назад |||| %{smart_count} минут назад" } -} \ No newline at end of file +} diff --git a/resources/i18n/sl.json b/resources/i18n/sl.json index 2ea1cad49..ceb56e9b7 100644 --- a/resources/i18n/sl.json +++ b/resources/i18n/sl.json @@ -219,19 +219,13 @@ "saveQueue": "Shrani čakalno vrsto na seznam predvajanja", "searchOrCreate": "Iščite po seznamih predvajanja ali vnesite besedilo, da ustvarite nove ...", "pressEnterToCreate": "Pritisnite Enter za ustvarjanje novega seznama predvajanja", - "removeFromSelection": "Odstrani iz izbora", - "uploadCover": "", - "removeCover": "" + "removeFromSelection": "Odstrani iz izbora" }, "message": { "duplicate_song": "Dodaj podvojene pesmi", "song_exist": "Seznamu predvajanja boste dodali duplikate. Jih želite dodati ali izpustiti?", "noPlaylistsFound": "Ni najdenih seznamov predvajanja", - "noPlaylists": "Ni na voljo seznamov predvajanja", - "coverUploaded": "", - "coverRemoved": "", - "coverUploadError": "", - "coverRemoveError": "" + "noPlaylists": "Ni na voljo seznamov predvajanja" } }, "radio": { @@ -718,4 +712,4 @@ "empty": "Nič se ne predvaja", "minutesAgo": "Pred %{smart_count} minuto |||| Pred %{smart_count} minutami" } -} \ No newline at end of file +} diff --git a/resources/i18n/sv.json b/resources/i18n/sv.json index b792943d0..23bd5fbc2 100644 --- a/resources/i18n/sv.json +++ b/resources/i18n/sv.json @@ -219,19 +219,13 @@ "saveQueue": "Spara kö till spellista", "searchOrCreate": "Sök spellista eller skapa ny...", "pressEnterToCreate": "Tryck Enter för att skapa ny spellista", - "removeFromSelection": "Ta bort från urval", - "uploadCover": "", - "removeCover": "" + "removeFromSelection": "Ta bort från urval" }, "message": { "duplicate_song": "Lägg till dubletter", "song_exist": "Vissa låtar finns redan i spellistan. Vill du lägga till dubbletterna eller hoppa över dem?", "noPlaylistsFound": "Hittade inga spellistor", - "noPlaylists": "Inga spellistor tillgängliga", - "coverUploaded": "", - "coverRemoved": "", - "coverUploadError": "", - "coverRemoveError": "" + "noPlaylists": "Inga spellistor tillgängliga" } }, "radio": { @@ -718,4 +712,4 @@ "empty": "Inget spelas", "minutesAgo": "%{smart_count} minut sedan |||| %{smart_count} minuter sedan" } -} \ No newline at end of file +} diff --git a/resources/i18n/th.json b/resources/i18n/th.json index d3d64bdbf..b445d7464 100644 --- a/resources/i18n/th.json +++ b/resources/i18n/th.json @@ -219,19 +219,13 @@ "saveQueue": "บันทึกคิวลงเพลย์ลิสต์", "searchOrCreate": "ค้นหาเพลย์ลิสต์หรือพิมพ์เพื่อสร้างใหม่", "pressEnterToCreate": "กด Enter เพื่อสร้างเพลย์ลิสต์", - "removeFromSelection": "เอาออกจากที่เลือกไว้", - "uploadCover": "", - "removeCover": "" + "removeFromSelection": "เอาออกจากที่เลือกไว้" }, "message": { "duplicate_song": "เพิ่มเพลงซ้ำ", "song_exist": "เพิ่มเพลงซ้ำกันในเพลย์ลิสต์ คุณจะเพิ่มเพลงต่อหรือข้าม", "noPlaylistsFound": "ไม่พบเพลย์ลิสต์", - "noPlaylists": "ไม่มีเพลย์ลิสต์อยู่", - "coverUploaded": "", - "coverRemoved": "", - "coverUploadError": "", - "coverRemoveError": "" + "noPlaylists": "ไม่มีเพลย์ลิสต์อยู่" } }, "radio": { @@ -718,4 +712,4 @@ "empty": "ไม่มีเพลงเล่น", "minutesAgo": "%{smart_count} นาทีที่แล้ว |||| %{smart_count} นาทีที่แล้ว" } -} \ No newline at end of file +} diff --git a/resources/i18n/zh-Hant.json b/resources/i18n/zh-Hant.json index aca2e4742..dabf61bdb 100644 --- a/resources/i18n/zh-Hant.json +++ b/resources/i18n/zh-Hant.json @@ -219,19 +219,13 @@ "saveQueue": "將播放佇列儲存到播放清單", "searchOrCreate": "搜尋播放清單,或輸入名稱來新建…", "pressEnterToCreate": "按 Enter 鍵建立新的播放清單", - "removeFromSelection": "移除選取項目", - "uploadCover": "上傳封面", - "removeCover": "移除封面" + "removeFromSelection": "移除選取項目" }, "message": { "duplicate_song": "加入重複的歌曲", "song_exist": "有重複歌曲正要加入播放清單,您要加入或略過重複歌曲?", "noPlaylistsFound": "找不到播放清單", - "noPlaylists": "暫無播放清單", - "coverUploaded": "已更新封面圖", - "coverRemoved": "已移除封面圖", - "coverUploadError": "上傳封面圖時發生錯誤", - "coverRemoveError": "移除封面圖時發生錯誤" + "noPlaylists": "暫無播放清單" } }, "radio": { @@ -597,7 +591,13 @@ "remove_all_missing_content": "您確定要從媒體庫中刪除所有遺失的檔案嗎?這將永久刪除它們的所有相關資訊,包括它們的播放次數和評分。", "noSimilarSongsFound": "找不到相似歌曲", "noTopSongsFound": "找不到熱門歌曲", - "startingInstantMix": "正在載入即時混音..." + "startingInstantMix": "正在載入即時混音...", + "uploadCover": "上傳封面", + "removeCover": "移除封面", + "coverUploaded": "已更新封面圖", + "coverRemoved": "已移除封面圖", + "coverUploadError": "上傳封面圖時發生錯誤", + "coverRemoveError": "移除封面圖時發生錯誤" }, "menu": { "library": "媒體庫", @@ -718,4 +718,4 @@ "empty": "無播放內容", "minutesAgo": "1 分鐘前 |||| %{smart_count} 分鐘前" } -} \ No newline at end of file +} diff --git a/scanner/controller_test.go b/scanner/controller_test.go index 2af52066b..d60d432b4 100644 --- a/scanner/controller_test.go +++ b/scanner/controller_test.go @@ -5,6 +5,7 @@ import ( "github.com/navidrome/navidrome/conf/configtest" "github.com/navidrome/navidrome/consts" + "github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/core/artwork" "github.com/navidrome/navidrome/core/metrics" "github.com/navidrome/navidrome/core/playlists" @@ -31,7 +32,7 @@ var _ = Describe("Controller", func() { DeferCleanup(configtest.SetupConfig()) ds = &tests.MockDataStore{RealDS: persistence.New(db.Db())} ds.MockedProperty = &tests.MockedPropertyRepo{} - ctrl = scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(), playlists.NewPlaylists(ds), metrics.NewNoopInstance()) + ctrl = scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(), playlists.NewPlaylists(ds, core.NewImageUploadService()), metrics.NewNoopInstance()) }) It("includes last scan error", func() { diff --git a/scanner/scanner_benchmark_test.go b/scanner/scanner_benchmark_test.go index 1ac7b50a4..8f0dcd340 100644 --- a/scanner/scanner_benchmark_test.go +++ b/scanner/scanner_benchmark_test.go @@ -12,6 +12,7 @@ import ( "github.com/dustin/go-humanize" "github.com/google/uuid" "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/core/artwork" "github.com/navidrome/navidrome/core/metrics" "github.com/navidrome/navidrome/core/playlists" @@ -40,7 +41,7 @@ func BenchmarkScan(b *testing.B) { ds := persistence.New(db.Db()) conf.Server.DevExternalScanner = false s := scanner.New(context.Background(), ds, artwork.NoopCacheWarmer(), events.NoopBroker(), - playlists.NewPlaylists(ds), metrics.NewNoopInstance()) + playlists.NewPlaylists(ds, core.NewImageUploadService()), metrics.NewNoopInstance()) fs := storagetest.FakeFS{} storagetest.Register("fake", &fs) diff --git a/scanner/scanner_multilibrary_test.go b/scanner/scanner_multilibrary_test.go index 6990f1984..856015239 100644 --- a/scanner/scanner_multilibrary_test.go +++ b/scanner/scanner_multilibrary_test.go @@ -11,6 +11,7 @@ import ( "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf/configtest" "github.com/navidrome/navidrome/consts" + "github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/core/artwork" "github.com/navidrome/navidrome/core/metrics" "github.com/navidrome/navidrome/core/playlists" @@ -77,7 +78,7 @@ var _ = Describe("Scanner - Multi-Library", Ordered, func() { Expect(ds.User(ctx).Put(&adminUser)).To(Succeed()) s = scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(), - playlists.NewPlaylists(ds), metrics.NewNoopInstance()) + playlists.NewPlaylists(ds, core.NewImageUploadService()), metrics.NewNoopInstance()) // Create two test libraries (let DB auto-assign IDs) lib1 = model.Library{Name: "Rock Collection", Path: "rock:///music"} diff --git a/scanner/scanner_selective_test.go b/scanner/scanner_selective_test.go index 6e4511179..594b74e38 100644 --- a/scanner/scanner_selective_test.go +++ b/scanner/scanner_selective_test.go @@ -8,6 +8,7 @@ import ( "github.com/Masterminds/squirrel" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf/configtest" + "github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/core/artwork" "github.com/navidrome/navidrome/core/metrics" "github.com/navidrome/navidrome/core/playlists" @@ -63,7 +64,7 @@ var _ = Describe("ScanFolders", Ordered, func() { Expect(ds.User(ctx).Put(&adminUser)).To(Succeed()) s = scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(), - playlists.NewPlaylists(ds), metrics.NewNoopInstance()) + playlists.NewPlaylists(ds, core.NewImageUploadService()), metrics.NewNoopInstance()) lib = model.Library{ID: 1, Name: "Fake Library", Path: "fake:///music"} Expect(ds.Library(ctx).Put(&lib)).To(Succeed()) diff --git a/scanner/scanner_test.go b/scanner/scanner_test.go index d5688a1dc..922d21e62 100644 --- a/scanner/scanner_test.go +++ b/scanner/scanner_test.go @@ -11,6 +11,7 @@ import ( "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf/configtest" "github.com/navidrome/navidrome/consts" + "github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/core/artwork" "github.com/navidrome/navidrome/core/metrics" "github.com/navidrome/navidrome/core/playlists" @@ -84,7 +85,7 @@ var _ = Describe("Scanner", Ordered, func() { Expect(ds.User(ctx).Put(&adminUser)).To(Succeed()) s = scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(), - playlists.NewPlaylists(ds), metrics.NewNoopInstance()) + playlists.NewPlaylists(ds, core.NewImageUploadService()), metrics.NewNoopInstance()) lib = model.Library{ID: 1, Name: "Fake Library", Path: "fake:///music"} Expect(ds.Library(ctx).Put(&lib)).To(Succeed()) diff --git a/server/e2e/e2e_suite_test.go b/server/e2e/e2e_suite_test.go index ef379ad05..cb851debf 100644 --- a/server/e2e/e2e_suite_test.go +++ b/server/e2e/e2e_suite_test.go @@ -442,7 +442,7 @@ var _ = BeforeSuite(func() { buildTestFS() s := scanner.New(ctx, initDS, artwork.NoopCacheWarmer(), events.NoopBroker(), - playlists.NewPlaylists(initDS), metrics.NewNoopInstance()) + playlists.NewPlaylists(initDS, core.NewImageUploadService()), metrics.NewNoopInstance()) _, err = s.ScanAll(ctx, true) Expect(err).ToNot(HaveOccurred()) @@ -479,7 +479,7 @@ func setupTestDB() { streamerSpy = &spyStreamer{} decider := stream.NewTranscodeDecider(ds, noopFFmpeg{}) s := scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(), - playlists.NewPlaylists(ds), metrics.NewNoopInstance()) + playlists.NewPlaylists(ds, core.NewImageUploadService()), metrics.NewNoopInstance()) router = subsonic.New( ds, noopArtwork{}, @@ -489,7 +489,7 @@ func setupTestDB() { noopProvider{}, s, events.NoopBroker(), - playlists.NewPlaylists(ds), + playlists.NewPlaylists(ds, core.NewImageUploadService()), noopPlayTracker{}, core.NewShare(ds), playback.PlaybackServer(nil), diff --git a/server/e2e/subsonic_multilibrary_test.go b/server/e2e/subsonic_multilibrary_test.go index f59187d00..a837da124 100644 --- a/server/e2e/subsonic_multilibrary_test.go +++ b/server/e2e/subsonic_multilibrary_test.go @@ -6,6 +6,7 @@ import ( "github.com/Masterminds/squirrel" "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/core/artwork" "github.com/navidrome/navidrome/core/metrics" "github.com/navidrome/navidrome/core/playlists" @@ -53,7 +54,7 @@ var _ = Describe("Multi-Library Support", Ordered, func() { // Run incremental scan to import lib2 content (lib1 files unchanged → skipped) s := scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(), - playlists.NewPlaylists(ds), metrics.NewNoopInstance()) + playlists.NewPlaylists(ds, core.NewImageUploadService()), metrics.NewNoopInstance()) _, err = s.ScanAll(ctx, false) Expect(err).ToNot(HaveOccurred()) diff --git a/server/nativeapi/artists.go b/server/nativeapi/artists.go new file mode 100644 index 000000000..1b78bb93e --- /dev/null +++ b/server/nativeapi/artists.go @@ -0,0 +1,72 @@ +package nativeapi + +import ( + "context" + "errors" + "io" + "net/http" + "time" + + "github.com/deluan/rest" + "github.com/go-chi/chi/v5" + "github.com/navidrome/navidrome/consts" + "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/server" +) + +func (api *Router) addArtistRoute(r chi.Router) { + constructor := func(ctx context.Context) rest.Repository { + return api.ds.Resource(ctx, model.Artist{}) + } + r.Route("/artist", func(r chi.Router) { + r.Get("/", rest.GetAll(constructor)) + r.Route("/{id}", func(r chi.Router) { + r.Use(server.URLParamsMiddleware) + r.Get("/", rest.Get(constructor)) + r.Post("/image", api.uploadArtistImage()) + r.Delete("/image", api.deleteArtistImage()) + }) + }) +} + +func (api *Router) uploadArtistImage() http.HandlerFunc { + return handleImageUpload(func(ctx context.Context, reader io.Reader, ext string) error { + artistID := chi.URLParamFromCtx(ctx, "id") + ar, err := api.ds.Artist(ctx).Get(artistID) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return model.ErrNotFound + } + return err + } + oldPath := ar.UploadedImagePath() + filename, err := api.imgUpload.SetImage(ctx, consts.EntityArtist, ar.ID, ar.Name, oldPath, reader, ext) + if err != nil { + return err + } + ar.UploadedImage = filename + now := time.Now() + ar.UpdatedAt = &now + return api.ds.Artist(ctx).Put(ar, "uploaded_image", "updated_at") + }) +} + +func (api *Router) deleteArtistImage() http.HandlerFunc { + return handleImageDelete(func(ctx context.Context) error { + artistID := chi.URLParamFromCtx(ctx, "id") + ar, err := api.ds.Artist(ctx).Get(artistID) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return model.ErrNotFound + } + return err + } + if err := api.imgUpload.RemoveImage(ctx, ar.UploadedImagePath()); err != nil { + return err + } + ar.UploadedImage = "" + now := time.Now() + ar.UpdatedAt = &now + return api.ds.Artist(ctx).Put(ar, "uploaded_image", "updated_at") + }) +} diff --git a/server/nativeapi/config_test.go b/server/nativeapi/config_test.go index d7368cabf..4e6e9e89b 100644 --- a/server/nativeapi/config_test.go +++ b/server/nativeapi/config_test.go @@ -28,7 +28,7 @@ var _ = Describe("Config API", func() { conf.Server.DevUIShowConfig = true // Enable config endpoint for tests ds = &tests.MockDataStore{} auth.Init(ds) - nativeRouter := New(ds, nil, nil, nil, tests.NewMockLibraryService(), tests.NewMockUserService(), nil, nil) + nativeRouter := New(ds, nil, nil, nil, tests.NewMockLibraryService(), tests.NewMockUserService(), nil, nil, nil) router = server.JWTVerifier(nativeRouter) // Create test users diff --git a/server/nativeapi/image_upload.go b/server/nativeapi/image_upload.go new file mode 100644 index 000000000..c29f14bdc --- /dev/null +++ b/server/nativeapi/image_upload.go @@ -0,0 +1,120 @@ +package nativeapi + +import ( + "context" + "errors" + "fmt" + "image" + _ "image/gif" + _ "image/jpeg" + _ "image/png" + "io" + "net/http" + "path/filepath" + "strings" + + "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/model/request" + _ "golang.org/x/image/webp" +) + +const maxImageSize = 10 << 20 // 10MB + +func checkImageUploadPermission(w http.ResponseWriter, r *http.Request) bool { + user, _ := request.UserFrom(r.Context()) + if !conf.Server.EnableCoverArtUpload && !user.IsAdmin { + http.Error(w, "cover art upload is disabled", http.StatusForbidden) + return false + } + return true +} + +func handleImageUpload(saveFn func(ctx context.Context, reader io.Reader, ext string) error) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if !checkImageUploadPermission(w, r) { + return + } + r.Body = http.MaxBytesReader(w, r.Body, maxImageSize) + if err := r.ParseMultipartForm(maxImageSize / 2); err != nil { + log.Error(ctx, "Error parsing multipart form", err) + http.Error(w, "file too large or invalid form", http.StatusBadRequest) + return + } + defer func() { + if r.MultipartForm != nil { + if err := r.MultipartForm.RemoveAll(); err != nil { + log.Warn(ctx, "Error removing multipart temp files", err) + } + } + }() + file, header, err := r.FormFile("image") + if err != nil { + log.Error(ctx, "Error reading uploaded file", err) + http.Error(w, "missing image file", http.StatusBadRequest) + return + } + defer file.Close() + _, format, err := image.DecodeConfig(file) + if err != nil { + log.Error(ctx, "Uploaded file is not a valid image", err) + http.Error(w, "invalid image file", http.StatusBadRequest) + return + } + if seeker, ok := file.(io.Seeker); ok { + if _, err := seeker.Seek(0, io.SeekStart); err != nil { + log.Error(ctx, "Error seeking file", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + ext := "." + format + if ext == "." { + ext = strings.ToLower(filepath.Ext(header.Filename)) + } + if ext == "" || ext == "." { + log.Error(ctx, "Could not determine image type", "filename", header.Filename) + http.Error(w, "could not determine image type", http.StatusBadRequest) + return + } + if err := saveFn(ctx, file, ext); err != nil { + if errors.Is(err, model.ErrNotAuthorized) { + http.Error(w, "not authorized", http.StatusForbidden) + return + } + if errors.Is(err, model.ErrNotFound) { + http.Error(w, "not found", http.StatusNotFound) + return + } + log.Error(ctx, "Error saving image", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, _ = fmt.Fprintf(w, `{"status":"ok"}`) + } +} + +func handleImageDelete(deleteFn func(ctx context.Context) error) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if !checkImageUploadPermission(w, r) { + return + } + if err := deleteFn(ctx); err != nil { + if errors.Is(err, model.ErrNotAuthorized) { + http.Error(w, "not authorized", http.StatusForbidden) + return + } + if errors.Is(err, model.ErrNotFound) { + http.Error(w, "not found", http.StatusNotFound) + return + } + log.Error(ctx, "Error removing image", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, _ = fmt.Fprintf(w, `{"status":"ok"}`) + } +} diff --git a/server/nativeapi/library_test.go b/server/nativeapi/library_test.go index 5b9cf7e4e..ed5564a41 100644 --- a/server/nativeapi/library_test.go +++ b/server/nativeapi/library_test.go @@ -29,7 +29,7 @@ var _ = Describe("Library API", func() { DeferCleanup(configtest.SetupConfig()) ds = &tests.MockDataStore{} auth.Init(ds) - nativeRouter := New(ds, nil, nil, nil, tests.NewMockLibraryService(), tests.NewMockUserService(), nil, nil) + nativeRouter := New(ds, nil, nil, nil, tests.NewMockLibraryService(), tests.NewMockUserService(), nil, nil, nil) router = server.JWTVerifier(nativeRouter) // Create test users diff --git a/server/nativeapi/native_api.go b/server/nativeapi/native_api.go index 3191991eb..3ef00ebb1 100644 --- a/server/nativeapi/native_api.go +++ b/server/nativeapi/native_api.go @@ -44,10 +44,11 @@ type Router struct { users core.User maintenance core.Maintenance pluginManager PluginManager + imgUpload core.ImageUploadService } -func New(ds model.DataStore, share core.Share, playlists playlistsvc.Playlists, insights metrics.Insights, libraryService core.Library, userService core.User, maintenance core.Maintenance, pluginManager PluginManager) *Router { - r := &Router{ds: ds, share: share, playlists: playlists, insights: insights, libs: libraryService, users: userService, maintenance: maintenance, pluginManager: pluginManager} +func New(ds model.DataStore, share core.Share, playlists playlistsvc.Playlists, insights metrics.Insights, libraryService core.Library, userService core.User, maintenance core.Maintenance, pluginManager PluginManager, imgUpload core.ImageUploadService) *Router { + r := &Router{ds: ds, share: share, playlists: playlists, insights: insights, libs: libraryService, users: userService, maintenance: maintenance, pluginManager: pluginManager, imgUpload: imgUpload} r.Handler = r.routes() return r } @@ -66,7 +67,7 @@ func (api *Router) routes() http.Handler { api.RX(r, "/user", api.users.NewRepository, true) api.R(r, "/song", model.MediaFile{}, false) api.R(r, "/album", model.Album{}, false) - api.R(r, "/artist", model.Artist{}, false) + api.addArtistRoute(r) api.R(r, "/genre", model.Genre{}, false) api.R(r, "/player", model.Player{}, true) api.R(r, "/transcoding", model.Transcoding{}, conf.Server.EnableTranscodingConfig) diff --git a/server/nativeapi/native_api_song_test.go b/server/nativeapi/native_api_song_test.go index b192e00ac..f0ee50ebb 100644 --- a/server/nativeapi/native_api_song_test.go +++ b/server/nativeapi/native_api_song_test.go @@ -94,7 +94,7 @@ var _ = Describe("Song Endpoints", func() { mfRepo.SetData(testSongs) // Create the native API router and wrap it with the JWTVerifier middleware - nativeRouter := New(ds, nil, nil, nil, tests.NewMockLibraryService(), tests.NewMockUserService(), nil, nil) + nativeRouter := New(ds, nil, nil, nil, tests.NewMockLibraryService(), tests.NewMockUserService(), nil, nil, nil) router = server.JWTVerifier(nativeRouter) w = httptest.NewRecorder() }) diff --git a/server/nativeapi/playlists.go b/server/nativeapi/playlists.go index 118528f68..ea1cf579b 100644 --- a/server/nativeapi/playlists.go +++ b/server/nativeapi/playlists.go @@ -5,25 +5,17 @@ import ( "encoding/json" "errors" "fmt" - "image" - _ "image/gif" - _ "image/jpeg" - _ "image/png" "io" "net/http" - "path/filepath" "strconv" "strings" "github.com/deluan/rest" "github.com/go-chi/chi/v5" - "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/core/playlists" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" - "github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/utils/req" - _ "golang.org/x/image/webp" ) type restHandler = func(rest.RepositoryConstructor, ...rest.Logger) http.HandlerFunc @@ -234,110 +226,16 @@ func getSongPlaylists(svc playlists.Playlists) http.HandlerFunc { } } -const maxImageSize = 10 << 20 // 10MB - func uploadPlaylistImage(pls playlists.Playlists) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - user, _ := request.UserFrom(ctx) - if !conf.Server.EnableCoverArtUpload && !user.IsAdmin { - http.Error(w, "cover art upload is disabled", http.StatusForbidden) - return - } - p := req.Params(r) - playlistId, _ := p.String(":id") - - if err := r.ParseMultipartForm(maxImageSize); err != nil { //nolint:gosec // size is limited by maxImageSize parameter - log.Error(ctx, "Error parsing multipart form", err) - http.Error(w, "file too large or invalid form", http.StatusBadRequest) - return - } - - file, header, err := r.FormFile("image") - if err != nil { - log.Error(ctx, "Error reading uploaded file", err) - http.Error(w, "missing image file", http.StatusBadRequest) - return - } - defer file.Close() - - // Validate the uploaded file is a valid image - _, format, err := image.DecodeConfig(file) - if err != nil { - log.Error(ctx, "Uploaded file is not a valid image", err) - http.Error(w, "invalid image file", http.StatusBadRequest) - return - } - - // Reset reader after DecodeConfig consumed some bytes - if seeker, ok := file.(io.Seeker); ok { - if _, err := seeker.Seek(0, io.SeekStart); err != nil { - log.Error(ctx, "Error seeking file", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - } - - // Determine file extension from decoded format or original filename - ext := "." + format - if ext == "." { - ext = strings.ToLower(filepath.Ext(header.Filename)) - } - if ext == "" || ext == "." { - log.Error(ctx, "Could not determine image type", "playlistId", playlistId, "filename", header.Filename) - http.Error(w, "could not determine image type", http.StatusBadRequest) - return - } - - err = pls.SetImage(ctx, playlistId, file, ext) - if errors.Is(err, model.ErrNotAuthorized) { - log.Error(ctx, "Not authorized to upload playlist image", "playlistId", playlistId, err) - http.Error(w, "not authorized", http.StatusForbidden) - return - } - if errors.Is(err, model.ErrNotFound) { - log.Error(ctx, "Playlist not found for image upload", "playlistId", playlistId, err) - http.Error(w, "not found", http.StatusNotFound) - return - } - if err != nil { - log.Error(ctx, "Error saving playlist image", "playlistId", playlistId, err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - _, _ = fmt.Fprintf(w, `{"status":"ok"}`) //nolint:gosec - } + return handleImageUpload(func(ctx context.Context, reader io.Reader, ext string) error { + playlistId := chi.URLParamFromCtx(ctx, "id") + return pls.SetImage(ctx, playlistId, reader, ext) + }) } func deletePlaylistImage(pls playlists.Playlists) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - user, _ := request.UserFrom(ctx) - if !conf.Server.EnableCoverArtUpload && !user.IsAdmin { - http.Error(w, "cover art upload is disabled", http.StatusForbidden) - return - } - p := req.Params(r) - playlistId, _ := p.String(":id") - - err := pls.RemoveImage(ctx, playlistId) - if errors.Is(err, model.ErrNotAuthorized) { - log.Error(ctx, "Not authorized to remove playlist image", "playlistId", playlistId, err) - http.Error(w, "not authorized", http.StatusForbidden) - return - } - if errors.Is(err, model.ErrNotFound) { - log.Error(ctx, "Playlist not found for image removal", "playlistId", playlistId, err) - http.Error(w, "not found", http.StatusNotFound) - return - } - if err != nil { - log.Error(ctx, "Error removing playlist image", "playlistId", playlistId, err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - _, _ = fmt.Fprintf(w, `{"status":"ok"}`) //nolint:gosec - } + return handleImageDelete(func(ctx context.Context) error { + playlistId := chi.URLParamFromCtx(ctx, "id") + return pls.RemoveImage(ctx, playlistId) + }) } diff --git a/server/nativeapi/playlists_test.go b/server/nativeapi/playlists_test.go index 7f0cd7de1..dfe6b9296 100644 --- a/server/nativeapi/playlists_test.go +++ b/server/nativeapi/playlists_test.go @@ -98,7 +98,7 @@ var _ = Describe("Playlist Tracks Endpoint", func() { err := userRepo.Put(&testUser) Expect(err).ToNot(HaveOccurred()) - nativeRouter := New(ds, nil, plsSvc, nil, tests.NewMockLibraryService(), tests.NewMockUserService(), nil, nil) + nativeRouter := New(ds, nil, plsSvc, nil, tests.NewMockLibraryService(), tests.NewMockUserService(), nil, nil, nil) router = server.JWTVerifier(nativeRouter) w = httptest.NewRecorder() }) diff --git a/server/nativeapi/plugin_test.go b/server/nativeapi/plugin_test.go index 7946b90fd..8fc88e09c 100644 --- a/server/nativeapi/plugin_test.go +++ b/server/nativeapi/plugin_test.go @@ -33,7 +33,7 @@ var _ = Describe("Plugin API", func() { ds = &tests.MockDataStore{} mockManager = &tests.MockPluginManager{} auth.Init(ds) - nativeRouter := New(ds, nil, nil, nil, tests.NewMockLibraryService(), tests.NewMockUserService(), nil, mockManager) + nativeRouter := New(ds, nil, nil, nil, tests.NewMockLibraryService(), tests.NewMockUserService(), nil, mockManager, nil) router = server.JWTVerifier(nativeRouter) // Create test users diff --git a/ui/src/artist/DesktopArtistDetails.jsx b/ui/src/artist/DesktopArtistDetails.jsx index 1e074ce4e..da8d06014 100644 --- a/ui/src/artist/DesktopArtistDetails.jsx +++ b/ui/src/artist/DesktopArtistDetails.jsx @@ -6,12 +6,13 @@ import CardContent from '@material-ui/core/CardContent' import CardMedia from '@material-ui/core/CardMedia' import ArtistExternalLinks from './ArtistExternalLink' import config from '../config' -import { LoveButton, RatingField } from '../common' +import { LoveButton, RatingField, ImageUploadOverlay } from '../common' import Lightbox from 'react-image-lightbox' import ExpandInfoDialog from '../dialogs/ExpandInfoDialog' import AlbumInfo from '../album/AlbumInfo' import subsonic from '../subsonic' import { SafeHTML } from '../common/SafeHTML' +import useArtistImageState from './useArtistImageState' const useStyles = makeStyles( (theme) => ({ @@ -57,6 +58,7 @@ const useStyles = makeStyles( alignItems: 'center', justifyContent: 'center', boxShadow: 'none', + position: 'relative', }, artistDetail: { flex: '1', @@ -85,36 +87,15 @@ const DesktopArtistDetails = ({ artistInfo, record, biography }) => { const [expanded, setExpanded] = useState(false) const classes = useStyles() const title = record.name - const [isLightboxOpen, setLightboxOpen] = React.useState(false) - const [imageLoading, setImageLoading] = React.useState(false) - const [imageError, setImageError] = React.useState(false) - - // Reset image state when artist changes - React.useEffect(() => { - setImageLoading(true) - setImageError(false) - }, [record.id]) - - const handleImageLoad = React.useCallback(() => { - setImageLoading(false) - setImageError(false) - }, []) - - const handleImageError = React.useCallback(() => { - setImageLoading(false) - setImageError(true) - }, []) - - const handleOpenLightbox = React.useCallback(() => { - if (!imageError) { - setLightboxOpen(true) - } - }, [imageError]) - - const handleCloseLightbox = React.useCallback( - () => setLightboxOpen(false), - [], - ) + const { + imageLoading, + imageError, + isLightboxOpen, + handleImageLoad, + handleImageError, + handleOpenLightbox, + handleCloseLightbox, + } = useArtistImageState(record.id) return (
@@ -135,6 +116,11 @@ const DesktopArtistDetails = ({ artistInfo, record, biography }) => { }} /> )} +
diff --git a/ui/src/artist/MobileArtistDetails.jsx b/ui/src/artist/MobileArtistDetails.jsx index 4947a4634..3add1e994 100644 --- a/ui/src/artist/MobileArtistDetails.jsx +++ b/ui/src/artist/MobileArtistDetails.jsx @@ -4,10 +4,11 @@ import { makeStyles } from '@material-ui/core/styles' import Card from '@material-ui/core/Card' import CardMedia from '@material-ui/core/CardMedia' import config from '../config' -import { LoveButton, RatingField } from '../common' +import { LoveButton, RatingField, ImageUploadOverlay } from '../common' import Lightbox from 'react-image-lightbox' import subsonic from '../subsonic' import { SafeHTML } from '../common/SafeHTML' +import useArtistImageState from './useArtistImageState' const useStyles = makeStyles( (theme) => ({ @@ -67,6 +68,7 @@ const useStyles = makeStyles( minWidth: '7rem', display: 'flex', borderRadius: '5em', + position: 'relative', }, loveButton: { top: theme.spacing(-0.2), @@ -87,36 +89,15 @@ const MobileArtistDetails = ({ artistInfo, biography, record }) => { const [expanded, setExpanded] = useState(false) const classes = useStyles({ img, expanded }) const title = record.name - const [isLightboxOpen, setLightboxOpen] = React.useState(false) - const [imageLoading, setImageLoading] = React.useState(false) - const [imageError, setImageError] = React.useState(false) - - // Reset image state when artist changes - React.useEffect(() => { - setImageLoading(true) - setImageError(false) - }, [record.id]) - - const handleImageLoad = React.useCallback(() => { - setImageLoading(false) - setImageError(false) - }, []) - - const handleImageError = React.useCallback(() => { - setImageLoading(false) - setImageError(true) - }, []) - - const handleOpenLightbox = React.useCallback(() => { - if (!imageError) { - setLightboxOpen(true) - } - }, [imageError]) - - const handleCloseLightbox = React.useCallback( - () => setLightboxOpen(false), - [], - ) + const { + imageLoading, + imageError, + isLightboxOpen, + handleImageLoad, + handleImageError, + handleOpenLightbox, + handleCloseLightbox, + } = useArtistImageState(record.id) return ( <> @@ -138,6 +119,11 @@ const MobileArtistDetails = ({ artistInfo, biography, record }) => { }} /> )} +
{ + const [imageLoading, setImageLoading] = useState(false) + const [imageError, setImageError] = useState(false) + const [isLightboxOpen, setLightboxOpen] = useState(false) + + useEffect(() => { + setImageLoading(true) + setImageError(false) + }, [recordId]) + + const handleImageLoad = useCallback(() => { + setImageLoading(false) + setImageError(false) + }, []) + + const handleImageError = useCallback(() => { + setImageLoading(false) + setImageError(true) + }, []) + + const handleOpenLightbox = useCallback(() => { + if (!imageError) { + setLightboxOpen(true) + } + }, [imageError]) + + const handleCloseLightbox = useCallback(() => setLightboxOpen(false), []) + + return { + imageLoading, + imageError, + isLightboxOpen, + handleImageLoad, + handleImageError, + handleOpenLightbox, + handleCloseLightbox, + } +} + +export default useArtistImageState diff --git a/ui/src/common/ImageUploadOverlay.jsx b/ui/src/common/ImageUploadOverlay.jsx new file mode 100644 index 000000000..e0d0d0a9a --- /dev/null +++ b/ui/src/common/ImageUploadOverlay.jsx @@ -0,0 +1,139 @@ +import { IconButton, Tooltip } from '@material-ui/core' +import { makeStyles } from '@material-ui/core/styles' +import PhotoCameraIcon from '@material-ui/icons/PhotoCamera' +import DeleteIcon from '@material-ui/icons/Delete' +import { useTranslate, useNotify, useRefresh } from 'react-admin' +import { useCallback, useRef } from 'react' +import config from '../config' +import { REST_URL } from '../consts' +import { httpClient } from '../dataProvider' + +const useStyles = makeStyles(() => ({ + coverOverlay: { + position: 'absolute', + bottom: 0, + right: 0, + display: 'flex', + gap: '2px', + padding: '2px', + backgroundColor: 'rgba(0,0,0,0.5)', + borderRadius: '4px 0 0 0', + opacity: 0, + transition: 'opacity 0.2s ease-in-out', + '*:hover > &': { + opacity: 1, + }, + }, + overlayButton: { + color: '#fff', + padding: '4px', + '&:hover': { + backgroundColor: 'rgba(255,255,255,0.2)', + }, + }, + overlayIcon: { + fontSize: '1.2rem', + }, +})) + +export const ImageUploadOverlay = ({ + entityType, + entityId, + hasUploadedImage, + onImageChange, +}) => { + const translate = useTranslate() + const notify = useNotify() + const refresh = useRefresh() + const classes = useStyles() + const fileInputRef = useRef(null) + + const canEdit = + config.enableCoverArtUpload || localStorage.getItem('role') === 'admin' + + const handleUploadClick = useCallback((e) => { + e.stopPropagation() + if (fileInputRef.current) { + fileInputRef.current.click() + } + }, []) + + const handleFileChange = useCallback( + async (e) => { + const file = e.target.files[0] + if (!file || !entityId) return + + const formData = new FormData() + formData.append('image', file) + + try { + await httpClient(`${REST_URL}/${entityType}/${entityId}/image`, { + method: 'POST', + headers: new Headers({}), + body: formData, + }) + notify(`message.coverUploaded`, 'success') + if (onImageChange) onImageChange() + refresh() + } catch (err) { + notify(`message.coverUploadError`, 'warning') + } + + e.target.value = '' + }, + [entityType, entityId, notify, refresh, onImageChange], + ) + + const handleRemoveCover = useCallback( + async (e) => { + e.stopPropagation() + if (!entityId) return + + try { + await httpClient(`${REST_URL}/${entityType}/${entityId}/image`, { + method: 'DELETE', + }) + notify(`message.coverRemoved`, 'success') + if (onImageChange) onImageChange() + refresh() + } catch (err) { + notify(`message.coverRemoveError`, 'warning') + } + }, + [entityType, entityId, notify, refresh, onImageChange], + ) + + if (!canEdit) return null + + return ( +
+ + + + + + {hasUploadedImage && ( + + + + + + )} + +
+ ) +} diff --git a/ui/src/common/index.js b/ui/src/common/index.js index a8dc354cb..953e38cf8 100644 --- a/ui/src/common/index.js +++ b/ui/src/common/index.js @@ -43,3 +43,4 @@ export * from './PathField.jsx' export * from './ParticipantsInfo' export * from './OverflowTooltip' export * from './useSearchRefocus' +export * from './ImageUploadOverlay' diff --git a/ui/src/i18n/en.json b/ui/src/i18n/en.json index baf4c9fa3..6c6592178 100644 --- a/ui/src/i18n/en.json +++ b/ui/src/i18n/en.json @@ -219,15 +219,9 @@ "makePrivate": "Make Private", "searchOrCreate": "Search playlists or type to create new...", "pressEnterToCreate": "Press Enter to create new playlist", - "removeFromSelection": "Remove from selection", - "uploadCover": "Upload Cover", - "removeCover": "Remove Cover" + "removeFromSelection": "Remove from selection" }, "message": { - "coverUploaded": "Cover art updated", - "coverRemoved": "Cover art removed", - "coverUploadError": "Error uploading cover art", - "coverRemoveError": "Error removing cover art", "duplicate_song": "Add duplicated songs", "song_exist": "There are duplicates being added to the playlist. Would you like to add the duplicates or skip them?", "noPlaylistsFound": "No playlists found", @@ -563,6 +557,12 @@ } }, "message": { + "uploadCover": "Upload Cover", + "removeCover": "Remove Cover", + "coverUploaded": "Cover art updated", + "coverRemoved": "Cover art removed", + "coverUploadError": "Error uploading cover art", + "coverRemoveError": "Error removing cover art", "note": "NOTE", "transcodingDisabled": "Changing the transcoding configuration through the web interface is disabled for security reasons. If you would like to change (edit or add) transcoding options, restart the server with the %{config} configuration option.", "transcodingEnabled": "Navidrome is currently running with %{config}, making it possible to run system commands from the transcoding settings using the web interface. We recommend to disable it for security reasons and only enable it when configuring Transcoding options.", diff --git a/ui/src/playlist/PlaylistDetails.jsx b/ui/src/playlist/PlaylistDetails.jsx index aeddf0d42..a2d5e753b 100644 --- a/ui/src/playlist/PlaylistDetails.jsx +++ b/ui/src/playlist/PlaylistDetails.jsx @@ -2,29 +2,23 @@ import { Card, CardContent, CardMedia, - IconButton, - Tooltip, Typography, useMediaQuery, } from '@material-ui/core' import { makeStyles } from '@material-ui/core/styles' -import PhotoCameraIcon from '@material-ui/icons/PhotoCamera' -import DeleteIcon from '@material-ui/icons/Delete' -import { useTranslate, useNotify, useRefresh } from 'react-admin' -import { useCallback, useRef, useState, useEffect } from 'react' +import { useTranslate } from 'react-admin' +import { useCallback, useState, useEffect } from 'react' import Lightbox from 'react-image-lightbox' import 'react-image-lightbox/style.css' import { CollapsibleComment, DurationField, + ImageUploadOverlay, SizeField, isWritable, OverflowTooltip, } from '../common' -import config from '../config' import subsonic from '../subsonic' -import { REST_URL } from '../consts' -import { httpClient } from '../dataProvider' const useStyles = makeStyles( (theme) => ({ @@ -82,31 +76,6 @@ const useStyles = makeStyles( coverLoading: { opacity: 0.5, }, - coverOverlay: { - position: 'absolute', - bottom: 0, - right: 0, - display: 'flex', - gap: '2px', - padding: '2px', - backgroundColor: 'rgba(0,0,0,0.5)', - borderRadius: '4px 0 0 0', - opacity: 0, - transition: 'opacity 0.2s ease-in-out', - '$coverParent:hover &': { - opacity: 1, - }, - }, - overlayButton: { - color: '#fff', - padding: '4px', - '&:hover': { - backgroundColor: 'rgba(255,255,255,0.2)', - }, - }, - overlayIcon: { - fontSize: '1.2rem', - }, title: { overflow: 'hidden', textOverflow: 'ellipsis', @@ -125,20 +94,14 @@ const useStyles = makeStyles( const PlaylistDetails = (props) => { const { record = {} } = props const translate = useTranslate() - const notify = useNotify() - const refresh = useRefresh() const classes = useStyles() const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('lg')) const [isLightboxOpen, setLightboxOpen] = useState(false) const [imageLoading, setImageLoading] = useState(false) const [imageError, setImageError] = useState(false) - const fileInputRef = useRef(null) const imageUrl = subsonic.getCoverArtUrl(record, 300, true) const fullImageUrl = subsonic.getCoverArtUrl(record) - const canEdit = - isWritable(record.ownerId) && - (config.enableCoverArtUpload || localStorage.getItem('role') === 'admin') // Reset image state when playlist changes useEffect(() => { @@ -164,60 +127,6 @@ const PlaylistDetails = (props) => { const handleCloseLightbox = useCallback(() => setLightboxOpen(false), []) - const handleUploadClick = useCallback( - (e) => { - e.stopPropagation() - if (fileInputRef.current) { - fileInputRef.current.click() - } - }, - [fileInputRef], - ) - - const handleFileChange = useCallback( - async (e) => { - const file = e.target.files[0] - if (!file || !record.id) return - - const formData = new FormData() - formData.append('image', file) - - try { - await httpClient(`${REST_URL}/playlist/${record.id}/image`, { - method: 'POST', - headers: new Headers({}), - body: formData, - }) - notify('resources.playlist.message.coverUploaded', 'success') - refresh() - } catch (err) { - notify('resources.playlist.message.coverUploadError', 'warning') - } - - // Reset file input so the same file can be re-selected - e.target.value = '' - }, - [record.id, notify, refresh], - ) - - const handleRemoveCover = useCallback( - async (e) => { - e.stopPropagation() - if (!record.id) return - - try { - await httpClient(`${REST_URL}/playlist/${record.id}/image`, { - method: 'DELETE', - }) - notify('resources.playlist.message.coverRemoved', 'success') - refresh() - } catch (err) { - notify('resources.playlist.message.coverRemoveError', 'warning') - } - }, - [record.id, notify, refresh], - ) - return (
@@ -237,40 +146,12 @@ const PlaylistDetails = (props) => { cursor: imageError ? 'default' : 'pointer', }} /> - {canEdit && ( -
- - - - - - {record.uploadedImage && ( - - - - - - )} - -
+ {isWritable(record.ownerId) && ( + )}
From cefa6e9619a9025ce07489b9e1d4a6ef190efd20 Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 16 Mar 2026 06:39:48 -0400 Subject: [PATCH 27/81] feat(ui): add CoverArtAvatar component and integrate it into artist and playlist lists Signed-off-by: Deluan --- ui/src/artist/ArtistList.jsx | 6 +++++ ui/src/artist/ArtistSimpleList.jsx | 7 +++++- ui/src/common/CoverArtAvatar.jsx | 36 ++++++++++++++++++++++++++++++ ui/src/common/index.js | 1 + ui/src/playlist/PlaylistList.jsx | 29 ++---------------------- 5 files changed, 51 insertions(+), 28 deletions(-) create mode 100644 ui/src/common/CoverArtAvatar.jsx diff --git a/ui/src/artist/ArtistList.jsx b/ui/src/artist/ArtistList.jsx index e175763e3..6c526a5a5 100644 --- a/ui/src/artist/ArtistList.jsx +++ b/ui/src/artist/ArtistList.jsx @@ -22,6 +22,7 @@ import { useDrag } from 'react-dnd' import clsx from 'clsx' import { ArtistContextMenu, + CoverArtAvatar, List, QuickFilter, useGetHandleArtistClick, @@ -43,6 +44,10 @@ const useStyles = makeStyles({ verticalAlign: 'text-top', }, row: { + '& td': { + paddingTop: '4px !important', + paddingBottom: '4px !important', + }, '&:hover': { '& $contextMenu': { visibility: 'visible', @@ -170,6 +175,7 @@ const ArtistListView = ({ hasShow, hasEdit, hasList, width, ...rest }) => { /> ) : ( + linkType(id)}> + + +
{data[id].name}
diff --git a/ui/src/common/CoverArtAvatar.jsx b/ui/src/common/CoverArtAvatar.jsx new file mode 100644 index 000000000..6610cb75e --- /dev/null +++ b/ui/src/common/CoverArtAvatar.jsx @@ -0,0 +1,36 @@ +import { useRecordContext } from 'react-admin' +import { Avatar } from '@material-ui/core' +import { makeStyles } from '@material-ui/core/styles' +import clsx from 'clsx' +import subsonic from '../subsonic' + +const useStyles = makeStyles({ + avatar: { + width: '55px', + height: '55px', + }, + square: { + borderRadius: '4px', + }, +}) + +export const CoverArtAvatar = ({ + record: recordProp, + variant = 'circular', +}) => { + const classes = useStyles() + const recordContext = useRecordContext() + const record = recordProp || recordContext + if (!record) return null + const square = variant !== 'circular' + return ( + + ) +} + +CoverArtAvatar.defaultProps = { label: '', sortable: false } diff --git a/ui/src/common/index.js b/ui/src/common/index.js index 953e38cf8..b93e40219 100644 --- a/ui/src/common/index.js +++ b/ui/src/common/index.js @@ -44,3 +44,4 @@ export * from './ParticipantsInfo' export * from './OverflowTooltip' export * from './useSearchRefocus' export * from './ImageUploadOverlay' +export * from './CoverArtAvatar' diff --git a/ui/src/playlist/PlaylistList.jsx b/ui/src/playlist/PlaylistList.jsx index 67c456f27..8732725bc 100644 --- a/ui/src/playlist/PlaylistList.jsx +++ b/ui/src/playlist/PlaylistList.jsx @@ -16,10 +16,10 @@ import { usePermissions, } from 'react-admin' import Switch from '@material-ui/core/Switch' -import { Avatar } from '@material-ui/core' import { makeStyles } from '@material-ui/core/styles' import { useMediaQuery } from '@material-ui/core' import { + CoverArtAvatar, DurationField, List, Writable, @@ -29,17 +29,11 @@ import { } from '../common' import PlaylistListActions from './PlaylistListActions' import ChangePublicStatusButton from './ChangePublicStatusButton' -import subsonic from '../subsonic' const useStyles = makeStyles((theme) => ({ button: { color: theme.palette.type === 'dark' ? 'white' : undefined, }, - coverArt: { - width: '40px', - height: '40px', - borderRadius: '4px', - }, })) const PlaylistFilter = (props) => { @@ -126,25 +120,6 @@ const ToggleAutoImport = ({ resource, source }) => { ) : null } -const CoverArtField = () => { - const classes = useStyles() - const record = useRecordContext() - if (!record) return null - return ( - - ) -} - -CoverArtField.defaultProps = { - label: '', - sortable: false, -} - const PlaylistListBulkActions = (props) => { const classes = useStyles() return ( @@ -204,7 +179,7 @@ const PlaylistList = (props) => { bulkActionButtons={!isXsmall && } > isWritable(r?.ownerId)}> - + {columns} From 9ae9134a9140be56a13417fd15ee30203dd43349 Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 16 Mar 2026 06:46:47 -0400 Subject: [PATCH 28/81] feat(ui): integrate CoverArtAvatar component into AlbumTableView Signed-off-by: Deluan --- ui/src/album/AlbumTableView.jsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ui/src/album/AlbumTableView.jsx b/ui/src/album/AlbumTableView.jsx index 1fa33d769..d1a89d512 100644 --- a/ui/src/album/AlbumTableView.jsx +++ b/ui/src/album/AlbumTableView.jsx @@ -14,6 +14,7 @@ import { makeStyles } from '@material-ui/core/styles' import { useDrag } from 'react-dnd' import { ArtistLinkField, + CoverArtAvatar, DurationField, RangeField, SimpleList, @@ -161,12 +162,18 @@ const AlbumTableView = ({       )} + leftIcon={(r) => ( + + + + )} linkType={'show'} rightIcon={(r) => } {...rest} /> ) : ( + {columns} Date: Mon, 16 Mar 2026 07:55:22 -0400 Subject: [PATCH 29/81] fix(db): normalize timestamps and fix recently added album sorting (#5176) * fix(db): normalize timestamps and fix recently added album sorting SQLite stores timestamps as TEXT and uses string comparison for ORDER BY. Timestamps in RFC3339 T-format ('2024-01-01T10:00:00Z') sort incorrectly against space-format ('2024-01-01 10:00:00+00:00') because 'T' (ASCII 84) > ' ' (ASCII 32), causing albums with T-format timestamps to appear as newer than they are in the "Recently Added" list. This adds a migration to normalize all T-format timestamps across all tables to the space-format expected by go-sqlite3, wraps the recently_added sort with datetime() to make it format-agnostic, and replaces the plain album timestamp indexes with expression indexes to maintain query performance. * fix(test): improve recently_added sort test robustness Use same-date timestamps (2024-01-15T08:00:00Z vs 2024-01-15 20:00:00) so the T-vs-space character difference at position 10 actually triggers the sorting bug. Initialize index variables to -1 and assert both test albums are found before comparing positions. * chore(db): update migration timestamp to 2026-03-16 --- .../20260316000000_normalize_timestamps.sql | 74 +++++++++++++++++++ persistence/album_repository.go | 4 +- persistence/album_repository_test.go | 47 ++++++++++++ 3 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 db/migrations/20260316000000_normalize_timestamps.sql diff --git a/db/migrations/20260316000000_normalize_timestamps.sql b/db/migrations/20260316000000_normalize_timestamps.sql new file mode 100644 index 000000000..a2e1183e9 --- /dev/null +++ b/db/migrations/20260316000000_normalize_timestamps.sql @@ -0,0 +1,74 @@ +-- +goose Up + +-- Normalize T-format timestamps (RFC3339Nano with 'T' separator) to SQLite-compatible format. +-- SQLite uses string comparison for ORDER BY on TEXT columns, so 'T' (ASCII 84) > ' ' (ASCII 32) +-- causes T-format timestamps to sort after space-format ones, breaking "Recently Added" ordering. + +UPDATE album SET created_at = replace(replace(created_at, 'T', ' '), 'Z', '+00:00') WHERE created_at LIKE '%T%'; +UPDATE album SET updated_at = replace(replace(updated_at, 'T', ' '), 'Z', '+00:00') WHERE updated_at LIKE '%T%'; +UPDATE album SET imported_at = replace(replace(imported_at, 'T', ' '), 'Z', '+00:00') WHERE imported_at LIKE '%T%'; +UPDATE album SET external_info_updated_at = replace(replace(external_info_updated_at, 'T', ' '), 'Z', '+00:00') WHERE external_info_updated_at LIKE '%T%'; + +UPDATE media_file SET created_at = replace(replace(created_at, 'T', ' '), 'Z', '+00:00') WHERE created_at LIKE '%T%'; +UPDATE media_file SET updated_at = replace(replace(updated_at, 'T', ' '), 'Z', '+00:00') WHERE updated_at LIKE '%T%'; +UPDATE media_file SET birth_time = replace(replace(birth_time, 'T', ' '), 'Z', '+00:00') WHERE birth_time LIKE '%T%'; + +UPDATE artist SET created_at = replace(replace(created_at, 'T', ' '), 'Z', '+00:00') WHERE created_at LIKE '%T%'; +UPDATE artist SET updated_at = replace(replace(updated_at, 'T', ' '), 'Z', '+00:00') WHERE updated_at LIKE '%T%'; +UPDATE artist SET external_info_updated_at = replace(replace(external_info_updated_at, 'T', ' '), 'Z', '+00:00') WHERE external_info_updated_at LIKE '%T%'; + +UPDATE annotation SET play_date = replace(replace(play_date, 'T', ' '), 'Z', '+00:00') WHERE play_date LIKE '%T%'; +UPDATE annotation SET starred_at = replace(replace(starred_at, 'T', ' '), 'Z', '+00:00') WHERE starred_at LIKE '%T%'; +UPDATE annotation SET rated_at = replace(replace(rated_at, 'T', ' '), 'Z', '+00:00') WHERE rated_at LIKE '%T%'; + +UPDATE playlist SET created_at = replace(replace(created_at, 'T', ' '), 'Z', '+00:00') WHERE created_at LIKE '%T%'; +UPDATE playlist SET updated_at = replace(replace(updated_at, 'T', ' '), 'Z', '+00:00') WHERE updated_at LIKE '%T%'; +UPDATE playlist SET evaluated_at = replace(replace(evaluated_at, 'T', ' '), 'Z', '+00:00') WHERE evaluated_at LIKE '%T%'; + +UPDATE user SET created_at = replace(replace(created_at, 'T', ' '), 'Z', '+00:00') WHERE created_at LIKE '%T%'; +UPDATE user SET updated_at = replace(replace(updated_at, 'T', ' '), 'Z', '+00:00') WHERE updated_at LIKE '%T%'; +UPDATE user SET last_login_at = replace(replace(last_login_at, 'T', ' '), 'Z', '+00:00') WHERE last_login_at LIKE '%T%'; +UPDATE user SET last_access_at = replace(replace(last_access_at, 'T', ' '), 'Z', '+00:00') WHERE last_access_at LIKE '%T%'; + +UPDATE player SET last_seen = replace(replace(last_seen, 'T', ' '), 'Z', '+00:00') WHERE last_seen LIKE '%T%'; + +UPDATE playqueue SET created_at = replace(replace(created_at, 'T', ' '), 'Z', '+00:00') WHERE created_at LIKE '%T%'; +UPDATE playqueue SET updated_at = replace(replace(updated_at, 'T', ' '), 'Z', '+00:00') WHERE updated_at LIKE '%T%'; + +UPDATE bookmark SET created_at = replace(replace(created_at, 'T', ' '), 'Z', '+00:00') WHERE created_at LIKE '%T%'; +UPDATE bookmark SET updated_at = replace(replace(updated_at, 'T', ' '), 'Z', '+00:00') WHERE updated_at LIKE '%T%'; + +UPDATE share SET created_at = replace(replace(created_at, 'T', ' '), 'Z', '+00:00') WHERE created_at LIKE '%T%'; +UPDATE share SET updated_at = replace(replace(updated_at, 'T', ' '), 'Z', '+00:00') WHERE updated_at LIKE '%T%'; +UPDATE share SET expires_at = replace(replace(expires_at, 'T', ' '), 'Z', '+00:00') WHERE expires_at LIKE '%T%'; +UPDATE share SET last_visited_at = replace(replace(last_visited_at, 'T', ' '), 'Z', '+00:00') WHERE last_visited_at LIKE '%T%'; + +UPDATE radio SET created_at = replace(replace(created_at, 'T', ' '), 'Z', '+00:00') WHERE created_at LIKE '%T%'; +UPDATE radio SET updated_at = replace(replace(updated_at, 'T', ' '), 'Z', '+00:00') WHERE updated_at LIKE '%T%'; + +UPDATE folder SET created_at = replace(replace(created_at, 'T', ' '), 'Z', '+00:00') WHERE created_at LIKE '%T%'; +UPDATE folder SET updated_at = replace(replace(updated_at, 'T', ' '), 'Z', '+00:00') WHERE updated_at LIKE '%T%'; +UPDATE folder SET images_updated_at = replace(replace(images_updated_at, 'T', ' '), 'Z', '+00:00') WHERE images_updated_at LIKE '%T%'; + +UPDATE library SET created_at = replace(replace(created_at, 'T', ' '), 'Z', '+00:00') WHERE created_at LIKE '%T%'; +UPDATE library SET updated_at = replace(replace(updated_at, 'T', ' '), 'Z', '+00:00') WHERE updated_at LIKE '%T%'; +UPDATE library SET last_scan_at = replace(replace(last_scan_at, 'T', ' '), 'Z', '+00:00') WHERE last_scan_at LIKE '%T%'; +UPDATE library SET last_scan_started_at = replace(replace(last_scan_started_at, 'T', ' '), 'Z', '+00:00') WHERE last_scan_started_at LIKE '%T%'; + +UPDATE scrobble_buffer SET play_time = replace(replace(play_time, 'T', ' '), 'Z', '+00:00') WHERE play_time LIKE '%T%'; +UPDATE scrobble_buffer SET enqueue_time = replace(replace(enqueue_time, 'T', ' '), 'Z', '+00:00') WHERE enqueue_time LIKE '%T%'; + +UPDATE plugin SET created_at = replace(replace(created_at, 'T', ' '), 'Z', '+00:00') WHERE created_at LIKE '%T%'; +UPDATE plugin SET updated_at = replace(replace(updated_at, 'T', ' '), 'Z', '+00:00') WHERE updated_at LIKE '%T%'; + +-- Replace plain indexes with expression indexes for datetime()-based sorting +DROP INDEX IF EXISTS album_created_at; +CREATE INDEX album_created_at ON album(datetime(created_at)); +DROP INDEX IF EXISTS album_updated_at; +CREATE INDEX album_updated_at ON album(datetime(updated_at)); + +-- +goose Down +DROP INDEX IF EXISTS album_created_at; +CREATE INDEX album_created_at ON album(created_at); +DROP INDEX IF EXISTS album_updated_at; +CREATE INDEX album_updated_at ON album(updated_at); diff --git a/persistence/album_repository.go b/persistence/album_repository.go index 7207bf5a2..c51a5beb1 100644 --- a/persistence/album_repository.go +++ b/persistence/album_repository.go @@ -143,9 +143,9 @@ var albumFilters = sync.OnceValue(func() map[string]filterFunc { func recentlyAddedSort() string { if conf.Server.RecentlyAddedByModTime { - return "updated_at" + return "datetime(album.updated_at)" } - return "created_at" + return "datetime(album.created_at)" } func recentlyPlayedFilter(string, any) Sqlizer { diff --git a/persistence/album_repository_test.go b/persistence/album_repository_test.go index 66b6eba9f..2792cec97 100644 --- a/persistence/album_repository_test.go +++ b/persistence/album_repository_test.go @@ -85,6 +85,53 @@ var _ = Describe("AlbumRepository", func() { }) }) + Describe("recently_added sort", func() { + It("sorts correctly regardless of timestamp format (T-format vs space-format)", func() { + // Both timestamps share the same date prefix "2024-01-15" so the T vs space + // character at position 10 determines sort order in raw string comparison. + // Without normalization, 'T' (ASCII 84) > ' ' (ASCII 32) makes the older + // T-format timestamp sort AFTER the newer space-format one. + + // Older album: morning of Jan 15, stored in T-format + olderAlbum := &model.Album{LibraryID: 1, ID: "ts-older", Name: "Older Album"} + Expect(albumRepo.Put(olderAlbum)).To(Succeed()) + _, err := albumRepo.executeSQL(squirrel.Update("album"). + Set("created_at", "2024-01-15T08:00:00Z"). + Where(squirrel.Eq{"id": "ts-older"})) + Expect(err).ToNot(HaveOccurred()) + + // Newer album: evening of Jan 15, stored in space-format + newerAlbum := &model.Album{LibraryID: 1, ID: "ts-newer", Name: "Newer Album"} + Expect(albumRepo.Put(newerAlbum)).To(Succeed()) + _, err = albumRepo.executeSQL(squirrel.Update("album"). + Set("created_at", "2024-01-15 20:00:00+00:00"). + Where(squirrel.Eq{"id": "ts-newer"})) + Expect(err).ToNot(HaveOccurred()) + + albums, err := albumRepo.GetAll(model.QueryOptions{Sort: "recently_added", Order: "desc"}) + Expect(err).ToNot(HaveOccurred()) + + // Find positions of our test albums + olderIdx, newerIdx := -1, -1 + for i, a := range albums { + switch a.ID { + case "ts-older": + olderIdx = i + case "ts-newer": + newerIdx = i + } + } + Expect(olderIdx).To(BeNumerically(">=", 0), "older album not found in results") + Expect(newerIdx).To(BeNumerically(">=", 0), "newer album not found in results") + // Newer album (evening, space-format) should come before older album (morning, T-format) in desc order + Expect(newerIdx).To(BeNumerically("<", olderIdx), + "Newer album (20:00 space-format) should sort before older album (08:00 T-format) in desc order") + + // Clean up + _, _ = albumRepo.executeSQL(squirrel.Delete("album").Where(squirrel.Eq{"id": []string{"ts-older", "ts-newer"}})) + }) + }) + Context("Filters", func() { var albumWithoutAnnotation model.Album From 2f5b2b51359db24573d965fe4b114a45c72d3b6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Mon, 16 Mar 2026 18:08:39 -0400 Subject: [PATCH 30/81] fix(artwork): fallback mediafile cover art to disc artwork before album (#5216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(artwork): fallback mediafile cover art to disc artwork before album Changed the mediafile cover art fallback chain to go through disc artwork before album artwork (mediafile → disc → album). Previously, mediafiles without embedded art fell back directly to album cover, bypassing any disc-specific artwork. Renamed AlbumCoverArtID() to DiscCoverArtID() to encapsulate the disc-vs-album decision in a single method, used by both CoverArtID() and the mediafile artwork reader. Signed-off-by: Deluan * fix(artwork): fix cache invalidation for mediafile and album cover art Include imagesUpdatedAt from album folders in the mediafile artwork reader's cache key, so that when a cover image file changes on disk (without audio metadata changes) the mediafile cache properly invalidates. Also include CoverArtPriority unconditionally in the album artwork reader's cache key hash, so that changing the priority order with external services disabled correctly invalidates the album cache. * fix(artwork): skip disc artwork resolution for single-disc albums Single-disc albums with DiscNumber=1 were unnecessarily routed through discArtworkReader, which does extra DB queries only to fall through to album art anyway. Now only multi-disc albums use the disc fallback path. * refactor(artwork): restore AlbumCoverArtID as a separate method Extract AlbumCoverArtID back out of DiscCoverArtID so the single-disc fallback path in reader_mediafile can reference it by name instead of inlining the artwork ID construction. --------- Signed-off-by: Deluan --- core/artwork/artwork_internal_test.go | 28 +++++++++++++++++++++++++-- core/artwork/reader_album.go | 5 +++-- core/artwork/reader_mediafile.go | 20 +++++++++++++++---- model/mediafile.go | 11 ++++++++++- model/mediafile_test.go | 17 ++++++++++++++-- 5 files changed, 70 insertions(+), 11 deletions(-) diff --git a/core/artwork/artwork_internal_test.go b/core/artwork/artwork_internal_test.go index 5ca32f401..4b2359898 100644 --- a/core/artwork/artwork_internal_test.go +++ b/core/artwork/artwork_internal_test.go @@ -28,7 +28,7 @@ var _ = Describe("Artwork", func() { var ffmpeg *tests.MockFFmpeg var folderRepo *fakeFolderRepo ctx := log.NewContext(context.TODO()) - var alOnlyEmbed, alEmbedNotFound, alOnlyExternal, alExternalNotFound, alMultipleCovers model.Album + var alOnlyEmbed, alEmbedNotFound, alOnlyExternal, alExternalNotFound, alMultipleCovers, alSingleDisc model.Album var arMultipleCovers model.Artist var mfWithEmbed, mfAnotherWithEmbed, mfWithoutEmbed, mfCorruptedCover model.MediaFile @@ -44,8 +44,9 @@ var _ = Describe("Artwork", func() { } alOnlyEmbed = model.Album{ID: "222", Name: "Only embed", EmbedArtPath: "tests/fixtures/artist/an-album/test.mp3", FolderIDs: []string{"f1"}} alEmbedNotFound = model.Album{ID: "333", Name: "Embed not found", EmbedArtPath: "tests/fixtures/NON_EXISTENT.mp3", FolderIDs: []string{"f1"}} - alOnlyExternal = model.Album{ID: "444", Name: "Only external", FolderIDs: []string{"f1"}} + alOnlyExternal = model.Album{ID: "444", Name: "Only external", FolderIDs: []string{"f1"}, Discs: model.Discs{1: "", 2: ""}} alExternalNotFound = model.Album{ID: "555", Name: "External not found", FolderIDs: []string{"f2"}} + alSingleDisc = model.Album{ID: "888", Name: "Single disc", FolderIDs: []string{"f1"}, Discs: model.Discs{1: ""}} arMultipleCovers = model.Artist{ID: "777", Name: "All options"} alMultipleCovers = model.Album{ ID: "666", @@ -193,6 +194,7 @@ var _ = Describe("Artwork", func() { ds.Album(ctx).(*tests.MockAlbumRepo).SetData(model.Albums{ alOnlyEmbed, alOnlyExternal, + alSingleDisc, }) ds.MediaFile(ctx).(*tests.MockMediaFileRepo).SetData(model.MediaFiles{ mfWithEmbed, @@ -236,6 +238,28 @@ var _ = Describe("Artwork", func() { Expect(err).ToNot(HaveOccurred()) Expect(path).To(Equal("al-444_0")) }) + It("falls back to disc cover art when media file has a disc number on a multi-disc album", func() { + mfWithDisc := model.MediaFile{ID: "46", Path: "tests/fixtures/test.ogg", AlbumID: "444", DiscNumber: 2} + Expect(ds.MediaFile(ctx).(*tests.MockMediaFileRepo).Put(&mfWithDisc)).To(Succeed()) + + aw, err := newMediafileArtworkReader(ctx, aw, model.MustParseArtworkID("mf-"+mfWithDisc.ID)) + Expect(err).ToNot(HaveOccurred()) + _, path, err := aw.Reader(ctx) + Expect(err).ToNot(HaveOccurred()) + // Should fall back to disc art, which itself falls back to album art + Expect(path).To(Equal("dc-444:2_0")) + }) + It("falls back to album cover art for single-disc albums even with a disc number", func() { + mfOnSingleDisc := model.MediaFile{ID: "47", Path: "tests/fixtures/test.ogg", AlbumID: "888", DiscNumber: 1} + Expect(ds.MediaFile(ctx).(*tests.MockMediaFileRepo).Put(&mfOnSingleDisc)).To(Succeed()) + + aw, err := newMediafileArtworkReader(ctx, aw, model.MustParseArtworkID("mf-"+mfOnSingleDisc.ID)) + Expect(err).ToNot(HaveOccurred()) + _, path, err := aw.Reader(ctx) + Expect(err).ToNot(HaveOccurred()) + // Single-disc album should skip disc art and go straight to album art + Expect(path).To(Equal("al-888_0")) + }) }) }) Describe("playlistArtworkReader", func() { diff --git a/core/artwork/reader_album.go b/core/artwork/reader_album.go index 98a2105eb..6de1d31d1 100644 --- a/core/artwork/reader_album.go +++ b/core/artwork/reader_album.go @@ -59,10 +59,11 @@ func newAlbumArtworkReader(ctx context.Context, artwork *artwork, artID model.Ar } func (a *albumArtworkReader) Key() string { - var hash [16]byte + hashInput := conf.Server.CoverArtPriority if conf.Server.EnableExternalServices { - hash = md5.Sum([]byte(conf.Server.Agents + conf.Server.CoverArtPriority)) + hashInput += conf.Server.Agents } + hash := md5.Sum([]byte(hashInput)) return fmt.Sprintf( "%s.%x.%t", a.cacheKey.Key(), diff --git a/core/artwork/reader_mediafile.go b/core/artwork/reader_mediafile.go index c72d9543d..cf25c8f5d 100644 --- a/core/artwork/reader_mediafile.go +++ b/core/artwork/reader_mediafile.go @@ -26,16 +26,22 @@ func newMediafileArtworkReader(ctx context.Context, artwork *artwork, artID mode if err != nil { return nil, err } + _, _, imagesUpdatedAt, err := loadAlbumFoldersPaths(ctx, artwork.ds, *al) + if err != nil { + return nil, err + } a := &mediafileArtworkReader{ a: artwork, mediafile: *mf, album: *al, } a.cacheKey.artID = artID - if al.UpdatedAt.After(mf.UpdatedAt) { + a.cacheKey.lastUpdate = mf.UpdatedAt + if al.UpdatedAt.After(a.cacheKey.lastUpdate) { a.cacheKey.lastUpdate = al.UpdatedAt - } else { - a.cacheKey.lastUpdate = mf.UpdatedAt + } + if imagesUpdatedAt != nil && imagesUpdatedAt.After(a.cacheKey.lastUpdate) { + a.cacheKey.lastUpdate = *imagesUpdatedAt } return a, nil } @@ -60,6 +66,12 @@ func (a *mediafileArtworkReader) Reader(ctx context.Context) (io.ReadCloser, str fromFFmpegTag(ctx, a.a.ffmpeg, path), } } - ff = append(ff, fromAlbum(ctx, a.a, a.mediafile.AlbumCoverArtID())) + // For multi-disc albums, fall back to disc artwork first; for single-disc albums, + // skip disc resolution (it would just fall through to album art anyway). + if len(a.album.Discs) > 1 { + ff = append(ff, fromAlbum(ctx, a.a, a.mediafile.DiscCoverArtID())) + } else { + ff = append(ff, fromAlbum(ctx, a.a, a.mediafile.AlbumCoverArtID())) + } return selectImageReader(ctx, a.artID, ff...) } diff --git a/model/mediafile.go b/model/mediafile.go index 20532bfb9..ec83b76fd 100644 --- a/model/mediafile.go +++ b/model/mediafile.go @@ -119,7 +119,16 @@ func (mf MediaFile) CoverArtID() ArtworkID { if mf.HasCoverArt && conf.Server.EnableMediaFileCoverArt { return artworkIDFromMediaFile(mf) } - // if it does not have a coverArt, fallback to the album cover + // Otherwise fallback to disc (if available) or album cover + return mf.DiscCoverArtID() +} + +// DiscCoverArtID returns the disc artwork ID when the media file has a disc number, +// otherwise it returns the album artwork ID. +func (mf MediaFile) DiscCoverArtID() ArtworkID { + if mf.DiscNumber > 0 { + return NewArtworkID(KindDiscArtwork, DiscArtworkID(mf.AlbumID, mf.DiscNumber), nil) + } return mf.AlbumCoverArtID() } diff --git a/model/mediafile_test.go b/model/mediafile_test.go index 207d3c155..038ac93d5 100644 --- a/model/mediafile_test.go +++ b/model/mediafile_test.go @@ -504,13 +504,26 @@ var _ = Describe("MediaFile", func() { Expect(id.Kind).To(Equal(KindMediaFileArtwork)) Expect(id.ID).To(Equal(mf.ID)) }) - It("returns its album id if HasCoverArt is false", func() { + It("returns disc art id if HasCoverArt is false and DiscNumber > 0", func() { + mf := MediaFile{ID: "111", AlbumID: "1", HasCoverArt: false, DiscNumber: 2} + id := mf.CoverArtID() + Expect(id.Kind).To(Equal(KindDiscArtwork)) + Expect(id.ID).To(Equal("1:2")) + }) + It("returns its album id if HasCoverArt is false and DiscNumber is 0", func() { mf := MediaFile{ID: "111", AlbumID: "1", HasCoverArt: false} id := mf.CoverArtID() Expect(id.Kind).To(Equal(KindAlbumArtwork)) Expect(id.ID).To(Equal(mf.AlbumID)) }) - It("returns its album id if EnableMediaFileCoverArt is disabled", func() { + It("returns disc art id if EnableMediaFileCoverArt is disabled and DiscNumber > 0", func() { + conf.Server.EnableMediaFileCoverArt = false + mf := MediaFile{ID: "111", AlbumID: "1", HasCoverArt: true, DiscNumber: 3} + id := mf.CoverArtID() + Expect(id.Kind).To(Equal(KindDiscArtwork)) + Expect(id.ID).To(Equal("1:3")) + }) + It("returns its album id if EnableMediaFileCoverArt is disabled and DiscNumber is 0", func() { conf.Server.EnableMediaFileCoverArt = false mf := MediaFile{ID: "111", AlbumID: "1", HasCoverArt: true} id := mf.CoverArtID() From 8f05f7815eb97b57a6e28a5e1ebabb420c1c5dcc Mon Sep 17 00:00:00 2001 From: Simon Teixidor Date: Tue, 17 Mar 2026 13:04:47 +0100 Subject: [PATCH 31/81] fix(server): use http.TimeFormat for Last-Modified header (#5219) Navidrome returns Last-Modified values like `Fri, 12 Dec 2025 03:32:26 UTC`. This is invalid according to RFC 7231 which requires HTTP dates to use GMT instead of UTC. Switch to http.TimeFormat instead of time.RFC1123 to resolve the issue. --- server/public/handle_images.go | 2 +- server/subsonic/media_retrieval.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/public/handle_images.go b/server/public/handle_images.go index f4985dea5..50f9238e5 100644 --- a/server/public/handle_images.go +++ b/server/public/handle_images.go @@ -60,7 +60,7 @@ func (pub *Router) handleImages(w http.ResponseWriter, r *http.Request) { defer imgReader.Close() w.Header().Set("Cache-Control", "public, max-age=315360000") - w.Header().Set("Last-Modified", lastUpdate.Format(time.RFC1123)) + w.Header().Set("Last-Modified", lastUpdate.Format(http.TimeFormat)) cnt, err := io.Copy(w, imgReader) if err != nil { log.Warn(ctx, "Error sending image", "count", cnt, err) diff --git a/server/subsonic/media_retrieval.go b/server/subsonic/media_retrieval.go index 54fcb5e3a..3faae1650 100644 --- a/server/subsonic/media_retrieval.go +++ b/server/subsonic/media_retrieval.go @@ -81,7 +81,7 @@ func (api *Router) GetCoverArt(w http.ResponseWriter, r *http.Request) (*respons defer imgReader.Close() w.Header().Set("cache-control", "public, max-age=315360000") - w.Header().Set("last-modified", lastUpdate.Format(time.RFC1123)) + w.Header().Set("last-modified", lastUpdate.Format(http.TimeFormat)) cnt, err := io.Copy(w, imgReader) if err != nil { From a0fe72809869ddd2da94458f059313aac18ea858 Mon Sep 17 00:00:00 2001 From: Deluan Date: Tue, 17 Mar 2026 12:14:57 -0400 Subject: [PATCH 32/81] fix(player): fix play next after transcoding changes Signed-off-by: Deluan --- ui/package-lock.json | 343 +++++++++++++------------------ ui/package.json | 2 +- ui/src/reducers/playerReducer.js | 26 ++- 3 files changed, 155 insertions(+), 216 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 04d6fe07c..7d1131005 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -23,7 +23,7 @@ "inflection": "^3.0.2", "jwt-decode": "^4.0.0", "lodash.throttle": "^4.1.1", - "navidrome-music-player": "4.25.1", + "navidrome-music-player": "4.25.2", "prop-types": "^15.8.1", "ra-data-json-server": "^3.19.12", "ra-i18n-polyglot": "^3.19.12", @@ -73,6 +73,106 @@ "vitest": "^4.0.3" } }, + "../../react-music-player": { + "name": "navidrome-music-player", + "version": "4.25.2", + "license": "MIT", + "dependencies": { + "@react-icons/all-files": "^4.1.0", + "classnames": "^2.3.1", + "downloadjs": "^1.4.7", + "is-mobile": "^2.2.2", + "prop-types": "^15.7.2", + "rc-slider": "^9.7.2", + "rc-switch": "^3.2.2", + "react-draggable": "^4.4.3", + "sortablejs": "^1.13.0" + }, + "devDependencies": { + "@babel/cli": "7.13.16", + "@babel/core": "7.14.0", + "@babel/plugin-proposal-class-properties": "7.13.0", + "@babel/plugin-proposal-object-rest-spread": "7.13.8", + "@babel/plugin-syntax-dynamic-import": "7.8.3", + "@babel/plugin-syntax-object-rest-spread": "7.8.3", + "@babel/plugin-transform-object-assign": "7.12.13", + "@babel/plugin-transform-runtime": "^7.13.15", + "@babel/preset-env": "7.14.1", + "@babel/preset-react": "^7.13.13", + "@babel/runtime": "7.14.0", + "@commitlint/config-conventional": "^12.1.1", + "@semantic-release/changelog": "^5.0.1", + "@semantic-release/git": "^9.0.0", + "@types/jest": "^26.0.23", + "@types/sortablejs": "^1.10.6", + "autoprefixer": "^10.2.5", + "babel-core": "^7.0.0-bridge.0", + "babel-eslint": "^10.1.0", + "babel-jest": "^26.6.3", + "babel-loader": "^8.2.2", + "babel-plugin-import": "^1.13.3", + "babel-plugin-transform-runtime": "^6.23.0", + "bundlesize": "^0.18.1", + "clean-webpack-plugin": "^3.0.0", + "codecov": "^3.8.2", + "commitizen": "^4.2.4", + "commitlint": "^12.1.1", + "copy-webpack-plugin": "latest", + "coveralls": "^3.1.0", + "cross-env": "^7.0.3", + "css-loader": "^5.2.4", + "css-minimizer-webpack-plugin": "^2.0.0", + "enzyme": "^3.11.0", + "enzyme-adapter-react-16": "^1.15.6", + "enzyme-to-json": "^3.6.2", + "esbuild-webpack-plugin": "^1.1.0", + "eslint": "^7.26.0", + "eslint-config-airbnb": "^18.2.1", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-babel": "^5.3.1", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-jest": "^24.3.6", + "eslint-plugin-jsx-a11y": "^6.4.1", + "eslint-plugin-prettier": "^3.4.0", + "eslint-plugin-react": "^7.23.2", + "file-loader": "^6.2.0", + "html-loader": "^2.1.2", + "html-webpack-plugin": "^5.3.1", + "husky": "^6.0.0", + "jest": "^26.6.3", + "jest-environment-jsdom": "^26.6.2", + "jsdom": "^16.5.3", + "less": "^4.1.1", + "less-loader": "^8.1.1", + "lint-staged": "^11.0.0", + "mini-css-extract-plugin": "^1.6.0", + "postcss": "^8.2.14", + "postcss-loader": "^5.2.0", + "power-assert": "^1.6.1", + "pre-commit": "^1.2.2", + "prettier": "^2.2.1", + "pretty-quick": "^3.1.0", + "react": "16.13.1", + "react-dom": "16.13.1", + "react-hot-loader": "^4.13.0", + "react-loader": "^2.4.7", + "rimraf": "^3.0.2", + "semantic-release": "^17.4.2", + "style-loader": "~2.0.0", + "stylelint": "^13.13.1", + "stylelint-config-prettier": "^8.0.2", + "stylelint-config-standard": "^22.0.0", + "uglifyjs-webpack-plugin": "^2.2.0", + "webpack": "^5.36.2", + "webpack-bundle-analyzer": "^4.4.1", + "webpack-cli": "^4.7.0", + "webpack-dev-server": "3.11.2" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/@adobe/css-tools": { "version": "4.4.4", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", @@ -129,6 +229,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -1743,6 +1844,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -1766,6 +1868,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2573,6 +2676,7 @@ "resolved": "https://registry.npmjs.org/@jsonforms/core/-/core-2.5.2.tgz", "integrity": "sha512-tl64cLC2dUrGvu2nTHRDEA5Yv3RfwzMCIlVaoSUSq44LakKLGJdkPl8j/fb07llpFqz0a7gEAmy/8gLdmwgaLQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.3", "ajv": "^6.10.2", @@ -2626,6 +2730,7 @@ "resolved": "https://registry.npmjs.org/@jsonforms/react/-/react-2.5.2.tgz", "integrity": "sha512-kZf2fq4urIBlFTCiBX95eKg8uojkyJj7FVDtIV739aVkJjE5+ihn1+kG1qLxYSxlGC7S24i12BZJzRetSRihBQ==", "license": "MIT", + "peer": true, "dependencies": { "lodash": "^4.17.15", "object-hash": "^2.0.0" @@ -2641,6 +2746,7 @@ "integrity": "sha512-tr7xekNlM9LjA6pagJmL8QCgZXaubWUwkJnoYcMKd4gw/t4XiyvnTkjdGrUVicyB2BsdaAv1tvow45bPM4sSwQ==", "deprecated": "Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.4.4", "@material-ui/styles": "^4.11.5", @@ -2687,6 +2793,7 @@ "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.3.tgz", "integrity": "sha512-IKHlyx6LDh8n19vzwH5RtHIOHl9Tu90aAAxcbWME6kp4dmvODM3UvOHJeMIDzUbd4muuJKHmlNoBN+mDY4XkBA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.4.4" }, @@ -2982,15 +3089,6 @@ "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==", "license": "MIT" }, - "node_modules/@react-icons/all-files": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@react-icons/all-files/-/all-files-4.1.0.tgz", - "integrity": "sha512-hxBI2UOuVaI3O/BhQfhtb4kcGn9ft12RWAFVMUeNjqqhLsHvFtzIkFaptBJpFDANTKoDfdVoHTKZDlwKCACbMQ==", - "license": "MIT", - "peerDependencies": { - "react": "*" - } - }, "node_modules/@redux-saga/core": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.4.2.tgz", @@ -3410,6 +3508,7 @@ "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", "license": "MIT", + "peer": true, "dependencies": { "hoist-non-react-statics": "^3.3.0" }, @@ -3462,6 +3561,7 @@ "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3483,6 +3583,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.90.tgz", "integrity": "sha512-P9beVR/x06U9rCJzSxtENnOr4BrbJ6VrsrDTc+73TtHv9XHhryXKbjGRB+6oooB2r0G/pQkD/S4dHo/7jUfwFw==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "^0.16", @@ -3652,6 +3753,7 @@ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -3983,6 +4085,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4664,6 +4767,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4885,12 +4989,6 @@ "node": ">= 6" } }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", - "license": "MIT" - }, "node_modules/cli-boxes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", @@ -5039,6 +5137,7 @@ "resolved": "https://registry.npmjs.org/connected-react-router/-/connected-react-router-6.9.3.tgz", "integrity": "sha512-4ThxysOiv/R2Dc4Cke1eJwjKwH1Y51VDwlOrOfs1LjpdYOVvCNjNkZDayo7+sx42EeGJPQUNchWkjAIJdXGIOQ==", "license": "MIT", + "peer": true, "dependencies": { "lodash.isequalwith": "^4.4.0", "prop-types": "^15.7.2" @@ -5480,12 +5579,6 @@ "dev": true, "license": "MIT" }, - "node_modules/dom-align": { - "version": "1.12.4", - "resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.12.4.tgz", - "integrity": "sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==", - "license": "MIT" - }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -5541,12 +5634,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/downloadjs": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/downloadjs/-/downloadjs-1.4.7.tgz", - "integrity": "sha512-LN1gO7+u9xjU5oEScGFKvXhYf7Y/empUIIEAGBs1LzUq/rg5duiDrkuH5A2lQGd5jfMOb9X9usDa2oVXwJ0U/Q==", - "license": "MIT" - }, "node_modules/downshift": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/downshift/-/downshift-3.2.7.tgz", @@ -5926,6 +6013,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6509,6 +6597,7 @@ "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.10.tgz", "integrity": "sha512-TL48Pi1oNHeMOHrKv1bCJUrWZDcD3DIG6AGYVNOnyZPr7Bd/pStN0pL+lfzF5BNoj/FclaoiaLenk4XUIFVYng==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.10.0" }, @@ -6525,6 +6614,7 @@ "resolved": "https://registry.npmjs.org/final-form-arrays/-/final-form-arrays-3.1.0.tgz", "integrity": "sha512-TWBvun+AopgBLw9zfTFHBllnKMVNEwCEyDawphPuBGGqNsuhGzhT7yewHys64KFFwzIs6KEteGLpKOwvTQEscQ==", "license": "MIT", + "peer": true, "peerDependencies": { "final-form": "^4.20.8" } @@ -6939,6 +7029,7 @@ "integrity": "sha512-hM9gltmtQLfmWPqoPreUtRdP3nZCSzQEw7l/JC+up5CxquDykhYFKzIzoFFeVev3AGFEULNvsbE8fpZPgxUYEQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", @@ -7052,6 +7143,7 @@ "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.1.2", "loose-envify": "^1.2.0", @@ -7598,12 +7690,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-mobile": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-2.2.2.tgz", - "integrity": "sha512-wW/SXnYJkTjs++tVK5b6kVITZpAZPtUrt9SF80vvxGiF/Oywal+COk1jlRkiVq15RFNEQKQY31TkV24/1T5cVg==", - "license": "MIT" - }, "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -8660,6 +8746,7 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "license": "MIT", + "peer": true, "engines": { "node": "*" } @@ -8703,25 +8790,8 @@ "license": "MIT" }, "node_modules/navidrome-music-player": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/navidrome-music-player/-/navidrome-music-player-4.25.1.tgz", - "integrity": "sha512-bHYr84ATUf/4+/PUoTpUSmpF4/igBx2UPhgnPqvda4FND+GJZtb1ikbMs1U+mhkNEUebe+2I29ob1zY7YZdtjg==", - "license": "MIT", - "dependencies": { - "@react-icons/all-files": "^4.1.0", - "classnames": "^2.3.1", - "downloadjs": "^1.4.7", - "is-mobile": "^2.2.2", - "prop-types": "^15.7.2", - "rc-slider": "^9.7.2", - "rc-switch": "^3.2.2", - "react-draggable": "^4.4.3", - "sortablejs": "^1.13.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } + "resolved": "../../react-music-player", + "link": true }, "node_modules/node-polyglot": { "version": "2.6.0", @@ -9379,6 +9449,7 @@ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -9470,6 +9541,7 @@ "resolved": "https://registry.npmjs.org/ra-core/-/ra-core-3.19.12.tgz", "integrity": "sha512-E0cM6OjEUtccaR+dR5mL1MLiVVYML0Yf7aPhpLEq4iue73X3+CKcLztInoBhWgeevPbFQwgAtsXhlpedeyrNNg==", "license": "MIT", + "peer": true, "dependencies": { "classnames": "~2.3.1", "date-fns": "^1.29.0", @@ -9723,128 +9795,6 @@ "rc": "cli.js" } }, - "node_modules/rc-align": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/rc-align/-/rc-align-4.0.15.tgz", - "integrity": "sha512-wqJtVH60pka/nOX7/IspElA8gjPNQKIx/ZqJ6heATCkXpe1Zg4cPVrMD2vC96wjsFFL8WsmhPbx9tdMo1qqlIA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "2.x", - "dom-align": "^1.7.0", - "rc-util": "^5.26.0", - "resize-observer-polyfill": "^1.5.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-motion": { - "version": "2.9.5", - "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz", - "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.11.1", - "classnames": "^2.2.1", - "rc-util": "^5.44.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-slider": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-9.7.5.tgz", - "integrity": "sha512-LV/MWcXFjco1epPbdw1JlLXlTgmWpB9/Y/P2yinf8Pg3wElHxA9uajN21lJiWtZjf5SCUekfSP6QMJfDo4t1hg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "^2.2.5", - "rc-tooltip": "^5.0.1", - "rc-util": "^5.16.1", - "shallowequal": "^1.1.0" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-switch": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-3.2.2.tgz", - "integrity": "sha512-+gUJClsZZzvAHGy1vZfnwySxj+MjLlGRyXKXScrtCTcmiYNPzxDFOxdQ/3pK1Kt/0POvwJ/6ALOR8gwdXGhs+A==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "^2.2.1", - "rc-util": "^5.0.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-tooltip": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-5.3.1.tgz", - "integrity": "sha512-e6H0dMD38EPaSPD2XC8dRfct27VvT2TkPdoBSuNl3RRZ5tspiY/c5xYEmGC0IrABvMBgque4Mr2SMZuliCvoiQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.11.2", - "classnames": "^2.3.1", - "rc-trigger": "^5.3.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-trigger": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/rc-trigger/-/rc-trigger-5.3.4.tgz", - "integrity": "sha512-mQv+vas0TwKcjAO2izNPkqR4j86OemLRmvL2nOzdP9OWNWA1ivoTt5hzFqYNW9zACwmTezRiN8bttrC7cZzYSw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "classnames": "^2.2.6", - "rc-align": "^4.0.0", - "rc-motion": "^2.0.0", - "rc-util": "^5.19.2" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-util": { - "version": "5.44.4", - "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz", - "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "react-is": "^18.2.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-util/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT" - }, "node_modules/rc/node_modules/ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", @@ -9865,6 +9815,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -9946,6 +9897,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -9965,20 +9917,6 @@ "prop-types": "^15.5.8" } }, - "node_modules/react-draggable": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz", - "integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==", - "license": "MIT", - "dependencies": { - "clsx": "^2.1.1", - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "react": ">= 16.3.0", - "react-dom": ">= 16.3.0" - } - }, "node_modules/react-dropzone": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-10.2.2.tgz", @@ -10018,6 +9956,7 @@ "resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.9.tgz", "integrity": "sha512-x3XYvozolECp3nIjly+4QqxdjSSWfcnpGEL5K8OBT6xmGrq5kBqbA6+/tOqoom9NwqIPPbxPNsOViFlbKgowbA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.15.4" }, @@ -10035,6 +9974,7 @@ "resolved": "https://registry.npmjs.org/react-final-form-arrays/-/react-final-form-arrays-3.1.4.tgz", "integrity": "sha512-siVFAolUAe29rMR6u8VwepoysUcUdh6MLV2OWnCtKpsPRUdT9VUgECjAPaVMAH2GROZNiVB9On1H9MMrm9gdpg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.19.4" }, @@ -10140,6 +10080,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.15.4", "@types/react-redux": "^7.1.20", @@ -10175,6 +10116,7 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -10195,6 +10137,7 @@ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -10376,6 +10319,7 @@ "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.9.2" } @@ -10385,6 +10329,7 @@ "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.4.2.tgz", "integrity": "sha512-QLIn/q+7MX/B+MkGJ/K6R3//60eJ4QNy65eqPsJrfGezbxdh1Jx+37VRKE2K4PsJnNET5JufJtgWdT30WBa+6w==", "license": "MIT", + "peer": true, "dependencies": { "@redux-saga/core": "^1.4.2" } @@ -10638,6 +10583,7 @@ "integrity": "sha512-oWKZLjYwTihnTeINcNenxIIDfeotkQ2GAjFJPe7aYsMONrwDwQQXcAl3Qv0qON7Hdc8RTsFomq22zotm/i6VVQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -10897,12 +10843,6 @@ "node": ">= 0.4" } }, - "node_modules/shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", - "license": "MIT" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -11025,12 +10965,6 @@ "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", "license": "MIT" }, - "node_modules/sortablejs": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz", - "integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==", - "license": "MIT" - }, "node_modules/source-map": { "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", @@ -11598,6 +11532,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -11819,6 +11754,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12063,6 +11999,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -12187,6 +12124,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -12200,6 +12138,7 @@ "integrity": "sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.17", "@vitest/mocker": "4.0.17", @@ -12716,6 +12655,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -12803,6 +12743,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, diff --git a/ui/package.json b/ui/package.json index 5b4deb773..d4c149b23 100644 --- a/ui/package.json +++ b/ui/package.json @@ -32,7 +32,7 @@ "inflection": "^3.0.2", "jwt-decode": "^4.0.0", "lodash.throttle": "^4.1.1", - "navidrome-music-player": "4.25.1", + "navidrome-music-player": "4.25.2", "prop-types": "^15.8.1", "ra-data-json-server": "^3.19.12", "ra-i18n-polyglot": "^3.19.12", diff --git a/ui/src/reducers/playerReducer.js b/ui/src/reducers/playerReducer.js index d3291633c..466a3ec87 100644 --- a/ui/src/reducers/playerReducer.js +++ b/ui/src/reducers/playerReducer.js @@ -134,30 +134,24 @@ const reduceAddTracks = (state, { data }) => { } const reducePlayNext = (state, { data }) => { + const newTracks = Object.keys(data).map((id) => mapToAudioLists(data[id])) const newQueue = [] const current = state.current || {} let foundPos = false - let currentIndex = 0 state.queue.forEach((item) => { newQueue.push(item) if (item.uuid === current.uuid) { foundPos = true - currentIndex = newQueue.length - 1 - Object.keys(data).forEach((id) => { - newQueue.push(mapToAudioLists(data[id])) - }) + newQueue.push(...newTracks) } }) if (!foundPos) { - Object.keys(data).forEach((id) => { - newQueue.push(mapToAudioLists(data[id])) - }) + newQueue.push(...newTracks) } return { ...state, queue: newQueue, - playIndex: foundPos ? currentIndex : undefined, clear: true, } } @@ -170,14 +164,18 @@ const reduceSetVolume = (state, { data: { volume } }) => { } const reduceSyncQueue = (state, { data: { audioInfo, audioLists } }) => { + // Only keep clear and playIndex alive when there is an actual pending + // track switch (playIndex differs from savedPlayIndex). This lets + // PLAYER_PLAY_TRACKS selections survive the sync, while allowing + // PLAYER_PLAY_NEXT (which sets playIndex to the current track) to + // reset immediately and avoid restarting playback. + const hasPendingSwitch = + state.playIndex != null && state.playIndex !== state.savedPlayIndex return { ...state, queue: audioLists, - // Keep clear and playIndex alive so the music player can still - // pick up a pending track selection set by PLAYER_PLAY_TRACKS. - // They will be consumed by the next PLAYER_CURRENT dispatch. - clear: state.playIndex != null ? state.clear : false, - playIndex: state.playIndex != null ? state.playIndex : undefined, + clear: hasPendingSwitch ? state.clear : false, + playIndex: hasPendingSwitch ? state.playIndex : undefined, } } From 693abe2f6bb9c2cbe36670883d3f1fe7afbcce22 Mon Sep 17 00:00:00 2001 From: Deluan Date: Tue, 17 Mar 2026 12:28:20 -0400 Subject: [PATCH 33/81] fix(build): regenerate package-lock.json for navidrome-music-player 4.25.2 The lockfile still referenced the local file path from testing, causing CI to fail resolving the navidrome-music-player import. Regenerated to point to the npm registry. --- ui/package-lock.json | 2229 ++++-------------------------------------- 1 file changed, 200 insertions(+), 2029 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 7d1131005..0e6183720 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -73,117 +73,13 @@ "vitest": "^4.0.3" } }, - "../../react-music-player": { - "name": "navidrome-music-player", - "version": "4.25.2", - "license": "MIT", - "dependencies": { - "@react-icons/all-files": "^4.1.0", - "classnames": "^2.3.1", - "downloadjs": "^1.4.7", - "is-mobile": "^2.2.2", - "prop-types": "^15.7.2", - "rc-slider": "^9.7.2", - "rc-switch": "^3.2.2", - "react-draggable": "^4.4.3", - "sortablejs": "^1.13.0" - }, - "devDependencies": { - "@babel/cli": "7.13.16", - "@babel/core": "7.14.0", - "@babel/plugin-proposal-class-properties": "7.13.0", - "@babel/plugin-proposal-object-rest-spread": "7.13.8", - "@babel/plugin-syntax-dynamic-import": "7.8.3", - "@babel/plugin-syntax-object-rest-spread": "7.8.3", - "@babel/plugin-transform-object-assign": "7.12.13", - "@babel/plugin-transform-runtime": "^7.13.15", - "@babel/preset-env": "7.14.1", - "@babel/preset-react": "^7.13.13", - "@babel/runtime": "7.14.0", - "@commitlint/config-conventional": "^12.1.1", - "@semantic-release/changelog": "^5.0.1", - "@semantic-release/git": "^9.0.0", - "@types/jest": "^26.0.23", - "@types/sortablejs": "^1.10.6", - "autoprefixer": "^10.2.5", - "babel-core": "^7.0.0-bridge.0", - "babel-eslint": "^10.1.0", - "babel-jest": "^26.6.3", - "babel-loader": "^8.2.2", - "babel-plugin-import": "^1.13.3", - "babel-plugin-transform-runtime": "^6.23.0", - "bundlesize": "^0.18.1", - "clean-webpack-plugin": "^3.0.0", - "codecov": "^3.8.2", - "commitizen": "^4.2.4", - "commitlint": "^12.1.1", - "copy-webpack-plugin": "latest", - "coveralls": "^3.1.0", - "cross-env": "^7.0.3", - "css-loader": "^5.2.4", - "css-minimizer-webpack-plugin": "^2.0.0", - "enzyme": "^3.11.0", - "enzyme-adapter-react-16": "^1.15.6", - "enzyme-to-json": "^3.6.2", - "esbuild-webpack-plugin": "^1.1.0", - "eslint": "^7.26.0", - "eslint-config-airbnb": "^18.2.1", - "eslint-config-prettier": "^8.3.0", - "eslint-plugin-babel": "^5.3.1", - "eslint-plugin-import": "^2.22.1", - "eslint-plugin-jest": "^24.3.6", - "eslint-plugin-jsx-a11y": "^6.4.1", - "eslint-plugin-prettier": "^3.4.0", - "eslint-plugin-react": "^7.23.2", - "file-loader": "^6.2.0", - "html-loader": "^2.1.2", - "html-webpack-plugin": "^5.3.1", - "husky": "^6.0.0", - "jest": "^26.6.3", - "jest-environment-jsdom": "^26.6.2", - "jsdom": "^16.5.3", - "less": "^4.1.1", - "less-loader": "^8.1.1", - "lint-staged": "^11.0.0", - "mini-css-extract-plugin": "^1.6.0", - "postcss": "^8.2.14", - "postcss-loader": "^5.2.0", - "power-assert": "^1.6.1", - "pre-commit": "^1.2.2", - "prettier": "^2.2.1", - "pretty-quick": "^3.1.0", - "react": "16.13.1", - "react-dom": "16.13.1", - "react-hot-loader": "^4.13.0", - "react-loader": "^2.4.7", - "rimraf": "^3.0.2", - "semantic-release": "^17.4.2", - "style-loader": "~2.0.0", - "stylelint": "^13.13.1", - "stylelint-config-prettier": "^8.0.2", - "stylelint-config-standard": "^22.0.0", - "uglifyjs-webpack-plugin": "^2.2.0", - "webpack": "^5.36.2", - "webpack-bundle-analyzer": "^4.4.1", - "webpack-cli": "^4.7.0", - "webpack-dev-server": "3.11.2" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, "node_modules/@adobe/css-tools": { "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", - "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", "dev": true, "license": "MIT" }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", - "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", "dev": true, "license": "MIT", "dependencies": { @@ -196,15 +92,11 @@ }, "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, "license": "ISC" }, "node_modules/@babel/code-frame": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", - "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", @@ -217,8 +109,6 @@ }, "node_modules/@babel/compat-data": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", - "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -226,8 +116,6 @@ }, "node_modules/@babel/core": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", - "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "license": "MIT", "peer": true, "dependencies": { @@ -257,8 +145,6 @@ }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -266,8 +152,6 @@ }, "node_modules/@babel/generator": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", - "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", "license": "MIT", "dependencies": { "@babel/parser": "^7.28.6", @@ -282,8 +166,6 @@ }, "node_modules/@babel/helper-annotate-as-pure": { "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "license": "MIT", "dependencies": { "@babel/types": "^7.27.3" @@ -294,8 +176,6 @@ }, "node_modules/@babel/helper-compilation-targets": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "license": "MIT", "dependencies": { "@babel/compat-data": "^7.28.6", @@ -310,8 +190,6 @@ }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -319,8 +197,6 @@ }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", - "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -340,8 +216,6 @@ }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -349,8 +223,6 @@ }, "node_modules/@babel/helper-create-regexp-features-plugin": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", - "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -366,8 +238,6 @@ }, "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -375,8 +245,6 @@ }, "node_modules/@babel/helper-define-polyfill-provider": { "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", - "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", @@ -391,8 +259,6 @@ }, "node_modules/@babel/helper-define-polyfill-provider/node_modules/resolve": { "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "license": "MIT", "dependencies": { "is-core-module": "^2.16.1", @@ -411,8 +277,6 @@ }, "node_modules/@babel/helper-globals": { "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -420,8 +284,6 @@ }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.28.5", @@ -433,8 +295,6 @@ }, "node_modules/@babel/helper-module-imports": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.28.6", @@ -446,8 +306,6 @@ }, "node_modules/@babel/helper-module-transforms": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.28.6", @@ -463,8 +321,6 @@ }, "node_modules/@babel/helper-optimise-call-expression": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "license": "MIT", "dependencies": { "@babel/types": "^7.27.1" @@ -475,8 +331,6 @@ }, "node_modules/@babel/helper-plugin-utils": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -484,8 +338,6 @@ }, "node_modules/@babel/helper-remap-async-to-generator": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", - "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", @@ -501,8 +353,6 @@ }, "node_modules/@babel/helper-replace-supers": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", - "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", "license": "MIT", "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", @@ -518,8 +368,6 @@ }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -531,8 +379,6 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -540,8 +386,6 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -549,8 +393,6 @@ }, "node_modules/@babel/helper-validator-option": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -558,8 +400,6 @@ }, "node_modules/@babel/helper-wrap-function": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", - "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", "license": "MIT", "dependencies": { "@babel/template": "^7.28.6", @@ -572,8 +412,6 @@ }, "node_modules/@babel/helpers": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "license": "MIT", "dependencies": { "@babel/template": "^7.28.6", @@ -585,8 +423,6 @@ }, "node_modules/@babel/parser": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", - "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", "license": "MIT", "dependencies": { "@babel/types": "^7.28.6" @@ -600,8 +436,6 @@ }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", - "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -616,8 +450,6 @@ }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", - "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -631,8 +463,6 @@ }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", - "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -646,8 +476,6 @@ }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", - "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -663,8 +491,6 @@ }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", - "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", @@ -679,8 +505,6 @@ }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -691,8 +515,6 @@ }, "node_modules/@babel/plugin-syntax-import-assertions": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", - "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -706,8 +528,6 @@ }, "node_modules/@babel/plugin-syntax-import-attributes": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", - "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -721,8 +541,6 @@ }, "node_modules/@babel/plugin-syntax-unicode-sets-regex": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", @@ -737,8 +555,6 @@ }, "node_modules/@babel/plugin-transform-arrow-functions": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -752,8 +568,6 @@ }, "node_modules/@babel/plugin-transform-async-generator-functions": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.6.tgz", - "integrity": "sha512-9knsChgsMzBV5Yh3kkhrZNxH3oCYAfMBkNNaVN4cP2RVlFPe8wYdwwcnOsAbkdDoV9UjFtOXWrWB52M8W4jNeA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", @@ -769,8 +583,6 @@ }, "node_modules/@babel/plugin-transform-async-to-generator": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", - "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.28.6", @@ -786,8 +598,6 @@ }, "node_modules/@babel/plugin-transform-block-scoped-functions": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", - "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -801,8 +611,6 @@ }, "node_modules/@babel/plugin-transform-block-scoping": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", - "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -816,8 +624,6 @@ }, "node_modules/@babel/plugin-transform-class-properties": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", - "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", @@ -832,8 +638,6 @@ }, "node_modules/@babel/plugin-transform-class-static-block": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", - "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", @@ -848,8 +652,6 @@ }, "node_modules/@babel/plugin-transform-classes": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", - "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -868,8 +670,6 @@ }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", - "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", @@ -884,8 +684,6 @@ }, "node_modules/@babel/plugin-transform-destructuring": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", - "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -900,8 +698,6 @@ }, "node_modules/@babel/plugin-transform-dotall-regex": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", - "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", @@ -916,8 +712,6 @@ }, "node_modules/@babel/plugin-transform-duplicate-keys": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", - "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -931,8 +725,6 @@ }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.28.6.tgz", - "integrity": "sha512-5suVoXjC14lUN6ZL9OLKIHCNVWCrqGqlmEp/ixdXjvgnEl/kauLvvMO/Xw9NyMc95Joj1AeLVPVMvibBgSoFlA==", "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", @@ -947,8 +739,6 @@ }, "node_modules/@babel/plugin-transform-dynamic-import": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", - "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -962,8 +752,6 @@ }, "node_modules/@babel/plugin-transform-explicit-resource-management": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", - "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", @@ -978,8 +766,6 @@ }, "node_modules/@babel/plugin-transform-exponentiation-operator": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", - "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -993,8 +779,6 @@ }, "node_modules/@babel/plugin-transform-export-namespace-from": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", - "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1008,8 +792,6 @@ }, "node_modules/@babel/plugin-transform-for-of": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1024,8 +806,6 @@ }, "node_modules/@babel/plugin-transform-function-name": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.27.1", @@ -1041,8 +821,6 @@ }, "node_modules/@babel/plugin-transform-json-strings": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", - "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1056,8 +834,6 @@ }, "node_modules/@babel/plugin-transform-literals": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1071,8 +847,6 @@ }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", - "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1086,8 +860,6 @@ }, "node_modules/@babel/plugin-transform-member-expression-literals": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", - "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1101,8 +873,6 @@ }, "node_modules/@babel/plugin-transform-modules-amd": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", - "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.27.1", @@ -1117,8 +887,6 @@ }, "node_modules/@babel/plugin-transform-modules-commonjs": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", - "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.28.6", @@ -1133,8 +901,6 @@ }, "node_modules/@babel/plugin-transform-modules-systemjs": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", - "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.28.3", @@ -1151,8 +917,6 @@ }, "node_modules/@babel/plugin-transform-modules-umd": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", - "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.27.1", @@ -1167,8 +931,6 @@ }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", @@ -1183,8 +945,6 @@ }, "node_modules/@babel/plugin-transform-new-target": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", - "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1198,8 +958,6 @@ }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", - "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1213,8 +971,6 @@ }, "node_modules/@babel/plugin-transform-numeric-separator": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", - "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1228,8 +984,6 @@ }, "node_modules/@babel/plugin-transform-object-rest-spread": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", - "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", @@ -1247,8 +1001,6 @@ }, "node_modules/@babel/plugin-transform-object-super": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", - "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1263,8 +1015,6 @@ }, "node_modules/@babel/plugin-transform-optional-catch-binding": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", - "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1278,8 +1028,6 @@ }, "node_modules/@babel/plugin-transform-optional-chaining": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", - "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", @@ -1294,8 +1042,6 @@ }, "node_modules/@babel/plugin-transform-parameters": { "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1309,8 +1055,6 @@ }, "node_modules/@babel/plugin-transform-private-methods": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", - "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", @@ -1325,8 +1069,6 @@ }, "node_modules/@babel/plugin-transform-private-property-in-object": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", - "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -1342,8 +1084,6 @@ }, "node_modules/@babel/plugin-transform-property-literals": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", - "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1357,8 +1097,6 @@ }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", "dev": true, "license": "MIT", "dependencies": { @@ -1373,8 +1111,6 @@ }, "node_modules/@babel/plugin-transform-react-jsx-source": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", "dev": true, "license": "MIT", "dependencies": { @@ -1389,8 +1125,6 @@ }, "node_modules/@babel/plugin-transform-regenerator": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.6.tgz", - "integrity": "sha512-eZhoEZHYQLL5uc1gS5e9/oTknS0sSSAtd5TkKMUp3J+S/CaUjagc0kOUPsEbDmMeva0nC3WWl4SxVY6+OBuxfw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1404,8 +1138,6 @@ }, "node_modules/@babel/plugin-transform-regexp-modifiers": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", - "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", @@ -1420,8 +1152,6 @@ }, "node_modules/@babel/plugin-transform-reserved-words": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", - "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1435,8 +1165,6 @@ }, "node_modules/@babel/plugin-transform-shorthand-properties": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1450,8 +1178,6 @@ }, "node_modules/@babel/plugin-transform-spread": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", - "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", @@ -1466,8 +1192,6 @@ }, "node_modules/@babel/plugin-transform-sticky-regex": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", - "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1481,8 +1205,6 @@ }, "node_modules/@babel/plugin-transform-template-literals": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1496,8 +1218,6 @@ }, "node_modules/@babel/plugin-transform-typeof-symbol": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", - "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1511,8 +1231,6 @@ }, "node_modules/@babel/plugin-transform-unicode-escapes": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", - "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1526,8 +1244,6 @@ }, "node_modules/@babel/plugin-transform-unicode-property-regex": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", - "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", @@ -1542,8 +1258,6 @@ }, "node_modules/@babel/plugin-transform-unicode-regex": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", - "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", @@ -1558,8 +1272,6 @@ }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", - "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", @@ -1574,8 +1286,6 @@ }, "node_modules/@babel/preset-env": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.6.tgz", - "integrity": "sha512-GaTI4nXDrs7l0qaJ6Rg06dtOXTBCG6TMDB44zbqofCIC4PqC7SEvmFFtpxzCDw9W5aJ7RKVshgXTLvLdBFV/qw==", "license": "MIT", "dependencies": { "@babel/compat-data": "^7.28.6", @@ -1658,8 +1368,6 @@ }, "node_modules/@babel/preset-env/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1667,8 +1375,6 @@ }, "node_modules/@babel/preset-modules": { "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -1681,8 +1387,6 @@ }, "node_modules/@babel/runtime": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1690,8 +1394,6 @@ }, "node_modules/@babel/runtime-corejs3": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.6.tgz", - "integrity": "sha512-kz2fAQ5UzjV7X7D3ySxmj3vRq89dTpqOZWv76Z6pNPztkwb/0Yj1Mtx1xFrYj6mbIHysxtBot8J4o0JLCblcFw==", "dev": true, "license": "MIT", "dependencies": { @@ -1703,8 +1405,6 @@ }, "node_modules/@babel/template": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.28.6", @@ -1717,8 +1417,6 @@ }, "node_modules/@babel/traverse": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", - "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.28.6", @@ -1735,8 +1433,6 @@ }, "node_modules/@babel/types": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", - "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -1748,8 +1444,6 @@ }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, "license": "MIT", "engines": { @@ -1758,8 +1452,6 @@ }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", - "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", "dev": true, "funding": [ { @@ -1778,8 +1470,6 @@ }, "node_modules/@csstools/css-calc": { "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", "dev": true, "funding": [ { @@ -1802,8 +1492,6 @@ }, "node_modules/@csstools/css-color-parser": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", - "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", "dev": true, "funding": [ { @@ -1830,8 +1518,6 @@ }, "node_modules/@csstools/css-parser-algorithms": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", - "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", "dev": true, "funding": [ { @@ -1854,8 +1540,6 @@ }, "node_modules/@csstools/css-tokenizer": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", "dev": true, "funding": [ { @@ -1875,14 +1559,10 @@ }, "node_modules/@date-io/core": { "version": "1.3.13", - "resolved": "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz", - "integrity": "sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA==", "license": "MIT" }, "node_modules/@date-io/moment": { "version": "1.3.11", - "resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-1.3.11.tgz", - "integrity": "sha512-pLEkqp8+P1DfC+QU8StaIANXoiadjJjoImLQCy0rhFAo0RVcJB9cM7mWr7fVgM49EjCwpA8a1JekNhuRLIJVwQ==", "license": "MIT", "dependencies": { "@date-io/core": "^1.3.11" @@ -1893,8 +1573,6 @@ }, "node_modules/@emotion/hash": { "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", - "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { @@ -1967,8 +1645,6 @@ }, "node_modules/@esbuild/darwin-arm64": { "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", "cpu": [ "arm64" ], @@ -2341,8 +2017,6 @@ }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2360,8 +2034,6 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -2370,8 +2042,6 @@ }, "node_modules/@eslint/eslintrc": { "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2394,8 +2064,6 @@ }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2405,8 +2073,6 @@ }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -2418,8 +2084,6 @@ }, "node_modules/@eslint/js": { "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, "license": "MIT", "engines": { @@ -2428,9 +2092,6 @@ }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2444,8 +2105,6 @@ }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2455,8 +2114,6 @@ }, "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -2468,8 +2125,6 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2482,16 +2137,11 @@ }, "node_modules/@humanwhocodes/object-schema": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", "license": "MIT", "engines": { "node": "20 || >=22" @@ -2499,8 +2149,6 @@ }, "node_modules/@isaacs/brace-expansion": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", - "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", "license": "MIT", "dependencies": { "@isaacs/balanced-match": "^4.0.1" @@ -2511,8 +2159,6 @@ }, "node_modules/@isaacs/cliui": { "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -2528,8 +2174,6 @@ }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { "node": ">=12" @@ -2540,8 +2184,6 @@ }, "node_modules/@isaacs/cliui/node_modules/ansi-styles": { "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", "engines": { "node": ">=12" @@ -2552,8 +2194,6 @@ }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -2569,8 +2209,6 @@ }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -2584,8 +2222,6 @@ }, "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -2601,8 +2237,6 @@ }, "node_modules/@jest/types": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2618,8 +2252,6 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -2628,8 +2260,6 @@ }, "node_modules/@jridgewell/remapping": { "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -2638,8 +2268,6 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "license": "MIT", "engines": { "node": ">=6.0.0" @@ -2647,8 +2275,6 @@ }, "node_modules/@jridgewell/source-map": { "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -2657,14 +2283,10 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2673,8 +2295,6 @@ }, "node_modules/@jsonforms/core": { "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@jsonforms/core/-/core-2.5.2.tgz", - "integrity": "sha512-tl64cLC2dUrGvu2nTHRDEA5Yv3RfwzMCIlVaoSUSq44LakKLGJdkPl8j/fb07llpFqz0a7gEAmy/8gLdmwgaLQ==", "license": "MIT", "peer": true, "dependencies": { @@ -2688,9 +2308,6 @@ }, "node_modules/@jsonforms/core/node_modules/uuid": { "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", "license": "MIT", "bin": { "uuid": "bin/uuid" @@ -2698,8 +2315,6 @@ }, "node_modules/@jsonforms/material-renderers": { "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@jsonforms/material-renderers/-/material-renderers-2.5.2.tgz", - "integrity": "sha512-0C6MVyhLoMOf1Byhgs9ZNvV4NWHdbMce6m7hW2LF1Mt9mK1wDfTTst+xM3g7K5b8FSiBiF5kMnTggJcRweAEBA==", "license": "MIT", "dependencies": { "@date-io/moment": "1.3.11", @@ -2717,9 +2332,6 @@ }, "node_modules/@jsonforms/material-renderers/node_modules/uuid": { "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", "license": "MIT", "bin": { "uuid": "bin/uuid" @@ -2727,8 +2339,6 @@ }, "node_modules/@jsonforms/react": { "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@jsonforms/react/-/react-2.5.2.tgz", - "integrity": "sha512-kZf2fq4urIBlFTCiBX95eKg8uojkyJj7FVDtIV739aVkJjE5+ihn1+kG1qLxYSxlGC7S24i12BZJzRetSRihBQ==", "license": "MIT", "peer": true, "dependencies": { @@ -2742,9 +2352,6 @@ }, "node_modules/@material-ui/core": { "version": "4.12.4", - "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.4.tgz", - "integrity": "sha512-tr7xekNlM9LjA6pagJmL8QCgZXaubWUwkJnoYcMKd4gw/t4XiyvnTkjdGrUVicyB2BsdaAv1tvow45bPM4sSwQ==", - "deprecated": "Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.", "license": "MIT", "peer": true, "dependencies": { @@ -2781,8 +2388,6 @@ }, "node_modules/@material-ui/core/node_modules/clsx": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", "license": "MIT", "engines": { "node": ">=6" @@ -2790,8 +2395,6 @@ }, "node_modules/@material-ui/icons": { "version": "4.11.3", - "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.3.tgz", - "integrity": "sha512-IKHlyx6LDh8n19vzwH5RtHIOHl9Tu90aAAxcbWME6kp4dmvODM3UvOHJeMIDzUbd4muuJKHmlNoBN+mDY4XkBA==", "license": "MIT", "peer": true, "dependencies": { @@ -2814,9 +2417,6 @@ }, "node_modules/@material-ui/lab": { "version": "4.0.0-alpha.61", - "resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.61.tgz", - "integrity": "sha512-rSzm+XKiNUjKegj8bzt5+pygZeckNLOr+IjykH8sYdVk7dE9y2ZuUSofiMV2bJk3qU+JHwexmw+q0RyNZB9ugg==", - "deprecated": "Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.", "license": "MIT", "dependencies": { "@babel/runtime": "^7.4.4", @@ -2842,8 +2442,6 @@ }, "node_modules/@material-ui/lab/node_modules/clsx": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", "license": "MIT", "engines": { "node": ">=6" @@ -2851,9 +2449,6 @@ }, "node_modules/@material-ui/pickers": { "version": "3.3.11", - "resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.3.11.tgz", - "integrity": "sha512-pDYjbjUeabapijS2FpSwK/ruJdk7IGeAshpLbKDa3PRRKRy7Nv6sXxAvUg2F+lID/NwUKgBmCYS5bzrl7Xxqzw==", - "deprecated": "This package no longer supported. It has been relaced by @mui/x-date-pickers", "license": "MIT", "dependencies": { "@babel/runtime": "^7.6.0", @@ -2873,8 +2468,6 @@ }, "node_modules/@material-ui/pickers/node_modules/clsx": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", "license": "MIT", "engines": { "node": ">=6" @@ -2882,9 +2475,6 @@ }, "node_modules/@material-ui/styles": { "version": "4.11.5", - "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.5.tgz", - "integrity": "sha512-o/41ot5JJiUsIETME9wVLAJrmIWL3j0R0Bj2kCOLbSfqEkKf0fmaPt+5vtblUh5eXr2S+J/8J3DaCb10+CzPGA==", - "deprecated": "Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.", "license": "MIT", "dependencies": { "@babel/runtime": "^7.4.4", @@ -2924,8 +2514,6 @@ }, "node_modules/@material-ui/styles/node_modules/clsx": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", "license": "MIT", "engines": { "node": ">=6" @@ -2933,8 +2521,6 @@ }, "node_modules/@material-ui/system": { "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.12.2.tgz", - "integrity": "sha512-6CSKu2MtmiJgcCGf6nBQpM8fLkuB9F55EKfbdTC80NND5wpTmKzwdhLYLH3zL4cLlK0gVaaltW7/wMuyTnN0Lw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.4.4", @@ -2962,8 +2548,6 @@ }, "node_modules/@material-ui/types": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", - "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==", "license": "MIT", "peerDependencies": { "@types/react": "*" @@ -2976,8 +2560,6 @@ }, "node_modules/@material-ui/utils": { "version": "4.11.3", - "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.3.tgz", - "integrity": "sha512-ZuQPV4rBK/V1j2dIkSSEcH5uT6AaHuKWFfotADHsC0wVL1NLd2WkFCm4ZZbX33iO4ydl6V0GPngKm8HZQ2oujg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.4.4", @@ -2994,8 +2576,6 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "license": "MIT", "dependencies": { @@ -3008,8 +2588,6 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", "engines": { @@ -3018,8 +2596,6 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", "dependencies": { @@ -3032,8 +2608,6 @@ }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", - "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", "license": "MIT", "engines": { "node": ">=12.22.0" @@ -3041,8 +2615,6 @@ }, "node_modules/@pnpm/network.ca-file": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", - "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", "license": "MIT", "dependencies": { "graceful-fs": "4.2.10" @@ -3053,14 +2625,10 @@ }, "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "license": "ISC" }, "node_modules/@pnpm/npm-conf": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-3.0.2.tgz", - "integrity": "sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA==", "license": "MIT", "dependencies": { "@pnpm/config.env-replace": "^1.1.0", @@ -3073,26 +2641,27 @@ }, "node_modules/@react-dnd/asap": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.1.tgz", - "integrity": "sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==", "license": "MIT" }, "node_modules/@react-dnd/invariant": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz", - "integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==", "license": "MIT" }, "node_modules/@react-dnd/shallowequal": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz", - "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==", "license": "MIT" }, + "node_modules/@react-icons/all-files": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@react-icons/all-files/-/all-files-4.1.0.tgz", + "integrity": "sha512-hxBI2UOuVaI3O/BhQfhtb4kcGn9ft12RWAFVMUeNjqqhLsHvFtzIkFaptBJpFDANTKoDfdVoHTKZDlwKCACbMQ==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/@redux-saga/core": { "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.4.2.tgz", - "integrity": "sha512-nIMLGKo6jV6Wc1sqtVQs1iqbB3Kq20udB/u9XEaZQisT6YZ0NRB8+4L6WqD/E+YziYutd27NJbG8EWUPkb7c6Q==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", @@ -3110,14 +2679,10 @@ }, "node_modules/@redux-saga/deferred": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.3.1.tgz", - "integrity": "sha512-0YZ4DUivWojXBqLB/TmuRRpDDz7tyq1I0AuDV7qi01XlLhM5m51W7+xYtIckH5U2cMlv9eAuicsfRAi1XHpXIg==", "license": "MIT" }, "node_modules/@redux-saga/delay-p": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.3.1.tgz", - "integrity": "sha512-597I7L5MXbD/1i3EmcaOOjL/5suxJD7p5tnbV1PiWnE28c2cYiIHqmSMK2s7us2/UrhOL2KTNBiD0qBg6KnImg==", "license": "MIT", "dependencies": { "@redux-saga/symbols": "^1.2.1" @@ -3125,8 +2690,6 @@ }, "node_modules/@redux-saga/is": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.2.1.tgz", - "integrity": "sha512-x3aWtX3GmQfEvn8dh0ovPbsXgK9JjpiR24wKztpGbZP8JZUWWvUgKrvnWZ/T/4iphOBftyVc9VrIwhAnsM+OFA==", "license": "MIT", "dependencies": { "@redux-saga/symbols": "^1.2.1", @@ -3135,27 +2698,19 @@ }, "node_modules/@redux-saga/symbols": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.2.1.tgz", - "integrity": "sha512-3dh+uDvpBXi7EUp/eO+N7eFM4xKaU4yuGBXc50KnZGzIrR/vlvkTFQsX13zsY8PB6sCFYAgROfPSRUj8331QSA==", "license": "MIT" }, "node_modules/@redux-saga/types": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.3.1.tgz", - "integrity": "sha512-YRCrJdhQLobGIQ8Cj1sta3nn6DrZDTSUnrIYhS2e5V590BmfVDleKoAquclAiKSBKWJwmuXTb+b4BL6rSHnahw==", "license": "MIT" }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", - "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", "dev": true, "license": "MIT" }, "node_modules/@rollup/plugin-node-resolve": { "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", - "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", "license": "MIT", "dependencies": { "@rollup/pluginutils": "^5.0.1", @@ -3178,8 +2733,6 @@ }, "node_modules/@rollup/plugin-node-resolve/node_modules/resolve": { "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "license": "MIT", "dependencies": { "is-core-module": "^2.16.1", @@ -3198,8 +2751,6 @@ }, "node_modules/@rollup/plugin-terser": { "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", - "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", "license": "MIT", "dependencies": { "serialize-javascript": "^6.0.1", @@ -3220,8 +2771,6 @@ }, "node_modules/@rollup/pluginutils": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -3242,14 +2791,10 @@ }, "node_modules/@rollup/pluginutils/node_modules/estree-walker": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, "node_modules/@rollup/pluginutils/node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" @@ -3260,15 +2805,11 @@ }, "node_modules/@standard-schema/spec": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, "license": "MIT" }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", - "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", "license": "Apache-2.0", "dependencies": { "ejs": "^3.1.6", @@ -3279,8 +2820,6 @@ }, "node_modules/@surma/rollup-plugin-off-main-thread/node_modules/magic-string": { "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", "license": "MIT", "dependencies": { "sourcemap-codec": "^1.4.8" @@ -3288,8 +2827,6 @@ }, "node_modules/@testing-library/dom": { "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", "peer": true, @@ -3309,8 +2846,6 @@ }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", - "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", "dev": true, "license": "MIT", "dependencies": { @@ -3329,15 +2864,11 @@ }, "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", "dev": true, "license": "MIT" }, "node_modules/@testing-library/react": { "version": "12.1.5", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.1.5.tgz", - "integrity": "sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg==", "dev": true, "license": "MIT", "dependencies": { @@ -3355,8 +2886,6 @@ }, "node_modules/@testing-library/react-hooks": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-7.0.2.tgz", - "integrity": "sha512-dYxpz8u9m4q1TuzfcUApqi8iFfR6R0FaMbr2hjZJy1uC8z+bO/K4v8Gs9eogGKYQop7QsrBTFkv/BCF7MzD2Cg==", "dev": true, "license": "MIT", "dependencies": { @@ -3385,8 +2914,6 @@ }, "node_modules/@testing-library/react/node_modules/@testing-library/dom": { "version": "8.20.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", - "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==", "dev": true, "license": "MIT", "dependencies": { @@ -3405,8 +2932,6 @@ }, "node_modules/@testing-library/react/node_modules/aria-query": { "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3415,8 +2940,6 @@ }, "node_modules/@testing-library/user-event": { "version": "14.6.1", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", - "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", "dev": true, "license": "MIT", "engines": { @@ -3429,15 +2952,11 @@ }, "node_modules/@types/aria-query": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -3450,8 +2969,6 @@ }, "node_modules/@types/babel__generator": { "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -3460,8 +2977,6 @@ }, "node_modules/@types/babel__template": { "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "devOptional": true, "license": "MIT", "dependencies": { @@ -3471,8 +2986,6 @@ }, "node_modules/@types/babel__traverse": { "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "devOptional": true, "license": "MIT", "dependencies": { @@ -3481,8 +2994,6 @@ }, "node_modules/@types/chai": { "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { @@ -3492,21 +3003,15 @@ }, "node_modules/@types/deep-eql": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.7", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", - "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", "license": "MIT", "peer": true, "dependencies": { @@ -3518,15 +3023,11 @@ }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true, "license": "MIT" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dev": true, "license": "MIT", "dependencies": { @@ -3535,8 +3036,6 @@ }, "node_modules/@types/istanbul-reports": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3545,20 +3044,14 @@ }, "node_modules/@types/json-schema": { "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, "node_modules/@types/minimist": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", "license": "MIT" }, "node_modules/@types/node": { "version": "24.10.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", - "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", "devOptional": true, "license": "MIT", "peer": true, @@ -3568,20 +3061,14 @@ }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "license": "MIT" }, "node_modules/@types/prop-types": { "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", "license": "MIT" }, "node_modules/@types/react": { "version": "17.0.90", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.90.tgz", - "integrity": "sha512-P9beVR/x06U9rCJzSxtENnOr4BrbJ6VrsrDTc+73TtHv9XHhryXKbjGRB+6oooB2r0G/pQkD/S4dHo/7jUfwFw==", "license": "MIT", "peer": true, "dependencies": { @@ -3592,8 +3079,6 @@ }, "node_modules/@types/react-dom": { "version": "17.0.26", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.26.tgz", - "integrity": "sha512-Z+2VcYXJwOqQ79HreLU/1fyQ88eXSSFh6I3JdrEHQIfYSI0kCQpTGvOrbE6jFGGYXKsHuwY9tBa/w5Uo6KzrEg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -3602,8 +3087,6 @@ }, "node_modules/@types/react-redux": { "version": "7.1.34", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz", - "integrity": "sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==", "license": "MIT", "dependencies": { "@types/hoist-non-react-statics": "^3.3.0", @@ -3614,8 +3097,6 @@ }, "node_modules/@types/react-test-renderer": { "version": "19.1.0", - "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz", - "integrity": "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3624,8 +3105,6 @@ }, "node_modules/@types/react-transition-group": { "version": "4.4.12", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", - "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", "license": "MIT", "peerDependencies": { "@types/react": "*" @@ -3633,33 +3112,23 @@ }, "node_modules/@types/react/node_modules/csstype": { "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/@types/resolve": { "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "license": "MIT" }, "node_modules/@types/scheduler": { "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", "license": "MIT" }, "node_modules/@types/semver": { "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", "dev": true, "license": "MIT" }, "node_modules/@types/styled-jsx": { "version": "2.2.9", - "resolved": "https://registry.npmjs.org/@types/styled-jsx/-/styled-jsx-2.2.9.tgz", - "integrity": "sha512-W/iTlIkGEyTBGTEvZCey8EgQlQ5l0DwMqi3iOXlLs2kyBwYTXHKEiU6IZ5EwoRwngL8/dGYuzezSup89ttVHLw==", "license": "MIT", "dependencies": { "@types/react": "*" @@ -3667,27 +3136,19 @@ }, "node_modules/@types/trusted-types": { "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, "node_modules/@types/uuid": { "version": "3.4.13", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.13.tgz", - "integrity": "sha512-pAeZeUbLE4Z9Vi9wsWV2bYPTweEHeJJy0G4pEjOA/FSvy1Ad5U5Km8iDV6TKre1mjBiVNfAdVHKruP8bAh4Q5A==", "license": "MIT" }, "node_modules/@types/whatwg-mimetype": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", - "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", "dev": true, "license": "MIT" }, "node_modules/@types/ws": { "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "dev": true, "license": "MIT", "dependencies": { @@ -3696,8 +3157,6 @@ }, "node_modules/@types/yargs": { "version": "15.0.20", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.20.tgz", - "integrity": "sha512-KIkX+/GgfFitlASYCGoSF+T4XRXhOubJLhkLVtSfsRTe9jWMmuM2g28zQ41BtPTG7TRBb2xHW+LCNVE9QR/vsg==", "dev": true, "license": "MIT", "dependencies": { @@ -3706,15 +3165,11 @@ }, "node_modules/@types/yargs-parser": { "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", "dev": true, "license": "MIT", "dependencies": { @@ -3749,8 +3204,6 @@ }, "node_modules/@typescript-eslint/parser": { "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "license": "BSD-2-Clause", "peer": true, @@ -3779,8 +3232,6 @@ }, "node_modules/@typescript-eslint/scope-manager": { "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", "dev": true, "license": "MIT", "dependencies": { @@ -3797,8 +3248,6 @@ }, "node_modules/@typescript-eslint/type-utils": { "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", "dev": true, "license": "MIT", "dependencies": { @@ -3825,8 +3274,6 @@ }, "node_modules/@typescript-eslint/types": { "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", "dev": true, "license": "MIT", "engines": { @@ -3839,8 +3286,6 @@ }, "node_modules/@typescript-eslint/typescript-estree": { "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3868,8 +3313,6 @@ }, "node_modules/@typescript-eslint/utils": { "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3894,8 +3337,6 @@ }, "node_modules/@typescript-eslint/visitor-keys": { "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", "dev": true, "license": "MIT", "dependencies": { @@ -3912,15 +3353,11 @@ }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true, "license": "ISC" }, "node_modules/@vitejs/plugin-react": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", - "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3940,8 +3377,6 @@ }, "node_modules/@vitest/coverage-v8": { "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.17.tgz", - "integrity": "sha512-/6zU2FLGg0jsd+ePZcwHRy3+WpNTBBhDY56P4JTRqUN/Dp6CvOEa9HrikcQ4KfV2b2kAHUFB4dl1SuocWXSFEw==", "dev": true, "license": "MIT", "dependencies": { @@ -3971,8 +3406,6 @@ }, "node_modules/@vitest/expect": { "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.17.tgz", - "integrity": "sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3989,8 +3422,6 @@ }, "node_modules/@vitest/mocker": { "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.17.tgz", - "integrity": "sha512-+ZtQhLA3lDh1tI2wxe3yMsGzbp7uuJSWBM1iTIKCbppWTSBN09PUC+L+fyNlQApQoR+Ps8twt2pbSSXg2fQVEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4016,8 +3447,6 @@ }, "node_modules/@vitest/pretty-format": { "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.17.tgz", - "integrity": "sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw==", "dev": true, "license": "MIT", "dependencies": { @@ -4029,8 +3458,6 @@ }, "node_modules/@vitest/runner": { "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.17.tgz", - "integrity": "sha512-JmuQyf8aMWoo/LmNFppdpkfRVHJcsgzkbCA+/Bk7VfNH7RE6Ut2qxegeyx2j3ojtJtKIbIGy3h+KxGfYfk28YQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4043,8 +3470,6 @@ }, "node_modules/@vitest/snapshot": { "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.17.tgz", - "integrity": "sha512-npPelD7oyL+YQM2gbIYvlavlMVWUfNNGZPcu0aEUQXt7FXTuqhmgiYupPnAanhKvyP6Srs2pIbWo30K0RbDtRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4058,8 +3483,6 @@ }, "node_modules/@vitest/spy": { "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.17.tgz", - "integrity": "sha512-I1bQo8QaP6tZlTomQNWKJE6ym4SHf3oLS7ceNjozxxgzavRAgZDc06T7kD8gb9bXKEgcLNt00Z+kZO6KaJ62Ew==", "dev": true, "license": "MIT", "funding": { @@ -4068,8 +3491,6 @@ }, "node_modules/@vitest/utils": { "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.17.tgz", - "integrity": "sha512-RG6iy+IzQpa9SB8HAFHJ9Y+pTzI+h8553MrciN9eC6TFBErqrQaTas4vG+MVj8S4uKk8uTT2p0vgZPnTdxd96w==", "dev": true, "license": "MIT", "dependencies": { @@ -4082,8 +3503,6 @@ }, "node_modules/acorn": { "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "peer": true, "bin": { @@ -4095,8 +3514,6 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -4105,8 +3522,6 @@ }, "node_modules/agent-base": { "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", "engines": { @@ -4115,8 +3530,6 @@ }, "node_modules/ajv": { "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -4131,8 +3544,6 @@ }, "node_modules/ansi-align": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", "license": "ISC", "dependencies": { "string-width": "^4.1.0" @@ -4140,8 +3551,6 @@ }, "node_modules/ansi-escapes": { "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "license": "MIT", "dependencies": { "type-fest": "^0.21.3" @@ -4155,8 +3564,6 @@ }, "node_modules/ansi-escapes/node_modules/type-fest": { "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" @@ -4167,8 +3574,6 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" @@ -4176,8 +3581,6 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -4191,8 +3594,6 @@ }, "node_modules/anymatch": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -4204,15 +3605,11 @@ }, "node_modules/argparse": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4221,8 +3618,6 @@ }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -4237,8 +3632,6 @@ }, "node_modules/array-includes": { "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4260,8 +3653,6 @@ }, "node_modules/array-union": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, "license": "MIT", "engines": { @@ -4270,8 +3661,6 @@ }, "node_modules/array.prototype.findlast": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4291,8 +3680,6 @@ }, "node_modules/array.prototype.flat": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, "license": "MIT", "dependencies": { @@ -4310,8 +3697,6 @@ }, "node_modules/array.prototype.flatmap": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, "license": "MIT", "dependencies": { @@ -4329,8 +3714,6 @@ }, "node_modules/array.prototype.tosorted": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, "license": "MIT", "dependencies": { @@ -4346,8 +3729,6 @@ }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", @@ -4367,8 +3748,6 @@ }, "node_modules/arrify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4376,8 +3755,6 @@ }, "node_modules/assertion-error": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", "engines": { @@ -4386,15 +3763,11 @@ }, "node_modules/ast-types-flow": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", - "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", "dev": true, "license": "MIT" }, "node_modules/ast-v8-to-istanbul": { "version": "0.3.10", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.10.tgz", - "integrity": "sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4405,21 +3778,15 @@ }, "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", "dev": true, "license": "MIT" }, "node_modules/async": { "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, "node_modules/async-function": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -4427,8 +3794,6 @@ }, "node_modules/at-least-node": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "license": "ISC", "engines": { "node": ">= 4.0.0" @@ -4436,8 +3801,6 @@ }, "node_modules/atomically": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.0.tgz", - "integrity": "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q==", "license": "MIT", "dependencies": { "stubborn-fs": "^2.0.0", @@ -4446,8 +3809,6 @@ }, "node_modules/attr-accept": { "version": "2.2.5", - "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", - "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", "license": "MIT", "engines": { "node": ">=4" @@ -4455,8 +3816,6 @@ }, "node_modules/autosuggest-highlight": { "version": "3.3.4", - "resolved": "https://registry.npmjs.org/autosuggest-highlight/-/autosuggest-highlight-3.3.4.tgz", - "integrity": "sha512-j6RETBD2xYnrVcoV1S5R4t3WxOlWZKyDQjkwnggDPSjF5L4jV98ZltBpvPvbkM1HtoSe5o+bNrTHyjPbieGeYA==", "license": "MIT", "dependencies": { "remove-accents": "^0.4.2" @@ -4464,8 +3823,6 @@ }, "node_modules/available-typed-arrays": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" @@ -4479,8 +3836,6 @@ }, "node_modules/axe-core": { "version": "4.11.1", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", - "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", "dev": true, "license": "MPL-2.0", "engines": { @@ -4489,8 +3844,6 @@ }, "node_modules/axobject-query": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4499,8 +3852,6 @@ }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.14", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", - "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", "license": "MIT", "dependencies": { "@babel/compat-data": "^7.27.7", @@ -4513,8 +3864,6 @@ }, "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4522,8 +3871,6 @@ }, "node_modules/babel-plugin-polyfill-corejs3": { "version": "0.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", - "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5", @@ -4535,8 +3882,6 @@ }, "node_modules/babel-plugin-polyfill-regenerator": { "version": "0.6.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", - "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5" @@ -4547,8 +3892,6 @@ }, "node_modules/babel-runtime": { "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", "license": "MIT", "dependencies": { "core-js": "^2.4.0", @@ -4557,14 +3900,10 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "funding": [ { "type": "github", @@ -4583,8 +3922,6 @@ }, "node_modules/baseline-browser-mapping": { "version": "2.9.15", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz", - "integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -4592,8 +3929,6 @@ }, "node_modules/binary-extensions": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "license": "MIT", "engines": { "node": ">=8" @@ -4604,8 +3939,6 @@ }, "node_modules/bl": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -4615,14 +3948,10 @@ }, "node_modules/blueimp-md5": { "version": "2.19.0", - "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", - "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", "license": "MIT" }, "node_modules/boxen": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", - "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", "license": "MIT", "dependencies": { "ansi-align": "^3.0.1", @@ -4643,8 +3972,6 @@ }, "node_modules/boxen/node_modules/ansi-regex": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { "node": ">=12" @@ -4655,8 +3982,6 @@ }, "node_modules/boxen/node_modules/camelcase": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", - "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", "license": "MIT", "engines": { "node": ">=16" @@ -4667,8 +3992,6 @@ }, "node_modules/boxen/node_modules/chalk": { "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -4679,14 +4002,10 @@ }, "node_modules/boxen/node_modules/emoji-regex": { "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "license": "MIT" }, "node_modules/boxen/node_modules/string-width": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", @@ -4702,8 +4021,6 @@ }, "node_modules/boxen/node_modules/strip-ansi": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -4717,8 +4034,6 @@ }, "node_modules/boxen/node_modules/type-fest": { "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" @@ -4729,8 +4044,6 @@ }, "node_modules/brace-expansion": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -4738,8 +4051,6 @@ }, "node_modules/braces": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -4750,8 +4061,6 @@ }, "node_modules/browserslist": { "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "funding": [ { "type": "opencollective", @@ -4784,8 +4093,6 @@ }, "node_modules/buffer": { "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "funding": [ { "type": "github", @@ -4808,14 +4115,10 @@ }, "node_modules/buffer-from": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, "node_modules/call-bind": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", @@ -4832,8 +4135,6 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4845,8 +4146,6 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -4861,14 +4160,10 @@ }, "node_modules/call-me-maybe": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", - "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", "license": "MIT" }, "node_modules/callsites": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "engines": { @@ -4877,8 +4172,6 @@ }, "node_modules/camelcase": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "license": "MIT", "engines": { "node": ">=6" @@ -4886,8 +4179,6 @@ }, "node_modules/camelcase-keys": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", "license": "MIT", "dependencies": { "camelcase": "^5.3.1", @@ -4903,8 +4194,6 @@ }, "node_modules/caniuse-lite": { "version": "1.0.30001765", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz", - "integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==", "funding": [ { "type": "opencollective", @@ -4923,8 +4212,6 @@ }, "node_modules/chai": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", "engines": { @@ -4933,8 +4220,6 @@ }, "node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -4949,14 +4234,10 @@ }, "node_modules/chardet": { "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "license": "MIT" }, "node_modules/chokidar": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -4979,8 +4260,6 @@ }, "node_modules/chokidar/node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -4989,10 +4268,14 @@ "node": ">= 6" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/cli-boxes": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", - "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", "license": "MIT", "engines": { "node": ">=10" @@ -5003,8 +4286,6 @@ }, "node_modules/cli-cursor": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "license": "MIT", "dependencies": { "restore-cursor": "^3.1.0" @@ -5015,8 +4296,6 @@ }, "node_modules/cli-spinners": { "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "license": "MIT", "engines": { "node": ">=6" @@ -5027,8 +4306,6 @@ }, "node_modules/cli-width": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", "license": "ISC", "engines": { "node": ">= 10" @@ -5036,8 +4313,6 @@ }, "node_modules/clone": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", "license": "MIT", "engines": { "node": ">=0.8" @@ -5045,8 +4320,6 @@ }, "node_modules/clsx": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", "engines": { "node": ">=6" @@ -5054,8 +4327,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -5066,20 +4337,14 @@ }, "node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/commander": { "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, "node_modules/common-tags": { "version": "1.8.2", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", - "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", "license": "MIT", "engines": { "node": ">=4.0.0" @@ -5087,21 +4352,15 @@ }, "node_modules/compute-scroll-into-view": { "version": "1.0.20", - "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", - "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==", "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, "node_modules/config-chain": { "version": "1.1.13", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", - "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", "license": "MIT", "dependencies": { "ini": "^1.3.4", @@ -5110,14 +4369,10 @@ }, "node_modules/config-chain/node_modules/ini": { "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, "node_modules/configstore": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-7.1.0.tgz", - "integrity": "sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg==", "license": "BSD-2-Clause", "dependencies": { "atomically": "^2.0.3", @@ -5134,8 +4389,6 @@ }, "node_modules/connected-react-router": { "version": "6.9.3", - "resolved": "https://registry.npmjs.org/connected-react-router/-/connected-react-router-6.9.3.tgz", - "integrity": "sha512-4ThxysOiv/R2Dc4Cke1eJwjKwH1Y51VDwlOrOfs1LjpdYOVvCNjNkZDayo7+sx42EeGJPQUNchWkjAIJdXGIOQ==", "license": "MIT", "peer": true, "dependencies": { @@ -5156,22 +4409,15 @@ }, "node_modules/convert-source-map": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, "node_modules/core-js": { "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", "hasInstallScript": true, "license": "MIT" }, "node_modules/core-js-compat": { "version": "3.47.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz", - "integrity": "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==", "license": "MIT", "dependencies": { "browserslist": "^4.28.0" @@ -5183,8 +4429,6 @@ }, "node_modules/core-js-pure": { "version": "3.47.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.47.0.tgz", - "integrity": "sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5195,8 +4439,6 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -5209,8 +4451,6 @@ }, "node_modules/crypto-random-string": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "license": "MIT", "engines": { "node": ">=8" @@ -5218,14 +4458,10 @@ }, "node_modules/css-mediaquery": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", - "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==", "license": "BSD" }, "node_modules/css-vendor": { "version": "2.0.8", - "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", - "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.3", @@ -5234,15 +4470,11 @@ }, "node_modules/css.escape": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", "dev": true, "license": "MIT" }, "node_modules/cssstyle": { "version": "4.6.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", - "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", "dev": true, "license": "MIT", "dependencies": { @@ -5255,21 +4487,15 @@ }, "node_modules/csstype": { "version": "2.6.21", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", - "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", "license": "MIT" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/data-urls": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, "license": "MIT", "dependencies": { @@ -5282,8 +4508,6 @@ }, "node_modules/data-urls/node_modules/whatwg-mimetype": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "license": "MIT", "engines": { @@ -5292,8 +4516,6 @@ }, "node_modules/data-view-buffer": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -5309,8 +4531,6 @@ }, "node_modules/data-view-byte-length": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -5326,8 +4546,6 @@ }, "node_modules/data-view-byte-offset": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -5343,14 +4561,10 @@ }, "node_modules/date-fns": { "version": "1.30.1", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", - "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -5366,8 +4580,6 @@ }, "node_modules/decamelize": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5375,8 +4587,6 @@ }, "node_modules/decamelize-keys": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", - "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", "license": "MIT", "dependencies": { "decamelize": "^1.1.0", @@ -5391,8 +4601,6 @@ }, "node_modules/decamelize-keys/node_modules/map-obj": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5400,15 +4608,11 @@ }, "node_modules/decimal.js": { "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "dev": true, "license": "MIT" }, "node_modules/decode-uri-component": { "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "license": "MIT", "engines": { "node": ">=0.10" @@ -5416,8 +4620,6 @@ }, "node_modules/deep-equal": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", - "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", "dev": true, "license": "MIT", "dependencies": { @@ -5449,15 +4651,11 @@ }, "node_modules/deep-equal/node_modules/isarray": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, "license": "MIT" }, "node_modules/deep-extend": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "license": "MIT", "engines": { "node": ">=4.0.0" @@ -5465,15 +4663,11 @@ }, "node_modules/deep-is": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, "node_modules/deepmerge": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5481,8 +4675,6 @@ }, "node_modules/defaults": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", "license": "MIT", "dependencies": { "clone": "^1.0.2" @@ -5493,8 +4685,6 @@ }, "node_modules/define-data-property": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -5510,8 +4700,6 @@ }, "node_modules/define-properties": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", @@ -5527,8 +4715,6 @@ }, "node_modules/dequal": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, "license": "MIT", "engines": { @@ -5537,8 +4723,6 @@ }, "node_modules/dir-glob": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, "license": "MIT", "dependencies": { @@ -5550,8 +4734,6 @@ }, "node_modules/dnd-core": { "version": "14.0.1", - "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-14.0.1.tgz", - "integrity": "sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==", "license": "MIT", "dependencies": { "@react-dnd/asap": "^4.0.0", @@ -5561,8 +4743,6 @@ }, "node_modules/doctrine": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5574,15 +4754,17 @@ }, "node_modules/dom-accessibility-api": { "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, "license": "MIT" }, + "node_modules/dom-align": { + "version": "1.12.4", + "resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.12.4.tgz", + "integrity": "sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==", + "license": "MIT" + }, "node_modules/dom-helpers": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.7", @@ -5591,14 +4773,10 @@ }, "node_modules/dom-helpers/node_modules/csstype": { "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/dompurify": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz", - "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==", "license": "(MPL-2.0 OR Apache-2.0)", "engines": { "node": ">=20" @@ -5609,8 +4787,6 @@ }, "node_modules/dot-prop": { "version": "9.0.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", - "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", "license": "MIT", "dependencies": { "type-fest": "^4.18.2" @@ -5624,8 +4800,6 @@ }, "node_modules/dot-prop/node_modules/type-fest": { "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" @@ -5634,10 +4808,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/downloadjs": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/downloadjs/-/downloadjs-1.4.7.tgz", + "integrity": "sha512-LN1gO7+u9xjU5oEScGFKvXhYf7Y/empUIIEAGBs1LzUq/rg5duiDrkuH5A2lQGd5jfMOb9X9usDa2oVXwJ0U/Q==", + "license": "MIT" + }, "node_modules/downshift": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/downshift/-/downshift-3.2.7.tgz", - "integrity": "sha512-mbUO9ZFhMGtksIeVWRFFjNOPN237VsUqZSEYi0VS0Wj38XNLzpgOBTUcUjdjFeB8KVgmrcRa6GGFkTbACpG6FA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.1.2", @@ -5651,14 +4829,10 @@ }, "node_modules/downshift/node_modules/react-is": { "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, "node_modules/dunder-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -5671,14 +4845,10 @@ }, "node_modules/eastasianwidth": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, "node_modules/ejs": { "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "license": "Apache-2.0", "dependencies": { "jake": "^10.8.5" @@ -5692,20 +4862,14 @@ }, "node_modules/electron-to-chromium": { "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", "license": "ISC" }, "node_modules/emoji-regex": { "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, "node_modules/entities": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5717,8 +4881,6 @@ }, "node_modules/error-ex": { "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -5726,8 +4888,6 @@ }, "node_modules/es-abstract": { "version": "1.24.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", - "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.2", @@ -5794,8 +4954,6 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -5803,8 +4961,6 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -5812,8 +4968,6 @@ }, "node_modules/es-get-iterator": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", "dev": true, "license": "MIT", "dependencies": { @@ -5833,15 +4987,11 @@ }, "node_modules/es-get-iterator/node_modules/isarray": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, "license": "MIT" }, "node_modules/es-iterator-helpers": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", - "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", "dev": true, "license": "MIT", "dependencies": { @@ -5868,15 +5018,11 @@ }, "node_modules/es-module-lexer": { "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -5887,8 +5033,6 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5902,8 +5046,6 @@ }, "node_modules/es-shim-unscopables": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, "license": "MIT", "dependencies": { @@ -5915,8 +5057,6 @@ }, "node_modules/es-to-primitive": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "license": "MIT", "dependencies": { "is-callable": "^1.2.7", @@ -5932,8 +5072,6 @@ }, "node_modules/esbuild": { "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5974,8 +5112,6 @@ }, "node_modules/escalade": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", "engines": { "node": ">=6" @@ -5983,8 +5119,6 @@ }, "node_modules/escape-goat": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", - "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", "license": "MIT", "engines": { "node": ">=12" @@ -5995,8 +5129,6 @@ }, "node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { @@ -6008,9 +5140,6 @@ }, "node_modules/eslint": { "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", "peer": true, @@ -6066,8 +5195,6 @@ }, "node_modules/eslint-config-prettier": { "version": "10.1.8", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", - "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", "bin": { @@ -6082,8 +5209,6 @@ }, "node_modules/eslint-plugin-jsx-a11y": { "version": "6.10.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", - "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -6112,8 +5237,6 @@ }, "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -6122,8 +5245,6 @@ }, "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -6133,8 +5254,6 @@ }, "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -6146,8 +5265,6 @@ }, "node_modules/eslint-plugin-react": { "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, "license": "MIT", "dependencies": { @@ -6179,8 +5296,6 @@ }, "node_modules/eslint-plugin-react-hooks": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", "dev": true, "license": "MIT", "engines": { @@ -6192,8 +5307,6 @@ }, "node_modules/eslint-plugin-react-refresh": { "version": "0.4.26", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", - "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -6202,8 +5315,6 @@ }, "node_modules/eslint-plugin-react/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -6213,8 +5324,6 @@ }, "node_modules/eslint-plugin-react/node_modules/doctrine": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -6226,8 +5335,6 @@ }, "node_modules/eslint-plugin-react/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -6239,8 +5346,6 @@ }, "node_modules/eslint-plugin-react/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -6249,8 +5354,6 @@ }, "node_modules/eslint-scope": { "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -6266,8 +5369,6 @@ }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { @@ -6279,8 +5380,6 @@ }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -6290,8 +5389,6 @@ }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -6303,8 +5400,6 @@ }, "node_modules/espree": { "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -6321,8 +5416,6 @@ }, "node_modules/esprima": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -6334,8 +5427,6 @@ }, "node_modules/esquery": { "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6347,8 +5438,6 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -6360,8 +5449,6 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -6370,8 +5457,6 @@ }, "node_modules/estree-walker": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { @@ -6380,8 +5465,6 @@ }, "node_modules/esutils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -6389,20 +5472,14 @@ }, "node_modules/eventemitter3": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", - "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", "license": "MIT" }, "node_modules/exenv": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", - "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==", "license": "BSD-3-Clause" }, "node_modules/expect-type": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -6411,8 +5488,6 @@ }, "node_modules/external-editor": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", "license": "MIT", "dependencies": { "chardet": "^0.7.0", @@ -6425,8 +5500,6 @@ }, "node_modules/external-editor/node_modules/iconv-lite": { "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -6437,14 +5510,10 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -6460,8 +5529,6 @@ }, "node_modules/fast-glob/node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "license": "ISC", "dependencies": { @@ -6473,21 +5540,15 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, "node_modules/fast-uri": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "funding": [ { "type": "github", @@ -6502,8 +5563,6 @@ }, "node_modules/fastq": { "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -6512,8 +5571,6 @@ }, "node_modules/figures": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "license": "MIT", "dependencies": { "escape-string-regexp": "^1.0.5" @@ -6527,8 +5584,6 @@ }, "node_modules/figures/node_modules/escape-string-regexp": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "license": "MIT", "engines": { "node": ">=0.8.0" @@ -6536,8 +5591,6 @@ }, "node_modules/file-entry-cache": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "license": "MIT", "dependencies": { @@ -6549,8 +5602,6 @@ }, "node_modules/file-selector": { "version": "0.1.19", - "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.19.tgz", - "integrity": "sha512-kCWw3+Aai8Uox+5tHCNgMFaUdgidxvMnLWO6fM5sZ0hA2wlHP5/DHGF0ECe84BiB95qdJbKNEJhWKVDvMN+JDQ==", "license": "MIT", "dependencies": { "tslib": "^2.0.1" @@ -6561,8 +5612,6 @@ }, "node_modules/filelist": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", "license": "Apache-2.0", "dependencies": { "minimatch": "^5.0.1" @@ -6570,8 +5619,6 @@ }, "node_modules/filelist/node_modules/minimatch": { "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -6582,8 +5629,6 @@ }, "node_modules/fill-range": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -6594,8 +5639,6 @@ }, "node_modules/final-form": { "version": "4.20.10", - "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.10.tgz", - "integrity": "sha512-TL48Pi1oNHeMOHrKv1bCJUrWZDcD3DIG6AGYVNOnyZPr7Bd/pStN0pL+lfzF5BNoj/FclaoiaLenk4XUIFVYng==", "license": "MIT", "peer": true, "dependencies": { @@ -6611,8 +5654,6 @@ }, "node_modules/final-form-arrays": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/final-form-arrays/-/final-form-arrays-3.1.0.tgz", - "integrity": "sha512-TWBvun+AopgBLw9zfTFHBllnKMVNEwCEyDawphPuBGGqNsuhGzhT7yewHys64KFFwzIs6KEteGLpKOwvTQEscQ==", "license": "MIT", "peer": true, "peerDependencies": { @@ -6621,8 +5662,6 @@ }, "node_modules/find-up": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -6638,8 +5677,6 @@ }, "node_modules/flat-cache": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "license": "MIT", "dependencies": { @@ -6653,15 +5690,11 @@ }, "node_modules/flatted": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, "node_modules/for-each": { "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "license": "MIT", "dependencies": { "is-callable": "^1.2.7" @@ -6675,8 +5708,6 @@ }, "node_modules/foreground-child": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -6691,8 +5722,6 @@ }, "node_modules/foreground-child/node_modules/signal-exit": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "license": "ISC", "engines": { "node": ">=14" @@ -6703,8 +5732,6 @@ }, "node_modules/fs-extra": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "license": "MIT", "dependencies": { "at-least-node": "^1.0.0", @@ -6718,16 +5745,11 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true, "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -6739,8 +5761,6 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6748,8 +5768,6 @@ }, "node_modules/function.prototype.name": { "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -6768,8 +5786,6 @@ }, "node_modules/functions-have-names": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6777,8 +5793,6 @@ }, "node_modules/generator-function": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -6786,8 +5800,6 @@ }, "node_modules/gensync": { "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -6795,8 +5807,6 @@ }, "node_modules/get-east-asian-width": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", "license": "MIT", "engines": { "node": ">=18" @@ -6807,8 +5817,6 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -6831,20 +5839,14 @@ }, "node_modules/get-node-dimensions": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-node-dimensions/-/get-node-dimensions-1.2.1.tgz", - "integrity": "sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ==", "license": "MIT" }, "node_modules/get-own-enumerable-property-symbols": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", - "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", "license": "ISC" }, "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -6856,8 +5858,6 @@ }, "node_modules/get-symbol-description": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -6873,9 +5873,6 @@ }, "node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", "dependencies": { @@ -6895,8 +5892,6 @@ }, "node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { @@ -6908,8 +5903,6 @@ }, "node_modules/glob/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -6919,8 +5912,6 @@ }, "node_modules/glob/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -6932,8 +5923,6 @@ }, "node_modules/global-directory": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", - "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", "license": "MIT", "dependencies": { "ini": "4.1.1" @@ -6947,8 +5936,6 @@ }, "node_modules/globals": { "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6963,8 +5950,6 @@ }, "node_modules/globalthis": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "license": "MIT", "dependencies": { "define-properties": "^1.2.1", @@ -6979,8 +5964,6 @@ }, "node_modules/globby": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, "license": "MIT", "dependencies": { @@ -7000,8 +5983,6 @@ }, "node_modules/gopd": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -7012,21 +5993,15 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true, "license": "MIT" }, "node_modules/happy-dom": { "version": "20.3.3", - "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.3.3.tgz", - "integrity": "sha512-hM9gltmtQLfmWPqoPreUtRdP3nZCSzQEw7l/JC+up5CxquDykhYFKzIzoFFeVev3AGFEULNvsbE8fpZPgxUYEQ==", "dev": true, "license": "MIT", "peer": true, @@ -7044,8 +6019,6 @@ }, "node_modules/hard-rejection": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", "license": "MIT", "engines": { "node": ">=6" @@ -7053,8 +6026,6 @@ }, "node_modules/has-bigints": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -7065,8 +6036,6 @@ }, "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "license": "MIT", "engines": { "node": ">=8" @@ -7074,8 +6043,6 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -7086,8 +6053,6 @@ }, "node_modules/has-proto": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.0" @@ -7101,8 +6066,6 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -7113,8 +6076,6 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -7128,8 +6089,6 @@ }, "node_modules/hasown": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -7140,8 +6099,6 @@ }, "node_modules/history": { "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", "license": "MIT", "peer": true, "dependencies": { @@ -7155,8 +6112,6 @@ }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", "license": "BSD-3-Clause", "dependencies": { "react-is": "^16.7.0" @@ -7164,20 +6119,14 @@ }, "node_modules/hoist-non-react-statics/node_modules/react-is": { "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, "node_modules/hosted-git-info": { "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "license": "ISC" }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7189,15 +6138,11 @@ }, "node_modules/html-escaper": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, "license": "MIT" }, "node_modules/http-proxy-agent": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", "dependencies": { @@ -7210,8 +6155,6 @@ }, "node_modules/https-proxy-agent": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { @@ -7224,14 +6167,10 @@ }, "node_modules/hyphenate-style-name": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", - "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", "license": "BSD-3-Clause" }, "node_modules/iconv-lite": { "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -7243,14 +6182,10 @@ }, "node_modules/idb": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", - "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", "license": "ISC" }, "node_modules/ieee754": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "funding": [ { "type": "github", @@ -7269,8 +6204,6 @@ }, "node_modules/ignore": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -7279,15 +6212,11 @@ }, "node_modules/immutable": { "version": "4.3.8", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.8.tgz", - "integrity": "sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==", "license": "MIT", "optional": true }, "node_modules/import-fresh": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7303,8 +6232,6 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { @@ -7313,8 +6240,6 @@ }, "node_modules/indent-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "license": "MIT", "engines": { "node": ">=8" @@ -7322,8 +6247,6 @@ }, "node_modules/inflection": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/inflection/-/inflection-3.0.2.tgz", - "integrity": "sha512-+Bg3+kg+J6JUWn8J6bzFmOWkTQ6L/NHfDRSYU+EVvuKHDxUDHAXgqixHfVlzuBQaPOTac8hn43aPhMNk6rMe3g==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -7331,9 +6254,6 @@ }, "node_modules/inflight": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, "license": "ISC", "dependencies": { @@ -7343,14 +6263,10 @@ }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, "node_modules/ini": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -7358,8 +6274,6 @@ }, "node_modules/inquirer": { "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", "license": "MIT", "dependencies": { "ansi-escapes": "^4.2.1", @@ -7382,8 +6296,6 @@ }, "node_modules/internal-slot": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7396,8 +6308,6 @@ }, "node_modules/is-arguments": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", "dev": true, "license": "MIT", "dependencies": { @@ -7413,8 +6323,6 @@ }, "node_modules/is-array-buffer": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -7430,14 +6338,10 @@ }, "node_modules/is-arrayish": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, "node_modules/is-async-function": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "license": "MIT", "dependencies": { "async-function": "^1.0.0", @@ -7455,8 +6359,6 @@ }, "node_modules/is-bigint": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "license": "MIT", "dependencies": { "has-bigints": "^1.0.2" @@ -7470,8 +6372,6 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -7482,8 +6382,6 @@ }, "node_modules/is-boolean-object": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -7498,8 +6396,6 @@ }, "node_modules/is-callable": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -7510,8 +6406,6 @@ }, "node_modules/is-core-module": { "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -7525,8 +6419,6 @@ }, "node_modules/is-data-view": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -7542,8 +6434,6 @@ }, "node_modules/is-date-object": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -7558,8 +6448,6 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7567,8 +6455,6 @@ }, "node_modules/is-finalizationregistry": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -7582,8 +6468,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", "engines": { "node": ">=8" @@ -7591,8 +6475,6 @@ }, "node_modules/is-generator-function": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "license": "MIT", "dependencies": { "call-bound": "^1.0.4", @@ -7610,8 +6492,6 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -7622,14 +6502,10 @@ }, "node_modules/is-in-browser": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", - "integrity": "sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==", "license": "MIT" }, "node_modules/is-in-ci": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz", - "integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==", "license": "MIT", "bin": { "is-in-ci": "cli.js" @@ -7643,8 +6519,6 @@ }, "node_modules/is-installed-globally": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", - "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", "license": "MIT", "dependencies": { "global-directory": "^4.0.1", @@ -7659,8 +6533,6 @@ }, "node_modules/is-installed-globally/node_modules/is-path-inside": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", - "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", "license": "MIT", "engines": { "node": ">=12" @@ -7671,8 +6543,6 @@ }, "node_modules/is-interactive": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", "license": "MIT", "engines": { "node": ">=8" @@ -7680,8 +6550,6 @@ }, "node_modules/is-map": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -7690,16 +6558,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-mobile": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-2.2.2.tgz", + "integrity": "sha512-wW/SXnYJkTjs++tVK5b6kVITZpAZPtUrt9SF80vvxGiF/Oywal+COk1jlRkiVq15RFNEQKQY31TkV24/1T5cVg==", + "license": "MIT" + }, "node_modules/is-module": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", "license": "MIT" }, "node_modules/is-negative-zero": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -7710,8 +6580,6 @@ }, "node_modules/is-npm": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.1.0.tgz", - "integrity": "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==", "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" @@ -7722,8 +6590,6 @@ }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "license": "MIT", "engines": { "node": ">=0.12.0" @@ -7731,8 +6597,6 @@ }, "node_modules/is-number-object": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -7747,8 +6611,6 @@ }, "node_modules/is-obj": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7756,8 +6618,6 @@ }, "node_modules/is-path-inside": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, "license": "MIT", "engines": { @@ -7766,8 +6626,6 @@ }, "node_modules/is-plain-obj": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7775,15 +6633,11 @@ }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true, "license": "MIT" }, "node_modules/is-regex": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -7800,8 +6654,6 @@ }, "node_modules/is-regexp": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7809,8 +6661,6 @@ }, "node_modules/is-set": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -7821,8 +6671,6 @@ }, "node_modules/is-shared-array-buffer": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -7836,8 +6684,6 @@ }, "node_modules/is-stream": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "license": "MIT", "engines": { "node": ">=8" @@ -7848,8 +6694,6 @@ }, "node_modules/is-string": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -7864,8 +6708,6 @@ }, "node_modules/is-symbol": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -7881,8 +6723,6 @@ }, "node_modules/is-typed-array": { "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" @@ -7896,8 +6736,6 @@ }, "node_modules/is-unicode-supported": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "license": "MIT", "engines": { "node": ">=10" @@ -7908,8 +6746,6 @@ }, "node_modules/is-weakmap": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -7920,8 +6756,6 @@ }, "node_modules/is-weakref": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -7935,8 +6769,6 @@ }, "node_modules/is-weakset": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -7951,20 +6783,14 @@ }, "node_modules/isarray": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -7973,8 +6799,6 @@ }, "node_modules/istanbul-lib-report": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -7988,8 +6812,6 @@ }, "node_modules/istanbul-reports": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -8002,8 +6824,6 @@ }, "node_modules/iterator.prototype": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, "license": "MIT", "dependencies": { @@ -8020,8 +6840,6 @@ }, "node_modules/jackspeak": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", - "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -8035,8 +6853,6 @@ }, "node_modules/jake": { "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", - "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", "license": "Apache-2.0", "dependencies": { "async": "^3.2.6", @@ -8052,14 +6868,10 @@ }, "node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -8071,8 +6883,6 @@ }, "node_modules/jsdom": { "version": "26.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", - "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", "dependencies": { @@ -8111,8 +6921,6 @@ }, "node_modules/jsdom/node_modules/whatwg-mimetype": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "license": "MIT", "engines": { @@ -8121,8 +6929,6 @@ }, "node_modules/jsesc": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -8133,28 +6939,19 @@ }, "node_modules/json-buffer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "license": "MIT" }, "node_modules/json-schema": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "license": "(AFL-2.1 OR BSD-3-Clause)" }, "node_modules/json-schema-ref-parser": { "version": "7.1.3", - "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-7.1.3.tgz", - "integrity": "sha512-/Lmyl0PW27dOmCO03PI339+1gs4Z2PlqIyUgzIOtoRp08zkkMCB30TRbdppbPO7WWzZX0uT98HqkDiZSujkmbA==", - "deprecated": "Please switch to @apidevtools/json-schema-ref-parser", "license": "MIT", "dependencies": { "call-me-maybe": "^1.0.1", @@ -8164,8 +6961,6 @@ }, "node_modules/json-schema-ref-parser/node_modules/argparse": { "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" @@ -8173,8 +6968,6 @@ }, "node_modules/json-schema-ref-parser/node_modules/js-yaml": { "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -8186,21 +6979,15 @@ }, "node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -8211,8 +6998,6 @@ }, "node_modules/jsonexport": { "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsonexport/-/jsonexport-2.5.2.tgz", - "integrity": "sha512-4joNLCxxUAmS22GN3GA5os/MYFnq8oqXOKvoCymmcT0MPz/QPZ5eA+Fh5sIPxUji45RKq8DdQ1yoKq91p4E9VA==", "license": "Apache-2.0", "bin": { "jsonexport": "bin/jsonexport.js" @@ -8220,8 +7005,6 @@ }, "node_modules/jsonfile": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -8232,8 +7015,6 @@ }, "node_modules/jsonpointer": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", - "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8241,8 +7022,6 @@ }, "node_modules/jss": { "version": "10.10.0", - "resolved": "https://registry.npmjs.org/jss/-/jss-10.10.0.tgz", - "integrity": "sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", @@ -8257,8 +7036,6 @@ }, "node_modules/jss-plugin-camel-case": { "version": "10.10.0", - "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.10.0.tgz", - "integrity": "sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", @@ -8268,8 +7045,6 @@ }, "node_modules/jss-plugin-default-unit": { "version": "10.10.0", - "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.10.0.tgz", - "integrity": "sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", @@ -8278,8 +7053,6 @@ }, "node_modules/jss-plugin-global": { "version": "10.10.0", - "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.10.0.tgz", - "integrity": "sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", @@ -8288,8 +7061,6 @@ }, "node_modules/jss-plugin-nested": { "version": "10.10.0", - "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.10.0.tgz", - "integrity": "sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", @@ -8299,8 +7070,6 @@ }, "node_modules/jss-plugin-props-sort": { "version": "10.10.0", - "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.10.0.tgz", - "integrity": "sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", @@ -8309,8 +7078,6 @@ }, "node_modules/jss-plugin-rule-value-function": { "version": "10.10.0", - "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.10.0.tgz", - "integrity": "sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", @@ -8320,8 +7087,6 @@ }, "node_modules/jss-plugin-vendor-prefixer": { "version": "10.10.0", - "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.10.0.tgz", - "integrity": "sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", @@ -8331,14 +7096,10 @@ }, "node_modules/jss/node_modules/csstype": { "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/jsx-ast-utils": { "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8353,8 +7114,6 @@ }, "node_modules/jwt-decode": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", - "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", "license": "MIT", "engines": { "node": ">=18" @@ -8362,8 +7121,6 @@ }, "node_modules/keyv": { "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { @@ -8372,8 +7129,6 @@ }, "node_modules/kind-of": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8381,8 +7136,6 @@ }, "node_modules/ky": { "version": "1.14.2", - "resolved": "https://registry.npmjs.org/ky/-/ky-1.14.2.tgz", - "integrity": "sha512-q3RBbsO5A5zrPhB6CaCS8ZUv+NWCXv6JJT4Em0i264G9W0fdPB8YRfnnEi7Dm7X7omAkBIPojzYJ2D1oHTHqug==", "license": "MIT", "engines": { "node": ">=18" @@ -8393,15 +7146,11 @@ }, "node_modules/language-subtag-registry": { "version": "0.3.23", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", "dev": true, "license": "CC0-1.0" }, "node_modules/language-tags": { "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", "dev": true, "license": "MIT", "dependencies": { @@ -8413,8 +7162,6 @@ }, "node_modules/latest-version": { "version": "9.0.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-9.0.0.tgz", - "integrity": "sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==", "license": "MIT", "dependencies": { "package-json": "^10.0.0" @@ -8428,8 +7175,6 @@ }, "node_modules/leven": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "license": "MIT", "engines": { "node": ">=6" @@ -8437,8 +7182,6 @@ }, "node_modules/levn": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8451,14 +7194,10 @@ }, "node_modules/lines-and-columns": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, "node_modules/locate-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -8473,45 +7212,31 @@ }, "node_modules/lodash": { "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash.debounce": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "license": "MIT" }, "node_modules/lodash.isequalwith": { "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.isequalwith/-/lodash.isequalwith-4.4.0.tgz", - "integrity": "sha512-dcZON0IalGBpRmJBmMkaoV7d3I80R2O+FrzsZyHdNSFrANq/cgDqKQNmAHE8UEj4+QYWwwhkQOVdLHiAopzlsQ==", "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, "license": "MIT" }, "node_modules/lodash.sortby": { "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", "license": "MIT" }, "node_modules/lodash.throttle": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", "license": "MIT" }, "node_modules/log-symbols": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "license": "MIT", "dependencies": { "chalk": "^4.1.0", @@ -8526,8 +7251,6 @@ }, "node_modules/loose-envify": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -8538,8 +7261,6 @@ }, "node_modules/lru-cache": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -8547,8 +7268,6 @@ }, "node_modules/lz-string": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", "bin": { @@ -8557,8 +7276,6 @@ }, "node_modules/magic-string": { "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8567,8 +7284,6 @@ }, "node_modules/magicast": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", - "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", "dev": true, "license": "MIT", "dependencies": { @@ -8579,8 +7294,6 @@ }, "node_modules/make-dir": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "license": "MIT", "dependencies": { @@ -8595,8 +7308,6 @@ }, "node_modules/map-obj": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", "license": "MIT", "engines": { "node": ">=8" @@ -8607,8 +7318,6 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -8616,8 +7325,6 @@ }, "node_modules/meow": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-7.1.1.tgz", - "integrity": "sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA==", "license": "MIT", "dependencies": { "@types/minimist": "^1.2.0", @@ -8641,8 +7348,6 @@ }, "node_modules/meow/node_modules/type-fest": { "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" @@ -8653,8 +7358,6 @@ }, "node_modules/merge2": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", "engines": { @@ -8663,8 +7366,6 @@ }, "node_modules/micromatch": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { @@ -8677,8 +7378,6 @@ }, "node_modules/mimic-fn": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "license": "MIT", "engines": { "node": ">=6" @@ -8686,8 +7385,6 @@ }, "node_modules/min-indent": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "license": "MIT", "engines": { "node": ">=4" @@ -8695,8 +7392,6 @@ }, "node_modules/minimatch": { "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, "license": "ISC", "dependencies": { @@ -8711,8 +7406,6 @@ }, "node_modules/minimist": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8720,8 +7413,6 @@ }, "node_modules/minimist-options": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", "license": "MIT", "dependencies": { "arrify": "^1.0.1", @@ -8734,8 +7425,6 @@ }, "node_modules/minipass": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -8743,8 +7432,6 @@ }, "node_modules/moment": { "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "license": "MIT", "peer": true, "engines": { @@ -8753,20 +7440,14 @@ }, "node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/mute-stream": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "license": "ISC" }, "node_modules/nanoid": { "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -8784,19 +7465,32 @@ }, "node_modules/natural-compare": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, "node_modules/navidrome-music-player": { - "resolved": "../../react-music-player", - "link": true + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/navidrome-music-player/-/navidrome-music-player-4.25.2.tgz", + "integrity": "sha512-k7RXHOOKHeJRCsfmpmQ+TkErndckFfvYMjzwVAKZvViw2PL9ubKWziPfruHZVQr4FiJd2oYKEuTNiWZgAK87CA==", + "license": "MIT", + "dependencies": { + "@react-icons/all-files": "^4.1.0", + "classnames": "^2.3.1", + "downloadjs": "^1.4.7", + "is-mobile": "^2.2.2", + "prop-types": "^15.7.2", + "rc-slider": "^9.7.2", + "rc-switch": "^3.2.2", + "react-draggable": "^4.4.3", + "sortablejs": "^1.13.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } }, "node_modules/node-polyglot": { "version": "2.6.0", - "resolved": "https://registry.npmjs.org/node-polyglot/-/node-polyglot-2.6.0.tgz", - "integrity": "sha512-ZZFkaYzIfGfBvSM6QhA9dM8EEaUJOVewzGSRcXWbJELXDj0lajAtKaENCYxvF5yE+TgHg6NQb0CmgYMsMdcNJQ==", "license": "BSD-2-Clause", "dependencies": { "hasown": "^2.0.2", @@ -8809,14 +7503,10 @@ }, "node_modules/node-releases": { "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "license": "MIT" }, "node_modules/normalize-package-data": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "license": "BSD-2-Clause", "dependencies": { "hosted-git-info": "^2.1.4", @@ -8827,8 +7517,6 @@ }, "node_modules/normalize-package-data/node_modules/resolve": { "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "license": "MIT", "dependencies": { "is-core-module": "^2.16.1", @@ -8847,8 +7535,6 @@ }, "node_modules/normalize-package-data/node_modules/semver": { "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "license": "ISC", "bin": { "semver": "bin/semver" @@ -8856,8 +7542,6 @@ }, "node_modules/normalize-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8865,15 +7549,11 @@ }, "node_modules/nwsapi": { "version": "2.2.23", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", - "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", "dev": true, "license": "MIT" }, "node_modules/object-assign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8881,8 +7561,6 @@ }, "node_modules/object-hash": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", - "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", "license": "MIT", "engines": { "node": ">= 6" @@ -8890,8 +7568,6 @@ }, "node_modules/object-inspect": { "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -8902,8 +7578,6 @@ }, "node_modules/object-is": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", "dev": true, "license": "MIT", "dependencies": { @@ -8919,8 +7593,6 @@ }, "node_modules/object-keys": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -8928,8 +7600,6 @@ }, "node_modules/object.assign": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -8948,8 +7618,6 @@ }, "node_modules/object.entries": { "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -8963,8 +7631,6 @@ }, "node_modules/object.fromentries": { "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8982,8 +7648,6 @@ }, "node_modules/object.values": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, "license": "MIT", "dependencies": { @@ -9001,8 +7665,6 @@ }, "node_modules/obug": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -9012,8 +7674,6 @@ }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", "dependencies": { @@ -9022,8 +7682,6 @@ }, "node_modules/onetime": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -9037,14 +7695,10 @@ }, "node_modules/ono": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ono/-/ono-6.0.1.tgz", - "integrity": "sha512-5rdYW/106kHqLeG22GE2MHKq+FlsxMERZev9DCzQX1zwkxnFwBivSn5i17a5O/rDmOJOdf4Wyt80UZljzx9+DA==", "license": "MIT" }, "node_modules/optionator": { "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { @@ -9061,8 +7715,6 @@ }, "node_modules/ora": { "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", "license": "MIT", "dependencies": { "bl": "^4.1.0", @@ -9084,8 +7736,6 @@ }, "node_modules/os-tmpdir": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9093,8 +7743,6 @@ }, "node_modules/own-keys": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.6", @@ -9110,8 +7758,6 @@ }, "node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9126,8 +7772,6 @@ }, "node_modules/p-locate": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -9142,8 +7786,6 @@ }, "node_modules/p-try": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "license": "MIT", "engines": { "node": ">=6" @@ -9151,8 +7793,6 @@ }, "node_modules/package-json": { "version": "10.0.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-10.0.1.tgz", - "integrity": "sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==", "license": "MIT", "dependencies": { "ky": "^1.2.0", @@ -9169,14 +7809,10 @@ }, "node_modules/package-json-from-dist": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { @@ -9188,8 +7824,6 @@ }, "node_modules/parse-json": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -9206,8 +7840,6 @@ }, "node_modules/parse5": { "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, "license": "MIT", "dependencies": { @@ -9219,8 +7851,6 @@ }, "node_modules/parse5/node_modules/entities": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -9232,8 +7862,6 @@ }, "node_modules/path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "license": "MIT", "engines": { "node": ">=8" @@ -9241,8 +7869,6 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "license": "MIT", "engines": { @@ -9251,8 +7877,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", "engines": { "node": ">=8" @@ -9260,14 +7884,10 @@ }, "node_modules/path-parse": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, "node_modules/path-scurry": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", - "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -9282,8 +7902,6 @@ }, "node_modules/path-scurry/node_modules/lru-cache": { "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -9291,8 +7909,6 @@ }, "node_modules/path-to-regexp": { "version": "1.9.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", - "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", "license": "MIT", "dependencies": { "isarray": "0.0.1" @@ -9300,8 +7916,6 @@ }, "node_modules/path-type": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, "license": "MIT", "engines": { @@ -9310,21 +7924,15 @@ }, "node_modules/pathe": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -9335,14 +7943,10 @@ }, "node_modules/popper.js": { "version": "1.16.1-lts", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", - "integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==", "license": "MIT" }, "node_modules/possible-typed-array-names": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -9350,8 +7954,6 @@ }, "node_modules/postcss": { "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -9379,8 +7981,6 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { @@ -9389,8 +7989,6 @@ }, "node_modules/prettier": { "version": "3.8.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.0.tgz", - "integrity": "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==", "dev": true, "license": "MIT", "bin": { @@ -9405,8 +8003,6 @@ }, "node_modules/pretty-bytes": { "version": "6.1.1", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", - "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", "dev": true, "license": "MIT", "engines": { @@ -9418,8 +8014,6 @@ }, "node_modules/pretty-format": { "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9433,8 +8027,6 @@ }, "node_modules/pretty-format/node_modules/ansi-styles": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", "engines": { @@ -9446,8 +8038,6 @@ }, "node_modules/prop-types": { "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", "peer": true, "dependencies": { @@ -9458,20 +8048,14 @@ }, "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, "node_modules/proto-list": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", "license": "ISC" }, "node_modules/punycode": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "license": "MIT", "engines": { "node": ">=6" @@ -9479,8 +8063,6 @@ }, "node_modules/pupa": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.3.0.tgz", - "integrity": "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==", "license": "MIT", "dependencies": { "escape-goat": "^4.0.0" @@ -9494,8 +8076,6 @@ }, "node_modules/query-string": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", - "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", "license": "MIT", "dependencies": { "decode-uri-component": "^0.2.0", @@ -9508,8 +8088,6 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { @@ -9529,8 +8107,6 @@ }, "node_modules/quick-lru": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", "license": "MIT", "engines": { "node": ">=8" @@ -9538,8 +8114,6 @@ }, "node_modules/ra-core": { "version": "3.19.12", - "resolved": "https://registry.npmjs.org/ra-core/-/ra-core-3.19.12.tgz", - "integrity": "sha512-E0cM6OjEUtccaR+dR5mL1MLiVVYML0Yf7aPhpLEq4iue73X3+CKcLztInoBhWgeevPbFQwgAtsXhlpedeyrNNg==", "license": "MIT", "peer": true, "dependencies": { @@ -9568,14 +8142,10 @@ }, "node_modules/ra-core/node_modules/classnames": { "version": "2.3.3", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.3.tgz", - "integrity": "sha512-1inzZmicIFcmUya7PGtUQeXtcF7zZpPnxtQoYOrz0uiOBGlLFa4ik4361seYL2JCcRDIyfdFHiwQolESFlw+Og==", "license": "MIT" }, "node_modules/ra-core/node_modules/inflection": { "version": "1.13.4", - "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", - "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", "engines": [ "node >= 0.4.0" ], @@ -9583,8 +8153,6 @@ }, "node_modules/ra-data-json-server": { "version": "3.19.12", - "resolved": "https://registry.npmjs.org/ra-data-json-server/-/ra-data-json-server-3.19.12.tgz", - "integrity": "sha512-SEa0ueZd9LUG6iuPnHd+MHWf7BTgLKjx3Eky16VvTsqf6ueHkMU8AZiH1pHzrdxV6ku5VL34MCYWVSIbm2iDnw==", "license": "MIT", "dependencies": { "query-string": "^5.1.1", @@ -9593,8 +8161,6 @@ }, "node_modules/ra-i18n-polyglot": { "version": "3.19.12", - "resolved": "https://registry.npmjs.org/ra-i18n-polyglot/-/ra-i18n-polyglot-3.19.12.tgz", - "integrity": "sha512-7VkNybY+RYVL5aDf8MdefYpRMkaELOjSXx7rrRY7PzVwmQzVe5ESoKBcH4Cob2M8a52pAlXY32dwmA3dZ91l/Q==", "license": "MIT", "dependencies": { "node-polyglot": "^2.2.2", @@ -9603,8 +8169,6 @@ }, "node_modules/ra-language-english": { "version": "3.19.12", - "resolved": "https://registry.npmjs.org/ra-language-english/-/ra-language-english-3.19.12.tgz", - "integrity": "sha512-aYY0ma74eXLuflPT9iXEQtVEDZxebw1NiQZ5pPGiBCpsq+hoiDWuzerLU13OdBHbySD5FHLuk89SkyAdfMtUaQ==", "license": "MIT", "dependencies": { "ra-core": "^3.19.12" @@ -9612,8 +8176,6 @@ }, "node_modules/ra-test": { "version": "3.19.12", - "resolved": "https://registry.npmjs.org/ra-test/-/ra-test-3.19.12.tgz", - "integrity": "sha512-SX6oi+VPADIeQeQlGWUVj2kgEYgLbizpzYMq+oacCmnAqvHezwnQ2MXrLDRK6C56YIl+t8DyY/ipYBiRPZnHbA==", "dev": true, "license": "MIT", "dependencies": { @@ -9633,8 +8195,6 @@ }, "node_modules/ra-test/node_modules/@testing-library/dom": { "version": "7.31.2", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.31.2.tgz", - "integrity": "sha512-3UqjCpey6HiTZT92vODYLPxTBWlM8ZOOjr3LX5F37/VRipW2M1kX6I/Cm4VXzteZqfGfagg8yXywpcOgQBlNsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9653,8 +8213,6 @@ }, "node_modules/ra-test/node_modules/@testing-library/react": { "version": "11.2.7", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-11.2.7.tgz", - "integrity": "sha512-tzRNp7pzd5QmbtXNG/mhdcl7Awfu/Iz1RaVHY75zTdOkmHCuzMhRL83gWHSgOAcjS3CCbyfwUHMZgRJb4kAfpA==", "dev": true, "license": "MIT", "dependencies": { @@ -9671,15 +8229,11 @@ }, "node_modules/ra-test/node_modules/@types/aria-query": { "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", - "integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==", "dev": true, "license": "MIT" }, "node_modules/ra-test/node_modules/aria-query": { "version": "4.2.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", - "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -9692,15 +8246,11 @@ }, "node_modules/ra-test/node_modules/classnames": { "version": "2.3.3", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.3.tgz", - "integrity": "sha512-1inzZmicIFcmUya7PGtUQeXtcF7zZpPnxtQoYOrz0uiOBGlLFa4ik4361seYL2JCcRDIyfdFHiwQolESFlw+Og==", "dev": true, "license": "MIT" }, "node_modules/ra-test/node_modules/pretty-format": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", - "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", "dev": true, "license": "MIT", "dependencies": { @@ -9715,8 +8265,6 @@ }, "node_modules/ra-ui-materialui": { "version": "3.19.12", - "resolved": "https://registry.npmjs.org/ra-ui-materialui/-/ra-ui-materialui-3.19.12.tgz", - "integrity": "sha512-8Zz88r5yprmUxOw9/F0A/kjjVmFMb2n+sjpel8fuOWtS6y++JWonDsvTwo4yIuSF9mC0fht3f/hd2KEHQdmj6Q==", "license": "MIT", "dependencies": { "autosuggest-highlight": "^3.1.1", @@ -9752,20 +8300,14 @@ }, "node_modules/ra-ui-materialui/node_modules/classnames": { "version": "2.2.6", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", - "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==", "license": "MIT" }, "node_modules/ra-ui-materialui/node_modules/dompurify": { "version": "2.5.9", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.9.tgz", - "integrity": "sha512-i6mvVmWN4xo9LrhCOZrDgSs9noW6nOahbrmzjRbPF36YPyj5Ue5lgok0MHDWkG7xzpWFO2OYttXdzM7rJxHvNA==", "license": "(MPL-2.0 OR Apache-2.0)" }, "node_modules/ra-ui-materialui/node_modules/inflection": { "version": "1.13.4", - "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", - "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", "engines": [ "node >= 0.4.0" ], @@ -9773,8 +8315,6 @@ }, "node_modules/randombytes": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" @@ -9782,8 +8322,6 @@ }, "node_modules/rc": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", @@ -9795,16 +8333,134 @@ "rc": "cli.js" } }, + "node_modules/rc-align": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/rc-align/-/rc-align-4.0.15.tgz", + "integrity": "sha512-wqJtVH60pka/nOX7/IspElA8gjPNQKIx/ZqJ6heATCkXpe1Zg4cPVrMD2vC96wjsFFL8WsmhPbx9tdMo1qqlIA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "dom-align": "^1.7.0", + "rc-util": "^5.26.0", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-motion": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz", + "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.44.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-slider": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-9.7.5.tgz", + "integrity": "sha512-LV/MWcXFjco1epPbdw1JlLXlTgmWpB9/Y/P2yinf8Pg3wElHxA9uajN21lJiWtZjf5SCUekfSP6QMJfDo4t1hg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-tooltip": "^5.0.1", + "rc-util": "^5.16.1", + "shallowequal": "^1.1.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-switch": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-3.2.2.tgz", + "integrity": "sha512-+gUJClsZZzvAHGy1vZfnwySxj+MjLlGRyXKXScrtCTcmiYNPzxDFOxdQ/3pK1Kt/0POvwJ/6ALOR8gwdXGhs+A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.1", + "rc-util": "^5.0.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tooltip": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-5.3.1.tgz", + "integrity": "sha512-e6H0dMD38EPaSPD2XC8dRfct27VvT2TkPdoBSuNl3RRZ5tspiY/c5xYEmGC0IrABvMBgque4Mr2SMZuliCvoiQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "classnames": "^2.3.1", + "rc-trigger": "^5.3.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-trigger": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/rc-trigger/-/rc-trigger-5.3.4.tgz", + "integrity": "sha512-mQv+vas0TwKcjAO2izNPkqR4j86OemLRmvL2nOzdP9OWNWA1ivoTt5hzFqYNW9zACwmTezRiN8bttrC7cZzYSw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "classnames": "^2.2.6", + "rc-align": "^4.0.0", + "rc-motion": "^2.0.0", + "rc-util": "^5.19.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util": { + "version": "5.44.4", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz", + "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/rc/node_modules/ini": { "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9812,8 +8468,6 @@ }, "node_modules/react": { "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", "license": "MIT", "peer": true, "dependencies": { @@ -9826,8 +8480,6 @@ }, "node_modules/react-admin": { "version": "3.19.12", - "resolved": "https://registry.npmjs.org/react-admin/-/react-admin-3.19.12.tgz", - "integrity": "sha512-LanWS3Yjie7n5GZI8v7oP73DSvQyCeZD0dpkC65IC0+UOhkInxa1zedJc8CyD3+ZwlgVC+CGqi6jQ1fo73Cdqw==", "license": "MIT", "dependencies": { "@material-ui/core": "^4.12.1", @@ -9855,8 +8507,6 @@ }, "node_modules/react-dnd": { "version": "14.0.5", - "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-14.0.5.tgz", - "integrity": "sha512-9i1jSgbyVw0ELlEVt/NkCUkxy1hmhJOkePoCH713u75vzHGyXhPDm28oLfc2NMSBjZRM1Y+wRjHXJT3sPrTy+A==", "license": "MIT", "dependencies": { "@react-dnd/invariant": "^2.0.0", @@ -9885,8 +8535,6 @@ }, "node_modules/react-dnd-html5-backend": { "version": "14.1.0", - "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz", - "integrity": "sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw==", "license": "MIT", "dependencies": { "dnd-core": "14.0.1" @@ -9894,8 +8542,6 @@ }, "node_modules/react-dom": { "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", "license": "MIT", "peer": true, "dependencies": { @@ -9909,18 +8555,28 @@ }, "node_modules/react-drag-listview": { "version": "0.1.9", - "resolved": "https://registry.npmjs.org/react-drag-listview/-/react-drag-listview-0.1.9.tgz", - "integrity": "sha512-/OsYevKtCUlw4FhJIfZPH7INHEmyl89sSC5COzonHW5Z2c8rHg4DNYFnUxOyqH+65o7sHweL13oaf6wr7dFvPA==", "license": "MIT", "dependencies": { "babel-runtime": "^6.26.0", "prop-types": "^15.5.8" } }, + "node_modules/react-draggable": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz", + "integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, "node_modules/react-dropzone": { "version": "10.2.2", - "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-10.2.2.tgz", - "integrity": "sha512-U5EKckXVt6IrEyhMMsgmHQiWTGLudhajPPG77KFSvgsMqNEHSyGpqWvOMc5+DhEah/vH4E1n+J5weBNLd5VtyA==", "license": "MIT", "dependencies": { "attr-accept": "^2.0.0", @@ -9936,8 +8592,6 @@ }, "node_modules/react-error-boundary": { "version": "3.1.4", - "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", - "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", "dev": true, "license": "MIT", "dependencies": { @@ -9953,8 +8607,6 @@ }, "node_modules/react-final-form": { "version": "6.5.9", - "resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.9.tgz", - "integrity": "sha512-x3XYvozolECp3nIjly+4QqxdjSSWfcnpGEL5K8OBT6xmGrq5kBqbA6+/tOqoom9NwqIPPbxPNsOViFlbKgowbA==", "license": "MIT", "peer": true, "dependencies": { @@ -9971,8 +8623,6 @@ }, "node_modules/react-final-form-arrays": { "version": "3.1.4", - "resolved": "https://registry.npmjs.org/react-final-form-arrays/-/react-final-form-arrays-3.1.4.tgz", - "integrity": "sha512-siVFAolUAe29rMR6u8VwepoysUcUdh6MLV2OWnCtKpsPRUdT9VUgECjAPaVMAH2GROZNiVB9On1H9MMrm9gdpg==", "license": "MIT", "peer": true, "dependencies": { @@ -9987,8 +8637,6 @@ }, "node_modules/react-ga": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/react-ga/-/react-ga-3.3.1.tgz", - "integrity": "sha512-4Vc0W5EvXAXUN/wWyxvsAKDLLgtJ3oLmhYYssx+YzphJpejtOst6cbIHCIyF50Fdxuf5DDKqRYny24yJ2y7GFQ==", "license": "Apache-2.0", "peerDependencies": { "prop-types": "^15.6.0", @@ -9997,8 +8645,6 @@ }, "node_modules/react-hotkeys": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/react-hotkeys/-/react-hotkeys-2.0.0.tgz", - "integrity": "sha512-3n3OU8vLX/pfcJrR3xJ1zlww6KS1kEJt0Whxc4FiGV+MJrQ1mYSYI3qS/11d2MJDFm8IhOXMTFQirfu6AVOF6Q==", "license": "ISC", "dependencies": { "prop-types": "^15.6.1" @@ -10009,8 +8655,6 @@ }, "node_modules/react-icons": { "version": "5.5.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", - "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", "license": "MIT", "peerDependencies": { "react": "*" @@ -10018,9 +8662,6 @@ }, "node_modules/react-image-lightbox": { "version": "5.1.4", - "resolved": "https://registry.npmjs.org/react-image-lightbox/-/react-image-lightbox-5.1.4.tgz", - "integrity": "sha512-kTiAODz091bgT7SlWNHab0LSMZAPJtlNWDGKv7pLlLY1krmf7FuG1zxE0wyPpeA8gPdwfr3cu6sPwZRqWsc3Eg==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "dependencies": { "prop-types": "^15.7.2", @@ -10033,20 +8674,14 @@ }, "node_modules/react-is": { "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "license": "MIT" }, "node_modules/react-lifecycles-compat": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", "license": "MIT" }, "node_modules/react-measure": { "version": "2.5.2", - "resolved": "https://registry.npmjs.org/react-measure/-/react-measure-2.5.2.tgz", - "integrity": "sha512-M+rpbTLWJ3FD6FXvYV6YEGvQ5tMayQ3fGrZhRPHrE9bVlBYfDCLuDcgNttYfk8IqfOI03jz6cbpqMRTUclQnaA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.2.0", @@ -10061,8 +8696,6 @@ }, "node_modules/react-modal": { "version": "3.16.3", - "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.3.tgz", - "integrity": "sha512-yCYRJB5YkeQDQlTt17WGAgFJ7jr2QYcWa1SHqZ3PluDmnKJ/7+tVU+E6uKyZ0nODaeEj+xCpK4LcSnKXLMC0Nw==", "license": "MIT", "dependencies": { "exenv": "^1.2.0", @@ -10077,8 +8710,6 @@ }, "node_modules/react-redux": { "version": "7.2.9", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", - "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", "license": "MIT", "peer": true, "dependencies": { @@ -10103,8 +8734,6 @@ }, "node_modules/react-refresh": { "version": "0.18.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", - "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", "dev": true, "license": "MIT", "engines": { @@ -10113,8 +8742,6 @@ }, "node_modules/react-router": { "version": "5.3.4", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", - "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", "license": "MIT", "peer": true, "dependencies": { @@ -10134,8 +8761,6 @@ }, "node_modules/react-router-dom": { "version": "5.3.4", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", - "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", "license": "MIT", "peer": true, "dependencies": { @@ -10153,14 +8778,10 @@ }, "node_modules/react-router/node_modules/react-is": { "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, "node_modules/react-transition-group": { "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", "license": "BSD-3-Clause", "dependencies": { "@babel/runtime": "^7.5.5", @@ -10175,8 +8796,6 @@ }, "node_modules/read-pkg": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", "license": "MIT", "dependencies": { "@types/normalize-package-data": "^2.4.0", @@ -10190,8 +8809,6 @@ }, "node_modules/read-pkg-up": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", "license": "MIT", "dependencies": { "find-up": "^4.1.0", @@ -10207,8 +8824,6 @@ }, "node_modules/read-pkg-up/node_modules/find-up": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -10220,8 +8835,6 @@ }, "node_modules/read-pkg-up/node_modules/locate-path": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -10232,8 +8845,6 @@ }, "node_modules/read-pkg-up/node_modules/p-limit": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -10247,8 +8858,6 @@ }, "node_modules/read-pkg-up/node_modules/p-locate": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -10259,8 +8868,6 @@ }, "node_modules/read-pkg-up/node_modules/type-fest": { "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=8" @@ -10268,8 +8875,6 @@ }, "node_modules/read-pkg/node_modules/type-fest": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=8" @@ -10277,8 +8882,6 @@ }, "node_modules/readable-stream": { "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -10291,8 +8894,6 @@ }, "node_modules/readdirp": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -10303,8 +8904,6 @@ }, "node_modules/redent": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", "license": "MIT", "dependencies": { "indent-string": "^4.0.0", @@ -10316,8 +8915,6 @@ }, "node_modules/redux": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", "license": "MIT", "peer": true, "dependencies": { @@ -10326,8 +8923,6 @@ }, "node_modules/redux-saga": { "version": "1.4.2", - "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.4.2.tgz", - "integrity": "sha512-QLIn/q+7MX/B+MkGJ/K6R3//60eJ4QNy65eqPsJrfGezbxdh1Jx+37VRKE2K4PsJnNET5JufJtgWdT30WBa+6w==", "license": "MIT", "peer": true, "dependencies": { @@ -10336,8 +8931,6 @@ }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -10358,14 +8951,10 @@ }, "node_modules/regenerate": { "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", "license": "MIT" }, "node_modules/regenerate-unicode-properties": { "version": "10.2.2", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", - "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", "license": "MIT", "dependencies": { "regenerate": "^1.4.2" @@ -10376,14 +8965,10 @@ }, "node_modules/regenerator-runtime": { "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", "license": "MIT" }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -10402,8 +8987,6 @@ }, "node_modules/regexpu-core": { "version": "6.4.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", - "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", "license": "MIT", "dependencies": { "regenerate": "^1.4.2", @@ -10419,8 +9002,6 @@ }, "node_modules/registry-auth-token": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.1.tgz", - "integrity": "sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==", "license": "MIT", "dependencies": { "@pnpm/npm-conf": "^3.0.2" @@ -10431,8 +9012,6 @@ }, "node_modules/registry-url": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", - "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", "license": "MIT", "dependencies": { "rc": "1.2.8" @@ -10446,14 +9025,10 @@ }, "node_modules/regjsgen": { "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", "license": "MIT" }, "node_modules/regjsparser": { "version": "0.13.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", - "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", "license": "BSD-2-Clause", "dependencies": { "jsesc": "~3.1.0" @@ -10464,14 +9039,10 @@ }, "node_modules/remove-accents": { "version": "0.4.4", - "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.4.tgz", - "integrity": "sha512-EpFcOa/ISetVHEXqu+VwI96KZBmq+a8LJnGkaeFw45epGlxIZz5dhEEnNZMsQXgORu3qaMoLX4qJCzOik6ytAg==", "license": "MIT" }, "node_modules/require-from-string": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10479,20 +9050,14 @@ }, "node_modules/reselect": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-3.0.1.tgz", - "integrity": "sha512-b/6tFZCmRhtBMa4xGqiiRp9jh9Aqi2A687Lo265cN0/QohJQEBPiQ52f4QB6i0eF3yp3hmLL21LSGBcML2dlxA==", "license": "MIT" }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", "license": "MIT" }, "node_modules/resolve": { "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, "license": "MIT", "dependencies": { @@ -10509,8 +9074,6 @@ }, "node_modules/resolve-from": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { @@ -10519,14 +9082,10 @@ }, "node_modules/resolve-pathname": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", - "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==", "license": "MIT" }, "node_modules/restore-cursor": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "license": "MIT", "dependencies": { "onetime": "^5.1.0", @@ -10538,8 +9097,6 @@ }, "node_modules/reusify": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -10549,8 +9106,6 @@ }, "node_modules/rifm": { "version": "0.7.0", - "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.7.0.tgz", - "integrity": "sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1" @@ -10561,9 +9116,6 @@ }, "node_modules/rimraf": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", "dependencies": { @@ -10579,8 +9131,6 @@ "node_modules/rollup": { "name": "@rollup/wasm-node", "version": "4.55.2", - "resolved": "https://registry.npmjs.org/@rollup/wasm-node/-/wasm-node-4.55.2.tgz", - "integrity": "sha512-oWKZLjYwTihnTeINcNenxIIDfeotkQ2GAjFJPe7aYsMONrwDwQQXcAl3Qv0qON7Hdc8RTsFomq22zotm/i6VVQ==", "devOptional": true, "license": "MIT", "peer": true, @@ -10600,15 +9150,11 @@ }, "node_modules/rrweb-cssom": { "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", - "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", "dev": true, "license": "MIT" }, "node_modules/run-async": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", "license": "MIT", "engines": { "node": ">=0.12.0" @@ -10616,8 +9162,6 @@ }, "node_modules/run-parallel": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -10640,8 +9184,6 @@ }, "node_modules/rxjs": { "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^1.9.0" @@ -10652,14 +9194,10 @@ }, "node_modules/rxjs/node_modules/tslib": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "license": "0BSD" }, "node_modules/safe-array-concat": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -10677,14 +9215,10 @@ }, "node_modules/safe-array-concat/node_modules/isarray": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "license": "MIT" }, "node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "funding": [ { "type": "github", @@ -10703,8 +9237,6 @@ }, "node_modules/safe-push-apply": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -10719,14 +9251,10 @@ }, "node_modules/safe-push-apply/node_modules/isarray": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "license": "MIT" }, "node_modules/safe-regex-test": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -10742,14 +9270,10 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, "node_modules/saxes": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, "license": "ISC", "dependencies": { @@ -10761,8 +9285,6 @@ }, "node_modules/scheduler": { "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", @@ -10771,15 +9293,11 @@ }, "node_modules/seamless-immutable": { "version": "7.1.4", - "resolved": "https://registry.npmjs.org/seamless-immutable/-/seamless-immutable-7.1.4.tgz", - "integrity": "sha512-XiUO1QP4ki4E2PHegiGAlu6r82o5A+6tRh7IkGGTVg/h+UoeX4nFBeCGPOhb4CYjvkqsfm/TUtvOMYC1xmV30A==", "license": "BSD-3-Clause", "optional": true }, "node_modules/semver": { "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -10790,8 +9308,6 @@ }, "node_modules/serialize-javascript": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" @@ -10799,8 +9315,6 @@ }, "node_modules/set-function-length": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -10816,8 +9330,6 @@ }, "node_modules/set-function-name": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -10831,8 +9343,6 @@ }, "node_modules/set-proto": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -10843,10 +9353,14 @@ "node": ">= 0.4" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -10857,8 +9371,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "license": "MIT", "engines": { "node": ">=8" @@ -10866,8 +9378,6 @@ }, "node_modules/side-channel": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -10885,8 +9395,6 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -10901,8 +9409,6 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -10919,8 +9425,6 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -10938,21 +9442,15 @@ }, "node_modules/siginfo": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, "license": "ISC" }, "node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, "node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", "engines": { @@ -10961,15 +9459,16 @@ }, "node_modules/smob": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", - "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "license": "MIT" + }, + "node_modules/sortablejs": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz", + "integrity": "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==", "license": "MIT" }, "node_modules/source-map": { "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "deprecated": "The work that was done in this beta branch won't be included in future versions", "license": "BSD-3-Clause", "dependencies": { "whatwg-url": "^7.0.0" @@ -10980,8 +9479,6 @@ }, "node_modules/source-map-js": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -10990,8 +9487,6 @@ }, "node_modules/source-map-support": { "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -11000,8 +9495,6 @@ }, "node_modules/source-map-support/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -11009,8 +9502,6 @@ }, "node_modules/source-map/node_modules/tr46": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", "license": "MIT", "dependencies": { "punycode": "^2.1.0" @@ -11018,14 +9509,10 @@ }, "node_modules/source-map/node_modules/webidl-conversions": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", "license": "BSD-2-Clause" }, "node_modules/source-map/node_modules/whatwg-url": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", "license": "MIT", "dependencies": { "lodash.sortby": "^4.7.0", @@ -11035,15 +9522,10 @@ }, "node_modules/sourcemap-codec": { "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "deprecated": "Please use @jridgewell/sourcemap-codec instead", "license": "MIT" }, "node_modules/spdx-correct": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "license": "Apache-2.0", "dependencies": { "spdx-expression-parse": "^3.0.0", @@ -11052,14 +9534,10 @@ }, "node_modules/spdx-exceptions": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", @@ -11068,34 +9546,24 @@ }, "node_modules/spdx-license-ids": { "version": "3.0.22", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", - "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", "license": "CC0-1.0" }, "node_modules/sprintf-js": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "license": "BSD-3-Clause" }, "node_modules/stackback": { "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, "license": "MIT" }, "node_modules/std-env": { "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true, "license": "MIT" }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -11107,8 +9575,6 @@ }, "node_modules/strict-uri-encode": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11116,8 +9582,6 @@ }, "node_modules/string_decoder": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -11125,8 +9589,6 @@ }, "node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -11140,8 +9602,6 @@ "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -11154,20 +9614,14 @@ }, "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/string-width/node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/string.prototype.includes": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", - "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", "dev": true, "license": "MIT", "dependencies": { @@ -11181,8 +9635,6 @@ }, "node_modules/string.prototype.matchall": { "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -11208,8 +9660,6 @@ }, "node_modules/string.prototype.repeat": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, "license": "MIT", "dependencies": { @@ -11219,8 +9669,6 @@ }, "node_modules/string.prototype.trim": { "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -11240,8 +9688,6 @@ }, "node_modules/string.prototype.trimend": { "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -11258,8 +9704,6 @@ }, "node_modules/string.prototype.trimstart": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -11275,8 +9719,6 @@ }, "node_modules/stringify-object": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", - "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", "license": "BSD-2-Clause", "dependencies": { "get-own-enumerable-property-symbols": "^3.0.0", @@ -11289,8 +9731,6 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -11302,8 +9742,6 @@ "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -11314,8 +9752,6 @@ }, "node_modules/strip-comments": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", - "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", "license": "MIT", "engines": { "node": ">=10" @@ -11323,8 +9759,6 @@ }, "node_modules/strip-indent": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", "license": "MIT", "dependencies": { "min-indent": "^1.0.0" @@ -11335,8 +9769,6 @@ }, "node_modules/strip-json-comments": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", "engines": { @@ -11348,8 +9780,6 @@ }, "node_modules/stubborn-fs": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", - "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==", "license": "MIT", "dependencies": { "stubborn-utils": "^1.0.1" @@ -11357,14 +9787,10 @@ }, "node_modules/stubborn-utils": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz", - "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", "license": "MIT" }, "node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -11375,8 +9801,6 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -11387,15 +9811,11 @@ }, "node_modules/symbol-tree": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true, "license": "MIT" }, "node_modules/temp-dir": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", - "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", "license": "MIT", "engines": { "node": ">=8" @@ -11403,8 +9823,6 @@ }, "node_modules/tempy": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", - "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", "license": "MIT", "dependencies": { "is-stream": "^2.0.0", @@ -11421,8 +9839,6 @@ }, "node_modules/tempy/node_modules/type-fest": { "version": "0.16.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", - "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" @@ -11433,8 +9849,6 @@ }, "node_modules/terser": { "version": "5.46.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", - "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -11451,40 +9865,28 @@ }, "node_modules/text-table": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true, "license": "MIT" }, "node_modules/through": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "license": "MIT" }, "node_modules/tiny-invariant": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, "node_modules/tiny-warning": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", "license": "MIT" }, "node_modules/tinybench": { "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, "license": "MIT" }, "node_modules/tinyexec": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", "dev": true, "license": "MIT", "engines": { @@ -11493,8 +9895,6 @@ }, "node_modules/tinyglobby": { "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11510,8 +9910,6 @@ }, "node_modules/tinyglobby/node_modules/fdir": { "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -11528,8 +9926,6 @@ }, "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "peer": true, @@ -11542,8 +9938,6 @@ }, "node_modules/tinyrainbow": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", "dev": true, "license": "MIT", "engines": { @@ -11552,8 +9946,6 @@ }, "node_modules/tldts": { "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", - "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11565,15 +9957,11 @@ }, "node_modules/tldts-core": { "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", - "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", "dev": true, "license": "MIT" }, "node_modules/tmp": { "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "license": "MIT", "dependencies": { "os-tmpdir": "~1.0.2" @@ -11584,8 +9972,6 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -11596,8 +9982,6 @@ }, "node_modules/tough-cookie": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", - "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -11609,8 +9993,6 @@ }, "node_modules/tr46": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "dev": true, "license": "MIT", "dependencies": { @@ -11622,8 +10004,6 @@ }, "node_modules/trim-newlines": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", "license": "MIT", "engines": { "node": ">=8" @@ -11631,8 +10011,6 @@ }, "node_modules/ts-api-utils": { "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", "dev": true, "license": "MIT", "engines": { @@ -11644,14 +10022,10 @@ }, "node_modules/tslib": { "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -11663,8 +10037,6 @@ }, "node_modules/type-fest": { "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -11676,8 +10048,6 @@ }, "node_modules/typed-array-buffer": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -11690,8 +10060,6 @@ }, "node_modules/typed-array-byte-length": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -11709,8 +10077,6 @@ }, "node_modules/typed-array-byte-offset": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -11730,8 +10096,6 @@ }, "node_modules/typed-array-length": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -11750,8 +10114,6 @@ }, "node_modules/typescript": { "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -11765,8 +10127,6 @@ }, "node_modules/typescript-compare": { "version": "0.0.2", - "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", - "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==", "license": "MIT", "dependencies": { "typescript-logic": "^0.0.0" @@ -11774,14 +10134,10 @@ }, "node_modules/typescript-logic": { "version": "0.0.0", - "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz", - "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==", "license": "MIT" }, "node_modules/typescript-tuple": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz", - "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==", "license": "MIT", "dependencies": { "typescript-compare": "^0.0.2" @@ -11789,8 +10145,6 @@ }, "node_modules/unbox-primitive": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -11807,15 +10161,11 @@ }, "node_modules/undici-types": { "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "devOptional": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", - "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", "license": "MIT", "engines": { "node": ">=4" @@ -11823,8 +10173,6 @@ }, "node_modules/unicode-match-property-ecmascript": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", "license": "MIT", "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", @@ -11836,8 +10184,6 @@ }, "node_modules/unicode-match-property-value-ecmascript": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", - "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", "license": "MIT", "engines": { "node": ">=4" @@ -11845,8 +10191,6 @@ }, "node_modules/unicode-property-aliases-ecmascript": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", - "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", "license": "MIT", "engines": { "node": ">=4" @@ -11854,8 +10198,6 @@ }, "node_modules/unique-string": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", "license": "MIT", "dependencies": { "crypto-random-string": "^2.0.0" @@ -11866,8 +10208,6 @@ }, "node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "license": "MIT", "engines": { "node": ">= 10.0.0" @@ -11875,8 +10215,6 @@ }, "node_modules/upath": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", "license": "MIT", "engines": { "node": ">=4", @@ -11885,8 +10223,6 @@ }, "node_modules/update-browserslist-db": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "funding": [ { "type": "opencollective", @@ -11915,8 +10251,6 @@ }, "node_modules/update-notifier": { "version": "7.3.1", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-7.3.1.tgz", - "integrity": "sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA==", "license": "BSD-2-Clause", "dependencies": { "boxen": "^8.0.1", @@ -11939,8 +10273,6 @@ }, "node_modules/update-notifier/node_modules/chalk": { "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -11951,8 +10283,6 @@ }, "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -11960,14 +10290,10 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, "node_modules/uuid": { "version": "13.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", - "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -11979,8 +10305,6 @@ }, "node_modules/validate-npm-package-license": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", @@ -11989,14 +10313,10 @@ }, "node_modules/value-equal": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", - "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==", "license": "MIT" }, "node_modules/vite": { "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", "peer": true, @@ -12071,8 +10391,6 @@ }, "node_modules/vite-plugin-pwa": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.2.0.tgz", - "integrity": "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw==", "dev": true, "license": "MIT", "dependencies": { @@ -12102,8 +10420,6 @@ }, "node_modules/vite/node_modules/fdir": { "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -12120,8 +10436,6 @@ }, "node_modules/vite/node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "peer": true, @@ -12134,8 +10448,6 @@ }, "node_modules/vitest": { "version": "4.0.17", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.17.tgz", - "integrity": "sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==", "dev": true, "license": "MIT", "peer": true, @@ -12213,8 +10525,6 @@ }, "node_modules/vitest/node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -12226,8 +10536,6 @@ }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, "license": "MIT", "dependencies": { @@ -12239,8 +10547,6 @@ }, "node_modules/warning": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", "license": "MIT", "dependencies": { "loose-envify": "^1.0.0" @@ -12248,8 +10554,6 @@ }, "node_modules/wcwidth": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", "license": "MIT", "dependencies": { "defaults": "^1.0.3" @@ -12257,8 +10561,6 @@ }, "node_modules/webidl-conversions": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -12267,9 +10569,6 @@ }, "node_modules/whatwg-encoding": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", "dev": true, "license": "MIT", "dependencies": { @@ -12281,8 +10580,6 @@ }, "node_modules/whatwg-mimetype": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", "dev": true, "license": "MIT", "engines": { @@ -12291,8 +10588,6 @@ }, "node_modules/whatwg-url": { "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "dev": true, "license": "MIT", "dependencies": { @@ -12305,14 +10600,10 @@ }, "node_modules/when-exit": { "version": "2.1.5", - "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", - "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==", "license": "MIT" }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -12326,8 +10617,6 @@ }, "node_modules/which-boxed-primitive": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "license": "MIT", "dependencies": { "is-bigint": "^1.1.0", @@ -12345,8 +10634,6 @@ }, "node_modules/which-builtin-type": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -12372,14 +10659,10 @@ }, "node_modules/which-builtin-type/node_modules/isarray": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "license": "MIT" }, "node_modules/which-collection": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "license": "MIT", "dependencies": { "is-map": "^2.0.3", @@ -12396,8 +10679,6 @@ }, "node_modules/which-typed-array": { "version": "1.1.20", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", - "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -12417,8 +10698,6 @@ }, "node_modules/why-is-node-running": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { @@ -12434,8 +10713,6 @@ }, "node_modules/widest-line": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", - "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", "license": "MIT", "dependencies": { "string-width": "^7.0.0" @@ -12449,8 +10726,6 @@ }, "node_modules/widest-line/node_modules/ansi-regex": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { "node": ">=12" @@ -12461,14 +10736,10 @@ }, "node_modules/widest-line/node_modules/emoji-regex": { "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "license": "MIT" }, "node_modules/widest-line/node_modules/string-width": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", @@ -12484,8 +10755,6 @@ }, "node_modules/widest-line/node_modules/strip-ansi": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -12499,8 +10768,6 @@ }, "node_modules/word-wrap": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { @@ -12509,8 +10776,6 @@ }, "node_modules/workbox-background-sync": { "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.0.tgz", - "integrity": "sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w==", "license": "MIT", "dependencies": { "idb": "^7.0.1", @@ -12519,8 +10784,6 @@ }, "node_modules/workbox-broadcast-update": { "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.4.0.tgz", - "integrity": "sha512-+eZQwoktlvo62cI0b+QBr40v5XjighxPq3Fzo9AWMiAosmpG5gxRHgTbGGhaJv/q/MFVxwFNGh/UwHZ/8K88lA==", "license": "MIT", "dependencies": { "workbox-core": "7.4.0" @@ -12528,8 +10791,6 @@ }, "node_modules/workbox-build": { "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.4.0.tgz", - "integrity": "sha512-Ntk1pWb0caOFIvwz/hfgrov/OJ45wPEhI5PbTywQcYjyZiVhT3UrwwUPl6TRYbTm4moaFYithYnl1lvZ8UjxcA==", "license": "MIT", "dependencies": { "@apideck/better-ajv-errors": "^0.3.1", @@ -12576,8 +10837,6 @@ }, "node_modules/workbox-build/node_modules/@apideck/better-ajv-errors": { "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", - "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", "license": "MIT", "dependencies": { "json-schema": "^0.4.0", @@ -12593,8 +10852,6 @@ }, "node_modules/workbox-build/node_modules/@rollup/plugin-babel": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", - "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.10.4", @@ -12616,8 +10873,6 @@ }, "node_modules/workbox-build/node_modules/@rollup/plugin-replace": { "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", - "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", "license": "MIT", "dependencies": { "@rollup/pluginutils": "^3.1.0", @@ -12629,8 +10884,6 @@ }, "node_modules/workbox-build/node_modules/@rollup/pluginutils": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", - "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", "license": "MIT", "dependencies": { "@types/estree": "0.0.39", @@ -12646,14 +10899,10 @@ }, "node_modules/workbox-build/node_modules/@types/estree": { "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", "license": "MIT" }, "node_modules/workbox-build/node_modules/ajv": { "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "peer": true, "dependencies": { @@ -12669,14 +10918,10 @@ }, "node_modules/workbox-build/node_modules/estree-walker": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", - "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", "license": "MIT" }, "node_modules/workbox-build/node_modules/glob": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", - "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", "license": "BlueOak-1.0.0", "dependencies": { "foreground-child": "^3.3.1", @@ -12698,14 +10943,10 @@ }, "node_modules/workbox-build/node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, "node_modules/workbox-build/node_modules/magic-string": { "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", "license": "MIT", "dependencies": { "sourcemap-codec": "^1.4.8" @@ -12713,8 +10954,6 @@ }, "node_modules/workbox-build/node_modules/minimatch": { "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/brace-expansion": "^5.0.0" @@ -12728,8 +10967,6 @@ }, "node_modules/workbox-build/node_modules/pretty-bytes": { "version": "5.6.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", - "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", "license": "MIT", "engines": { "node": ">=6" @@ -12740,8 +10977,6 @@ }, "node_modules/workbox-build/node_modules/rollup": { "version": "2.79.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", - "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "license": "MIT", "peer": true, "bin": { @@ -12756,8 +10991,6 @@ }, "node_modules/workbox-cacheable-response": { "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.0.tgz", - "integrity": "sha512-0Fb8795zg/x23ISFkAc7lbWes6vbw34DGFIMw31cwuHPgDEC/5EYm6m/ZkylLX0EnEbbOyOCLjKgFS/Z5g0HeQ==", "license": "MIT", "dependencies": { "workbox-core": "7.4.0" @@ -12765,8 +10998,6 @@ }, "node_modules/workbox-cli": { "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-cli/-/workbox-cli-7.4.0.tgz", - "integrity": "sha512-BTc9CbW+aXMyIxBdW2mX+dLYHwTeCdKARX0zpjLvR/mZ2ho/7d9XWckwgFGLQRsJfcxml5WngNqp1PG7+qa9Ug==", "license": "MIT", "dependencies": { "chalk": "^4.1.0", @@ -12792,8 +11023,6 @@ }, "node_modules/workbox-cli/node_modules/glob": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", - "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", "license": "BlueOak-1.0.0", "dependencies": { "foreground-child": "^3.3.1", @@ -12815,8 +11044,6 @@ }, "node_modules/workbox-cli/node_modules/minimatch": { "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/brace-expansion": "^5.0.0" @@ -12830,8 +11057,6 @@ }, "node_modules/workbox-cli/node_modules/pretty-bytes": { "version": "5.6.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", - "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", "license": "MIT", "engines": { "node": ">=6" @@ -12842,14 +11067,10 @@ }, "node_modules/workbox-core": { "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.0.tgz", - "integrity": "sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ==", "license": "MIT" }, "node_modules/workbox-expiration": { "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.4.0.tgz", - "integrity": "sha512-V50p4BxYhtA80eOvulu8xVfPBgZbkxJ1Jr8UUn0rvqjGhLDqKNtfrDfjJKnLz2U8fO2xGQJTx/SKXNTzHOjnHw==", "license": "MIT", "dependencies": { "idb": "^7.0.1", @@ -12858,8 +11079,6 @@ }, "node_modules/workbox-google-analytics": { "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.4.0.tgz", - "integrity": "sha512-MVPXQslRF6YHkzGoFw1A4GIB8GrKym/A5+jYDUSL+AeJw4ytQGrozYdiZqUW1TPQHW8isBCBtyFJergUXyNoWQ==", "license": "MIT", "dependencies": { "workbox-background-sync": "7.4.0", @@ -12870,8 +11089,6 @@ }, "node_modules/workbox-navigation-preload": { "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.4.0.tgz", - "integrity": "sha512-etzftSgdQfjMcfPgbfaZCfM2QuR1P+4o8uCA2s4rf3chtKTq/Om7g/qvEOcZkG6v7JZOSOxVYQiOu6PbAZgU6w==", "license": "MIT", "dependencies": { "workbox-core": "7.4.0" @@ -12879,8 +11096,6 @@ }, "node_modules/workbox-precaching": { "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.0.tgz", - "integrity": "sha512-VQs37T6jDqf1rTxUJZXRl3yjZMf5JX/vDPhmx2CPgDDKXATzEoqyRqhYnRoxl6Kr0rqaQlp32i9rtG5zTzIlNg==", "license": "MIT", "dependencies": { "workbox-core": "7.4.0", @@ -12890,8 +11105,6 @@ }, "node_modules/workbox-range-requests": { "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.4.0.tgz", - "integrity": "sha512-3Vq854ZNuP6Y0KZOQWLaLC9FfM7ZaE+iuQl4VhADXybwzr4z/sMmnLgTeUZLq5PaDlcJBxYXQ3U91V7dwAIfvw==", "license": "MIT", "dependencies": { "workbox-core": "7.4.0" @@ -12899,8 +11112,6 @@ }, "node_modules/workbox-recipes": { "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.4.0.tgz", - "integrity": "sha512-kOkWvsAn4H8GvAkwfJTbwINdv4voFoiE9hbezgB1sb/0NLyTG4rE7l6LvS8lLk5QIRIto+DjXLuAuG3Vmt3cxQ==", "license": "MIT", "dependencies": { "workbox-cacheable-response": "7.4.0", @@ -12913,8 +11124,6 @@ }, "node_modules/workbox-routing": { "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.0.tgz", - "integrity": "sha512-C/ooj5uBWYAhAqwmU8HYQJdOjjDKBp9MzTQ+otpMmd+q0eF59K+NuXUek34wbL0RFrIXe/KKT+tUWcZcBqxbHQ==", "license": "MIT", "dependencies": { "workbox-core": "7.4.0" @@ -12922,8 +11131,6 @@ }, "node_modules/workbox-strategies": { "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.0.tgz", - "integrity": "sha512-T4hVqIi5A4mHi92+5EppMX3cLaVywDp8nsyUgJhOZxcfSV/eQofcOA6/EMo5rnTNmNTpw0rUgjAI6LaVullPpg==", "license": "MIT", "dependencies": { "workbox-core": "7.4.0" @@ -12931,8 +11138,6 @@ }, "node_modules/workbox-streams": { "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.4.0.tgz", - "integrity": "sha512-QHPBQrey7hQbnTs5GrEVoWz7RhHJXnPT+12qqWM378orDMo5VMJLCkCM1cnCk+8Eq92lccx/VgRZ7WAzZWbSLg==", "license": "MIT", "dependencies": { "workbox-core": "7.4.0", @@ -12941,14 +11146,10 @@ }, "node_modules/workbox-sw": { "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.4.0.tgz", - "integrity": "sha512-ltU+Kr3qWR6BtbdlMnCjobZKzeV1hN+S6UvDywBrwM19TTyqA03X66dzw1tEIdJvQ4lYKkBFox6IAEhoSEZ8Xw==", "license": "MIT" }, "node_modules/workbox-window": { "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.0.tgz", - "integrity": "sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==", "license": "MIT", "dependencies": { "@types/trusted-types": "^2.0.2", @@ -12957,8 +11158,6 @@ }, "node_modules/wrap-ansi": { "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", @@ -12975,8 +11174,6 @@ "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -12992,8 +11189,6 @@ }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { "node": ">=12" @@ -13004,8 +11199,6 @@ }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", "engines": { "node": ">=12" @@ -13016,14 +11209,10 @@ }, "node_modules/wrap-ansi/node_modules/emoji-regex": { "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "license": "MIT" }, "node_modules/wrap-ansi/node_modules/string-width": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", @@ -13039,8 +11228,6 @@ }, "node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -13054,15 +11241,11 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, "license": "ISC" }, "node_modules/ws": { "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "dev": true, "license": "MIT", "engines": { @@ -13083,8 +11266,6 @@ }, "node_modules/xdg-basedir": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", - "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", "license": "MIT", "engines": { "node": ">=12" @@ -13095,8 +11276,6 @@ }, "node_modules/xml-name-validator": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -13105,21 +11284,15 @@ }, "node_modules/xmlchars": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true, "license": "MIT" }, "node_modules/yallist": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "license": "ISC" }, "node_modules/yargs-parser": { "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "license": "ISC", "dependencies": { "camelcase": "^5.0.0", @@ -13131,8 +11304,6 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { From f39d75e7d230be7cf31dd9d84ce8987bf4651835 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:20:10 +0000 Subject: [PATCH 34/81] fix(subsonic): never omit duration for AlbumID3 (#5217) --- .../Responses AlbumWithSongsID3 with data should match .JSON | 1 + .../Responses AlbumWithSongsID3 with data should match .XML | 2 +- ...Responses AlbumWithSongsID3 without data should match .JSON | 3 ++- .../Responses AlbumWithSongsID3 without data should match .XML | 2 +- ...umWithSongsID3 without data should match OpenSubsonic .JSON | 1 + ...bumWithSongsID3 without data should match OpenSubsonic .XML | 2 +- server/subsonic/responses/responses.go | 2 +- server/subsonic/responses/responses_test.go | 2 +- 8 files changed, 9 insertions(+), 6 deletions(-) diff --git a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .JSON b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .JSON index 6ed471e8b..07678407a 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .JSON @@ -8,6 +8,7 @@ "id": "1", "name": "album", "artist": "artist", + "duration": 292, "genre": "rock", "userRating": 4, "genres": [ diff --git a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .XML b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .XML index 67dcf6bd7..f7b23cb4e 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .XML @@ -1,5 +1,5 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .JSON b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .JSON index fbeded48a..14e96939e 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .JSON @@ -6,6 +6,7 @@ "openSubsonic": true, "album": { "id": "", - "name": "" + "name": "", + "duration": 0 } } diff --git a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .XML b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .XML index 159967c1d..868265347 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match OpenSubsonic .JSON b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match OpenSubsonic .JSON index 758aef0cb..446368fa5 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match OpenSubsonic .JSON +++ b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match OpenSubsonic .JSON @@ -7,6 +7,7 @@ "album": { "id": "", "name": "", + "duration": 0, "userRating": 0, "genres": [], "musicBrainzId": "", diff --git a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match OpenSubsonic .XML b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match OpenSubsonic .XML index 159967c1d..868265347 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match OpenSubsonic .XML +++ b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match OpenSubsonic .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/responses.go b/server/subsonic/responses/responses.go index b70f3b128..b9a39b6f9 100644 --- a/server/subsonic/responses/responses.go +++ b/server/subsonic/responses/responses.go @@ -250,7 +250,7 @@ type AlbumID3 struct { ArtistId string `xml:"artistId,attr,omitempty" json:"artistId,omitempty"` CoverArt string `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"` SongCount int32 `xml:"songCount,attr,omitempty" json:"songCount,omitempty"` - Duration int32 `xml:"duration,attr,omitempty" json:"duration,omitempty"` + Duration int32 `xml:"duration,attr" json:"duration"` PlayCount int64 `xml:"playCount,attr,omitempty" json:"playCount,omitempty"` Created *time.Time `xml:"created,attr,omitempty" json:"created,omitempty"` Starred *time.Time `xml:"starred,attr,omitempty" json:"starred,omitempty"` diff --git a/server/subsonic/responses/responses_test.go b/server/subsonic/responses/responses_test.go index ccf15afe3..15f2da9c6 100644 --- a/server/subsonic/responses/responses_test.go +++ b/server/subsonic/responses/responses_test.go @@ -288,7 +288,7 @@ var _ = Describe("Responses", func() { Context("with data", func() { BeforeEach(func() { album := AlbumID3{ - Id: "1", Name: "album", Artist: "artist", Genre: "rock", + Id: "1", Name: "album", Artist: "artist", Duration: 292, Genre: "rock", } album.OpenSubsonicAlbumID3 = &OpenSubsonicAlbumID3{ Genres: []ItemGenre{{Name: "rock"}, {Name: "progressive"}}, From ad92b752bec101ef13a88fcace674579ce90b094 Mon Sep 17 00:00:00 2001 From: Deluan Date: Tue, 17 Mar 2026 18:34:13 -0400 Subject: [PATCH 35/81] chore(deps): update dependencies for go-sqlite3, golang.org/x packages Signed-off-by: Deluan --- go.mod | 22 +++++++++++----------- go.sum | 44 ++++++++++++++++++++++---------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/go.mod b/go.mod index 28fae78e3..487b57ef7 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,7 @@ require ( github.com/kardianos/service v1.2.4 github.com/kr/pretty v0.3.1 github.com/lestrrat-go/jwx/v3 v3.0.13 - github.com/mattn/go-sqlite3 v1.14.34 + github.com/mattn/go-sqlite3 v1.14.37 github.com/microcosm-cc/bluemonday v1.0.27 github.com/mileusna/useragent v1.3.5 github.com/onsi/ginkgo/v2 v2.28.1 @@ -58,12 +58,12 @@ require ( github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 go.senan.xyz/taglib v0.11.1 go.uber.org/goleak v1.3.0 - golang.org/x/image v0.36.0 - golang.org/x/net v0.51.0 + golang.org/x/image v0.37.0 + golang.org/x/net v0.52.0 golang.org/x/sync v0.20.0 golang.org/x/sys v0.42.0 - golang.org/x/term v0.40.0 - golang.org/x/text v0.34.0 + golang.org/x/term v0.41.0 + golang.org/x/text v0.35.0 golang.org/x/time v0.15.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -80,13 +80,13 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1 // indirect - github.com/ebitengine/purego v0.8.3 // indirect + github.com/ebitengine/purego v0.10.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gobwas/glob v0.2.3 // indirect - github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-json v0.10.6 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc // indirect @@ -134,10 +134,10 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.48.0 // indirect - golang.org/x/mod v0.33.0 // indirect - golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 // indirect - golang.org/x/tools v0.42.0 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect + golang.org/x/tools v0.43.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/ini.v1 v1.67.1 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect diff --git a/go.sum b/go.sum index fd02f812e..d7b16d9d3 100644 --- a/go.sum +++ b/go.sum @@ -56,8 +56,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1 h1:idfl8M8rPW93NehFw5H1qqH8yG158t5POr+LX9avbJY= github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q= -github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc= -github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw= github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= @@ -96,8 +96,8 @@ github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPE github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gohugoio/hashstructure v0.6.0 h1:7wMB/2CfXoThFYhdWRGv3u3rUM761Cq29CxUW+NltUg= @@ -177,8 +177,8 @@ github.com/maruel/natural v1.3.0 h1:VsmCsBmEyrR46RomtgHs5hbKADGRVtliHTyCOLFBpsg= github.com/maruel/natural v1.3.0/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= -github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg= +github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= @@ -319,19 +319,19 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= -golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc= -golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4= +golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA= +golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -343,8 +343,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -372,8 +372,8 @@ golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= -golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 h1:bTLqdHv7xrGlFbvf5/TXNxy/iUwwdkjhqQTJDjW7aj0= -golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -382,8 +382,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -394,8 +394,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -405,8 +405,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= From b013b71ba9fd77999cea8f7465f2faeaa8a8ebb4 Mon Sep 17 00:00:00 2001 From: Deluan Date: Tue, 17 Mar 2026 19:39:00 -0400 Subject: [PATCH 36/81] fix(server): clean up uploaded artist images during GC When artists are purged during garbage collection, any custom uploaded cover images were left orphaned on disk. Modified purgeEmpty() to query for uploaded_image filenames before the bulk DELETE, then remove the corresponding files from disk afterwards. Image cleanup is best-effort to avoid failing the GC if a file is already missing or inaccessible. Also populated album_artists entries in the persistence test suite setup to reflect the actual album-artist relationships from test data, ensuring purgeEmpty() doesn't inadvertently delete shared test artists. --- persistence/artist_repository.go | 30 +++++++++- persistence/artist_repository_test.go | 86 +++++++++++++++++++++++++++ persistence/persistence_suite_test.go | 22 +++++++ 3 files changed, 137 insertions(+), 1 deletion(-) diff --git a/persistence/artist_repository.go b/persistence/artist_repository.go index 7f3d61540..e75a0e58c 100644 --- a/persistence/artist_repository.go +++ b/persistence/artist_repository.go @@ -4,7 +4,9 @@ import ( "cmp" "context" "encoding/json" + "errors" "fmt" + "os" "slices" "strings" "time" @@ -12,6 +14,7 @@ import ( . "github.com/Masterminds/squirrel" "github.com/deluan/rest" "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/utils" @@ -315,7 +318,19 @@ func (r *artistRepository) GetIndex(includeMissing bool, libraryIds []int, roles } func (r *artistRepository) purgeEmpty() error { - del := Delete(r.tableName).Where("id not in (select artist_id from album_artists)") + orphanFilter := "id not in (select artist_id from album_artists)" + + // Collect uploaded image filenames before deleting + sel := Select("uploaded_image").From(r.tableName). + Where(orphanFilter). + Where("uploaded_image != ''") + var imageFiles []string + if err := r.queryAllSlice(sel, &imageFiles); err != nil && !errors.Is(err, model.ErrNotFound) { + return fmt.Errorf("collecting artist images for cleanup: %w", err) + } + + // Delete orphan artists + del := Delete(r.tableName).Where(orphanFilter) c, err := r.executeSQL(del) if err != nil { return fmt.Errorf("purging empty artists: %w", err) @@ -323,6 +338,19 @@ func (r *artistRepository) purgeEmpty() error { if c > 0 { log.Debug(r.ctx, "Purged empty artists", "totalDeleted", c) } + + if len(imageFiles) == 0 { + return nil + } + + // Best-effort cleanup of uploaded image files + log.Debug(r.ctx, "Cleaning up artist images", "totalImages", len(imageFiles)) + for _, filename := range imageFiles { + path := model.UploadedImagePath(consts.EntityArtist, filename) + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + log.Warn(r.ctx, "Failed to remove artist image during GC", "path", path, err) + } + } return nil } diff --git a/persistence/artist_repository_test.go b/persistence/artist_repository_test.go index 90e449e8d..e2904466c 100644 --- a/persistence/artist_repository_test.go +++ b/persistence/artist_repository_test.go @@ -3,11 +3,14 @@ package persistence import ( "context" "encoding/json" + "os" + "path/filepath" "github.com/Masterminds/squirrel" "github.com/deluan/rest" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf/configtest" + "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/utils" @@ -829,6 +832,89 @@ var _ = Describe("ArtistRepository", func() { }) }) }) + + Describe("purgeEmpty", func() { + var repo *artistRepository + var tmpDir string + + BeforeEach(func() { + DeferCleanup(configtest.SetupConfig()) + tmpDir = GinkgoT().TempDir() + conf.Server.DataFolder = tmpDir + + ctx := request.WithUser(GinkgoT().Context(), adminUser) + repo = NewArtistRepository(ctx, GetDBXBuilder()).(*artistRepository) + }) + + // Helper to create an artist image file on disk and return its path + createImageFile := func(filename string) string { + dir := filepath.Join(tmpDir, consts.ArtworkFolder, consts.EntityArtist) + Expect(os.MkdirAll(dir, 0755)).To(Succeed()) + path := filepath.Join(dir, filename) + Expect(os.WriteFile(path, []byte("fake image data"), 0600)).To(Succeed()) + return path + } + + It("removes uploaded image files for purged artists", func() { + // Create an orphan artist (not in album_artists) with an uploaded image + orphanArtist := model.Artist{ID: "orphan-with-image", Name: "Orphan Artist", UploadedImage: "orphan-with-image_Orphan_Artist.jpg"} + Expect(repo.Put(&orphanArtist)).To(Succeed()) + imgPath := createImageFile("orphan-with-image_Orphan_Artist.jpg") + + Expect(repo.purgeEmpty()).To(Succeed()) + + // Artist should be gone from DB + exists, err := repo.Exists("orphan-with-image") + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeFalse()) + + // Image file should be removed from disk + _, err = os.Stat(imgPath) + Expect(os.IsNotExist(err)).To(BeTrue()) + }) + + It("handles missing image files gracefully", func() { + // Artist has UploadedImage set but no actual file on disk + orphanArtist := model.Artist{ID: "orphan-no-file", Name: "Ghost Image", UploadedImage: "orphan-no-file_Ghost_Image.jpg"} + Expect(repo.Put(&orphanArtist)).To(Succeed()) + + Expect(repo.purgeEmpty()).To(Succeed()) + + // Artist should be gone from DB + exists, err := repo.Exists("orphan-no-file") + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeFalse()) + }) + + It("does not delete images for artists that are kept", func() { + // Create an artist with an uploaded image AND an album_artists entry so it won't be purged + keptArtist := model.Artist{ID: "kept-artist", Name: "Kept Artist", UploadedImage: "kept-artist_Kept_Artist.jpg"} + Expect(repo.Put(&keptArtist)).To(Succeed()) + imgPath := createImageFile("kept-artist_Kept_Artist.jpg") + + // Insert an album_artists record to keep this artist from being purged + _, err := repo.executeSQL(squirrel.Insert("album_artists"). + SetMap(map[string]any{"album_id": "101", "artist_id": "kept-artist", "role": "artist", "sub_role": ""})) + Expect(err).ToNot(HaveOccurred()) + + DeferCleanup(func() { + _, _ = repo.executeSQL(squirrel.Delete("album_artists").Where(squirrel.Eq{"artist_id": "kept-artist"})) + _ = repo.delete(squirrel.Eq{"id": "kept-artist"}) + }) + + Expect(repo.purgeEmpty()).To(Succeed()) + + // Artist should still exist (check directly, bypassing library filter) + var ids []string + err = repo.queryAllSlice(squirrel.Select("id").From("artist").Where(squirrel.Eq{"id": "kept-artist"}), &ids) + Expect(err).ToNot(HaveOccurred()) + Expect(ids).To(HaveLen(1)) + + // Image file should still be on disk + _, err = os.Stat(imgPath) + Expect(err).ToNot(HaveOccurred()) + }) + }) }) // Helper function to create an artist with proper library association. diff --git a/persistence/persistence_suite_test.go b/persistence/persistence_suite_test.go index 0ee1570a1..3ed443129 100644 --- a/persistence/persistence_suite_test.go +++ b/persistence/persistence_suite_test.go @@ -5,6 +5,7 @@ import ( "path/filepath" "testing" + "github.com/Masterminds/squirrel" _ "github.com/mattn/go-sqlite3" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/db" @@ -211,6 +212,27 @@ var _ = BeforeSuite(func() { } } + // Populate album_artists based on the AlbumArtistID relationships in testAlbums + artistIDs := map[string]bool{} + for _, a := range testArtists { + artistIDs[a.ID] = true + } + for i := range testAlbums { + a := testAlbums[i] + if a.AlbumArtistID == "" || !artistIDs[a.AlbumArtistID] { + continue + } + _, err := alr.executeSQL(squirrel.Insert("album_artists").SetMap(map[string]any{ + "album_id": a.ID, + "artist_id": a.AlbumArtistID, + "role": "artist", + "sub_role": "", + })) + if err != nil { + panic(err) + } + } + mr := NewMediaFileRepository(ctx, conn) for i := range testSongs { err := mr.Put(&testSongs[i]) From d2a54243a88daed840b946b78ca3612ba8818ce3 Mon Sep 17 00:00:00 2001 From: Deluan Date: Tue, 17 Mar 2026 20:24:21 -0400 Subject: [PATCH 37/81] fix(ui): prevent layout flash on album grid during cover loading Added aspect-ratio: 1 to the cover container so it reserves the correct square dimensions immediately on first render, before react-measure reports the container width. Previously, contentRect.bounds.width started as undefined/0, causing images to render with zero height and producing a brief flash of compressed tiles before the measurement callback fired. --- ui/src/album/AlbumGridView.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui/src/album/AlbumGridView.jsx b/ui/src/album/AlbumGridView.jsx index b7db39730..c0d88d50e 100644 --- a/ui/src/album/AlbumGridView.jsx +++ b/ui/src/album/AlbumGridView.jsx @@ -94,6 +94,11 @@ const useStyles = makeStyles( ) const useCoverStyles = makeStyles({ + coverContainer: { + width: '100%', + aspectRatio: '1', + overflow: 'hidden', + }, cover: { display: 'inline-block', width: '100%', @@ -150,7 +155,7 @@ const Cover = withContentRect('bounds')(({ }, []) return ( -
+
Date: Tue, 17 Mar 2026 20:49:35 -0400 Subject: [PATCH 38/81] fix(ui): hide pagination during album list loading Added a custom AlbumListPagination component that returns null while the list is loading, preventing stale pagination controls from appearing alongside the Loading spinner when navigating to the Random album view. --- ui/src/album/AlbumList.jsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ui/src/album/AlbumList.jsx b/ui/src/album/AlbumList.jsx index f10f8dbd3..d00d97701 100644 --- a/ui/src/album/AlbumList.jsx +++ b/ui/src/album/AlbumList.jsx @@ -10,6 +10,7 @@ import { ReferenceArrayInput, ReferenceInput, SearchInput, + useListContext, usePermissions, useRefresh, useTranslate, @@ -174,6 +175,14 @@ const AlbumListTitle = ({ albumListType }) => { return } +const AlbumListPagination = (props) => { + const { loading } = useListContext() + if (loading) { + return null + } + return <Pagination {...props} /> +} + const randomStartingSeed = Math.random().toString() const AlbumList = (props) => { @@ -234,7 +243,7 @@ const AlbumList = (props) => { actions={<AlbumListActions />} filters={<AlbumFilter />} perPage={perPage} - pagination={<Pagination rowsPerPageOptions={perPageOptions} />} + pagination={<AlbumListPagination rowsPerPageOptions={perPageOptions} />} title={<AlbumListTitle albumListType={albumListType} />} > {albumView.grid ? ( From b5164c61abfd7485bf51b41709a7bfe6c0d46db7 Mon Sep 17 00:00:00 2001 From: Deluan <deluan@navidrome.org> Date: Tue, 17 Mar 2026 21:34:00 -0400 Subject: [PATCH 39/81] build(worktree): add script for setting up git worktrees Signed-off-by: Deluan <deluan@navidrome.org> --- .gitignore | 3 ++- Makefile | 33 +++++++++++++++++++++++ scripts/setup-worktree.sh | 57 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100755 scripts/setup-worktree.sh diff --git a/.gitignore b/.gitignore index db8c0abcf..73475a53a 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ AGENTS.md *.wasm *.ndp openspec/ -go.work* \ No newline at end of file +go.work* +.worktrees/ \ No newline at end of file diff --git a/Makefile b/Makefile index c9c88f506..3bad5b620 100644 --- a/Makefile +++ b/Makefile @@ -233,6 +233,39 @@ get-music: ##@Development Download some free music from Navidrome's demo instanc .PHONY: get-music +########################################## +#### Worktrees + +WORKTREES_DIR := .worktrees + +wt: check_go_env ##@Worktrees Create and setup a git worktree. Usage: make wt name=feature-name [go=1] + @if [ -z "${name}" ]; then echo "Usage: make wt name=<branch-name> [go=1]"; exit 1; fi + @mkdir -p $(WORKTREES_DIR) + @echo "Creating worktree for branch '${name}'..." + @git worktree add $(WORKTREES_DIR)/${name} -b ${name} 2>/dev/null || \ + git worktree add $(WORKTREES_DIR)/${name} ${name} + @if [ -n "${go}" ]; then \ + ./scripts/setup-worktree.sh $(WORKTREES_DIR)/${name} --go-only; \ + else \ + ./scripts/setup-worktree.sh $(WORKTREES_DIR)/${name}; \ + fi + @echo "\nWorktree ready at $(WORKTREES_DIR)/${name}" + @echo " cd $(WORKTREES_DIR)/${name}" +.PHONY: wt + +rm-wt: ##@Worktrees Remove a git worktree. Usage: make rm-wt name=feature-name + @if [ -z "${name}" ]; then echo "Usage: make rm-wt name=<branch-name>"; exit 1; fi + @if [ ! -d "$(WORKTREES_DIR)/${name}" ]; then echo "Worktree '${name}' not found in $(WORKTREES_DIR)/"; exit 1; fi + @echo "Removing worktree '${name}'..." + @git worktree remove --force $(WORKTREES_DIR)/${name} + @echo "Worktree '${name}' removed." + @echo "Note: branch '${name}' still exists. Delete it with: git branch -D ${name}" +.PHONY: rm-wt + +ls-wt: ##@Worktrees List all active git worktrees + @git worktree list +.PHONY: ls-wt + ########################################## #### Miscellaneous diff --git a/scripts/setup-worktree.sh b/scripts/setup-worktree.sh new file mode 100755 index 000000000..65113f3b2 --- /dev/null +++ b/scripts/setup-worktree.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# +# Setup a git worktree for Navidrome development. +# This script is called automatically by `make worktree` and by Claude Code's +# worktree isolation, but can also be run standalone: +# +# ./scripts/setup-worktree.sh <worktree-path> [--go-only] +# +# Options: +# --go-only Skip frontend (npm) setup. Useful for agents working only on Go code. +# +set -euo pipefail + +WORKTREE_PATH="${1:?Usage: $0 <worktree-path> [--go-only]}" +GO_ONLY="${2:-}" + +# Resolve the main worktree root (where the original repo lives) +MAIN_WORKTREE="$(git -C "$WORKTREE_PATH" worktree list --porcelain | head -1 | sed 's/^worktree //')" + +if [ ! -d "$WORKTREE_PATH" ]; then + echo "ERROR: Worktree path does not exist: $WORKTREE_PATH" + exit 1 +fi + +cd "$WORKTREE_PATH" + +echo "==> Setting up worktree at $WORKTREE_PATH" + +# 1. Download Go dependencies +echo "==> Downloading Go dependencies..." +go mod download + +# 2. Install frontend dependencies (unless --go-only) +if [ "$GO_ONLY" != "--go-only" ]; then + echo "==> Installing frontend dependencies..." + (cd ui && npm ci --prefer-offline --no-audit 2>/dev/null || npm ci) +else + echo "==> Skipping frontend setup (--go-only)" +fi + +# 3. Create required directories +mkdir -p data + +# 4. Copy navidrome.toml from main worktree if it exists and not already present +if [ ! -f navidrome.toml ] && [ -f "$MAIN_WORKTREE/navidrome.toml" ]; then + echo "==> Copying navidrome.toml from main worktree..." + cp "$MAIN_WORKTREE/navidrome.toml" navidrome.toml +fi + +# 5. Copy existing database from main worktree (already migrated and scanned) +# This is much faster than running migrations + a full scan from scratch. +if [ ! -f data/navidrome.db ] && [ -f "$MAIN_WORKTREE/data/navidrome.db" ]; then + echo "==> Copying database from main worktree (pre-migrated, pre-scanned)..." + cp "$MAIN_WORKTREE/data/navidrome.db" data/navidrome.db +fi + +echo "==> Worktree setup complete: $WORKTREE_PATH" From 31d94acfe7fbbe09759d91d409043bbb4c1004cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= <deluan@navidrome.org> Date: Wed, 18 Mar 2026 08:03:46 -0400 Subject: [PATCH 40/81] fix(scanner): widen WASM panic recovery to cover tag/property reading (#5223) * fix(scanner): widen WASM panic recovery to cover tag/property reading The panic recovery in gotaglib's extractMetadata was only inside openFile(), which covers taglib.OpenStream(). Panics from f.AllTags() and f.Properties() (e.g. readString crashes on malformed files) were uncaught, crashing the scanner subprocess with exit status 2. Move the recover() up to extractMetadata() so it covers the entire tag reading lifecycle, matching the CGO taglib wrapper's approach. Fixes #5220 * fix(scanner): use consistent log key "filePath" in panic recovery * fix(scanner): include stack trace in WASM panic recovery log Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org> --- adapters/gotaglib/gotaglib.go | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/adapters/gotaglib/gotaglib.go b/adapters/gotaglib/gotaglib.go index 9b71cb462..7ea98a442 100644 --- a/adapters/gotaglib/gotaglib.go +++ b/adapters/gotaglib/gotaglib.go @@ -58,7 +58,20 @@ func (e extractor) Version() string { return "unknown" } -func (e extractor) extractMetadata(filePath string) (*metadata.Info, error) { +func (e extractor) extractMetadata(filePath string) (info *metadata.Info, err error) { + // Recover from panics in the WASM runtime that can occur during any taglib + // operation (opening, reading tags, or reading properties). This catches crashes + // from malformed files or WASM runtime issues (e.g., wazero mmap failures on + // hardened systems with MemoryDenyWriteExecute=true). + debug.SetPanicOnFault(true) + defer func() { + if r := recover(); r != nil { + log.Error("gotaglib: WASM runtime panic reading file. Skipping", "filePath", filePath, "panic", r) + debug.PrintStack() + err = fmt.Errorf("WASM runtime panic: %v", r) + } + }() + f, close, err := e.openFile(filePath) if err != nil { log.Warn("gotaglib: Error reading metadata from file. Skipping", "filePath", filePath, err) @@ -112,16 +125,6 @@ func (e extractor) extractMetadata(filePath string) (*metadata.Info, error) { // openFile opens the file at filePath using the extractor's filesystem. // It returns a TagLib File handle and a cleanup function to close resources. func (e extractor) openFile(filePath string) (f *taglib.File, closeFunc func(), err error) { - // Recover from panics in the WASM runtime (e.g., wazero failing to mmap executable memory - // on hardened systems like NixOS with MemoryDenyWriteExecute=true) - debug.SetPanicOnFault(true) - defer func() { - if r := recover(); r != nil { - log.Error("WASM runtime panic: This may be caused by a hardened system that blocks executable memory mapping.", "file", filePath, "panic", r) - err = fmt.Errorf("WASM runtime panic (hardened system?): %v", r) - } - }() - // Open the file from the filesystem file, err := e.fs.Open(filePath) if err != nil { From 00b8fbd7894a0124973fc77404845a2d0f703e1e Mon Sep 17 00:00:00 2001 From: Deluan <deluan@navidrome.org> Date: Wed, 18 Mar 2026 07:59:10 -0400 Subject: [PATCH 41/81] feat(artwork): add UIThumbnailSize constant and update cache warmer to pre-cache thumbnails Signed-off-by: Deluan <deluan@navidrome.org> --- consts/consts.go | 1 + core/artwork/cache_warmer.go | 17 +++++++++-------- core/artwork/cache_warmer_test.go | 31 ++++++++++++++++++++++++++++++- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/consts/consts.go b/consts/consts.go index 9f4387ae6..38b277eab 100644 --- a/consts/consts.go +++ b/consts/consts.go @@ -71,6 +71,7 @@ const ( PlaceholderAlbumArt = "album-placeholder.webp" PlaceholderAvatar = "logo-192x192.png" UICoverArtSize = 300 + UIThumbnailSize = 80 DefaultUIVolume = 100 DefaultUISearchDebounceMs = 200 diff --git a/core/artwork/cache_warmer.go b/core/artwork/cache_warmer.go index f13820d00..83c98c806 100644 --- a/core/artwork/cache_warmer.go +++ b/core/artwork/cache_warmer.go @@ -142,14 +142,15 @@ func (a *cacheWarmer) doCacheImage(ctx context.Context, id model.ArtworkID) erro ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() - r, _, err := a.artwork.Get(ctx, id, consts.UICoverArtSize, true) - if err != nil { - return fmt.Errorf("caching id='%s': %w", id, err) - } - defer r.Close() - _, err = io.Copy(io.Discard, r) - if err != nil { - return err + for _, size := range []int{consts.UICoverArtSize, consts.UIThumbnailSize} { + 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 + } } return nil } diff --git a/core/artwork/cache_warmer_test.go b/core/artwork/cache_warmer_test.go index abf4f259a..47e187f41 100644 --- a/core/artwork/cache_warmer_test.go +++ b/core/artwork/cache_warmer_test.go @@ -6,11 +6,13 @@ import ( "fmt" "io" "strings" + "sync" "sync/atomic" "time" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf/configtest" + "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/utils/cache" . "github.com/onsi/ginkgo/v2" @@ -173,20 +175,47 @@ var _ = Describe("CacheWarmer", func() { return len(cw.buffer) }).Should(Equal(0)) }) + + It("pre-caches both UICoverArtSize and UIThumbnailSize", func() { + cw := NewCacheWarmer(aw, fc).(*cacheWarmer) + cw.PreCache(model.MustParseArtworkID("al-1")) + + Eventually(func() int { + cw.mutex.Lock() + defer cw.mutex.Unlock() + return len(cw.buffer) + }).Should(Equal(0)) + + sizes := aw.getCachedSizes() + Expect(sizes).To(ContainElements(consts.UICoverArtSize, consts.UIThumbnailSize)) + }) }) }) type mockArtwork struct { - err error + err error + mu sync.Mutex + cachedSizes []int } func (m *mockArtwork) Get(ctx context.Context, artID model.ArtworkID, size int, square bool) (io.ReadCloser, time.Time, error) { if m.err != nil { return nil, time.Time{}, m.err } + m.mu.Lock() + m.cachedSizes = append(m.cachedSizes, size) + m.mu.Unlock() return io.NopCloser(strings.NewReader("test")), time.Now(), nil } +func (m *mockArtwork) getCachedSizes() []int { + m.mu.Lock() + defer m.mu.Unlock() + result := make([]int, len(m.cachedSizes)) + copy(result, m.cachedSizes) + return result +} + func (m *mockArtwork) GetOrPlaceholder(ctx context.Context, id string, size int, square bool) (io.ReadCloser, time.Time, error) { return m.Get(ctx, model.ArtworkID{}, size, square) } From 3f7226d253cdde7bdd948f364acf61c64946bc20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= <deluan@navidrome.org> Date: Wed, 18 Mar 2026 12:39:03 -0400 Subject: [PATCH 42/81] fix(server): improve transcoding failure diagnostics and error responses (#5227) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(server): capture ffmpeg stderr and warn on empty transcoded output When ffmpeg fails during transcoding (e.g., missing codec like libopus), the error was silently discarded because stderr was sent to io.Discard and the HTTP response returned 200 OK with a 0-byte body. - Capture ffmpeg stderr in a bounded buffer (4KB) and include it in the error message when the process exits with a non-zero status code - Log a warning when transcoded output is 0 bytes, guiding users to check codec support and enable Trace logging for details - Remove log level guard so transcoding errors are always logged, not just at Debug level Signed-off-by: Deluan <deluan@navidrome.org> * fix(server): return proper error responses for empty transcoded output Instead of returning HTTP 200 with 0-byte body when transcoding fails, return a Subsonic error response (for stream/download/getTranscodeStream) or HTTP 500 (for public shared streams). This gives clients a clear signal that the request failed rather than a misleading empty success. Signed-off-by: Deluan <deluan@navidrome.org> * test(e2e): add tests for empty transcoded stream error responses Add E2E tests verifying that stream and download endpoints return Subsonic error responses when transcoding produces empty output. Extend spyStreamer with SimulateEmptyStream and SimulateError fields to support failure injection in tests. Signed-off-by: Deluan <deluan@navidrome.org> * refactor(server): extract stream serving logic into Stream.Serve method Extract the duplicated non-seekable stream serving logic (header setup, estimateContentLength, HEAD draining, io.Copy with error/empty detection) from server/subsonic/stream.go and server/public/handle_streams.go into a single Stream.Serve method on core/stream. Both callers now delegate to it, eliminating ~30 lines of near-identical code. * fix(server): return 200 with empty body for stream/download on empty transcoded output Don't return a Subsonic error response when transcoding produces empty output on stream/download endpoints — just log the error and return 200 with an empty body. The getTranscodeStream and public share endpoints still return HTTP 500 for empty output. Stream.Serve now returns (int64, error) so callers can check the byte count. --------- Signed-off-by: Deluan <deluan@navidrome.org> --- core/ffmpeg/ffmpeg.go | 42 +++++++++++++++---- core/ffmpeg/ffmpeg_test.go | 40 ++++++++++++++++++ core/stream/media_streamer.go | 58 +++++++++++++++++++++++++-- server/e2e/e2e_suite_test.go | 17 ++++++-- server/e2e/subsonic_stream_test.go | 55 +++++++++++++++++++++++++ server/e2e/subsonic_transcode_test.go | 31 ++++++++++++++ server/public/handle_streams.go | 32 ++------------- server/subsonic/stream.go | 54 ++++--------------------- server/subsonic/transcode.go | 6 ++- 9 files changed, 244 insertions(+), 91 deletions(-) diff --git a/core/ffmpeg/ffmpeg.go b/core/ffmpeg/ffmpeg.go index 2b06250a1..33d6733c8 100644 --- a/core/ffmpeg/ffmpeg.go +++ b/core/ffmpeg/ffmpeg.go @@ -1,6 +1,7 @@ package ffmpeg import ( + "bytes" "context" "encoding/json" "errors" @@ -258,10 +259,11 @@ func (e *ffmpeg) start(ctx context.Context, args []string, input ...io.Reader) ( type ffCmd struct { *io.PipeReader - out *io.PipeWriter - args []string - cmd *exec.Cmd - input io.Reader // optional stdin source + out *io.PipeWriter + args []string + cmd *exec.Cmd + input io.Reader // optional stdin source + stderr *bytes.Buffer } func (j *ffCmd) start(ctx context.Context) error { @@ -270,10 +272,12 @@ func (j *ffCmd) start(ctx context.Context) error { if j.input != nil { cmd.Stdin = j.input } + j.stderr = &bytes.Buffer{} + stderrWriter := &limitedWriter{buf: j.stderr, limit: 4096} if log.IsGreaterOrEqualTo(log.LevelTrace) { - cmd.Stderr = os.Stderr + cmd.Stderr = io.MultiWriter(os.Stderr, stderrWriter) } else { - cmd.Stderr = io.Discard + cmd.Stderr = stderrWriter } j.cmd = cmd @@ -287,7 +291,11 @@ func (j *ffCmd) wait() { if err := j.cmd.Wait(); err != nil { var exitErr *exec.ExitError if errors.As(err, &exitErr) { - _ = j.out.CloseWithError(fmt.Errorf("%s exited with non-zero status code: %d", j.args[0], exitErr.ExitCode())) + errMsg := fmt.Sprintf("%s exited with non-zero status code: %d", j.args[0], exitErr.ExitCode()) + if stderrOutput := strings.TrimSpace(j.stderr.String()); stderrOutput != "" { + errMsg += ": " + stderrOutput + } + _ = j.out.CloseWithError(errors.New(errMsg)) } else { _ = j.out.CloseWithError(fmt.Errorf("waiting %s cmd: %w", j.args[0], err)) } @@ -296,6 +304,26 @@ func (j *ffCmd) wait() { _ = j.out.Close() } +// limitedWriter wraps a bytes.Buffer and stops writing once the limit is reached. +// Writes that would exceed the limit are silently discarded to prevent unbounded memory usage. +type limitedWriter struct { + buf *bytes.Buffer + limit int +} + +func (w *limitedWriter) Write(p []byte) (int, error) { + n := len(p) + remaining := w.limit - w.buf.Len() + if remaining <= 0 { + return n, nil // Discard but report success to avoid breaking the writer + } + if len(p) > remaining { + p = p[:remaining] + } + w.buf.Write(p) + return n, nil // Always report full write to avoid ErrShortWrite from io.MultiWriter +} + // formatCodecMap maps target format to ffmpeg codec flag. var formatCodecMap = map[string]string{ "mp3": "libmp3lame", diff --git a/core/ffmpeg/ffmpeg_test.go b/core/ffmpeg/ffmpeg_test.go index 23e419219..04663828f 100644 --- a/core/ffmpeg/ffmpeg_test.go +++ b/core/ffmpeg/ffmpeg_test.go @@ -604,6 +604,46 @@ var _ = Describe("ffmpeg", func() { }) }) + Context("stderr capture", func() { + BeforeEach(func() { + if runtime.GOOS == "windows" { + Skip("stderr capture tests use /bin/sh, skipping on Windows") + } + }) + + It("should include stderr in error when process fails", func() { + ff := &ffmpeg{} + ctx := GinkgoT().Context() + + // Directly call start() with a bash command that writes to stderr and fails + args := []string{"/bin/sh", "-c", "echo 'codec not found: libopus' >&2; exit 1"} + stream, err := ff.start(ctx, args) + Expect(err).ToNot(HaveOccurred()) + defer stream.Close() + + buf := make([]byte, 1024) + _, err = stream.Read(buf) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("codec not found: libopus")) + }) + + It("should not include stderr in error when process succeeds", func() { + ff := &ffmpeg{} + ctx := GinkgoT().Context() + + // Command that writes to stderr but exits successfully + args := []string{"/bin/sh", "-c", "echo 'warning: something' >&2; printf 'output'"} + stream, err := ff.start(ctx, args) + Expect(err).ToNot(HaveOccurred()) + defer stream.Close() + + buf := make([]byte, 1024) + n, err := stream.Read(buf) + Expect(err).ToNot(HaveOccurred()) + Expect(string(buf[:n])).To(Equal("output")) + }) + }) + Context("with mock process behavior", func() { var longRunningCmd string BeforeEach(func() { diff --git a/core/stream/media_streamer.go b/core/stream/media_streamer.go index 062a13884..de03b4d2f 100644 --- a/core/stream/media_streamer.go +++ b/core/stream/media_streamer.go @@ -5,8 +5,9 @@ import ( "fmt" "io" "mime" + "net/http" "os" - "strings" + "strconv" "sync" "time" @@ -17,6 +18,7 @@ import ( "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/utils/cache" + "github.com/navidrome/navidrome/utils/req" ) type MediaStreamer interface { @@ -51,6 +53,9 @@ func (j *streamJob) Key() string { return fmt.Sprintf("%s.%s.%d.%d.%d.%d.%s.%d", j.mf.ID, j.mf.UpdatedAt.Format(time.RFC3339Nano), j.bitRate, j.sampleRate, j.bitDepth, j.channels, j.format, j.offset) } +// NewStream creates a Stream for the given MediaFile and Request. It handles both raw streaming (no transcoding) +// and transcoded streaming based on the requested format and bitrate. It also logs detailed information about +// the streaming request and whether the transcoding result was served from cache or not. func (ms *mediaStreamer) NewStream(ctx context.Context, mf *model.MediaFile, req Request) (*Stream, error) { var format string var bitRate int @@ -133,14 +138,59 @@ func (s *Stream) EstimatedContentLength() int { return int(s.mf.Duration * float32(s.bitRate) / 8 * 1024) } -// NewTestStream creates a Stream for testing purposes. -func NewTestStream(mf *model.MediaFile, format string, bitRate int) *Stream { +// Serve writes the stream to the HTTP response. For seekable streams it uses http.ServeContent +// (supporting range requests). For non-seekable streams it writes directly and logs any errors. +// Returns the number of bytes written and an error only when io.Copy fails with 0 bytes written +// (meaning the HTTP 200 status has not been flushed yet and the caller can still send an error response). +// Empty output (0 bytes, no error) is logged but not treated as an error. +func (s *Stream) Serve(ctx context.Context, w http.ResponseWriter, r *http.Request) (int64, error) { + if s.Seekable() { + http.ServeContent(w, r, s.Name(), s.ModTime(), s) + return -1, nil + } + + w.Header().Set("Accept-Ranges", "none") + w.Header().Set("Content-Type", s.ContentType()) + + if req.Params(r).BoolOr("estimateContentLength", false) { + length := strconv.Itoa(s.EstimatedContentLength()) + log.Trace(ctx, "Estimated content-length", "contentLength", length) + w.Header().Set("Content-Length", length) + } + + if r.Method == http.MethodHead { + go func() { _, _ = io.Copy(io.Discard, s) }() + return 0, nil + } + + id := s.mf.ID + c, err := io.Copy(w, s) + if err != nil { + log.Error(ctx, "Error sending transcoded file", "id", id, err) + if c == 0 { + w.Header().Del("Content-Length") + return 0, fmt.Errorf("sending transcoded file: %w", err) + } + return c, nil + } + if c == 0 { + log.Error(ctx, "Transcoding returned empty output, ffmpeg may have failed. "+ + "Check that ffmpeg supports the requested codec. Enable Trace logging for ffmpeg stderr details", + "id", id, "format", s.ContentType()) + } else { + log.Trace(ctx, "Success sending transcoded file", "id", id, "size", c) + } + return c, nil +} + +// NewStream creates a non-seekable Stream from the given components. +func NewStream(mf *model.MediaFile, format string, bitRate int, r io.ReadCloser) *Stream { return &Stream{ ctx: context.Background(), mf: mf, format: format, bitRate: bitRate, - ReadCloser: io.NopCloser(strings.NewReader("")), + ReadCloser: r, } } diff --git a/server/e2e/e2e_suite_test.go b/server/e2e/e2e_suite_test.go index cb851debf..262a5ed36 100644 --- a/server/e2e/e2e_suite_test.go +++ b/server/e2e/e2e_suite_test.go @@ -11,6 +11,7 @@ import ( "net/url" "os" "path/filepath" + "strings" "testing" "testing/fstest" "time" @@ -287,18 +288,28 @@ func (n noopArtwork) GetOrPlaceholder(_ context.Context, _ string, _ int, _ bool // spyStreamer captures the Request passed to NewStream for test assertions, // then returns a minimal fake Stream so the handler completes without error. type spyStreamer struct { - LastRequest stream.Request - LastMediaFile *model.MediaFile + LastRequest stream.Request + LastMediaFile *model.MediaFile + SimulateError error // When set, NewStream returns this error + SimulateEmptyStream bool // When true, returns a 0-byte stream (simulates ffmpeg producing no output) } func (s *spyStreamer) NewStream(_ context.Context, mf *model.MediaFile, req stream.Request) (*stream.Stream, error) { s.LastRequest = req s.LastMediaFile = mf + if s.SimulateError != nil { + return nil, s.SimulateError + } format := req.Format if format == "" || format == "raw" { format = mf.Suffix } - return stream.NewTestStream(mf, format, req.BitRate), nil + content := "fake audio data" + if s.SimulateEmptyStream { + content = "" + } + r := io.NopCloser(strings.NewReader(content)) + return stream.NewStream(mf, format, req.BitRate, r), nil } // noopFFmpeg implements ffmpeg.FFmpeg with no-op methods. diff --git a/server/e2e/subsonic_stream_test.go b/server/e2e/subsonic_stream_test.go index d144dc4eb..6a11c1740 100644 --- a/server/e2e/subsonic_stream_test.go +++ b/server/e2e/subsonic_stream_test.go @@ -1,9 +1,12 @@ package e2e import ( + "encoding/json" + "errors" "net/http" "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/server/subsonic/responses" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -124,4 +127,56 @@ var _ = Describe("stream.view (legacy streaming)", Ordered, func() { Expect(streamerSpy.LastRequest.Offset).To(Equal(30)) }) }) + + Describe("stream creation failure", func() { + BeforeEach(func() { + streamerSpy.SimulateError = errors.New("ffmpeg exited with non-zero status code: 1: Unknown encoder 'libopus'") + }) + AfterEach(func() { + streamerSpy.SimulateError = nil + }) + + It("returns a Subsonic error for stream endpoint", func() { + w := doRawReq("stream", "id", flacTrackID, "format", "opus") + Expect(w.Code).To(Equal(http.StatusOK)) // Subsonic errors are returned as 200 + + var wrapper responses.JsonWrapper + Expect(json.Unmarshal(w.Body.Bytes(), &wrapper)).To(Succeed()) + Expect(wrapper.Subsonic.Status).To(Equal(responses.StatusFailed)) + Expect(wrapper.Subsonic.Error).ToNot(BeNil()) + }) + + It("returns a Subsonic error for download endpoint", func() { + conf.Server.EnableDownloads = true + w := doRawReq("download", "id", flacTrackID, "format", "opus") + Expect(w.Code).To(Equal(http.StatusOK)) + + var wrapper responses.JsonWrapper + Expect(json.Unmarshal(w.Body.Bytes(), &wrapper)).To(Succeed()) + Expect(wrapper.Subsonic.Status).To(Equal(responses.StatusFailed)) + Expect(wrapper.Subsonic.Error).ToNot(BeNil()) + }) + }) + + Describe("empty transcoded output", func() { + BeforeEach(func() { + streamerSpy.SimulateEmptyStream = true + }) + AfterEach(func() { + streamerSpy.SimulateEmptyStream = false + }) + + It("returns 200 with empty body for stream endpoint", func() { + w := doRawReq("stream", "id", flacTrackID, "format", "opus") + Expect(w.Code).To(Equal(http.StatusOK)) + Expect(w.Body.Len()).To(Equal(0)) + }) + + It("returns 200 with empty body for download endpoint", func() { + conf.Server.EnableDownloads = true + w := doRawReq("download", "id", flacTrackID, "format", "opus") + Expect(w.Code).To(Equal(http.StatusOK)) + Expect(w.Body.Len()).To(Equal(0)) + }) + }) }) diff --git a/server/e2e/subsonic_transcode_test.go b/server/e2e/subsonic_transcode_test.go index a9a180dc8..f134448df 100644 --- a/server/e2e/subsonic_transcode_test.go +++ b/server/e2e/subsonic_transcode_test.go @@ -1,6 +1,7 @@ package e2e import ( + "errors" "net/http" "time" @@ -602,6 +603,36 @@ var _ = Describe("Transcode Endpoints", Ordered, func() { mf.UpdatedAt = originalUpdatedAt Expect(ds.MediaFile(ctx).Put(mf)).To(Succeed()) }) + + It("returns 500 when stream creation fails", func() { + // Get a valid decision token + resp := doPostReq("getTranscodeDecision", mp3OnlyClient, "mediaId", flacTrackID, "mediaType", "song") + Expect(resp.Status).To(Equal(responses.StatusOK)) + token := resp.TranscodeDecision.TranscodeParams + Expect(token).ToNot(BeEmpty()) + + // Simulate streamer failure (e.g., ffmpeg missing codec) + streamerSpy.SimulateError = errors.New("ffmpeg exited with non-zero status code: 1: Unknown encoder 'libopus'") + defer func() { streamerSpy.SimulateError = nil }() + + w := doRawReq("getTranscodeStream", "mediaId", flacTrackID, "mediaType", "song", "transcodeParams", token) + Expect(w.Code).To(Equal(http.StatusInternalServerError)) + }) + + It("returns 500 when transcoded stream is empty", func() { + // Get a valid decision token + resp := doPostReq("getTranscodeDecision", mp3OnlyClient, "mediaId", flacTrackID, "mediaType", "song") + Expect(resp.Status).To(Equal(responses.StatusOK)) + token := resp.TranscodeDecision.TranscodeParams + Expect(token).ToNot(BeEmpty()) + + // Simulate ffmpeg producing 0 bytes + streamerSpy.SimulateEmptyStream = true + defer func() { streamerSpy.SimulateEmptyStream = false }() + + w := doRawReq("getTranscodeStream", "mediaId", flacTrackID, "mediaType", "song", "transcodeParams", token) + Expect(w.Code).To(Equal(http.StatusInternalServerError)) + }) }) Describe("round-trip: decision then stream", func() { diff --git a/server/public/handle_streams.go b/server/public/handle_streams.go index 6cdf8b44a..daa09c375 100644 --- a/server/public/handle_streams.go +++ b/server/public/handle_streams.go @@ -2,7 +2,6 @@ package public import ( "errors" - "io" "net/http" "strconv" @@ -54,34 +53,9 @@ func (pub *Router) handleStream(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Content-Duration", strconv.FormatFloat(float64(stream.Duration()), 'G', -1, 32)) - if stream.Seekable() { - http.ServeContent(w, r, stream.Name(), stream.ModTime(), stream) - } else { - // If the stream doesn't provide a size (i.e. is not seekable), we can't support ranges/content-length - w.Header().Set("Accept-Ranges", "none") - w.Header().Set("Content-Type", stream.ContentType()) - - estimateContentLength := p.BoolOr("estimateContentLength", false) - - // if Client requests the estimated content-length, send it - if estimateContentLength { - length := strconv.Itoa(stream.EstimatedContentLength()) - log.Trace(ctx, "Estimated content-length", "contentLength", length) - w.Header().Set("Content-Length", length) - } - - if r.Method == http.MethodHead { - go func() { _, _ = io.Copy(io.Discard, stream) }() - } else { - c, err := io.Copy(w, stream) - if log.IsGreaterOrEqualTo(log.LevelDebug) { - if err != nil { - log.Error(ctx, "Error sending shared transcoded file", "id", info.id, err) - } else { - log.Trace(ctx, "Success sending shared transcode file", "id", info.id, "size", c) - } - } - } + n, err := stream.Serve(ctx, w, r) + if err != nil || n == 0 { + http.Error(w, "internal error", http.StatusInternalServerError) } } diff --git a/server/subsonic/stream.go b/server/subsonic/stream.go index ebebb97f1..b49af2b24 100644 --- a/server/subsonic/stream.go +++ b/server/subsonic/stream.go @@ -1,15 +1,12 @@ package subsonic import ( - "context" "fmt" - "io" "net/http" "strconv" "strings" "github.com/navidrome/navidrome/conf" - "github.com/navidrome/navidrome/core/stream" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" @@ -17,38 +14,6 @@ import ( "github.com/navidrome/navidrome/utils/req" ) -func (api *Router) serveStream(ctx context.Context, w http.ResponseWriter, r *http.Request, stream *stream.Stream, id string) { - if stream.Seekable() { - http.ServeContent(w, r, stream.Name(), stream.ModTime(), stream) - } else { - // If the stream doesn't provide a size (i.e. is not seekable), we can't support ranges/content-length - w.Header().Set("Accept-Ranges", "none") - w.Header().Set("Content-Type", stream.ContentType()) - - estimateContentLength := req.Params(r).BoolOr("estimateContentLength", false) - - // if Client requests the estimated content-length, send it - if estimateContentLength { - length := strconv.Itoa(stream.EstimatedContentLength()) - log.Trace(ctx, "Estimated content-length", "contentLength", length) - w.Header().Set("Content-Length", length) - } - - if r.Method == http.MethodHead { - go func() { _, _ = io.Copy(io.Discard, stream) }() - } else { - c, err := io.Copy(w, stream) - if log.IsGreaterOrEqualTo(log.LevelDebug) { - if err != nil { - log.Error(ctx, "Error sending transcoded file", "id", id, err) - } else { - log.Trace(ctx, "Success sending transcode file", "id", id, "size", c) - } - } - } - } -} - func (api *Router) Stream(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { ctx := r.Context() p := req.Params(r) @@ -81,9 +46,8 @@ func (api *Router) Stream(w http.ResponseWriter, r *http.Request) (*responses.Su w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Content-Duration", strconv.FormatFloat(float64(stream.Duration()), 'G', -1, 32)) - api.serveStream(ctx, w, r, stream, id) - - return nil, nil + _, err = stream.Serve(ctx, w, r) + return nil, err } func (api *Router) Download(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { @@ -151,20 +115,18 @@ func (api *Router) Download(w http.ResponseWriter, r *http.Request) (*responses. disposition := fmt.Sprintf("attachment; filename=\"%s\"", stream.Name()) w.Header().Set("Content-Disposition", disposition) - api.serveStream(ctx, w, r, stream, id) - return nil, nil + _, err = stream.Serve(ctx, w, r) + return nil, err case *model.Album: setHeaders(v.Name) - err = api.archiver.ZipAlbum(ctx, id, format, maxBitRate, w) + return nil, api.archiver.ZipAlbum(ctx, id, format, maxBitRate, w) case *model.Artist: setHeaders(v.Name) - err = api.archiver.ZipArtist(ctx, id, format, maxBitRate, w) + return nil, api.archiver.ZipArtist(ctx, id, format, maxBitRate, w) case *model.Playlist: setHeaders(v.Name) - err = api.archiver.ZipPlaylist(ctx, id, format, maxBitRate, w) + return nil, api.archiver.ZipPlaylist(ctx, id, format, maxBitRate, w) default: - err = model.ErrNotFound + return nil, model.ErrNotFound } - - return nil, err } diff --git a/server/subsonic/transcode.go b/server/subsonic/transcode.go index 64e74d460..4e494b324 100644 --- a/server/subsonic/transcode.go +++ b/server/subsonic/transcode.go @@ -395,7 +395,9 @@ func (api *Router) GetTranscodeStream(w http.ResponseWriter, r *http.Request) (* w.Header().Set("X-Content-Type-Options", "nosniff") - api.serveStream(ctx, w, r, stream, mediaID) - + n, err := stream.Serve(ctx, w, r) + if err != nil || n == 0 { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } return nil, nil } From ba8d4278900066aee27319998a0c549a6941572d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= <deluan@navidrome.org> Date: Wed, 18 Mar 2026 18:57:33 -0400 Subject: [PATCH 43/81] feat(ui): add cover art support for internet radio stations (#5229) * feat(artwork): add KindRadioArtwork and EntityRadio constant * feat(model): add UploadedImage field and artwork methods to Radio * feat(model): add Radio to GetEntityByID lookup chain * feat(db): add uploaded_image column to radio table * feat(artwork): add radio artwork reader with uploaded image fallback * feat(api): add radio image upload/delete endpoints * feat(ui): add radio artwork ID prefix to getCoverArtUrl * feat(ui): add cover art display and upload to RadioEdit * feat(ui): add cover art thumbnails to radio list * feat(ui): prefer artwork URL in radio player helper * refactor: remove redundant code in radio artwork - Remove duplicate Avatar rendering in RadioList by reusing CoverArtField - Remove redundant UpdatedAt assignment in radio image handlers (already set by repository Put) * refactor(ui): extract shared useImageLoadingState hook Move image loading/error/lightbox state management into a shared useImageLoadingState hook in common/. Consolidates duplicated logic from AlbumDetails, PlaylistDetails, RadioEdit, and artist detail views. * feat(ui): use radio placeholder icon when no uploaded image Remove album placeholder fallback from radio artwork reader so radios without an uploaded image return ErrUnavailable. On the frontend, show the internet-radio-icon.svg placeholder instead of requesting server artwork when no image is uploaded, allowing favicon fallback in the player. * refactor(ui): update defaultOff fields in useSelectedFields for RadioList Signed-off-by: Deluan <deluan@navidrome.org> * fix: address code review feedback - Add missing alt attribute to CardMedia in RadioEdit for accessibility - Fix UpdateInternetRadio to preserve UploadedImage field by fetching existing radio before updating (prevents Subsonic API from clearing custom artwork) - Add Reader() level tests to verify ErrUnavailable is returned when radio has no uploaded image * refactor: add colsToUpdate to RadioRepository.Put Use the base sqlRepository.put with column filtering instead of hand-rolled SQL. UpdateInternetRadio now specifies only the Subsonic API fields, preventing UploadedImage from being cleared. Image upload/delete handlers specify only UploadedImage. * fix: ensure UpdatedAt is included in colsToUpdate for radio Put --------- Signed-off-by: Deluan <deluan@navidrome.org> --- consts/consts.go | 1 + core/artwork/artwork.go | 2 + core/artwork/reader_radio.go | 40 +++++++++ core/artwork/reader_radio_test.go | 84 +++++++++++++++++++ ...20260318182414_add_radio_uploaded_image.go | 22 +++++ model/artwork_id.go | 10 +++ model/get_entity.go | 4 + model/radio.go | 29 +++++-- model/radio_test.go | 42 ++++++++++ persistence/radio_repository.go | 24 ++---- server/nativeapi/native_api.go | 2 +- server/nativeapi/radios.go | 70 ++++++++++++++++ server/subsonic/radio.go | 2 +- tests/mock_radio_repository.go | 2 +- ui/src/album/AlbumDetails.jsx | 39 +++------ ui/src/artist/DesktopArtistDetails.jsx | 10 ++- ui/src/artist/MobileArtistDetails.jsx | 10 ++- ui/src/common/index.js | 1 + .../useImageLoadingState.js} | 10 +-- ui/src/consts.js | 2 + ui/src/playlist/PlaylistDetails.jsx | 38 +++------ ui/src/radio/RadioEdit.jsx | 67 ++++++++++++++- ui/src/radio/RadioList.jsx | 21 ++++- ui/src/radio/helper.jsx | 24 ++++-- ui/src/subsonic/index.js | 3 + 25 files changed, 450 insertions(+), 109 deletions(-) create mode 100644 core/artwork/reader_radio.go create mode 100644 core/artwork/reader_radio_test.go create mode 100644 db/migrations/20260318182414_add_radio_uploaded_image.go create mode 100644 model/radio_test.go create mode 100644 server/nativeapi/radios.go rename ui/src/{artist/useArtistImageState.js => common/useImageLoadingState.js} (76%) diff --git a/consts/consts.go b/consts/consts.go index 38b277eab..6fb6c5dac 100644 --- a/consts/consts.go +++ b/consts/consts.go @@ -108,6 +108,7 @@ const ( const ( EntityArtist = "artist" EntityPlaylist = "playlist" + EntityRadio = "radio" ) const ( diff --git a/core/artwork/artwork.go b/core/artwork/artwork.go index 4a0a32afc..b8c395c12 100644 --- a/core/artwork/artwork.go +++ b/core/artwork/artwork.go @@ -124,6 +124,8 @@ func (a *artwork) getArtworkReader(ctx context.Context, artID model.ArtworkID, s artReader, err = newPlaylistArtworkReader(ctx, a, artID) case model.KindDiscArtwork: artReader, err = newDiscArtworkReader(ctx, a, artID) + case model.KindRadioArtwork: + artReader, err = newRadioArtworkReader(ctx, a, artID) default: return nil, ErrUnavailable } diff --git a/core/artwork/reader_radio.go b/core/artwork/reader_radio.go new file mode 100644 index 000000000..22db6e302 --- /dev/null +++ b/core/artwork/reader_radio.go @@ -0,0 +1,40 @@ +package artwork + +import ( + "context" + "io" + "time" + + "github.com/navidrome/navidrome/model" +) + +type radioArtworkReader struct { + cacheKey + a *artwork + radio model.Radio +} + +func newRadioArtworkReader(ctx context.Context, artwork *artwork, artID model.ArtworkID) (*radioArtworkReader, error) { + r, err := artwork.ds.Radio(ctx).Get(artID.ID) + if err != nil { + return nil, err + } + a := &radioArtworkReader{a: artwork, radio: *r} + a.cacheKey.artID = artID + a.cacheKey.lastUpdate = r.UpdatedAt + return a, nil +} + +func (a *radioArtworkReader) LastUpdated() time.Time { + return a.lastUpdate +} + +func (a *radioArtworkReader) Reader(ctx context.Context) (io.ReadCloser, string, error) { + return selectImageReader(ctx, a.artID, + a.fromRadioUploadedImage(), + ) +} + +func (a *radioArtworkReader) fromRadioUploadedImage() sourceFunc { + return fromLocalFile(a.radio.UploadedImagePath()) +} diff --git a/core/artwork/reader_radio_test.go b/core/artwork/reader_radio_test.go new file mode 100644 index 000000000..1f5bc9084 --- /dev/null +++ b/core/artwork/reader_radio_test.go @@ -0,0 +1,84 @@ +package artwork + +import ( + "context" + "os" + "path/filepath" + + "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/conf/configtest" + "github.com/navidrome/navidrome/model" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("radioArtworkReader", func() { + var ( + tempDir string + reader *radioArtworkReader + ) + + BeforeEach(func() { + DeferCleanup(configtest.SetupConfig()) + tempDir = GinkgoT().TempDir() + conf.Server.DataFolder = tempDir + + Expect(os.MkdirAll(filepath.Join(tempDir, "artwork", "radio"), 0755)).To(Succeed()) + + reader = &radioArtworkReader{} + }) + + Describe("fromRadioUploadedImage", func() { + When("radio has an uploaded image", func() { + It("returns the uploaded image", func() { + imgPath := filepath.Join(tempDir, "artwork", "radio", "rd-1_test.jpg") + Expect(os.WriteFile(imgPath, []byte("uploaded radio image"), 0600)).To(Succeed()) + + reader.radio = model.Radio{ID: "rd-1", UploadedImage: "rd-1_test.jpg"} + sf := reader.fromRadioUploadedImage() + r, path, err := sf() + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + Expect(path).To(Equal(imgPath)) + r.Close() + }) + }) + + When("radio has no uploaded image", func() { + It("returns nil reader (falls through)", func() { + reader.radio = model.Radio{ID: "rd-1"} + sf := reader.fromRadioUploadedImage() + r, path, err := sf() + Expect(err).ToNot(HaveOccurred()) + Expect(r).To(BeNil()) + Expect(path).To(BeEmpty()) + }) + }) + }) + + Describe("Reader", func() { + When("radio has an uploaded image", func() { + It("returns the image reader", func() { + imgPath := filepath.Join(tempDir, "artwork", "radio", "rd-1_test.jpg") + Expect(os.WriteFile(imgPath, []byte("uploaded radio image"), 0600)).To(Succeed()) + + reader.radio = model.Radio{ID: "rd-1", UploadedImage: "rd-1_test.jpg"} + reader.cacheKey.artID = model.ArtworkID{Kind: model.KindRadioArtwork, ID: "rd-1"} + r, _, err := reader.Reader(context.Background()) + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + r.Close() + }) + }) + + When("radio has no uploaded image", func() { + It("returns ErrUnavailable", func() { + reader.radio = model.Radio{ID: "rd-1"} + reader.cacheKey.artID = model.ArtworkID{Kind: model.KindRadioArtwork, ID: "rd-1"} + r, _, err := reader.Reader(context.Background()) + Expect(err).To(MatchError(ErrUnavailable)) + Expect(r).To(BeNil()) + }) + }) + }) +}) diff --git a/db/migrations/20260318182414_add_radio_uploaded_image.go b/db/migrations/20260318182414_add_radio_uploaded_image.go new file mode 100644 index 000000000..e92a6d2ef --- /dev/null +++ b/db/migrations/20260318182414_add_radio_uploaded_image.go @@ -0,0 +1,22 @@ +package migrations + +import ( + "context" + "database/sql" + + "github.com/pressly/goose/v3" +) + +func init() { + goose.AddMigrationContext(upAddRadioUploadedImage, downAddRadioUploadedImage) +} + +func upAddRadioUploadedImage(ctx context.Context, tx *sql.Tx) error { + _, err := tx.ExecContext(ctx, `ALTER TABLE radio ADD COLUMN uploaded_image VARCHAR(255) NOT NULL DEFAULT ''`) + return err +} + +func downAddRadioUploadedImage(ctx context.Context, tx *sql.Tx) error { + // This code is executed when the migration is rolled back. + return nil +} diff --git a/model/artwork_id.go b/model/artwork_id.go index 8d935f427..1bd146c1f 100644 --- a/model/artwork_id.go +++ b/model/artwork_id.go @@ -23,6 +23,7 @@ var ( KindAlbumArtwork = Kind{"al", "album"} KindPlaylistArtwork = Kind{"pl", "playlist"} KindDiscArtwork = Kind{"dc", "disc"} + KindRadioArtwork = Kind{"ra", "radio"} ) var artworkKindMap = map[string]Kind{ @@ -31,6 +32,7 @@ var artworkKindMap = map[string]Kind{ KindAlbumArtwork.prefix: KindAlbumArtwork, KindPlaylistArtwork.prefix: KindPlaylistArtwork, KindDiscArtwork.prefix: KindDiscArtwork, + KindRadioArtwork.prefix: KindRadioArtwork, } type ArtworkID struct { @@ -139,3 +141,11 @@ func artworkIDFromArtist(ar Artist) ArtworkID { ID: ar.ID, } } + +func artworkIDFromRadio(r Radio) ArtworkID { + return ArtworkID{ + Kind: KindRadioArtwork, + ID: r.ID, + LastUpdate: r.UpdatedAt, + } +} diff --git a/model/get_entity.go b/model/get_entity.go index 26f718396..60972b2e9 100644 --- a/model/get_entity.go +++ b/model/get_entity.go @@ -22,5 +22,9 @@ func GetEntityByID(ctx context.Context, ds DataStore, id string) (any, error) { if err == nil { return mf, nil } + r, err := ds.Radio(ctx).Get(id) + if err == nil { + return r, nil + } return nil, err } diff --git a/model/radio.go b/model/radio.go index 567d32e44..86f27c24c 100644 --- a/model/radio.go +++ b/model/radio.go @@ -1,14 +1,27 @@ package model -import "time" +import ( + "time" + + "github.com/navidrome/navidrome/consts" +) type Radio struct { - ID string `structs:"id" json:"id"` - StreamUrl string `structs:"stream_url" json:"streamUrl"` - Name string `structs:"name" json:"name"` - HomePageUrl string `structs:"home_page_url" json:"homePageUrl"` - CreatedAt time.Time `structs:"created_at" json:"createdAt"` - UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"` + ID string `structs:"id" json:"id"` + StreamUrl string `structs:"stream_url" json:"streamUrl"` + Name string `structs:"name" json:"name"` + HomePageUrl string `structs:"home_page_url" json:"homePageUrl"` + UploadedImage string `structs:"uploaded_image" json:"uploadedImage,omitempty"` + CreatedAt time.Time `structs:"created_at" json:"createdAt"` + UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"` +} + +func (r Radio) CoverArtID() ArtworkID { + return artworkIDFromRadio(r) +} + +func (r Radio) UploadedImagePath() string { + return UploadedImagePath(consts.EntityRadio, r.UploadedImage) } type Radios []Radio @@ -19,5 +32,5 @@ type RadioRepository interface { Delete(id string) error Get(id string) (*Radio, error) GetAll(options ...QueryOptions) (Radios, error) - Put(u *Radio) error + Put(u *Radio, colsToUpdate ...string) error } diff --git a/model/radio_test.go b/model/radio_test.go new file mode 100644 index 000000000..dc421454e --- /dev/null +++ b/model/radio_test.go @@ -0,0 +1,42 @@ +package model_test + +import ( + "path/filepath" + "time" + + "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/conf/configtest" + "github.com/navidrome/navidrome/model" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Radio", func() { + Describe("CoverArtID", func() { + It("returns a radio artwork ID", func() { + now := time.Now() + r := model.Radio{ID: "rd-1", UpdatedAt: now} + artID := r.CoverArtID() + Expect(artID.Kind).To(Equal(model.KindRadioArtwork)) + Expect(artID.ID).To(Equal("rd-1")) + Expect(artID.LastUpdate).To(Equal(now)) + }) + }) + + Describe("UploadedImagePath", func() { + BeforeEach(func() { + DeferCleanup(configtest.SetupConfig()) + conf.Server.DataFolder = "/data" + }) + + It("returns empty string when no image uploaded", func() { + r := model.Radio{ID: "rd-1"} + Expect(r.UploadedImagePath()).To(BeEmpty()) + }) + + It("returns full path when image is set", func() { + r := model.Radio{ID: "rd-1", UploadedImage: "rd-1_test.jpg"} + Expect(r.UploadedImagePath()).To(Equal(filepath.Join("/data", "artwork", "radio", "rd-1_test.jpg"))) + }) + }) +}) diff --git a/persistence/radio_repository.go b/persistence/radio_repository.go index 543b76c5e..a073643db 100644 --- a/persistence/radio_repository.go +++ b/persistence/radio_repository.go @@ -58,34 +58,20 @@ func (r *radioRepository) GetAll(options ...model.QueryOptions) (model.Radios, e return res, err } -func (r *radioRepository) Put(radio *model.Radio) error { +func (r *radioRepository) Put(radio *model.Radio, colsToUpdate ...string) error { if !r.isPermitted() { return rest.ErrPermissionDenied } - var values map[string]any - radio.UpdatedAt = time.Now() - if radio.ID == "" { radio.CreatedAt = time.Now() radio.ID = id.NewRandom() - values, _ = toSQLArgs(*radio) - } else { - values, _ = toSQLArgs(*radio) - update := Update(r.tableName).Where(Eq{"id": radio.ID}).SetMap(values) - count, err := r.executeSQL(update) - - if err != nil { - return err - } else if count > 0 { - return nil - } } - - values["created_at"] = time.Now() - insert := Insert(r.tableName).SetMap(values) - _, err := r.executeSQL(insert) + if len(colsToUpdate) > 0 { + colsToUpdate = append(colsToUpdate, "UpdatedAt") + } + _, err := r.put(radio.ID, radio, colsToUpdate...) return err } diff --git a/server/nativeapi/native_api.go b/server/nativeapi/native_api.go index 3ef00ebb1..669c4d7b5 100644 --- a/server/nativeapi/native_api.go +++ b/server/nativeapi/native_api.go @@ -71,7 +71,7 @@ func (api *Router) routes() http.Handler { api.R(r, "/genre", model.Genre{}, false) api.R(r, "/player", model.Player{}, true) api.R(r, "/transcoding", model.Transcoding{}, conf.Server.EnableTranscodingConfig) - api.R(r, "/radio", model.Radio{}, true) + api.addRadioRoute(r) api.R(r, "/tag", model.Tag{}, true) if conf.Server.EnableSharing { api.RX(r, "/share", api.share.NewRepository, true) diff --git a/server/nativeapi/radios.go b/server/nativeapi/radios.go new file mode 100644 index 000000000..701c6c926 --- /dev/null +++ b/server/nativeapi/radios.go @@ -0,0 +1,70 @@ +package nativeapi + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/deluan/rest" + "github.com/go-chi/chi/v5" + "github.com/navidrome/navidrome/consts" + "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/server" +) + +func (api *Router) addRadioRoute(r chi.Router) { + constructor := func(ctx context.Context) rest.Repository { + return api.ds.Resource(ctx, model.Radio{}) + } + r.Route("/radio", func(r chi.Router) { + r.Get("/", rest.GetAll(constructor)) + r.Post("/", rest.Post(constructor)) + r.Route("/{id}", func(r chi.Router) { + r.Use(server.URLParamsMiddleware) + r.Get("/", rest.Get(constructor)) + r.Put("/", rest.Put(constructor)) + r.Delete("/", rest.Delete(constructor)) + r.Post("/image", api.uploadRadioImage()) + r.Delete("/image", api.deleteRadioImage()) + }) + }) +} + +func (api *Router) uploadRadioImage() http.HandlerFunc { + return handleImageUpload(func(ctx context.Context, reader io.Reader, ext string) error { + radioID := chi.URLParamFromCtx(ctx, "id") + radio, err := api.ds.Radio(ctx).Get(radioID) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return model.ErrNotFound + } + return err + } + oldPath := radio.UploadedImagePath() + filename, err := api.imgUpload.SetImage(ctx, consts.EntityRadio, radio.ID, radio.Name, oldPath, reader, ext) + if err != nil { + return err + } + radio.UploadedImage = filename + return api.ds.Radio(ctx).Put(radio, "UploadedImage") + }) +} + +func (api *Router) deleteRadioImage() http.HandlerFunc { + return handleImageDelete(func(ctx context.Context) error { + radioID := chi.URLParamFromCtx(ctx, "id") + radio, err := api.ds.Radio(ctx).Get(radioID) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return model.ErrNotFound + } + return err + } + if err := api.imgUpload.RemoveImage(ctx, radio.UploadedImagePath()); err != nil { + return err + } + radio.UploadedImage = "" + return api.ds.Radio(ctx).Put(radio, "UploadedImage") + }) +} diff --git a/server/subsonic/radio.go b/server/subsonic/radio.go index 9f2cd48f6..c66268344 100644 --- a/server/subsonic/radio.go +++ b/server/subsonic/radio.go @@ -103,7 +103,7 @@ func (api *Router) UpdateInternetRadio(r *http.Request) (*responses.Subsonic, er Name: name, } - err = api.ds.Radio(ctx).Put(radio) + err = api.ds.Radio(ctx).Put(radio, "StreamUrl", "HomePageUrl", "Name") if err != nil { return nil, err } diff --git a/tests/mock_radio_repository.go b/tests/mock_radio_repository.go index 279b735db..c50a529e5 100644 --- a/tests/mock_radio_repository.go +++ b/tests/mock_radio_repository.go @@ -73,7 +73,7 @@ func (m *MockedRadioRepo) GetAll(qo ...model.QueryOptions) (model.Radios, error) return m.All, nil } -func (m *MockedRadioRepo) Put(radio *model.Radio) error { +func (m *MockedRadioRepo) Put(radio *model.Radio, _ ...string) error { if m.Err { return errors.New("error") } diff --git a/ui/src/album/AlbumDetails.jsx b/ui/src/album/AlbumDetails.jsx index bd6a41523..c5d9a7ac4 100644 --- a/ui/src/album/AlbumDetails.jsx +++ b/ui/src/album/AlbumDetails.jsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { Card, CardContent, @@ -29,6 +29,7 @@ import { RatingField, SizeField, useAlbumsPerPage, + useImageLoadingState, } from '../common' import config from '../config' import { formatFullDate, intersperse } from '../utils' @@ -220,11 +221,17 @@ const AlbumDetails = (props) => { const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs')) const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('lg')) const classes = useStyles() - const [isLightboxOpen, setLightboxOpen] = useState(false) const [expanded, setExpanded] = useState(false) const [albumInfo, setAlbumInfo] = useState() - const [imageLoading, setImageLoading] = useState(false) - const [imageError, setImageError] = useState(false) + const { + imageLoading, + imageError, + isLightboxOpen, + handleImageLoad, + handleImageError, + handleOpenLightbox, + handleCloseLightbox, + } = useImageLoadingState(record.id) let notes = albumInfo?.notes || record.notes @@ -247,33 +254,9 @@ const AlbumDetails = (props) => { }) }, [record]) - // Reset image state when album changes - useEffect(() => { - setImageLoading(true) - setImageError(false) - }, [record.id]) - const imageUrl = subsonic.getCoverArtUrl(record, 300) const fullImageUrl = subsonic.getCoverArtUrl(record) - const handleImageLoad = useCallback(() => { - setImageLoading(false) - setImageError(false) - }, []) - - const handleImageError = useCallback(() => { - setImageLoading(false) - setImageError(true) - }, []) - - const handleOpenLightbox = useCallback(() => { - if (!imageError) { - setLightboxOpen(true) - } - }, [imageError]) - - const handleCloseLightbox = useCallback(() => setLightboxOpen(false), []) - return ( <Card className={classes.root}> <div className={classes.cardContents}> diff --git a/ui/src/artist/DesktopArtistDetails.jsx b/ui/src/artist/DesktopArtistDetails.jsx index da8d06014..7052b1634 100644 --- a/ui/src/artist/DesktopArtistDetails.jsx +++ b/ui/src/artist/DesktopArtistDetails.jsx @@ -6,13 +6,17 @@ import CardContent from '@material-ui/core/CardContent' import CardMedia from '@material-ui/core/CardMedia' import ArtistExternalLinks from './ArtistExternalLink' import config from '../config' -import { LoveButton, RatingField, ImageUploadOverlay } from '../common' +import { + LoveButton, + RatingField, + ImageUploadOverlay, + useImageLoadingState, +} from '../common' import Lightbox from 'react-image-lightbox' import ExpandInfoDialog from '../dialogs/ExpandInfoDialog' import AlbumInfo from '../album/AlbumInfo' import subsonic from '../subsonic' import { SafeHTML } from '../common/SafeHTML' -import useArtistImageState from './useArtistImageState' const useStyles = makeStyles( (theme) => ({ @@ -95,7 +99,7 @@ const DesktopArtistDetails = ({ artistInfo, record, biography }) => { handleImageError, handleOpenLightbox, handleCloseLightbox, - } = useArtistImageState(record.id) + } = useImageLoadingState(record.id) return ( <div className={classes.root}> diff --git a/ui/src/artist/MobileArtistDetails.jsx b/ui/src/artist/MobileArtistDetails.jsx index 3add1e994..03cc4de8f 100644 --- a/ui/src/artist/MobileArtistDetails.jsx +++ b/ui/src/artist/MobileArtistDetails.jsx @@ -4,11 +4,15 @@ import { makeStyles } from '@material-ui/core/styles' import Card from '@material-ui/core/Card' import CardMedia from '@material-ui/core/CardMedia' import config from '../config' -import { LoveButton, RatingField, ImageUploadOverlay } from '../common' +import { + LoveButton, + RatingField, + ImageUploadOverlay, + useImageLoadingState, +} from '../common' import Lightbox from 'react-image-lightbox' import subsonic from '../subsonic' import { SafeHTML } from '../common/SafeHTML' -import useArtistImageState from './useArtistImageState' const useStyles = makeStyles( (theme) => ({ @@ -97,7 +101,7 @@ const MobileArtistDetails = ({ artistInfo, biography, record }) => { handleImageError, handleOpenLightbox, handleCloseLightbox, - } = useArtistImageState(record.id) + } = useImageLoadingState(record.id) return ( <> diff --git a/ui/src/common/index.js b/ui/src/common/index.js index b93e40219..a7d6a43c4 100644 --- a/ui/src/common/index.js +++ b/ui/src/common/index.js @@ -45,3 +45,4 @@ export * from './OverflowTooltip' export * from './useSearchRefocus' export * from './ImageUploadOverlay' export * from './CoverArtAvatar' +export * from './useImageLoadingState' diff --git a/ui/src/artist/useArtistImageState.js b/ui/src/common/useImageLoadingState.js similarity index 76% rename from ui/src/artist/useArtistImageState.js rename to ui/src/common/useImageLoadingState.js index bd7e4ad96..3528b0f3f 100644 --- a/ui/src/artist/useArtistImageState.js +++ b/ui/src/common/useImageLoadingState.js @@ -1,11 +1,11 @@ import { useState, useEffect, useCallback } from 'react' /** - * Manages image loading/error state and lightbox open/close for artist detail views. - * Resets when record.id changes. + * Manages image loading/error state and lightbox open/close. + * Resets when recordId changes. */ -const useArtistImageState = (recordId) => { - const [imageLoading, setImageLoading] = useState(false) +export const useImageLoadingState = (recordId) => { + const [imageLoading, setImageLoading] = useState(true) const [imageError, setImageError] = useState(false) const [isLightboxOpen, setLightboxOpen] = useState(false) @@ -42,5 +42,3 @@ const useArtistImageState = (recordId) => { handleCloseLightbox, } } - -export default useArtistImageState diff --git a/ui/src/consts.js b/ui/src/consts.js index 30731a080..472cd4940 100644 --- a/ui/src/consts.js +++ b/ui/src/consts.js @@ -24,6 +24,8 @@ DraggableTypes.ALL.push( DraggableTypes.ARTIST, ) +export const RADIO_PLACEHOLDER_IMAGE = 'internet-radio-icon.svg' + export const DEFAULT_SHARE_BITRATE = 128 export const BITRATE_CHOICES = [ diff --git a/ui/src/playlist/PlaylistDetails.jsx b/ui/src/playlist/PlaylistDetails.jsx index a2d5e753b..911e6c716 100644 --- a/ui/src/playlist/PlaylistDetails.jsx +++ b/ui/src/playlist/PlaylistDetails.jsx @@ -7,7 +7,6 @@ import { } from '@material-ui/core' import { makeStyles } from '@material-ui/core/styles' import { useTranslate } from 'react-admin' -import { useCallback, useState, useEffect } from 'react' import Lightbox from 'react-image-lightbox' import 'react-image-lightbox/style.css' import { @@ -17,6 +16,7 @@ import { SizeField, isWritable, OverflowTooltip, + useImageLoadingState, } from '../common' import subsonic from '../subsonic' @@ -96,37 +96,19 @@ const PlaylistDetails = (props) => { const translate = useTranslate() const classes = useStyles() const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('lg')) - const [isLightboxOpen, setLightboxOpen] = useState(false) - const [imageLoading, setImageLoading] = useState(false) - const [imageError, setImageError] = useState(false) + const { + imageLoading, + imageError, + isLightboxOpen, + handleImageLoad, + handleImageError, + handleOpenLightbox, + handleCloseLightbox, + } = useImageLoadingState(record.id) const imageUrl = subsonic.getCoverArtUrl(record, 300, true) const fullImageUrl = subsonic.getCoverArtUrl(record) - // Reset image state when playlist changes - useEffect(() => { - setImageLoading(true) - setImageError(false) - }, [record.id]) - - const handleImageLoad = useCallback(() => { - setImageLoading(false) - setImageError(false) - }, []) - - const handleImageError = useCallback(() => { - setImageLoading(false) - setImageError(true) - }, []) - - const handleOpenLightbox = useCallback(() => { - if (!imageError) { - setLightboxOpen(true) - } - }, [imageError]) - - const handleCloseLightbox = useCallback(() => setLightboxOpen(false), []) - return ( <Card className={classes.root}> <div className={classes.cardContents}> diff --git a/ui/src/radio/RadioEdit.jsx b/ui/src/radio/RadioEdit.jsx index f00f889f3..6b1d2df79 100644 --- a/ui/src/radio/RadioEdit.jsx +++ b/ui/src/radio/RadioEdit.jsx @@ -6,8 +6,37 @@ import { TextInput, useTranslate, } from 'react-admin' +import { CardMedia } from '@material-ui/core' +import { makeStyles } from '@material-ui/core/styles' import { urlValidate } from '../utils/validations' -import { Title } from '../common' +import { Title, ImageUploadOverlay, useImageLoadingState } from '../common' +import subsonic from '../subsonic' +import { RADIO_PLACEHOLDER_IMAGE } from '../consts' + +const useStyles = makeStyles({ + coverParent: { + display: 'inline-flex', + position: 'relative', + width: '8rem', + height: '8rem', + marginBottom: '1em', + }, + cover: { + width: '8rem', + height: '8rem', + objectFit: 'cover', + cursor: 'pointer', + transition: 'opacity 0.3s ease-in-out', + }, + coverLoading: { + opacity: 0.5, + }, + placeholder: { + width: '8rem', + height: '8rem', + objectFit: 'contain', + }, +}) const RadioTitle = ({ record }) => { const translate = useTranslate() @@ -21,6 +50,7 @@ const RadioEdit = (props) => { return ( <Edit title={<RadioTitle />} {...props}> <SimpleForm variant="outlined" {...props}> + <RadioCoverArt /> <TextInput source="name" validate={[required()]} /> <TextInput type="url" @@ -41,4 +71,39 @@ const RadioEdit = (props) => { ) } +const RadioCoverArt = ({ record }) => { + const classes = useStyles() + const { imageLoading, handleImageLoad, handleImageError } = + useImageLoadingState(record?.id) + + if (!record) return null + + return ( + <div className={classes.coverParent}> + {record.uploadedImage ? ( + <CardMedia + component="img" + src={subsonic.getCoverArtUrl(record, 300, true)} + className={`${classes.cover} ${imageLoading ? classes.coverLoading : ''}`} + onLoad={handleImageLoad} + onError={handleImageError} + title={record.name} + alt={record.name} + /> + ) : ( + <img + src={RADIO_PLACEHOLDER_IMAGE} + className={classes.placeholder} + alt={record.name} + /> + )} + <ImageUploadOverlay + entityType="radio" + entityId={record.id} + hasUploadedImage={!!record.uploadedImage} + /> + </div> + ) +} + export default RadioEdit diff --git a/ui/src/radio/RadioList.jsx b/ui/src/radio/RadioList.jsx index 3d1adacc9..582fcaffc 100644 --- a/ui/src/radio/RadioList.jsx +++ b/ui/src/radio/RadioList.jsx @@ -1,4 +1,4 @@ -import { makeStyles, useMediaQuery } from '@material-ui/core' +import { Avatar, makeStyles, useMediaQuery } from '@material-ui/core' import React, { cloneElement } from 'react' import { CreateButton, @@ -16,9 +16,11 @@ import { } from 'react-admin' import { List } from '../common' import { ToggleFieldsMenu, useSelectedFields } from '../common' +import subsonic from '../subsonic' import { StreamField } from './StreamField' import { setTrack } from '../actions' import { songFromRadio } from './helper' +import { RADIO_PLACEHOLDER_IMAGE } from '../consts' import { useDispatch } from 'react-redux' const useStyles = makeStyles({ @@ -73,6 +75,19 @@ const RadioListActions = ({ ) } +const avatarStyle = { width: 40, height: 40 } + +const CoverArtField = ({ record }) => { + if (!record) return null + const src = record.uploadedImage + ? subsonic.getCoverArtUrl(record, 40, true) + : RADIO_PLACEHOLDER_IMAGE + return ( + <Avatar src={src} variant="rounded" style={avatarStyle} alt={record.name} /> + ) +} +CoverArtField.defaultProps = { label: '' } + const RadioList = ({ permissions, ...props }) => { const classes = useStyles() const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs')) @@ -80,6 +95,7 @@ const RadioList = ({ permissions, ...props }) => { const isAdmin = permissions === 'admin' const toggleableFields = { + coverArt: <CoverArtField source="id" sortable={false} />, name: <TextField source="name" />, homePageUrl: ( <UrlField @@ -97,7 +113,7 @@ const RadioList = ({ permissions, ...props }) => { const columns = useSelectedFields({ resource: 'radio', columns: toggleableFields, - defaultOff: ['createdAt'], + defaultOff: ['streamUrl', 'createdAt'], }) const handleRowClick = async (id, basePath, record) => { @@ -117,6 +133,7 @@ const RadioList = ({ permissions, ...props }) => { > {isXsmall ? ( <SimpleList + leftAvatar={(r) => <CoverArtField record={r} />} leftIcon={(r) => ( <StreamField record={r} diff --git a/ui/src/radio/helper.jsx b/ui/src/radio/helper.jsx index 57de244b9..4c313d3bc 100644 --- a/ui/src/radio/helper.jsx +++ b/ui/src/radio/helper.jsx @@ -1,16 +1,24 @@ +import subsonic from '../subsonic' +import { RADIO_PLACEHOLDER_IMAGE } from '../consts' + export async function songFromRadio(radio) { if (!radio) { return undefined } - let cover = 'internet-radio-icon.svg' - try { - const url = new URL(radio.homePageUrl ?? radio.streamUrl) - url.pathname = '/favicon.ico' - await resourceExists(url) - cover = url.toString() - } catch { - // ignore + let cover = RADIO_PLACEHOLDER_IMAGE + if (radio.uploadedImage) { + cover = subsonic.getCoverArtUrl(radio, 300, true) + } else { + // Try favicon as fallback + try { + const url = new URL(radio.homePageUrl ?? radio.streamUrl) + url.pathname = '/favicon.ico' + await resourceExists(url) + cover = url.toString() + } catch { + // No cover available + } } return { diff --git a/ui/src/subsonic/index.js b/ui/src/subsonic/index.js index 65155e8f4..3579619aa 100644 --- a/ui/src/subsonic/index.js +++ b/ui/src/subsonic/index.js @@ -86,6 +86,9 @@ const getCoverArtUrl = (record, size, square) => { } else if (record.sync !== undefined) { // This is a playlist return baseUrl(url('getCoverArt', 'pl-' + record.id, options)) + } else if (record.streamUrl !== undefined) { + // This is a radio station + return baseUrl(url('getCoverArt', 'ra-' + record.id, options)) } else { return baseUrl(url('getCoverArt', 'ar-' + record.id, options)) } From f7b60c79527f4ddc10c960acb64d0975cb9ffdb8 Mon Sep 17 00:00:00 2001 From: Deluan <deluan@navidrome.org> Date: Thu, 19 Mar 2026 13:14:24 -0400 Subject: [PATCH 44/81] fix(tests): fix race condition in CacheWarmer pre-cache size test The test was checking that the buffer was drained before asserting on cached sizes, but the buffer is cleared before processBatch completes. Use Eventually on getCachedSizes() directly to properly wait for the artwork caching to finish. --- core/artwork/cache_warmer_test.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/core/artwork/cache_warmer_test.go b/core/artwork/cache_warmer_test.go index 47e187f41..6ddda00d6 100644 --- a/core/artwork/cache_warmer_test.go +++ b/core/artwork/cache_warmer_test.go @@ -180,14 +180,9 @@ var _ = Describe("CacheWarmer", func() { cw := NewCacheWarmer(aw, fc).(*cacheWarmer) cw.PreCache(model.MustParseArtworkID("al-1")) - Eventually(func() int { - cw.mutex.Lock() - defer cw.mutex.Unlock() - return len(cw.buffer) - }).Should(Equal(0)) - - sizes := aw.getCachedSizes() - Expect(sizes).To(ContainElements(consts.UICoverArtSize, consts.UIThumbnailSize)) + Eventually(func() []int { + return aw.getCachedSizes() + }).Should(ContainElements(consts.UICoverArtSize, consts.UIThumbnailSize)) }) }) }) From a4c289b28c76bac6352b9500e5d783bab5602e99 Mon Sep 17 00:00:00 2001 From: JRoshthen1 <37882816+JRoshthen1@users.noreply.github.com> Date: Thu, 19 Mar 2026 18:33:09 +0100 Subject: [PATCH 45/81] feat(ui): add Slovak language translation (#5231) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(i18n): Add Slovak language translation Signed-off-by: jrosh <martin@jrosh.eu> * fix(i18n): Fix typos and add missing translations Signed-off-by: jrosh <martin@jrosh.eu> --------- Signed-off-by: jrosh <martin@jrosh.eu> Co-authored-by: Deluan Quintão <deluan@navidrome.org> --- resources/i18n/sk.json | 723 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 723 insertions(+) create mode 100644 resources/i18n/sk.json diff --git a/resources/i18n/sk.json b/resources/i18n/sk.json new file mode 100644 index 000000000..af5afade7 --- /dev/null +++ b/resources/i18n/sk.json @@ -0,0 +1,723 @@ +{ + "languageName": "Slovenčina", + "resources": { + "song": { + "name": "Skladba |||| Skladieb", + "fields": { + "albumArtist": "Interpret albumu", + "duration": "Dĺžka", + "trackNumber": "#", + "playCount": "Počet prehratí", + "title": "Názov", + "artist": "Interpret", + "composer": "Skladateľ", + "album": "Album", + "path": "Cesta k súboru", + "libraryName": "Knižnica", + "genre": "Žáner", + "compilation": "Kompilácia", + "year": "Rok", + "size": "Veľkosť súboru", + "updatedAt": "Nahrané", + "bitRate": "Prenosová rýchlosť", + "bitDepth": "Bitová hĺbka", + "sampleRate": "Vzorkovacia frekvencia", + "channels": "Kanály", + "disc": "Disk %{discNumber}", + "discSubtitle": "Podtitul disku", + "starred": "Obľúbené", + "comment": "Komentár", + "rating": "Hodnotenie", + "quality": "Kvalita", + "bpm": "BPM", + "playDate": "Naposledy prehraná skladba", + "createdAt": "Pridané", + "grouping": "Zoskupovanie", + "mood": "Nálada", + "participants": "Ďalší účastníci", + "tags": "Ďalšie značky", + "mappedTags": "Mapované značky", + "rawTags": "Nespracované značky", + "missing": "Chýbajúce" + }, + "actions": { + "addToQueue": "Prehrať neskôr", + "playNow": "Prehrať teraz", + "addToPlaylist": "Pridať do zoznamu skladieb", + "showInPlaylist": "Zobraziť v zozname skladieb", + "shuffleAll": "Zamiešať všetko", + "download": "Stiahnuť", + "playNext": "Prehrať ako ďalšie", + "info": "Získať informácie", + "instantMix": "Okamžitý mix" + } + }, + "album": { + "name": "Album |||| Albumy", + "fields": { + "albumArtist": "Interpret albumu", + "artist": "Interpret", + "duration": "Dĺžka", + "songCount": "Skladby", + "playCount": "Počet prehratí", + "size": "Veľkosť", + "name": "Názov", + "libraryName": "Knižnica", + "genre": "Žáner", + "compilation": "Kompilácia", + "year": "Rok", + "date": "Dátum záznamu", + "originalDate": "Pôvodné", + "releaseDate": "Vydané", + "releases": "Vydanie |||| Vydania", + "released": "Vydané", + "updatedAt": "Aktualizované", + "comment": "Komentár", + "rating": "Hodnotenie", + "createdAt": "Pridané", + "recordLabel": "Štítok", + "catalogNum": "Katalógové číslo", + "releaseType": "Typ vydania", + "grouping": "Zoskupovanie", + "media": "Médiá", + "mood": "Nálada", + "missing": "Chýbajúce" + }, + "actions": { + "playAll": "Prehrať", + "playNext": "Prehrať ako ďalšie", + "addToQueue": "Prehrať neskôr", + "share": "Zdieľať", + "shuffle": "Zamiešať", + "addToPlaylist": "Pridať do zoznamu skladieb", + "download": "Stiahnuť", + "info": "Získať informácie" + }, + "lists": { + "all": "Všetko", + "random": "Náhodné", + "recentlyAdded": "Nedávno pridané", + "recentlyPlayed": "Nedávno prehrané", + "mostPlayed": "Najviac prehrávané", + "starred": "Obľúbené", + "topRated": "Najlepšie hodnotené" + } + }, + "artist": { + "name": "Interpret |||| Interpreti", + "fields": { + "name": "Názov", + "albumCount": "Počet albumov", + "songCount": "Počet skladieb", + "size": "Veľkosť", + "playCount": "Prehrania", + "rating": "Hodnotenie", + "genre": "Žáner", + "role": "Rola", + "missing": "Chýbajúci" + }, + "roles": { + "albumartist": "Interpret albumu |||| Interpreti albumov", + "artist": "Interpret |||| Interpreti", + "composer": "Skladateľ |||| Skladatelia", + "conductor": "Dirigent |||| Dirigenti", + "lyricist": "Textár |||| Textári", + "arranger": "Aranžér |||| Aranžéri", + "producer": "Producent |||| Producenti", + "director": "Režisér |||| Režiséri", + "engineer": "Zvukový technik |||| Zvukoví technici", + "mixer": "Mixér |||| Mixéri", + "remixer": "Remixér |||| Remixéri", + "djmixer": "DJ Mixér |||| DJ Mixéri", + "performer": "Účinkujúci |||| Účinkujúci", + "maincredit": "Interpret albumu alebo interpret |||| Interpreti albumov alebo interpreti" + }, + "actions": { + "topSongs": "Najpopulárnejšie skladby", + "shuffle": "Zamiešať", + "radio": "Rádio" + } + }, + "user": { + "name": "Používateľ |||| Používatelia", + "fields": { + "userName": "Používateľské meno", + "isAdmin": "Správca", + "lastLoginAt": "Naposledy prihlásený", + "lastAccessAt": "Posledný Prístup", + "updatedAt": "Upravený", + "name": "Meno", + "password": "Heslo", + "createdAt": "Vytvorený", + "changePassword": "Zmeniť heslo?", + "currentPassword": "Súčastné heslo", + "newPassword": "Nové heslo", + "token": "Token", + "libraries": "Knižnice" + }, + "helperTexts": { + "name": "Zmena mena sa zobrazí až po ďalšom prihlásení", + "libraries": "Vyberte konkrétne knižnice pre tohto používateľa alebo nechajte pole prázdne, ak chcete použiť predvolené knižnice" + }, + "notifications": { + "created": "Používateľ vytvorený", + "updated": "Používateľ upravený", + "deleted": "Používateľ odstránený" + }, + "validation": { + "librariesRequired": "Pre používateľov bez administrátorských práv musí byť vybratá aspoň jedna knižnica" + }, + "message": { + "listenBrainzToken": "Vložte svoj používateľský ListenBrainz token.", + "clickHereForToken": "Kliknite sem pre získanie svojho tokenu", + "selectAllLibraries": "Vybrať všetky knižnice", + "adminAutoLibraries": "Administrátori majú automaticky prístup ku všetkým knižniciam" + } + }, + "player": { + "name": "Prehrávač |||| Prehrávače", + "fields": { + "name": "Názov", + "transcodingId": "ID transkódovania", + "maxBitRate": "Max. prenosová rýchlosť", + "client": "Klient", + "userName": "Používateľské meno", + "lastSeen": "Naposledy videný", + "reportRealPath": "Skutočná cesta hlásenia", + "scrobbleEnabled": "Odosielať scrobbling na externé služby" + } + }, + "transcoding": { + "name": "Transkódovanie |||| Transkódovania", + "fields": { + "name": "Názov", + "targetFormat": "Cieľový formát", + "defaultBitRate": "Predvolená prenosová rýchlosť", + "command": "Príkaz" + } + }, + "playlist": { + "name": "Zoznam skladieb |||| Zoznamy skladieb", + "fields": { + "name": "Názov", + "duration": "Dĺžka", + "ownerName": "Autor", + "public": "Verejný", + "updatedAt": "Nahraný", + "createdAt": "Vytvorený", + "songCount": "Skladby", + "comment": "Komentár", + "sync": "Auto-import", + "path": "Importovať z" + }, + "actions": { + "selectPlaylist": "Vybrať zoznam skladieb:", + "addNewPlaylist": "Vytvoriť \"%{name}\"", + "export": "Export", + "saveQueue": "Uložiť rad do zoznamu skladieb", + "makePublic": "Zverejniť", + "makePrivate": "Nastaviť ako súkromné", + "searchOrCreate": "Vyhľadajte zoznamy skladieb alebo napíšte pre vytvorenie nového...", + "pressEnterToCreate": "Stlačte Enter pre vytvorenie nového zoznamu skladieb", + "removeFromSelection": "Odstrániť z výberu" + }, + "message": { + "duplicate_song": "Pridať duplicitné položky", + "song_exist": "Pridávate duplikát už existujúcej položky v zozname skladieb. Chcete pridať duplikát alebo ho preskočiť?", + "noPlaylistsFound": "Žiadne zoznamy skladieb sa nenašli", + "noPlaylists": "Žiadne zoznamy skladieb nie sú dostupné" + } + }, + "radio": { + "name": "Rádio |||| Rádiá", + "fields": { + "name": "Názov", + "streamUrl": "URL streamu", + "homePageUrl": "URL stránky", + "updatedAt": "Nahrané", + "createdAt": "Vytvorené" + }, + "actions": { + "playNow": "Spustiť" + } + }, + "share": { + "name": "Zdieľanie |||| Zdieľania", + "fields": { + "username": "Zdieľané", + "url": "URL", + "description": "Popis", + "downloadable": "Povoliť sťahovanie?", + "contents": "Obsah", + "expiresAt": "Vyprší", + "lastVisitedAt": "Naposledy navštívené", + "visitCount": "Počet návštev", + "format": "Formát", + "maxBitRate": "Max. Bit Rate", + "updatedAt": "Nahrané", + "createdAt": "Vytvorené" + }, + "notifications": {}, + "actions": {} + }, + "missing": { + "name": "Chýbajúci súbor |||| Chýbajúce súbory", + "empty": "Žiadne chýbajúce súbory", + "fields": { + "path": "Cesta", + "size": "Veľkosť", + "libraryName": "Knižnica", + "updatedAt": "Zmizol dňa" + }, + "actions": { + "remove": "Odstrániť", + "remove_all": "Odstrániť všetky" + }, + "notifications": { + "removed": "Chýbajúce súbory odstránené" + } + }, + "library": { + "name": "Knižnica |||| Knižnice", + "fields": { + "name": "Názov", + "path": "Cesta", + "remotePath": "Vzdialená cesta", + "lastScanAt": "Posledný sken", + "songCount": "Skladby", + "albumCount": "Albumy", + "artistCount": "Interpreti", + "totalSongs": "Skladby", + "totalAlbums": "Albumy", + "totalArtists": "Interpreti", + "totalFolders": "Priečinky", + "totalFiles": "Súbory", + "totalMissingFiles": "Chýbajúce súbory", + "totalSize": "Celková veľkosť", + "totalDuration": "Dĺžka", + "defaultNewUsers": "Predvolené pre nových používateľov", + "createdAt": "Vytvorené", + "updatedAt": "Aktualizované" + }, + "sections": { + "basic": "Základné informácie", + "statistics": "Štatistiky" + }, + "actions": { + "scan": "Skenovať knižnicu", + "quickScan": "Rýchly sken", + "fullScan": "Úplný sken", + "manageUsers": "Spravovať prístup používateľov", + "viewDetails": "Zobraziť detaily" + }, + "notifications": { + "created": "Knižnica úspešne vytvorená", + "updated": "Knižnica úspešne aktualizovaná", + "deleted": "Knižnica úspešne odstránená", + "scanStarted": "Skenovanie knižnice spustené", + "quickScanStarted": "Rýchly sken spustený", + "fullScanStarted": "Úplný sken spustený", + "scanError": "Chyba pri spustení skenu. Skontrolujte logy", + "scanCompleted": "Skenovanie knižnice dokončené" + }, + "validation": { + "nameRequired": "Názov knižnice je povinný", + "pathRequired": "Cesta ku knižnici je povinná", + "pathNotDirectory": "Cesta ku knižnici musí byť priečinok", + "pathNotFound": "Cesta ku knižnici sa nenašla", + "pathNotAccessible": "Cesta ku knižnici nie je dostupná", + "pathInvalid": "Neplatná cesta ku knižnici" + }, + "messages": { + "deleteConfirm": "Ste si istý, že chcete odstrániť túto knižnicu? Tým sa odstránia všetky súvisiace dáta a prístupy používateľov.", + "scanInProgress": "Skenovanie prebieha...", + "noLibrariesAssigned": "Tomuto používateľovi nie sú priradené žiadne knižnice" + } + }, + "plugin": { + "name": "Plugin |||| Pluginy", + "fields": { + "id": "ID", + "name": "Názov", + "description": "Popis", + "version": "Verzia", + "author": "Autor", + "website": "Webová stránka", + "permissions": "Oprávnenia", + "enabled": "Povolený", + "status": "Stav", + "path": "Cesta", + "lastError": "Chyba", + "hasError": "Chyba", + "updatedAt": "Aktualizovaný", + "createdAt": "Nainštalovaný", + "configKey": "Kľúč", + "configValue": "Hodnota", + "allUsers": "Povoliť všetkých používateľov", + "selectedUsers": "Vybraní používatelia", + "allLibraries": "Povoliť všetky knižnice", + "selectedLibraries": "Vybrané knižnice", + "allowWriteAccess": "Povoliť prístup na zápis" + }, + "sections": { + "status": "Stav", + "info": "Informácie o plugine", + "configuration": "Konfigurácia", + "manifest": "Manifest", + "usersPermission": "Oprávnenia používateľov", + "libraryPermission": "Oprávnenia knižnice" + }, + "status": { + "enabled": "Povolený", + "disabled": "Zakázaný" + }, + "actions": { + "enable": "Povoliť", + "disable": "Zakázať", + "disabledDueToError": "Opravte chybu pred povolením", + "disabledUsersRequired": "Vyberte používateľov pred povolením", + "disabledLibrariesRequired": "Vyberte knižnice pred povolením", + "addConfig": "Pridať konfiguráciu", + "rescan": "Znovu skenovať" + }, + "notifications": { + "enabled": "Plugin povolený", + "disabled": "Plugin zakázaný", + "updated": "Plugin aktualizovaný", + "error": "Chyba pri aktualizácii pluginu" + }, + "validation": { + "invalidJson": "Konfigurácia musí byť platný JSON" + }, + "messages": { + "configHelp": "Nakonfigurujte plugin pomocou párov kľúč-hodnota. Nechajte prázdne, ak plugin nevyžaduje žiadnu konfiguráciu.", + "configValidationError": "Overenie konfigurácie zlyhalo:", + "schemaRenderError": "Nie je možné zobraziť konfiguračný formulár. Schéma pluginu môže byť neplatná.", + "clickPermissions": "Kliknite na oprávnenie pre detaily", + "noConfig": "Žiadna konfigurácia nastavená", + "allUsersHelp": "Keď je povolené, plugin bude mať prístup ku všetkým používateľom, vrátane tých vytvorených v budúcnosti.", + "noUsers": "Žiadni používatelia nevybraní", + "permissionReason": "Dôvod", + "usersRequired": "Tento plugin vyžaduje prístup k informáciám o používateľoch. Vyberte, ku ktorým používateľom má plugin prístup, alebo povolte 'Povoliť všetkých používateľov'.", + "allLibrariesHelp": "Keď je povolené, plugin bude mať prístup ku všetkým knižniciam, vrátane tých vytvorených v budúcnosti.", + "noLibraries": "Žiadne knižnice nevybrané", + "librariesRequired": "Tento plugin vyžaduje prístup k informáciám o knižniciach. Vyberte, ku ktorým knižniciam má plugin prístup, alebo povolte 'Povoliť všetky knižnice'.", + "allowWriteAccessHelp": "Keď je povolené, plugin môže upravovať súbory v adresároch knižníc. Predvolene majú pluginy prístup iba na čítanie.", + "requiredHosts": "Požadovaní hostitelia" + }, + "placeholders": { + "configKey": "kľúč", + "configValue": "hodnota" + } + } + }, + "ra": { + "auth": { + "welcome1": "Ďakujeme, že ste si nainštalovali Navidrome!", + "welcome2": "Najskôr vytvorte účet správcu", + "confirmPassword": "Potvrďte heslo", + "buttonCreateAdmin": "Vytvoriť správcu", + "auth_check_error": "Pre pokračovanie sa prosím prihláste", + "user_menu": "Profil", + "username": "Používateľské meno", + "password": "Heslo", + "sign_in": "Prihlásiť sa", + "sign_in_error": "Overenie zlyhalo, skúste to znova", + "logout": "Odhlásiť sa", + "insightsCollectionNote": "Navidrome zhromažďuje anonymné údaje\n o používaní, aby pomohol zlepšiť projekt.\nKliknite [sem] a dozviete sa viac a v prípade\npotreby sa odhláste." + }, + "validation": { + "invalidChars": "Prosím, používajte iba písmená a čísla", + "passwordDoesNotMatch": "Heslá sa nezhodujú", + "required": "Povinné pole", + "minLength": "Musí obsahovať najmenej %{min} znakov", + "maxLength": "Môže obsahovať maximálne %{max} znakov", + "minValue": "Musí byť aspoň %{min}", + "maxValue": "Môže byť maximálne %{max}", + "number": "Musí byť číslo", + "email": "Musí byť platná e-mailová adresa", + "oneOf": "Musí spĺňať jedno z: %{options}", + "regex": "Musí byť v špecifickom formáte (regexp): %{pattern}", + "unique": "Musí byť jedinečný", + "url": "Musí byť platná URL" + }, + "action": { + "add_filter": "Pridať filter", + "add": "Pridať", + "back": "Ísť späť", + "bulk_actions": "1 vybraná |||| %{smart_count} vybraných", + "bulk_actions_mobile": "1 |||| %{smart_count}", + "cancel": "Zrušiť", + "clear_input_value": "Vymazať hodnotu", + "clone": "Klonovať", + "confirm": "Potvrdiť", + "create": "Vytvoriť", + "delete": "Vymazať", + "edit": "Upraviť", + "export": "Exportovať", + "list": "Zoznam", + "refresh": "Obnoviť", + "remove_filter": "Odstrániť filter", + "remove": "Odstrániť", + "save": "Uložiť", + "search": "Vyhľadať", + "show": "Zobraziť", + "sort": "Zoradiť", + "undo": "Vrátiť", + "expand": "Rozbaliť", + "close": "Zavrieť", + "open_menu": "Otvoriť ponuku", + "close_menu": "Zavrieť ponuku", + "unselect": "Zrušiť výber", + "skip": "Preskočiť", + "share": "Zdieľať", + "download": "Stiahnuť" + }, + "boolean": { + "true": "Áno", + "false": "Nie" + }, + "page": { + "create": "Vytvoriť %{name}", + "dashboard": "Dashboard", + "edit": "%{name} #%{id}", + "error": "Niečo sa pokazilo", + "list": "%{name}", + "loading": "Načítavanie", + "not_found": "Nenájdené", + "show": "%{name} #%{id}", + "empty": "Zatiaľ žiaden %{name}.", + "invite": "Chcete pridať nové?" + }, + "input": { + "file": { + "upload_several": "Presuňte súbory pre nahranie alebo kliknite pre výber.", + "upload_single": "Presuňte súbor pre nahranie alebo kliknite pre jeho výber." + }, + "image": { + "upload_several": "Presuňte obrázky pre nahranie alebo kliknite pre výber.", + "upload_single": "Presuňte obrázok pre nahranie alebo kliknite pre jeho výber." + }, + "references": { + "all_missing": "Referencované dáta sa nenašli.", + "many_missing": "Aspoň jedna z referencií už nie je dostupná.", + "single_missing": "Referencia sa zdá byť nedostupná." + }, + "password": { + "toggle_visible": "Skryť heslo", + "toggle_hidden": "Zobraziť heslo" + } + }, + "message": { + "about": "O Navidrome", + "are_you_sure": "Ste si istý?", + "bulk_delete_content": "Ste si istý, že chcete vymazať %{name}? |||| Ste si istý, že chcete vymazať týchto %{smart_count} položiek?", + "bulk_delete_title": "Vymazať %{name} |||| Vymazať %{smart_count} %{name} položiek", + "delete_content": "Ste si istý, že chcete vymazať túto položku?", + "delete_title": "Vymazať %{name} #%{id}", + "details": "Detaily", + "error": "Vyskytla sa chyba klienta a vaša požiadavka nemohla byť splnená.", + "invalid_form": "Formulár nie je platný. Prosím skontrolujte ho.", + "loading": "Stránka sa načítava, prosím počkajte", + "no": "Nie", + "not_found": "Zadali ste nesprávnu adresu URL, alebo ste nasledovali nesprávny odkaz.", + "yes": "Áno", + "unsaved_changes": "Niektoré vaše zmeny neboli uložené. Ste si istí, že ich chcete ignorovať?" + }, + "navigation": { + "no_results": "Nenašli sa žiadne výsledky", + "no_more_results": "Stránka číslo %{page} je mimo rozsah. Skúste predchádzajúcu.", + "page_out_of_boundaries": "Stránka číslo %{page} je mimo rozsah", + "page_out_from_end": "Nemožno ísť za poslednú stranu", + "page_out_from_begin": "Nemožno ísť pred prvú stranu", + "page_range_info": "%{offsetBegin}-%{offsetEnd} z %{total}", + "page_rows_per_page": "Položiek na stránke:", + "next": "Ďalší", + "prev": "Predchádzajúci", + "skip_nav": "Preskočiť na obsah" + }, + "notification": { + "updated": "Prvok aktualizovaný |||| %{smart_count} prvkov aktualizovaných", + "created": "Prvok vytvorený", + "deleted": "Prvok vymazaný |||| %{smart_count} prvkov vymazaných", + "bad_item": "Nesprávny prvok", + "item_doesnt_exist": "Prvok neexistuje", + "http_error": "Chyba komunikácie servera", + "data_provider_error": "Chyba dataProvideru. Detaily nájdete v konzole.", + "i18n_error": "Nemožno načítať preklady pre vybraný jazyk", + "canceled": "Akcia zrušená", + "logged_out": "Vaša relácia skončila, prosím pripojte sa znova.", + "new_version": "Je dostupná nová verzia! Prosím obnovte toto okno." + }, + "toggleFieldsMenu": { + "columnsToDisplay": "Stĺpce na zobrazenie", + "layout": "Rozloženie", + "grid": "Mriežka", + "table": "Tabuľka" + } + }, + "message": { + "uploadCover": "Nahrať obrázok obalu", + "removeCover": "Odstrániť obrázok obalu", + "coverUploaded": "Obrázok obalu albumu aktualizovaný", + "coverRemoved": "Obrázok obalu albumu odstránený", + "coverUploadError": "Chyba pri nahrávaní obrázku obalu albumu", + "coverRemoveError": "Chyba pri odstraňovaní obrázku obalu albumu", + "note": "POZNÁMKA", + "transcodingDisabled": "Zmena nastavení transkódovania je vo webovom prostredí vypnutá z bezpečnostných dôvodov. Ak chcete zmeniť (upraviť alebo pridať) možnosti transkódovania, reštartujte server s možnosťou %{config}.", + "transcodingEnabled": "Navidrome práve beží s možnosťou %{config}, ktorá umožňuje spúšťanie systémových príkazov z nastavení transkódovania pomocou webového rozhrania. Odporúčame ju vypnúť z bezpečnostných dôvodov a používať ju iba pri úprave nastavení transkódovania.", + "songsAddedToPlaylist": "1 skladba pridaná do zoznamu skladieb |||| %{smart_count} skladieb pridaných do zoznamu skladieb", + "noSimilarSongsFound": "Nenašli sa žiadne podobné skladby", + "startingInstantMix": "Načítava sa Instant Mix...", + "noTopSongsFound": "Nenašli sa žiadne top skladby", + "noPlaylistsAvailable": "Žiadne nie sú dostupné", + "delete_user_title": "Odstrániť používateľa '%{name}'", + "delete_user_content": "Ste si istí, že chcete odstrániť tohto používateľa a všetky jeho dáta (vrátane zoznamov skladieb a nastavení)?", + "remove_missing_title": "Odstráňte chýbajúce súbory", + "remove_missing_content": "Naozaj chcete odstrániť vybraté chýbajúce súbory z databázy? Týmto sa natrvalo odstránia všetky odkazy na ne vrátane ich počtu prehratí a hodnotení.", + "remove_all_missing_title": "Odstráňte všetky chýbajúce súbory", + "remove_all_missing_content": "Naozaj chcete z databázy odstrániť všetky chýbajúce súbory? Týmto sa natrvalo odstránia všetky odkazy na ne vrátane ich počtu prehratí a hodnotení.", + "notifications_blocked": "Zablokovali ste si oznámenia pre túto stránku v nastaveniach vášho prehliadača", + "notifications_not_available": "Tento prehliadač nepodporuje oznámenia na ploche alebo nepristupujete k Navidrome cez https", + "lastfmLinkSuccess": "Last.fm úspešne pripojené a scrobbling zapnutý", + "lastfmLinkFailure": "Last.fm sa nepodarilo pripojiť", + "lastfmUnlinkSuccess": "Last.fm odpojené a scrobbling vypnutý", + "lastfmUnlinkFailure": "Last.fm sa nepodarilo odpojiť", + "listenBrainzLinkSuccess": "ListenBrainz úspešne pripojený a scrobbling zapnutý ako používateľ: %{user}", + "listenBrainzLinkFailure": "ListenBrainz sa nepodarilo pripojiť: %{error}", + "listenBrainzUnlinkSuccess": "ListenBrainz odpojený a scrobbling vypnutý", + "listenBrainzUnlinkFailure": "ListenBrainz sa nepodarilo odpojiť", + "openIn": { + "lastfm": "Otvoriť na Last.fm", + "musicbrainz": "Otvoriť na MusicBrainz" + }, + "lastfmLink": "Čítať ďalej...", + "shareOriginalFormat": "Zdieľať v pôvodnom formáte", + "shareDialogTitle": "Zdieľať %{resource} '%{name}'", + "shareBatchDialogTitle": "Zdieľať 1 %{resource} |||| Zdieľať %{smart_count} %{resource}", + "shareCopyToClipboard": "Skopírovať do schránky: Ctrl+C, Enter", + "shareSuccess": "URL skopírovaná do schránky: %{url}", + "shareFailure": "Chyba pri kopírovaní URL %{url} do schránky", + "downloadDialogTitle": "Stiahnuť %{resource} '%{name}' (%{size})", + "downloadOriginalFormat": "Stiahnuť v pôvodnom formáte" + }, + "menu": { + "library": "Knižnica", + "librarySelector": { + "allLibraries": "Všetky knižnice (%{count})", + "multipleLibraries": "%{selected} z %{total} knižníc", + "selectLibraries": "Vyberte knižnice", + "none": "Žiadne" + }, + "settings": "Nastavenia", + "version": "Verzia", + "theme": "Téma", + "personal": { + "name": "Osobné", + "options": { + "theme": "Téma", + "language": "Jazyk", + "defaultView": "Predvolená stránka", + "desktop_notifications": "Oznámenia na ploche", + "lastfmNotConfigured": "Kľúč API Last.fm nie je nakonfigurovaný", + "lastfmScrobbling": "Scrobblovať na Last.fm", + "listenBrainzScrobbling": "Scrobblovať na ListenBrainz", + "replaygain": "Mód ReplayGain", + "preAmp": "ReplayGain PreAmp (dB)", + "gain": { + "none": "Vypnuté", + "album": "Použiť Album Gain", + "track": "Použiť Track Gain" + } + } + }, + "albumList": "Albumy", + "playlists": "Zoznamy skladieb", + "sharedPlaylists": "Zdieľané zoznamy skladieb", + "about": "O Navidrome" + }, + "player": { + "playListsText": "Rad", + "openText": "Otvoriť", + "closeText": "Zavrieť", + "notContentText": "Žiadne skladby", + "clickToPlayText": "Kliknite pre prehranie", + "clickToPauseText": "Kliknite pre pozastavenie", + "nextTrackText": "Ďalšia skladba", + "previousTrackText": "Predchádzajúca skladba", + "reloadText": "Znovu načítať", + "volumeText": "Hlasitosť", + "toggleLyricText": "Prepnúť text", + "toggleMiniModeText": "Zmenšiť", + "destroyText": "Zničiť", + "downloadText": "Stiahnuť", + "removeAudioListsText": "Vymazať zoznam", + "clickToDeleteText": "Kliknite pre odstránenie %{name}", + "emptyLyricText": "Bez textu", + "playModeText": { + "order": "Po poradí", + "orderLoop": "Opakovať", + "singleLoop": "Opakovať raz", + "shufflePlay": "Zamiešať" + } + }, + "about": { + "links": { + "homepage": "Domovská stránka", + "source": "Zdrojový kód", + "featureRequests": "Požiadavky na funkcie", + "lastInsightsCollection": "Posledný zber štatistík", + "insights": { + "disabled": "Zakázané", + "waiting": "Čakanie" + } + }, + "tabs": { + "about": "O aplikácii", + "config": "Konfigurácia" + }, + "config": { + "configName": "Názov konfigurácie", + "environmentVariable": "Premenná prostredia", + "currentValue": "Aktuálna hodnota", + "configurationFile": "Konfiguračný súbor", + "exportToml": "Exportovať konfiguráciu (TOML)", + "downloadToml": "Stiahnuť konfiguráciu (TOML)", + "exportSuccess": "Konfigurácia exportovaná do schránky vo formáte TOML", + "exportFailed": "Nepodarilo sa skopírovať konfiguráciu", + "devFlagsHeader": "Vývojové príznaky (môžu byť zmenené/odstránené)", + "devFlagsComment": "Toto sú experimentálne nastavenia a môžu byť odstránené v budúcich verziách" + } + }, + "activity": { + "title": "Aktivita", + "totalScanned": "Naskenované priečinky", + "quickScan": "Rýchly sken", + "fullScan": "Úplný sken", + "selectiveScan": "Selektívne", + "serverUptime": "Doba od spustenia", + "serverDown": "OFFLINE", + "scanType": "Posledný Sken", + "status": "Chyba skenovania", + "elapsedTime": "Uplynutý čas" + }, + "nowPlaying": { + "title": "Práve hrá", + "empty": "Nič sa neprehráva", + "minutesAgo": "pred %{smart_count} minútou |||| pred %{smart_count} minútami" + }, + "help": { + "title": "Klávesové skratky Navidrome", + "hotkeys": { + "show_help": "Zobraziť túto nápovedu", + "toggle_menu": "Prepnúť bočné menu", + "toggle_play": "Prehrať / Pozastaviť", + "prev_song": "Predchádzajúca skladba", + "next_song": "Nasledujúca skladba", + "current_song": "Prejsť na aktuálnu skladbu", + "vol_up": "Zvýšiť hlasitosť", + "vol_down": "Znížiť hlasitosť", + "toggle_love": "Pridať túto skladbu do obľúbených" + } + } +} \ No newline at end of file From 5cd1fcb49237defc59d3f1c9af4bd5307b7c9e5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= <deluan@navidrome.org> Date: Fri, 20 Mar 2026 08:57:13 -0400 Subject: [PATCH 46/81] feat(scheduler): add crontab(5) random ~ syntax support (#5233) * feat(scheduler): add CrontabSchedule with crontab(5) random ~ syntax Implement ParseCrontab() that extends robfig/cron with support for the crontab(5) random ~ operator (e.g., 0~30 * * * *). Random values are resolved fresh on each Next() call for load spreading. Supports A~B, ~B, A~, and bare ~ forms in all 6 fields (including seconds). Expressions without ~ delegate to robfig's standard parser with zero overhead. Integrates into scheduler.Add() and conf.validateSchedule() so that scanner.schedule and backup.schedule config values accept ~ syntax. * refactor(scheduler): resolve random ~ values once at parse time Change from per-Next() randomization to per-parse randomization, matching crontab(5) semantics. This prevents double-firing within the same period when random values land after the current time. ParseCrontab now resolves ~ fields to concrete values, substitutes them into the spec string, and delegates to robfig's parser. This eliminates CrontabSchedule, randomField, and resolveField entirely. * test(scheduler): replace WaitGroup with channel for job execution synchronization Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org> --- conf/configuration.go | 10 +- scheduler/crontab_schedule.go | 133 ++++++++++++++++++++ scheduler/crontab_schedule_test.go | 194 +++++++++++++++++++++++++++++ scheduler/scheduler.go | 3 +- scheduler/scheduler_test.go | 40 +++--- 5 files changed, 347 insertions(+), 33 deletions(-) create mode 100644 scheduler/crontab_schedule.go create mode 100644 scheduler/crontab_schedule_test.go diff --git a/conf/configuration.go b/conf/configuration.go index 9efa352bd..a5c253746 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -16,8 +16,8 @@ import ( "github.com/kr/pretty" "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/scheduler" "github.com/navidrome/navidrome/utils/run" - "github.com/robfig/cron/v3" "github.com/spf13/viper" ) @@ -570,15 +570,9 @@ func validateBackupSchedule() error { } func validateSchedule(schedule, field string) (string, error) { - if _, err := time.ParseDuration(schedule); err == nil { - schedule = "@every " + schedule - } - c := cron.New() - id, err := c.AddFunc(schedule, func() {}) + _, err := scheduler.ParseCrontab(schedule) if err != nil { log.Error(fmt.Sprintf("Invalid %s. Please read format spec at https://pkg.go.dev/github.com/robfig/cron#hdr-CRON_Expression_Format", field), "schedule", schedule, err) - } else { - c.Remove(id) } return schedule, err } diff --git a/scheduler/crontab_schedule.go b/scheduler/crontab_schedule.go new file mode 100644 index 000000000..5de1ae145 --- /dev/null +++ b/scheduler/crontab_schedule.go @@ -0,0 +1,133 @@ +package scheduler + +import ( + "fmt" + "math/rand/v2" + "strconv" + "strings" + "time" + + "github.com/robfig/cron/v3" +) + +var parser = cron.NewParser( + cron.SecondOptional | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor, +) + +// ParseCrontab parses a cron expression with support for the crontab(5) random ~ syntax. +// Random values are resolved once at parse time. If no ~ is present, it delegates to +// robfig/cron's standard parser. Duration strings (e.g., "5m") are converted to "@every 5m". +func ParseCrontab(spec string) (cron.Schedule, error) { + if spec == "" { + return nil, fmt.Errorf("empty spec string") + } + + if _, err := time.ParseDuration(spec); err == nil { + spec = "@every " + spec + } + + if !strings.Contains(spec, "~") { + return parser.Parse(spec) + } + + // Handle TZ=/CRON_TZ= prefix + var tzPrefix string + if strings.HasPrefix(spec, "TZ=") || strings.HasPrefix(spec, "CRON_TZ=") { + i := strings.Index(spec, " ") + if i == -1 { + return nil, fmt.Errorf("missing spec after timezone") + } + tzPrefix = spec[:i] + " " + spec = strings.TrimSpace(spec[i:]) + } + + // @ descriptors cannot contain ~ + if strings.HasPrefix(spec, "@") { + return nil, fmt.Errorf("random ~ syntax cannot be used with descriptors: %s", spec) + } + + fields := strings.Fields(spec) + fields, err := normalizeFields(fields) + if err != nil { + return nil, err + } + + // Resolve each ~ field to a concrete random value + for i, field := range fields { + if !strings.Contains(field, "~") { + continue + } + if strings.ContainsAny(field, ",/") { + return nil, fmt.Errorf("random ~ cannot be combined with lists or steps: %s", field) + } + v, parseErr := resolveRandomField(field, fieldBounds[i]) + if parseErr != nil { + return nil, parseErr + } + fields[i] = strconv.FormatUint(uint64(v), 10) + } + + // Re-assemble and parse with robfig + resolved := tzPrefix + strings.Join(fields, " ") + return parser.Parse(resolved) +} + +type bounds struct { + min, max uint +} + +var fieldBounds = [6]bounds{ + {0, 59}, // Second + {0, 59}, // Minute + {0, 23}, // Hour + {1, 31}, // Dom + {1, 12}, // Month + {0, 6}, // Dow +} + +// resolveRandomField parses a ~ field and returns a random value within the range. +func resolveRandomField(field string, b bounds) (uint, error) { + parts := strings.SplitN(field, "~", 2) + + min := b.min + max := b.max + + if parts[0] != "" { + v, err := strconv.ParseUint(parts[0], 10, 0) + if err != nil { + return 0, fmt.Errorf("invalid random range start: %s", parts[0]) + } + min = uint(v) + } + + if parts[1] != "" { + v, err := strconv.ParseUint(parts[1], 10, 0) + if err != nil { + return 0, fmt.Errorf("invalid random range end: %s", parts[1]) + } + max = uint(v) + } + + if min < b.min { + return 0, fmt.Errorf("random range start (%d) below minimum (%d): %s", min, b.min, field) + } + if max > b.max { + return 0, fmt.Errorf("random range end (%d) above maximum (%d): %s", max, b.max, field) + } + if min > max { + return 0, fmt.Errorf("random range start (%d) beyond end (%d): %s", min, max, field) + } + + return min + uint(rand.IntN(int(max-min+1))), nil //nolint:gosec // Cryptographic randomness not needed for schedule jitter +} + +func normalizeFields(fields []string) ([]string, error) { + switch len(fields) { + case 5: + return append([]string{"0"}, fields...), nil + case 6: + return fields, nil + default: + return nil, fmt.Errorf("expected 5 or 6 fields, found %d: %v", len(fields), fields) + } +} diff --git a/scheduler/crontab_schedule_test.go b/scheduler/crontab_schedule_test.go new file mode 100644 index 000000000..b1e26f1de --- /dev/null +++ b/scheduler/crontab_schedule_test.go @@ -0,0 +1,194 @@ +package scheduler + +import ( + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/robfig/cron/v3" +) + +var _ = Describe("ParseCrontab", func() { + Describe("standard expressions", func() { + It("parses a 5-field expression", func() { + sched, err := ParseCrontab("5 * * * *") + Expect(err).ToNot(HaveOccurred()) + Expect(sched).To(BeAssignableToTypeOf(&cron.SpecSchedule{})) + }) + + It("parses a 6-field expression with seconds", func() { + sched, err := ParseCrontab("30 5 * * * *") + Expect(err).ToNot(HaveOccurred()) + Expect(sched).To(BeAssignableToTypeOf(&cron.SpecSchedule{})) + }) + + It("converts duration string to @every", func() { + sched, err := ParseCrontab("5m") + Expect(err).ToNot(HaveOccurred()) + Expect(sched).To(BeAssignableToTypeOf(cron.ConstantDelaySchedule{})) + }) + + It("returns error for empty string", func() { + _, err := ParseCrontab("") + Expect(err).To(HaveOccurred()) + }) + }) + + Describe("random ~ syntax", func() { + It("resolves A~B to a value within range", func() { + sched, err := ParseCrontab("0~30 * * * *") + Expect(err).ToNot(HaveOccurred()) + spec := sched.(*cron.SpecSchedule) + minute := findSetBit(spec.Minute) + Expect(minute).To(BeNumerically(">=", 0)) + Expect(minute).To(BeNumerically("<=", 30)) + }) + + It("resolves ~ alone to full field range", func() { + sched, err := ParseCrontab("~ * * * *") + Expect(err).ToNot(HaveOccurred()) + spec := sched.(*cron.SpecSchedule) + minute := findSetBit(spec.Minute) + Expect(minute).To(BeNumerically(">=", 0)) + Expect(minute).To(BeNumerically("<=", 59)) + }) + + It("resolves ~B as min~B", func() { + sched, err := ParseCrontab("~15 * * * *") + Expect(err).ToNot(HaveOccurred()) + spec := sched.(*cron.SpecSchedule) + minute := findSetBit(spec.Minute) + Expect(minute).To(BeNumerically(">=", 0)) + Expect(minute).To(BeNumerically("<=", 15)) + }) + + It("resolves A~ as A~max", func() { + sched, err := ParseCrontab("15~ * * * *") + Expect(err).ToNot(HaveOccurred()) + spec := sched.(*cron.SpecSchedule) + minute := findSetBit(spec.Minute) + Expect(minute).To(BeNumerically(">=", 15)) + Expect(minute).To(BeNumerically("<=", 59)) + }) + + It("resolves multiple random fields independently", func() { + sched, err := ParseCrontab("0~30 0~12 * * *") + Expect(err).ToNot(HaveOccurred()) + spec := sched.(*cron.SpecSchedule) + Expect(findSetBit(spec.Minute)).To(BeNumerically("<=", 30)) + Expect(findSetBit(spec.Hour)).To(BeNumerically("<=", 12)) + }) + + It("resolves ~ in DOM field with correct bounds", func() { + sched, err := ParseCrontab("0 0 ~ * *") + Expect(err).ToNot(HaveOccurred()) + spec := sched.(*cron.SpecSchedule) + dom := findSetBit(spec.Dom) + Expect(dom).To(BeNumerically(">=", 1)) + Expect(dom).To(BeNumerically("<=", 31)) + }) + + It("resolves ~ in month field with correct bounds", func() { + sched, err := ParseCrontab("0 0 1 ~ *") + Expect(err).ToNot(HaveOccurred()) + spec := sched.(*cron.SpecSchedule) + month := findSetBit(spec.Month) + Expect(month).To(BeNumerically(">=", 1)) + Expect(month).To(BeNumerically("<=", 12)) + }) + + It("resolves ~ in DOW field with correct bounds", func() { + sched, err := ParseCrontab("0 0 * * ~") + Expect(err).ToNot(HaveOccurred()) + spec := sched.(*cron.SpecSchedule) + dow := findSetBit(spec.Dow) + Expect(dow).To(BeNumerically(">=", 0)) + Expect(dow).To(BeNumerically("<=", 6)) + }) + + It("preserves TZ= prefix through resolution", func() { + sched, err := ParseCrontab("TZ=America/New_York 0~30 * * * *") + Expect(err).ToNot(HaveOccurred()) + spec := sched.(*cron.SpecSchedule) + nyc, _ := time.LoadLocation("America/New_York") + Expect(spec.Location).To(Equal(nyc)) + }) + + It("preserves non-random fields", func() { + sched, err := ParseCrontab("0~30 10 * * *") + Expect(err).ToNot(HaveOccurred()) + spec := sched.(*cron.SpecSchedule) + Expect(spec.Hour & (1 << 10)).ToNot(BeZero()) + }) + + It("resolves to a stable value across repeated Next calls", func() { + sched, err := ParseCrontab("0~30 * * * *") + Expect(err).ToNot(HaveOccurred()) + + ref := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC) + first := sched.Next(ref) + for range 50 { + Expect(sched.Next(ref)).To(Equal(first)) + } + }) + }) + + Describe("error cases", func() { + It("rejects min > max", func() { + _, err := ParseCrontab("30~0 * * * *") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("beyond end")) + }) + + It("rejects value above field maximum", func() { + _, err := ParseCrontab("0~60 * * * *") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("above maximum")) + }) + + It("rejects value below field minimum", func() { + _, err := ParseCrontab("0 0 0~15 * *") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("below minimum")) + }) + + It("rejects ~ mixed with comma (list)", func() { + _, err := ParseCrontab("0~30,45 * * * *") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("cannot be combined")) + }) + + It("rejects ~ mixed with slash (step)", func() { + _, err := ParseCrontab("0~30/5 * * * *") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("cannot be combined")) + }) + + It("rejects @ descriptor with ~", func() { + _, err := ParseCrontab("@every 0~30m") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("descriptor")) + }) + + It("rejects wrong number of fields", func() { + _, err := ParseCrontab("0~30 * *") + Expect(err).To(HaveOccurred()) + }) + + It("rejects non-numeric range values", func() { + _, err := ParseCrontab("a~b * * * *") + Expect(err).To(HaveOccurred()) + }) + }) +}) + +// findSetBit returns the lowest bit position set in v, ignoring the starBit (bit 63). +func findSetBit(v uint64) int { + v &^= 1 << 63 // clear starBit + for i := 0; i < 63; i++ { + if v&(1<<uint(i)) != 0 { + return i + } + } + return -1 +} diff --git a/scheduler/scheduler.go b/scheduler/scheduler.go index b377e7947..cb53af421 100644 --- a/scheduler/scheduler.go +++ b/scheduler/scheduler.go @@ -33,10 +33,11 @@ func (s *scheduler) Run(ctx context.Context) { } func (s *scheduler) Add(crontab string, cmd func()) (int, error) { - entryID, err := s.c.AddFunc(crontab, cmd) + schedule, err := ParseCrontab(crontab) if err != nil { return 0, err } + entryID := s.c.Schedule(schedule, cron.FuncJob(cmd)) return int(entryID), nil } diff --git a/scheduler/scheduler_test.go b/scheduler/scheduler_test.go index 4737ae389..795f07115 100644 --- a/scheduler/scheduler_test.go +++ b/scheduler/scheduler_test.go @@ -1,19 +1,16 @@ package scheduler import ( - "sync" "testing" "time" "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/tests" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/robfig/cron/v3" ) func TestScheduler(t *testing.T) { - tests.Init(t, false) log.SetLevel(log.LevelFatal) RegisterFailHandler(Fail) RunSpecs(t, "Scheduler Suite") @@ -33,54 +30,49 @@ var _ = Describe("Scheduler", func() { }) It("adds and executes a job", func() { - wg := sync.WaitGroup{} - wg.Add(1) + done := make(chan struct{}) - executed := false - id, err := s.Add("@every 100ms", func() { - executed = true - wg.Done() + id, err := s.Add("@every 50ms", func() { + close(done) }) Expect(err).ToNot(HaveOccurred()) Expect(id).ToNot(BeZero()) - wg.Wait() - Expect(executed).To(BeTrue()) + Eventually(done).Should(BeClosed()) + }) + + It("adds a job with random ~ syntax", func() { + id, err := s.Add("0~59 * * * *", func() {}) + Expect(err).ToNot(HaveOccurred()) + Expect(id).ToNot(BeZero()) + s.Remove(id) }) It("removes a job", func() { - // Use a WaitGroup to ensure the job executes once - wg := sync.WaitGroup{} - wg.Add(1) + done := make(chan struct{}) counter := 0 - id, err := s.Add("@every 100ms", func() { + id, err := s.Add("@every 50ms", func() { counter++ if counter == 1 { - wg.Done() // Signal that the job has executed once + close(done) } }) - Expect(err).ToNot(HaveOccurred()) Expect(id).ToNot(BeZero()) - // Wait for the job to execute at least once - wg.Wait() - // Verify job executed + Eventually(done).Should(BeClosed()) Expect(counter).To(Equal(1)) // Remove the job s.Remove(id) - // Store the counter value - currentCount := counter - // Wait some time to ensure job doesn't execute again time.Sleep(200 * time.Millisecond) // Verify counter didn't increase - Expect(counter).To(Equal(currentCount)) + Expect(counter).To(Equal(1)) }) }) From 03844a9a369afd247c30bc93103cbee90319ae32 Mon Sep 17 00:00:00 2001 From: Deluan <deluan@navidrome.org> Date: Fri, 20 Mar 2026 18:16:07 -0400 Subject: [PATCH 47/81] feat(plugins): add NoFollowRedirects option to HTTPRequest Allow plugins to opt out of automatic redirect following on a per-request basis. When set to true, the response returns the redirect status code and Location header directly instead of following to the final destination. --- plugins/host/http.go | 11 +++++----- plugins/host_httpclient.go | 14 +++++++++++++ plugins/host_httpclient_test.go | 20 +++++++++++++++++++ plugins/pdk/go/host/nd_host_http.go | 11 +++++----- plugins/pdk/go/host/nd_host_http_stub.go | 11 +++++----- .../pdk/rust/nd-pdk-host/src/nd_host_http.rs | 2 ++ 6 files changed, 54 insertions(+), 15 deletions(-) diff --git a/plugins/host/http.go b/plugins/host/http.go index b61361c57..96e832a0e 100644 --- a/plugins/host/http.go +++ b/plugins/host/http.go @@ -4,11 +4,12 @@ import "context" // HTTPRequest represents an outbound HTTP request from a plugin. type HTTPRequest struct { - Method string `json:"method"` - URL string `json:"url"` - Headers map[string]string `json:"headers,omitempty"` - Body []byte `json:"body,omitempty"` - TimeoutMs int32 `json:"timeoutMs,omitempty"` + Method string `json:"method"` + URL string `json:"url"` + Headers map[string]string `json:"headers,omitempty"` + NoFollowRedirects bool `json:"noFollowRedirects,omitempty"` + Body []byte `json:"body,omitempty"` + TimeoutMs int32 `json:"timeoutMs,omitempty"` } // HTTPResponse represents the response from an outbound HTTP request. diff --git a/plugins/host_httpclient.go b/plugins/host_httpclient.go index 4dc8a2b9e..f1d64deb7 100644 --- a/plugins/host_httpclient.go +++ b/plugins/host_httpclient.go @@ -22,6 +22,12 @@ const ( httpClientMaxResponseBodyLen = 10 * 1024 * 1024 // 10 MB ) +// contextKey is used for per-request redirect control via context. +type contextKey struct{} + +// noFollowRedirectsKey signals the CheckRedirect callback to stop following redirects. +var noFollowRedirectsKey = contextKey{} + // httpServiceImpl implements host.HTTPService. type httpServiceImpl struct { pluginName string @@ -44,6 +50,9 @@ func newHTTPService(pluginName string, permission *HTTPPermission) *httpServiceI // Timeout is set per-request via context deadline, not here. // CheckRedirect validates hosts and enforces redirect limits. CheckRedirect: func(req *http.Request, via []*http.Request) error { + if req.Context().Value(noFollowRedirectsKey) != nil { + return http.ErrUseLastResponse + } if len(via) >= httpClientMaxRedirects { log.Warn(req.Context(), "HTTP redirect limit exceeded", "plugin", svc.pluginName, "url", req.URL.String(), "redirectCount", len(via)) return http.ErrUseLastResponse @@ -80,6 +89,11 @@ func (s *httpServiceImpl) Send(ctx context.Context, request host.HTTPRequest) (* ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() + // Signal CheckRedirect to not follow redirects for this request + if request.NoFollowRedirects { + ctx = context.WithValue(ctx, noFollowRedirectsKey, true) + } + // Build request body method := strings.ToUpper(request.Method) var body io.Reader diff --git a/plugins/host_httpclient_test.go b/plugins/host_httpclient_test.go index 29796b052..27e92d59d 100644 --- a/plugins/host_httpclient_test.go +++ b/plugins/host_httpclient_test.go @@ -311,6 +311,26 @@ var _ = Describe("httpServiceImpl", func() { Expect(err.Error()).To(ContainSubstring("context canceled")) }) + It("should not follow redirects when NoFollowRedirects is true", func() { + dest := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("final")) + })) + defer dest.Close() + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, dest.URL, http.StatusFound) + })) + resp, err := svc.Send(context.Background(), host.HTTPRequest{ + Method: "GET", + URL: ts.URL, + TimeoutMs: 1000, + NoFollowRedirects: true, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(int32(302))) + Expect(resp.Headers["Location"]).To(Equal(dest.URL)) + Expect(string(resp.Body)).ToNot(Equal("final")) + }) + It("should send request headers", func() { ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(r.Header.Get("X-Custom"))) diff --git a/plugins/pdk/go/host/nd_host_http.go b/plugins/pdk/go/host/nd_host_http.go index d77db4762..d999d3718 100644 --- a/plugins/pdk/go/host/nd_host_http.go +++ b/plugins/pdk/go/host/nd_host_http.go @@ -17,11 +17,12 @@ import ( // HTTPRequest represents the HTTPRequest data structure. // HTTPRequest represents an outbound HTTP request from a plugin. type HTTPRequest struct { - Method string `json:"method"` - URL string `json:"url"` - Headers map[string]string `json:"headers"` - Body []byte `json:"body"` - TimeoutMs int32 `json:"timeoutMs"` + Method string `json:"method"` + URL string `json:"url"` + Headers map[string]string `json:"headers"` + NoFollowRedirects bool `json:"noFollowRedirects"` + Body []byte `json:"body"` + TimeoutMs int32 `json:"timeoutMs"` } // HTTPResponse represents the HTTPResponse data structure. diff --git a/plugins/pdk/go/host/nd_host_http_stub.go b/plugins/pdk/go/host/nd_host_http_stub.go index b5d1eee75..2f15a91a9 100644 --- a/plugins/pdk/go/host/nd_host_http_stub.go +++ b/plugins/pdk/go/host/nd_host_http_stub.go @@ -13,11 +13,12 @@ import "github.com/stretchr/testify/mock" // HTTPRequest represents the HTTPRequest data structure. // HTTPRequest represents an outbound HTTP request from a plugin. type HTTPRequest struct { - Method string `json:"method"` - URL string `json:"url"` - Headers map[string]string `json:"headers"` - Body []byte `json:"body"` - TimeoutMs int32 `json:"timeoutMs"` + Method string `json:"method"` + URL string `json:"url"` + Headers map[string]string `json:"headers"` + NoFollowRedirects bool `json:"noFollowRedirects"` + Body []byte `json:"body"` + TimeoutMs int32 `json:"timeoutMs"` } // HTTPResponse represents the HTTPResponse data structure. diff --git a/plugins/pdk/rust/nd-pdk-host/src/nd_host_http.rs b/plugins/pdk/rust/nd-pdk-host/src/nd_host_http.rs index d3bb2d326..1c44cd2f3 100644 --- a/plugins/pdk/rust/nd-pdk-host/src/nd_host_http.rs +++ b/plugins/pdk/rust/nd-pdk-host/src/nd_host_http.rs @@ -38,6 +38,8 @@ pub struct HTTPRequest { #[serde(default)] pub headers: std::collections::HashMap<String, String>, #[serde(default)] + pub no_follow_redirects: bool, + #[serde(default)] #[serde(with = "base64_bytes")] pub body: Vec<u8>, #[serde(default)] From 400a079fcd54b816b4742c9a7d44eea31fcab0da Mon Sep 17 00:00:00 2001 From: Deluan <deluan@navidrome.org> Date: Sat, 21 Mar 2026 19:19:03 -0400 Subject: [PATCH 48/81] fix(ui): fix hover overlay not covering full album cover Removed marginBottom: '3px' from tileBar and tileBarMobile styles that was causing the hover overlay to not fully cover the album cover art. The margin pushed the absolutely-positioned GridListTileBar up, leaving a visible gap at the bottom. This became apparent after d2a54243a added aspectRatio: 1 to the cover container. --- ui/src/album/AlbumGridView.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/src/album/AlbumGridView.jsx b/ui/src/album/AlbumGridView.jsx index c0d88d50e..6b44c5fe9 100644 --- a/ui/src/album/AlbumGridView.jsx +++ b/ui/src/album/AlbumGridView.jsx @@ -33,13 +33,11 @@ const useStyles = makeStyles( transition: 'all 150ms ease-out', opacity: 0, textAlign: 'left', - marginBottom: '3px', background: 'linear-gradient(to top, rgba(0,0,0,0.7) 0%,rgba(0,0,0,0.4) 70%,rgba(0,0,0,0) 100%)', }, tileBarMobile: { textAlign: 'left', - marginBottom: '3px', background: 'linear-gradient(to top, rgba(0,0,0,0.7) 0%,rgba(0,0,0,0.4) 70%,rgba(0,0,0,0) 100%)', }, From cb396f3dba3084ab3d581c5f7c89c353291d5d44 Mon Sep 17 00:00:00 2001 From: Deluan <deluan@navidrome.org> Date: Sun, 22 Mar 2026 14:54:28 -0400 Subject: [PATCH 49/81] 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 <deluan@navidrome.org> --- 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')(({ <div ref={dragAlbumRef}> <img key={record.id} // Force re-render when record changes - src={subsonic.getCoverArtUrl(record, 300, true)} + src={subsonic.getCoverArtUrl(record, COVER_ART_SIZE, true)} alt={record.name} className={`${classes.cover} ${imageLoading ? classes.coverLoading : ''}`} onLoad={handleImageLoad} diff --git a/ui/src/artist/DesktopArtistDetails.jsx b/ui/src/artist/DesktopArtistDetails.jsx index 7052b1634..bc2312477 100644 --- a/ui/src/artist/DesktopArtistDetails.jsx +++ b/ui/src/artist/DesktopArtistDetails.jsx @@ -15,6 +15,7 @@ import { import Lightbox from 'react-image-lightbox' import ExpandInfoDialog from '../dialogs/ExpandInfoDialog' import AlbumInfo from '../album/AlbumInfo' +import { COVER_ART_SIZE } from '../consts' import subsonic from '../subsonic' import { SafeHTML } from '../common/SafeHTML' @@ -109,7 +110,7 @@ const DesktopArtistDetails = ({ artistInfo, record, biography }) => { <CardMedia key={record.id} component="img" - src={subsonic.getCoverArtUrl(record, 300)} + src={subsonic.getCoverArtUrl(record, COVER_ART_SIZE)} className={`${classes.cover} ${imageLoading ? classes.coverLoading : ''}`} onClick={handleOpenLightbox} onLoad={handleImageLoad} diff --git a/ui/src/artist/MobileArtistDetails.jsx b/ui/src/artist/MobileArtistDetails.jsx index 03cc4de8f..9c6cd88ae 100644 --- a/ui/src/artist/MobileArtistDetails.jsx +++ b/ui/src/artist/MobileArtistDetails.jsx @@ -11,6 +11,7 @@ import { useImageLoadingState, } from '../common' import Lightbox from 'react-image-lightbox' +import { COVER_ART_SIZE } from '../consts' import subsonic from '../subsonic' import { SafeHTML } from '../common/SafeHTML' @@ -112,7 +113,7 @@ const MobileArtistDetails = ({ artistInfo, biography, record }) => { <CardMedia key={record.id} component="img" - src={subsonic.getCoverArtUrl(record, 300)} + src={subsonic.getCoverArtUrl(record, COVER_ART_SIZE)} className={`${classes.cover} ${imageLoading ? classes.coverLoading : ''}`} onClick={handleOpenLightbox} onLoad={handleImageLoad} diff --git a/ui/src/common/CoverArtAvatar.jsx b/ui/src/common/CoverArtAvatar.jsx index 6610cb75e..5642f2504 100644 --- a/ui/src/common/CoverArtAvatar.jsx +++ b/ui/src/common/CoverArtAvatar.jsx @@ -2,6 +2,7 @@ import { useRecordContext } from 'react-admin' import { Avatar } from '@material-ui/core' import { makeStyles } from '@material-ui/core/styles' import clsx from 'clsx' +import { COVER_ART_SIZE } from '../consts' import subsonic from '../subsonic' const useStyles = makeStyles({ @@ -25,7 +26,7 @@ export const CoverArtAvatar = ({ const square = variant !== 'circular' return ( <Avatar - src={subsonic.getCoverArtUrl(record, 80, square)} + src={subsonic.getCoverArtUrl(record, COVER_ART_SIZE, square)} variant={variant} className={clsx(classes.avatar, square && classes.square)} alt={record.name} diff --git a/ui/src/consts.js b/ui/src/consts.js index 472cd4940..68ec478f1 100644 --- a/ui/src/consts.js +++ b/ui/src/consts.js @@ -26,6 +26,8 @@ DraggableTypes.ALL.push( export const RADIO_PLACEHOLDER_IMAGE = 'internet-radio-icon.svg' +export const COVER_ART_SIZE = 600 + export const DEFAULT_SHARE_BITRATE = 128 export const BITRATE_CHOICES = [ diff --git a/ui/src/playlist/PlaylistDetails.jsx b/ui/src/playlist/PlaylistDetails.jsx index 911e6c716..f2f528d4b 100644 --- a/ui/src/playlist/PlaylistDetails.jsx +++ b/ui/src/playlist/PlaylistDetails.jsx @@ -18,6 +18,7 @@ import { OverflowTooltip, useImageLoadingState, } from '../common' +import { COVER_ART_SIZE } from '../consts' import subsonic from '../subsonic' const useStyles = makeStyles( @@ -106,7 +107,7 @@ const PlaylistDetails = (props) => { 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 ? ( <CardMedia component="img" - src={subsonic.getCoverArtUrl(record, 300, true)} + src={subsonic.getCoverArtUrl(record, COVER_ART_SIZE, true)} className={`${classes.cover} ${imageLoading ? classes.coverLoading : ''}`} onLoad={handleImageLoad} onError={handleImageError} diff --git a/ui/src/radio/helper.jsx b/ui/src/radio/helper.jsx index 4c313d3bc..b278c0d7d 100644 --- a/ui/src/radio/helper.jsx +++ b/ui/src/radio/helper.jsx @@ -1,5 +1,5 @@ import subsonic from '../subsonic' -import { RADIO_PLACEHOLDER_IMAGE } from '../consts' +import { COVER_ART_SIZE, RADIO_PLACEHOLDER_IMAGE } from '../consts' export async function songFromRadio(radio) { if (!radio) { @@ -8,7 +8,7 @@ export async function songFromRadio(radio) { let cover = RADIO_PLACEHOLDER_IMAGE if (radio.uploadedImage) { - cover = subsonic.getCoverArtUrl(radio, 300, true) + cover = subsonic.getCoverArtUrl(radio, COVER_ART_SIZE, true) } else { // Try favicon as fallback try { diff --git a/ui/src/subsonic/index.test.js b/ui/src/subsonic/index.test.js index 2544e03f8..38b910b08 100644 --- a/ui/src/subsonic/index.test.js +++ b/ui/src/subsonic/index.test.js @@ -1,4 +1,5 @@ import { vi } from 'vitest' +import { COVER_ART_SIZE } from '../consts' import subsonic from './index' describe('getCoverArtUrl', () => { @@ -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 <deluan@navidrome.org> Date: Sun, 22 Mar 2026 15:20:15 -0400 Subject: [PATCH 50/81] 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 <deluan@navidrome.org> --- 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 <deluan@navidrome.org> Date: Mon, 23 Mar 2026 11:39:52 -0400 Subject: [PATCH 51/81] 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 <deluan@navidrome.org> Date: Mon, 23 Mar 2026 11:59:11 -0400 Subject: [PATCH 52/81] 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 53/81] 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] <support@github.com> 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 54/81] 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() } From 356b0716b69b5648df53613c7f3d905741f34af8 Mon Sep 17 00:00:00 2001 From: Tom Boucher <trekkie@nomorestars.com> Date: Mon, 23 Mar 2026 18:32:05 -0400 Subject: [PATCH 55/81] fix(scanner): exclude Vorbis VERSION from albumversion tag mapping (#5194) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Vorbis/FLAC VERSION field is for track-level disambiguation (e.g. remix, live, 30s edit), not album versioning. Including it in the albumversion aliases caused albums to split incorrectly when tracks had different VERSION values and no MusicBrainz Album ID was set. Remove 'version' from the albumversion aliases in mappings.yaml. Users who want the old behavior can re-add it via Tags config. Update the PID test to use 'albumversion' directly instead of 'version' as the raw PID attribute, with a realistic value. Fixes #5082 Co-authored-by: Deluan Quintão <deluan@navidrome.org> --- model/metadata/persistent_ids_test.go | 12 ++++++------ resources/mappings.yaml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/model/metadata/persistent_ids_test.go b/model/metadata/persistent_ids_test.go index ad81eaa53..9f1dacbd4 100644 --- a/model/metadata/persistent_ids_test.go +++ b/model/metadata/persistent_ids_test.go @@ -65,7 +65,7 @@ var _ = Describe("getPID", func() { Context("calculated attributes", func() { BeforeEach(func() { DeferCleanup(configtest.SetupConfig()) - conf.Server.PID.Album = "musicbrainz_albumid|albumartistid,album,version,releasedate" + conf.Server.PID.Album = "musicbrainz_albumid|albumartistid,album,albumversion,releasedate" }) When("field is title", func() { It("should return the pid", func() { @@ -88,13 +88,13 @@ var _ = Describe("getPID", func() { It("should return the pid", func() { spec := "albumid|title" md.tags = map[model.TagName][]string{ - "title": {"title"}, - "album": {"album name"}, - "version": {"version"}, - "releasedate": {"2021-01-01"}, + "title": {"title"}, + "album": {"album name"}, + "albumversion": {"deluxe edition"}, + "releasedate": {"2021-01-01"}, } mf.AlbumArtist = "Album Artist" - Expect(getPID(mf, md, spec, false)).To(Equal("(((album artist)\\album name\\version\\2021-01-01))")) + Expect(getPID(mf, md, spec, false)).To(Equal("(((album artist)\\album name\\deluxe edition\\2021-01-01))")) }) }) When("field is albumartistid", func() { diff --git a/resources/mappings.yaml b/resources/mappings.yaml index d1da5c620..19ba0b090 100644 --- a/resources/mappings.yaml +++ b/resources/mappings.yaml @@ -81,7 +81,7 @@ main: albumsort: aliases: [ tsoa, albumsort, soal, wm/albumsortorder ] albumversion: - aliases: [albumversion, musicbrainz_albumcomment, musicbrainz album comment, version] + aliases: [albumversion, musicbrainz_albumcomment, musicbrainz album comment] album: true genre: aliases: [ tcon, genre, ©gen, wm/genre, ignr ] From 0a0f1779cbe32f80606d6a94a7cc587ee9676bdd Mon Sep 17 00:00:00 2001 From: cafecitopuro <60331056+cafecitopuro@users.noreply.github.com> Date: Tue, 24 Mar 2026 23:39:02 +0000 Subject: [PATCH 56/81] feat(ui): add Nutball theme (#4544) * add nutball theme * fix album grid outline * cleanup * Pagination fix + accent color update * Fix animation on Activity icon * style: fix prettier formatting in nutball theme --------- Co-authored-by: Deluan <deluan@navidrome.org> --- ui/src/themes/index.js | 2 + ui/src/themes/nutball.css.js | 380 +++++++++++++++++++ ui/src/themes/nutball.js | 684 +++++++++++++++++++++++++++++++++++ 3 files changed, 1066 insertions(+) create mode 100644 ui/src/themes/nutball.css.js create mode 100644 ui/src/themes/nutball.js diff --git a/ui/src/themes/index.js b/ui/src/themes/index.js index d0eb742f8..f65948438 100644 --- a/ui/src/themes/index.js +++ b/ui/src/themes/index.js @@ -11,6 +11,7 @@ import GruvboxDarkTheme from './gruvboxDark' import CatppuccinMacchiatoTheme from './catppuccinMacchiato' import DraculaTheme from './dracula' import NuclearTheme from './nuclear' +import NutballTheme from './nutball' import AmusicTheme from './amusic' import SquiddiesGlassTheme from './SquiddiesGlass' import NautilineTheme from './nautiline' @@ -33,6 +34,7 @@ export default { NautilineTheme, NordTheme, NuclearTheme, + NutballTheme, SpotifyTheme, SquiddiesGlassTheme, } diff --git a/ui/src/themes/nutball.css.js b/ui/src/themes/nutball.css.js new file mode 100644 index 000000000..aaf1c0e16 --- /dev/null +++ b/ui/src/themes/nutball.css.js @@ -0,0 +1,380 @@ +const stylesheet = ` +html { + scrollbar-width: none; +} +body { + -ms-overflow-style: none; + font-family: monospace; +} +body::-webkit-scrollbar, body::-webkit-scrollbar-button { + display: none; +} +.react-jinke-music-player-main .music-player-panel { + background-color: white!important; + box-shadow: none; + font-family: monospace; + color: black; + border-top: 1px solid black; +} +.react-jinke-music-player-main .music-player-panel .panel-content div.img-content { + animation: none; + box-shadow: none; + border-radius: 5px; +} +.react-jinke-music-player-main .music-player-panel .panel-content .progress-bar-content { + flex: 0 0 auto; + width: calc(50% - 150px); + margin-left: 10px; + padding: 0; +} +section.audio-main { + position: absolute; + width: calc(100% - 131px)!important; + bottom: 0; + margin-bottom: 10px; +} +span.audio-title { + margin-bottom: 20px; +} +span.audio-title .songTitle { + color: black!important; +} +.react-jinke-music-player-main .music-player-panel .panel-content .player-content { + flex: 1; + margin-bottom: 20px; + padding-left: 0; +} +div.player-content > span:first-child { + flex: 1!important; + justify-content: flex-start!important; +} +div.player-content > span:first-child svg { + width: 50px; +} +.react-jinke-music-player-main .music-player-panel .panel-content .player-content > .group { + flex: 0; +} +.play-sounds svg, .loop-btn svg, .audio-lists-btn svg, .destroy-btn { + margin-left: 0!important; +} +.play-sounds svg, .loop-btn svg, .audio-lists-btn svg, .destroy-btn svg { + width: 20px; +} +.react-jinke-music-player-main .music-player-panel .panel-content .player-content .audio-lists-btn { + padding: 0; +} +.react-jinke-music-player-main .music-player-panel .panel-content .player-content .audio-lists-btn .audio-lists-icon svg { + height: .75em; +} +.react-jinke-music-player-main .music-player-panel .panel-content .progress-bar-content .audio-main .current-time, .react-jinke-music-player-main .music-player-panel .panel-content .progress-bar-content .audio-main .duration { + flex-basis: 0; +} +.progress-bar > div:nth-child(2) > div:nth-child(4) { + transform: translateX(-50%) translateY(5%) !important; +} +.progress-load-bar { + display: none; +} +.sound-operation > div:nth-child(4) { + transform: translateX(-50%) translateY(5%) !important; +} +.react-jinke-music-player-main .music-player-panel .panel-content .player-content .play-sounds .sound-operation { + width: 60px; +} +.rc-slider { + border-radius: 0px; + border: 1px solid black; + padding: 3px 0!important; +} +.rc-slider .rc-slider-handle { + box-shadow: none!important; + border-radius: 0px; + background-color: black!important; + border: hidden!important; +} +.rc-slider[style*="left: 0%"] { + transform: translateX(0) !important; +} +.rc-slider .rc-slider-track { + display: none; +} +.react-jinke-music-player-main .rc-slider-rail, .react-jinke-music-player-main.light-theme .rc-slider-rail { + background-color: white!important; +} +.react-jinke-music-player-main .music-player-panel .panel-content .player-content .play-sounds .sounds-icon { + margin-right: 10px; +} +.lyric-btn { + display: none!important; +} +button[data-testid="save-queue-button"] { + display: none!important; +} +.react-jinke-music-player-main .music-player-panel .panel-content .player-content .audio-lists-btn { + box-shadow: 0 0 0 0; + margin: 0; + margin-left: -8px; + margin-right: -5px; +} +.audio-lists-btn:hover span, +.audio-lists-btn:hover svg { + color: #a8fe40!important; +} +.react-jinke-music-player-main.light-theme .audio-lists-btn { + background-color: white!important; +} +.react-jinke-music-player-main .music-player-panel .panel-content .player-content .audio-lists-btn .audio-lists-num { + color: grey; + margin-left: 5px; + font-size: .7rem; +} +.react-jinke-music-player-main .music-player-panel .panel-content .player-content .hide-panel { + margin-left: 2px; +} +.react-jinke-music-player-main .music-player-panel .panel-content .player-content .hide-panel svg { + stroke-width: 15px; + stroke: #fff; + height: .8em; +} +@media screen and (max-width: 810px) { + .react-jinke-music-player-main .music-player-panel .panel-content .player-content .play-sounds .sounds-icon { + margin-left: 5px; + margin-right: 0; + } + .react-jinke-music-player-main .music-player-panel .panel-content .player-content .loop-btn { + margin-left: 5px; + } + .play-sounds svg, .loop-btn svg, .audio-lists-btn svg, .destroy-btn { + margin-left: -3px!important; + } +} +.panel-content li { + flex-grow: 0; +} +.react-jinke-music-player .music-player-controller, +.react-jinke-music-player-main.light-theme .music-player-controller { + border-radius: 5px; + box-shadow: none; +} +.react-jinke-music-player .music-player-controller:hover, +.react-jinke-music-player .music-player-controller:has(+ .destroy-btn:hover) { + border: 1px solid black; +} +.react-jinke-music-player .music-player-controller .controller-title, +.react-jinke-music-player .music-player-controller .music-player-controller-setting { + display: none; +} +.react-jinke-music-player .music-player-controller.music-player-playing:before { + animation: none; + border: none; +} +@media screen and (max-width:767px) { + .react-jinke-music-player .music-player .destroy-btn { + right: 0; + } + .react-jinke-music-player-main .destroy-btn svg { + font-size: 10px; + } +} +.react-jinke-music-player-main svg { + transition: none; +} +.react-jinke-music-player-main svg, .react-jinke-music-player-main.light-theme svg { + color: black; +} +.react-jinke-music-player-main svg:active, .react-jinke-music-player-main svg:hover, +.react-jinke-music-player-main.light-theme svg:active, .react-jinke-music-player-main.light-theme svg:hover { + color: #a8fe40; +} +.react-jinke-music-player-main .play-mode-title { + font-family: monospace; + background-color: white; + color: black; +} +.react-jinke-music-player-mobile, +.react-jinke-music-player-main.light-theme .react-jinke-music-player-mobile { + font-family: monospace; + background-color: rgba(255, 255, 255, .9); + color: black!important; + justify-content: center; + padding: 50px; +} +.react-jinke-music-player-mobile:before { + content: " "; + display: block; + position: absolute; + margin-left: auto; + margin-right: auto; + left: 0; + right: 0; + text-align: center; + width: 90%; + height: 700px; + background-color: white; + border: 1px solid black; + z-index: -1; + border-radius: 4px; +} +.react-jinke-music-player-mobile-header { + align-items: start; + margin-bottom: 4rem; + justify-content: start; +} +.react-jinke-music-player-mobile-header-title { + text-align: left; + padding: 0; +} +.react-jinke-music-player-mobile-header-right { + color: black; +} +.react-jinke-music-player-mobile > .group { + flex: 0; +} +.react-jinke-music-player-mobile-cover, +.react-jinke-music-player-main.light-theme .react-jinke-music-player-mobile-cover { + border-radius: 5px; + box-shadow: none; + animation: none; + border: 1px solid black; + margin: 0 auto 4rem auto; + width: auto; + height: auto; +} +.react-jinke-music-player-mobile-cover .cover { + animation: none; +} +.react-jinke-music-player-mobile-progress .current-time { + /* margin-right: 17px; */ +} +.react-jinke-music-player-mobile-progress .current-time, .react-jinke-music-player-mobile-progress .duration { + color: black!important; +} +.react-jinke-music-player-mobile-progress .rc-slider { + height: 24px; +} +.react-jinke-music-player-mobile-progress .rc-slider-handle { + border: 2px solid black; + margin-top: -4px; + height: 24px; + width: 24px; +} +.react-jinke-music-player-mobile-toggle { + margin-bottom: 1rem; + padding: 2rem 0; +} +.react-jinke-music-player-mobile-operation .items .item svg { + color: black!important; + font-size: 2rem; + width: 2rem; +} +.react-jinke-music-player-mobile-operation .items .item svg:hover, +.react-jinke-music-player-mobile-operation .items .item button:hover svg { + color: #a8fe40!important; +} +.react-jinke-music-player-mobile-operation .items .item .MuiIconButton-root:hover { + background-color: rgba(0, 0, 0, 0.0); +} +.react-jinke-music-player-mobile-operation .MuiButtonBase-root.Mui-disabled { + cursor: pointer; + pointer-events: auto; +} +.react-jinke-music-player-mobile-operation .items li:nth-child(5) svg { + font-size: 1.4rem; +} +.react-jinke-music-player-mobile-operation .items li:nth-child(5) svg g path:nth-child(2) { + stroke-width: .4px; +} +.react-jinke-music-player-mobile-operation .items li:nth-child(2), +.react-jinke-music-player-mobile-operation .items li:nth-child(3) { + display: none; +} +.react-jinke-music-player-mobile-play-model-tip { + display: none; +} +.audio-lists-panel { + overflow-y: scroll; + scrollbar-width: none; + border-radius: .625rem; + bottom: 6.25rem; +} +.react-jinke-music-player-main.light-theme .audio-lists-panel { + font-family: monospace; + box-shadow: none; + border: 1px solid black; +} +.react-jinke-music-player-main.light-theme .audio-lists-panel-header { + text-shadow: none; + border-bottom: 1px solid black; +} +.audio-lists-panel-header-line { + width: 0; +} +.audio-lists-panel-header-close-btn:hover svg { + animation: none; +} +.audio-lists-panel-content .audio-item, +.react-jinke-music-player-main.light-theme .audio-item { + border-radius: 0px; + margin: 0; + border-bottom: none; + box-shadow: none; + transition: none; +} +.react-jinke-music-player-main.light-theme .audio-lists-panel .audio-lists-panel-content .audio-item:nth-child(2n+1) { + background-color: white!important; +} +.react-jinke-music-player-main.light-theme .audio-lists-panel .audio-lists-panel-content .audio-item:nth-child(2n+1):hover { + background-color: #fafafa!important; +} +.audio-lists-panel-content .audio-item .player-singer { + width: unset; + padding-right: 20px; +} +.audio-lists-panel-content .audio-item .player-delete:hover svg { + color: #a8fe40!important; + animation: none; +} +.react-jinke-music-player-main .audio-lists-panel-content .audio-item:active .group:not([class=".player-delete"]) svg, +.react-jinke-music-player-main .audio-lists-panel-content .audio-item:hover .group:not([class=".player-delete"]) svg, +.react-jinke-music-player-main.light-theme .audio-item:active svg, +.react-jinke-music-player-main.light-theme .audio-item:hover svg { + color: black; +} +.audio-lists-panel-content .audio-item .player-delete { + justify-content: center; + width: 25px; +} +.audio-lists-panel-content .audio-item .player-delete svg { + font-size: 20px; +} +.react-jinke-music-player-main.light-theme .audio-lists-panel .audio-item.playing, .react-jinke-music-player-main.light-theme .audio-lists-panel .audio-item.playing svg { + color: #a8fe40!important; +} +.audio-lists-panel-content .audio-item .player-name, +.audio-lists-panel-content .audio-item .player-singer, +.react-jinke-music-player-main.light-theme .audio-item.playing .player-singer, +.react-jinke-music-player-main.light-theme .audio-lists-panel .audio-item.playing, .react-jinke-music-player-main.light-theme .audio-lists-panel .audio-item.playing .player-delete svg { + color: black!important; +} +.audio-lists-panel-mobile { + height: 750px !important; + top: calc(100vh / 2 - 375px) !important; + width: 91% !important; + margin: 0 auto; +} +.audio-lists-panel-mobile .audio-lists-panel-content { + height: auto!important; +} +.audio-lists-panel-content { + scrollbar-width: none; +} +@keyframes fromOut { + 0% { + transform:scale(1) translateZ(0) + } + to { + transform:scale(1) translate3d(0,150%,0); + } +} +` +export default stylesheet diff --git a/ui/src/themes/nutball.js b/ui/src/themes/nutball.js new file mode 100644 index 000000000..f43250f5c --- /dev/null +++ b/ui/src/themes/nutball.js @@ -0,0 +1,684 @@ +import stylesheet from './nutball.css.js' + +export default { + themeName: 'Nutball', + palette: { + primary: { + main: '#80ea00', + light: '#fff', + }, + secondary: { + main: '#80ea00', + contrastText: '#fff', + }, + }, + typography: { + fontFamily: 'monospace', + h6: { + fontSize: '1rem', + }, + h4: { + fontSize: '1.2rem', + }, + h1: { + fontSize: '1.4rem', + }, + body: { + fontFamily: 'monospace', + }, + }, + overrides: { + MuiAppBar: { + root: { + borderBottom: '1px solid black', + }, + colorSecondary: { + color: 'black', + backgroundColor: 'white', + }, + }, + MuiPaper: { + elevation1: { + boxShadow: 'none', + }, + elevation4: { + boxShadow: 'none', + }, + elevation6: { + boxShadow: 'none', + }, + elevation8: { + boxShadow: 'none', + border: '1px solid black', + }, + elevation16: { + boxShadow: 'none', + borderRight: '1px solid grey!important', + }, + elevation24: { + boxShadow: 'none', + border: '1px solid black', + }, + }, + MuiButton: { + root: { + color: '#80ea00', + border: '1px solid rgba(0, 0, 0, 0.23)', + transition: 'none', + '&[aria-label="Grid"]': { + width: '50%', + marginLeft: '15px', + marginRight: '2px', + marginBottom: '10px', + '& .MuiButton-label': { + justifyContent: 'center', + }, + }, + '&[aria-label="Table"]': { + width: '50%', + marginRight: '15px', + marginLeft: '2px', + marginBottom: '10px', + '& .MuiButton-label': { + justifyContent: 'center', + }, + }, + }, + textPrimary: { + color: 'rgba(0,0,0,.57)', + '&:hover': { + borderColor: 'black', + backgroundColor: '#eaeaea', + }, + '&[aria-label="Grid"]': { + color: 'black', + borderColor: 'black!important', + }, + '&[aria-label="Table"]': { + color: 'black', + borderColor: 'black!important', + }, + }, + textSecondary: { + color: 'rgba(0,0,0,.57)', + '&:hover': { + borderColor: 'black', + backgroundColor: '#eaeaea', + }, + '&[aria-label="Grid"]': { + color: 'grey', + borderColor: 'grey!important', + }, + '&[aria-label="Table"]': { + color: 'grey', + borderColor: 'grey!important', + }, + }, + label: { + '& svg': { + display: 'none', + }, + '& span': { + paddingLeft: '0', + }, + }, + contained: { + boxShadow: 'none', + '&:hover': { + boxShadow: 'none', + }, + }, + }, + MuiButtonGroup: { + groupedTextHorizontal: { + justifyContent: 'flex-start', + margin: '0 .5rem', + '& button': { + width: '25%', + }, + }, + groupedTextPrimary: { + '&:not(:last-child)': { + border: 'none', + }, + }, + }, + MuiIconButton: { + root: { + '&[aria-label="Settings"]': { + padding: '12px!important', + marginRight: '-9px!important', + }, + }, + }, + MuiSwitch: { + thumb: { + color: '#eaeaea', + boxShadow: 'none', + borderRadius: '0', + }, + track: { + borderRadius: '0', + }, + switchBase: { + color: '#eaeaea', + }, + }, + MuiCheckbox: { + root: { + '& svg': { + width: '.8em', + }, + }, + }, + PrivateSwitchBase: { + root: { + padding: '8px 8px', + }, + }, + RaButton: { + button: { + marginRight: '10px', + lineHeight: 'normal', + }, + }, + MuiMenu: { + list: { + '& p': { + fontSize: '.85rem', + }, + '& p:first-of-type': { + margin: '6px 1rem', + }, + '& li:has(span.MuiCheckbox-root)': { + marginLeft: '-10px', + }, + '& span.MuiCheckbox-root .MuiSvgIcon-root': { + width: '.75em', + }, + }, + }, + MuiMenuItem: { + root: { + fontSize: '.85rem', + minHeight: 'inherit', + '&[aria-label="Clear value"]:before': { + display: 'block', + content: "'(any)'", + }, + }, + }, + MuiListItem: { + button: { + '& span.MuiCheckbox-root': { + padding: '0px 8px', + }, + }, + }, + MuiTooltip: { + tooltip: { + backgroundColor: 'rgb(117 117 117)', + }, + }, + MuiCircularProgress: { + root: { + color: '#80ea00!important', + }, + }, + MuiAvatar: { + img: { + borderRadius: '5px', + }, + }, + MuiFab: { + root: { + boxShadow: 'none', + }, + }, + MuiTableHead: { + root: { + boxShadow: 'none!important', + }, + }, + MuiTableCell: { + root: { + borderBottom: 'none', + }, + sizeSmall: { + '&:last-child': { + textAlign: 'right', + }, + '&:last-child:is(th)': { + paddingRight: '45px', + }, + }, + }, + MuiTablePagination: { + root: { + fontSize: '.6rem', + }, + caption: { + fontSize: '.6rem', + }, + menuItem: { + fontSize: '.6rem', + }, + }, + MuiTabs: { + root: { + marginBottom: '1rem', + }, + }, + MuiToolbar: { + gutters: { + '@media (min-width: 600px)': { + paddingLeft: '16px', + }, + }, + }, + RaListToolbar: { + toolbar: { + alignItems: 'start', + '& form:has(> div:nth-child(3))': { + paddingBottom: '.6rem', + }, + }, + actions: { + paddingRight: '0!important', + marginTop: '-8px', + textWrap: 'nowrap', + '@media (max-width: 599.95px)': { + marginTop: '3px', + }, + '& .MuiButton-text': { + height: '2.5rem', + padding: '7px 10px', + marginRight: '0', + }, + }, + }, + RaTopToolbar: { + root: { + '& div:first-of-type > div:first-of-type': { + display: 'flex', + flexWrap: 'wrap', + rowGap: '10px', + }, + '& div:first-of-type > div:first-of-type button': { + height: '2rem', + }, + '& div:first-of-type > div:first-of-type .MuiIconButton-root': { + padding: '0', + marginRight: '2rem', + }, + '& div:first-of-type > div:nth-of-type(2)': { + height: '2rem', + }, + }, + }, + RaToolbar: { + toolbar: { + backgroundColor: 'white', + }, + }, + RaFilterButton: { + root: { + textWrap: 'nowrap', + '& button': { + '@media (max-width: 599.95px)': { + padding: '12px', + }, + }, + "&[resource*='song']": { + marginLeft: '10px', + }, + }, + }, + RaDeleteWithUndoButton: { + deleteButton: { + color: 'rgba(0,0,0,.57)', + '&:hover': { + backgroundColor: 'rgba(0, 0, 0, 0.04)', + }, + }, + }, + RaAutocompleteSuggestionList: { + suggestionsContainer: { + borderRadius: '4px', + outline: '1px solid black', + backgroundColor: 'white', + }, + }, + RaEmpty: { + message: { + marginTop: '3rem', + }, + icon: { + display: 'none', + }, + }, + RaAutocompleteArrayInput: { + chipContainerOutlined: { + '&:empty': { + margin: '0', + }, + margin: '10px 0', + }, + chip: { + margin: '4px 4px 4px 0!important', + }, + inputInput: { + flexGrow: '0', + '& #genre_id': { + flexGrow: '0', + }, + }, + }, + RaLayout: { + content: { + width: '100%', + }, + }, + RaDatagrid: { + headerCell: { + fontWeight: 'bold', + }, + }, + NDAlbumShow: { + albumActions: { + padding: '0', + alignItems: 'center', + margin: '1rem 0', + }, + }, + MuiCardContent: { + root: { + fontFamily: 'monospace', + fontSize: '.8rem', + '& #now-playing-title': { + fontSize: '.8rem', + }, + '&:last-child': { + paddingBottom: '16px', + }, + '&[class*="makeStyles-usernameWrap-"]': { + paddingBottom: '16px', + }, + }, + }, + MuiDialogContent: { + root: { + '& .MuiTableCell-sizeSmall:last-child': { + textAlign: 'left', + }, + }, + }, + MuiGridList: { + root: { + '&:empty': { + display: 'none', + }, + backgroundColor: 'white', + borderRadius: '4px', + }, + }, + MuiGridListTile: { + root: { + '@media (max-width: 599.95px)': { + padding: '7px!important', + }, + }, + tile: { + '& img': { + borderRadius: '5px', + }, + }, + }, + NDAlbumGridView: { + root: { + '&:has(.MuiGridList-root:empty)': { + display: 'none', + }, + }, + albumContainer: { + border: '1px solid white', + borderRadius: '5px', + '& a:hover img': { + outline: '1px solid black', + }, + '& a:hover > div:nth-of-type(2)': { + border: 'none', + outline: '1px solid black', + }, + }, + albumLink: { + paddingRight: '6px', + }, + albumSubtitle: { + fontFamily: 'monospace', + }, + tileBar: { + transition: 'all 50ms ease-out', + }, + tileBarMobile: { + transition: 'all 50ms ease-out', + borderLeft: '1px solid black', + borderRight: '1px solid black', + borderBottom: '1px solid black', + }, + }, + MuiGridListTileBar: { + root: { + height: '30px!important', + background: 'white!important', + borderTop: '1px solid black', + borderBottom: '1px solid black', + borderRadius: '0 0 5px 5px', + }, + titleWrap: { + marginLeft: '0px', + }, + titlePositionBottom: { + bottom: '0', + }, + subtitle: { + '& button': { + color: 'black!important', + }, + }, + actionIcon: { + '& button': { + color: 'black!important', + }, + }, + }, + RaFilter: { + form: { + width: '100%', + '& div.filter-field:first-child': { + flex: '1 100%', + '& [class*="RaSearchInput-input-"]': { + width: '100%', + }, + }, + }, + }, + MuiInputAdornment: { + positionEnd: { + justifyContent: 'flex-end', + }, + }, + RaFilterFormInput: { + body: { + '& label': { + transform: 'translate(14px, -6px) scale(0.75)!important', + backgroundColor: '#fafafa', + padding: '0 5px', + }, + }, + hideButton: { + order: '1', + marginLeft: '2px', + top: '-7px', + padding: '8px', + }, + spacer: { + order: '2', + }, + }, + RaPaginationActions: { + actions: { + '& button': { + border: 'none', + fontSize: '.6rem', + }, + }, + }, + NDAlbumDetails: { + cover: { + borderRadius: '5px', + }, + content: { + padding: '0', + marginLeft: '1rem', + }, + externalLinks: { + marginTop: '5px', + }, + notes: { + display: 'none', + }, + root: { + '& p': { + fontSize: '.7rem', + backgroundColor: '#e0e0e0', + borderRadius: '10px', + width: 'fit-content', + padding: '2px 7px', + }, + }, + }, + NDPlaylistDetails: { + cover: { + borderRadius: '5px', + }, + }, + NDDesktopArtistDetails: { + cover: { + borderRadius: '0px', + }, + }, + NDMobileArtistDetails: { + bgContainer: { + background: + 'linear-gradient(to bottom, rgb(255 255 255 / 51%), rgb(250 250 250))!important', + }, + }, + NDArtistShow: { + actionsContainer: { + '& button': { + padding: '4px 5px', + fontSize: '0.8125rem', + height: '2rem', + }, + }, + }, + NDAudioPlayer: { + audioTitle: { + color: 'black', + }, + }, + NDLogin: { + main: { + background: 'white', + '& .MuiFormLabel-root': { + color: '#000', + }, + '& .MuiFormLabel-root.Mui-error': { + color: '#000', + }, + '& .MuiInput-underline:before': { + borderBottom: 'none', + }, + '& .MuiInput-underline:after': { + borderBottom: 'none', + }, + '& .MuiFormHelperText-root.Mui-error': { + color: '#000', + paddingLeft: '10px', + }, + '& .MuiInput-underline:hover:not(.Mui-disabled):before': { + borderBottom: 'none', + }, + }, + card: { + minWidth: 300, + marginTop: '6em', + backgroundColor: '#ffffffe6', + border: '1px solid black', + }, + avatar: { + marginTop: '1rem', + '& img': { + filter: 'invert(1)', + }, + }, + icon: {}, + input: { + '& .MuiInput-root': { + border: '1px solid black', + borderRadius: '4px', + padding: '10px', + }, + '& .MuiInputLabel-root': { + padding: '10px', + }, + '& .MuiInputLabel-shrink': { + transform: 'translate(0, -5.5px) scale(0.75)', + }, + }, + actions: { + marginTop: '2rem', + }, + button: { + boxShadow: 'none', + '&:hover': { + boxShadow: 'none', + backgroundColor: 'rgb(117, 177, 44)', + }, + }, + systemNameLink: { + fontFamily: 'monospace', + marginBottom: '1rem', + color: 'black', + '&:before': { + content: "'Welcome to '", + }, + '&:after': { + content: "' *~*!'", + }, + }, + }, + MuiCssBaseline: { + '@global': { + '*::-webkit-scrollbar': { + display: 'none', + }, + }, + }, + MuiBackdrop: { + root: { + backgroundColor: 'rgba(255, 255, 255, 0.5)', + }, + }, + RaLoading: { + message: { + fontFamily: 'monospace', + }, + }, + }, + player: { + theme: 'light', + stylesheet, + }, +} From 4c919368481ad4ddc5b0b00974ecef821cb5a9b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 18:24:46 -0400 Subject: [PATCH 57/81] chore(deps): bump picomatch in /ui (#5248) Bumps and [picomatch](https://github.com/micromatch/picomatch). These dependencies needed to be updated together. Updates `picomatch` from 2.3.1 to 2.3.2 - [Release notes](https://github.com/micromatch/picomatch/releases) - [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2) Updates `picomatch` from 4.0.3 to 4.0.4 - [Release notes](https://github.com/micromatch/picomatch/releases) - [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2) --- updated-dependencies: - dependency-name: picomatch dependency-version: 2.3.2 dependency-type: indirect - dependency-name: picomatch dependency-version: 4.0.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ui/package-lock.json | 59 +++++++++++--------------------------------- 1 file changed, 15 insertions(+), 44 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 0e6183720..a9b83d76e 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -117,7 +117,6 @@ "node_modules/@babel/core": { "version": "7.28.6", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -1530,7 +1529,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1552,7 +1550,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2296,7 +2293,6 @@ "node_modules/@jsonforms/core": { "version": "2.5.2", "license": "MIT", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.3", "ajv": "^6.10.2", @@ -2340,7 +2336,6 @@ "node_modules/@jsonforms/react": { "version": "2.5.2", "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.15", "object-hash": "^2.0.0" @@ -2353,7 +2348,6 @@ "node_modules/@material-ui/core": { "version": "4.12.4", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.4.4", "@material-ui/styles": "^4.11.5", @@ -2396,7 +2390,6 @@ "node_modules/@material-ui/icons": { "version": "4.11.3", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.4.4" }, @@ -2794,7 +2787,9 @@ "license": "MIT" }, "node_modules/@rollup/pluginutils/node_modules/picomatch": { - "version": "4.0.3", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" @@ -3013,7 +3008,6 @@ "node_modules/@types/hoist-non-react-statics": { "version": "3.3.7", "license": "MIT", - "peer": true, "dependencies": { "hoist-non-react-statics": "^3.3.0" }, @@ -3054,7 +3048,6 @@ "version": "24.10.9", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3070,7 +3063,6 @@ "node_modules/@types/react": { "version": "17.0.90", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "^0.16", @@ -3206,7 +3198,6 @@ "version": "6.21.0", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -3504,7 +3495,6 @@ "node_modules/acorn": { "version": "8.15.0", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4076,7 +4066,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4390,7 +4379,6 @@ "node_modules/connected-react-router": { "version": "6.9.3", "license": "MIT", - "peer": true, "dependencies": { "lodash.isequalwith": "^4.4.0", "prop-types": "^15.7.2" @@ -5142,7 +5130,6 @@ "version": "8.57.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -5640,7 +5627,6 @@ "node_modules/final-form": { "version": "4.20.10", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.10.0" }, @@ -5655,7 +5641,6 @@ "node_modules/final-form-arrays": { "version": "3.1.0", "license": "MIT", - "peer": true, "peerDependencies": { "final-form": "^4.20.8" } @@ -6004,7 +5989,6 @@ "version": "20.3.3", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", @@ -6100,7 +6084,6 @@ "node_modules/history": { "version": "4.10.1", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.1.2", "loose-envify": "^1.2.0", @@ -7433,7 +7416,6 @@ "node_modules/moment": { "version": "2.30.1", "license": "MIT", - "peer": true, "engines": { "node": "*" } @@ -7932,7 +7914,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -8039,7 +8023,6 @@ "node_modules/prop-types": { "version": "15.8.1", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -8115,7 +8098,6 @@ "node_modules/ra-core": { "version": "3.19.12", "license": "MIT", - "peer": true, "dependencies": { "classnames": "~2.3.1", "date-fns": "^1.29.0", @@ -8469,7 +8451,6 @@ "node_modules/react": { "version": "17.0.2", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -8543,7 +8524,6 @@ "node_modules/react-dom": { "version": "17.0.2", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -8608,7 +8588,6 @@ "node_modules/react-final-form": { "version": "6.5.9", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.15.4" }, @@ -8624,7 +8603,6 @@ "node_modules/react-final-form-arrays": { "version": "3.1.4", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.19.4" }, @@ -8711,7 +8689,6 @@ "node_modules/react-redux": { "version": "7.2.9", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.15.4", "@types/react-redux": "^7.1.20", @@ -8743,7 +8720,6 @@ "node_modules/react-router": { "version": "5.3.4", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -8762,7 +8738,6 @@ "node_modules/react-router-dom": { "version": "5.3.4", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -8916,7 +8891,6 @@ "node_modules/redux": { "version": "4.2.1", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.9.2" } @@ -8924,7 +8898,6 @@ "node_modules/redux-saga": { "version": "1.4.2", "license": "MIT", - "peer": true, "dependencies": { "@redux-saga/core": "^1.4.2" } @@ -9133,7 +9106,6 @@ "version": "4.55.2", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -9925,10 +9897,11 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10116,7 +10089,6 @@ "version": "5.9.3", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10319,7 +10291,6 @@ "version": "7.3.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -10435,10 +10406,11 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10450,7 +10422,6 @@ "version": "4.0.17", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.17", "@vitest/mocker": "4.0.17", @@ -10524,7 +10495,9 @@ } }, "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.3", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -10904,7 +10877,6 @@ "node_modules/workbox-build/node_modules/ajv": { "version": "8.18.0", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -10978,7 +10950,6 @@ "node_modules/workbox-build/node_modules/rollup": { "version": "2.79.2", "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, From 33e20d355ef2c5edfe1cbeafd98062744a0d5b67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= <deluan@navidrome.org> Date: Wed, 25 Mar 2026 21:30:40 -0400 Subject: [PATCH 58/81] fix(ui): cancel in-flight image requests on pagination, cache across remounts (#5249) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(ui): cancel in-flight image requests on pagination and cache across remounts When paginating quickly through list/grid views, image requests for previous pages were never canceled, queuing on the server and blocking new images. This adds a useImageUrl hook that loads images via fetch() with AbortController, so requests are canceled when components unmount. A module-level cache (URL → blob URL) with reference counting ensures React Admin refreshes display images instantly without re-fetching. * feat(ui): update AlbumListPagination to conditionally render based on albumListType Signed-off-by: Deluan <deluan@navidrome.org> * feat(ui): abort all in-flight image fetches on pagination change Pagination component now watches page/perPage via useListContext and calls abortAllInFlight() when either changes, freeing the browser connection pool immediately for the next page's data request. Also adds empty placeholder style to CoverArtAvatar so it renders as a clean transparent area while loading instead of the default person icon. * Revert "feat(ui): abort all in-flight image fetches on pagination change" This reverts commit 3bc09f9d0374aa63572a381e38a30e2f2cec4da8. * fix(ui): limit concurrent image fetches to prevent connection pool saturation With <img src>, the browser prioritizes API requests over image loads. With fetch(), all requests compete equally for the HTTP/1.1 connection pool (6 per origin), causing API requests to queue behind images and making pagination feel unresponsive. Caps concurrent image fetches at 4 with a pending queue, leaving connections free for API requests. Queued fetches for unmounted components are removed without ever hitting the network. * fix(ui): fix queued fetch not aborted on unmount Set queued=false when doFetch executes from the pending queue, so cleanup correctly calls controller.abort() instead of searching an already-drained queue. --------- Signed-off-by: Deluan <deluan@navidrome.org> --- ui/src/album/AlbumGridView.jsx | 27 +--- ui/src/album/AlbumList.jsx | 13 +- ui/src/common/CoverArtAvatar.jsx | 23 ++- ui/src/common/index.js | 1 + ui/src/common/useImageUrl.js | 144 ++++++++++++++++++ ui/src/common/useImageUrl.test.js | 234 ++++++++++++++++++++++++++++++ ui/src/radio/RadioList.jsx | 16 +- 7 files changed, 423 insertions(+), 35 deletions(-) create mode 100644 ui/src/common/useImageUrl.js create mode 100644 ui/src/common/useImageUrl.test.js diff --git a/ui/src/album/AlbumGridView.jsx b/ui/src/album/AlbumGridView.jsx index e90e7a77b..c8a161571 100644 --- a/ui/src/album/AlbumGridView.jsx +++ b/ui/src/album/AlbumGridView.jsx @@ -18,6 +18,7 @@ import { PlayButton, ArtistLinkField, OverflowTooltip, + useImageUrl, } from '../common' import { COVER_ART_SIZE, DraggableTypes } from '../consts' import clsx from 'clsx' @@ -105,7 +106,7 @@ const useCoverStyles = makeStyles({ transition: 'opacity 0.3s ease-in-out', }, coverLoading: { - opacity: 0.5, + opacity: 0, }, }) @@ -125,8 +126,6 @@ const Cover = withContentRect('bounds')(({ // Force height to be the same as the width determined by the GridList // noinspection JSSuspiciousNameCombination const classes = useCoverStyles({ height: contentRect.bounds.width }) - const [imageLoading, setImageLoading] = React.useState(true) - const [imageError, setImageError] = React.useState(false) const [, dragAlbumRef] = useDrag( () => ({ type: DraggableTypes.ALBUM, @@ -136,32 +135,16 @@ const Cover = withContentRect('bounds')(({ [record], ) - // Reset image state when record changes - React.useEffect(() => { - setImageLoading(true) - setImageError(false) - }, [record.id]) - - const handleImageLoad = React.useCallback(() => { - setImageLoading(false) - setImageError(false) - }, []) - - const handleImageError = React.useCallback(() => { - setImageLoading(false) - setImageError(true) - }, []) + const url = subsonic.getCoverArtUrl(record, COVER_ART_SIZE, true) + const { imgUrl, loading: imageLoading } = useImageUrl(url) return ( <div ref={measureRef} className={classes.coverContainer}> <div ref={dragAlbumRef}> <img - key={record.id} // Force re-render when record changes - src={subsonic.getCoverArtUrl(record, COVER_ART_SIZE, true)} + src={imgUrl || undefined} alt={record.name} className={`${classes.cover} ${imageLoading ? classes.coverLoading : ''}`} - onLoad={handleImageLoad} - onError={handleImageError} /> </div> </div> diff --git a/ui/src/album/AlbumList.jsx b/ui/src/album/AlbumList.jsx index d00d97701..28a24981c 100644 --- a/ui/src/album/AlbumList.jsx +++ b/ui/src/album/AlbumList.jsx @@ -175,12 +175,12 @@ const AlbumListTitle = ({ albumListType }) => { return <Title subTitle={title} args={{ smart_count: 2 }} /> } -const AlbumListPagination = (props) => { +const AlbumListPagination = ({ albumListType, ...rest }) => { const { loading } = useListContext() - if (loading) { + if (loading && albumListType === 'random') { return null } - return <Pagination {...props} /> + return <Pagination {...rest} /> } const randomStartingSeed = Math.random().toString() @@ -243,7 +243,12 @@ const AlbumList = (props) => { actions={<AlbumListActions />} filters={<AlbumFilter />} perPage={perPage} - pagination={<AlbumListPagination rowsPerPageOptions={perPageOptions} />} + pagination={ + <AlbumListPagination + rowsPerPageOptions={perPageOptions} + albumListType={albumListType} + /> + } title={<AlbumListTitle albumListType={albumListType} />} > {albumView.grid ? ( diff --git a/ui/src/common/CoverArtAvatar.jsx b/ui/src/common/CoverArtAvatar.jsx index 5642f2504..bc6dd8a3c 100644 --- a/ui/src/common/CoverArtAvatar.jsx +++ b/ui/src/common/CoverArtAvatar.jsx @@ -4,12 +4,16 @@ import { makeStyles } from '@material-ui/core/styles' import clsx from 'clsx' import { COVER_ART_SIZE } from '../consts' import subsonic from '../subsonic' +import { useImageUrl } from './useImageUrl' const useStyles = makeStyles({ avatar: { width: '55px', height: '55px', }, + avatarEmpty: { + backgroundColor: 'transparent', + }, square: { borderRadius: '4px', }, @@ -22,15 +26,26 @@ export const CoverArtAvatar = ({ const classes = useStyles() const recordContext = useRecordContext() const record = recordProp || recordContext - if (!record) return null const square = variant !== 'circular' + const url = record + ? subsonic.getCoverArtUrl(record, COVER_ART_SIZE, square) + : null + const { imgUrl } = useImageUrl(url) + if (!record) return null return ( <Avatar - src={subsonic.getCoverArtUrl(record, COVER_ART_SIZE, square)} + src={imgUrl || undefined} variant={variant} - className={clsx(classes.avatar, square && classes.square)} + className={clsx( + classes.avatar, + square && classes.square, + !imgUrl && classes.avatarEmpty, + )} alt={record.name} - /> + > + {/* Empty child prevents default person icon while loading */} + {!imgUrl && <span />} + </Avatar> ) } diff --git a/ui/src/common/index.js b/ui/src/common/index.js index a7d6a43c4..362a0ced3 100644 --- a/ui/src/common/index.js +++ b/ui/src/common/index.js @@ -46,3 +46,4 @@ export * from './useSearchRefocus' export * from './ImageUploadOverlay' export * from './CoverArtAvatar' export * from './useImageLoadingState' +export * from './useImageUrl' diff --git a/ui/src/common/useImageUrl.js b/ui/src/common/useImageUrl.js new file mode 100644 index 000000000..9bcc70d74 --- /dev/null +++ b/ui/src/common/useImageUrl.js @@ -0,0 +1,144 @@ +import { useEffect, useState, useRef } from 'react' + +// Persists across component mount/unmount cycles so that +// React Admin refreshes (which remount list items) don't re-fetch images. +const cache = new Map() +const MAX_CACHE_SIZE = 300 + +// Limit concurrent fetches to leave browser connections free for API requests. +// Browsers allow ~6 connections per origin on HTTP/1.1; reserving 2 for API +// calls prevents image fetches from blocking pagination/data requests. +const MAX_CONCURRENT = 4 +let activeFetches = 0 +const pendingQueue = [] + +const processQueue = () => { + while (pendingQueue.length > 0 && activeFetches < MAX_CONCURRENT) { + const next = pendingQueue.shift() + next() + } +} + +// Evicts oldest unused entries (Map iterates in insertion order). +const evictIfNeeded = () => { + if (cache.size <= MAX_CACHE_SIZE) return + for (const [key, entry] of cache) { + if (cache.size <= MAX_CACHE_SIZE) break + if (entry.refCount === 0) { + if (entry.blobUrl) URL.revokeObjectURL(entry.blobUrl) + cache.delete(key) + } + } +} + +/** + * Loads an image via fetch() with AbortController so that in-flight requests + * are canceled on unmount (e.g., during pagination). Uses a module-level cache + * so remounting returns the cached blob URL instantly. + */ +export const useImageUrl = (url) => { + const cached = url ? cache.get(url) : null + const [imgUrl, setImgUrl] = useState(cached?.blobUrl || null) + const [loading, setLoading] = useState(!!url && !cached) + const [error, setError] = useState(cached?.error || false) + const abortedRef = useRef(false) + + useEffect(() => { + abortedRef.current = false + + if (!url) { + setImgUrl(null) + setLoading(false) + setError(false) + return + } + + // Re-check: another component's effect may have populated the cache + // between this component's render and effect execution. + const entry = cache.get(url) + if (entry) { + entry.refCount++ + setImgUrl(entry.blobUrl) + setLoading(false) + setError(entry.error || false) + return () => { + entry.refCount-- + } + } + + const controller = new AbortController() + let queued = true + setImgUrl(null) + setLoading(true) + setError(false) + + const doFetch = () => { + queued = false + activeFetches++ + fetch(url, { signal: controller.signal }) + .then((res) => { + if (!res.ok) { + throw new Error(`HTTP ${res.status}`) + } + return res.blob() + }) + .then((blob) => { + activeFetches-- + processQueue() + // Guard against late resolution after abort + if (abortedRef.current) { + return + } + const objectUrl = URL.createObjectURL(blob) + // Handle concurrent fetches: if another component already cached + // this URL, use its entry and discard our blob. + const existing = cache.get(url) + if (existing && existing.blobUrl) { + existing.refCount++ + URL.revokeObjectURL(objectUrl) + setImgUrl(existing.blobUrl) + } else { + cache.set(url, { blobUrl: objectUrl, refCount: 1 }) + evictIfNeeded() + setImgUrl(objectUrl) + } + setLoading(false) + }) + .catch((err) => { + activeFetches-- + processQueue() + if (err.name === 'AbortError') { + return // Expected on unmount or URL change + } + // Cache the error so repeated mounts don't re-fetch broken URLs + cache.set(url, { blobUrl: null, error: true, refCount: 0 }) + setError(true) + setLoading(false) + }) + } + + if (activeFetches < MAX_CONCURRENT) { + queued = false + doFetch() + } else { + pendingQueue.push(doFetch) + } + + return () => { + abortedRef.current = true + if (queued) { + // Remove from queue if not yet started + const idx = pendingQueue.indexOf(doFetch) + if (idx !== -1) pendingQueue.splice(idx, 1) + } else { + controller.abort() + } + const entry = cache.get(url) + if (entry) { + entry.refCount-- + } + } + }, [url]) + + return { imgUrl, loading, error } +} diff --git a/ui/src/common/useImageUrl.test.js b/ui/src/common/useImageUrl.test.js new file mode 100644 index 000000000..976317105 --- /dev/null +++ b/ui/src/common/useImageUrl.test.js @@ -0,0 +1,234 @@ +import { renderHook, act } from '@testing-library/react-hooks' +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest' + +// Helper to flush all pending promises +const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0)) + +// We need a fresh module for each test to reset the module-level cache +let useImageUrl + +describe('useImageUrl', () => { + let abortSpy + let OriginalAbortController + let originalCreateObjectURL + let originalRevokeObjectURL + let originalFetch + + beforeEach(async () => { + // Reset module to clear the cache + vi.resetModules() + const mod = await import('./useImageUrl') + useImageUrl = mod.useImageUrl + + abortSpy = vi.fn() + OriginalAbortController = global.AbortController + originalCreateObjectURL = global.URL.createObjectURL + originalRevokeObjectURL = global.URL.revokeObjectURL + originalFetch = global.fetch + + global.AbortController = function () { + this.signal = 'mock-signal' + this.abort = abortSpy + } + global.URL.createObjectURL = vi.fn(() => 'blob:mock-url') + global.URL.revokeObjectURL = vi.fn() + }) + + afterEach(() => { + global.AbortController = OriginalAbortController + global.URL.createObjectURL = originalCreateObjectURL + global.URL.revokeObjectURL = originalRevokeObjectURL + global.fetch = originalFetch + vi.restoreAllMocks() + }) + + it('should return null values when url is null', () => { + const { result } = renderHook(() => useImageUrl(null)) + + expect(result.current.loading).toBe(false) + expect(result.current.imgUrl).toBeNull() + expect(result.current.error).toBe(false) + }) + + it('should return loading state initially', () => { + global.fetch = vi.fn(() => new Promise(() => {})) + const { result } = renderHook(() => + useImageUrl('http://example.com/img.jpg'), + ) + + expect(result.current.loading).toBe(true) + expect(result.current.imgUrl).toBeNull() + expect(result.current.error).toBe(false) + }) + + it('should fetch image and return blob URL on success', async () => { + const mockBlob = new Blob(['image-data'], { type: 'image/png' }) + global.fetch = vi.fn(() => + Promise.resolve({ + ok: true, + blob: () => Promise.resolve(mockBlob), + }), + ) + + const { result } = renderHook(() => + useImageUrl('http://example.com/img.jpg'), + ) + + await act(async () => { + await flushPromises() + }) + + expect(result.current.loading).toBe(false) + expect(result.current.imgUrl).toBe('blob:mock-url') + expect(result.current.error).toBe(false) + expect(global.fetch).toHaveBeenCalledWith('http://example.com/img.jpg', { + signal: 'mock-signal', + }) + }) + + it('should set error on HTTP failure', async () => { + global.fetch = vi.fn(() => Promise.resolve({ ok: false, status: 404 })) + + const { result } = renderHook(() => + useImageUrl('http://example.com/missing.jpg'), + ) + + await act(async () => { + await flushPromises() + }) + + expect(result.current.loading).toBe(false) + expect(result.current.imgUrl).toBeNull() + expect(result.current.error).toBe(true) + }) + + it('should abort fetch on unmount', async () => { + global.fetch = vi.fn(() => new Promise(() => {})) + + const { unmount } = renderHook(() => + useImageUrl('http://example.com/img.jpg'), + ) + + await act(async () => { + await flushPromises() + }) + + unmount() + + expect(abortSpy).toHaveBeenCalled() + }) + + it('should abort previous fetch when URL changes', async () => { + const abortSpies = [] + global.AbortController = function () { + const spy = vi.fn() + abortSpies.push(spy) + this.signal = `signal-${abortSpies.length}` + this.abort = spy + } + + const mockBlob = new Blob(['data'], { type: 'image/png' }) + global.fetch = vi.fn(() => + Promise.resolve({ + ok: true, + blob: () => Promise.resolve(mockBlob), + }), + ) + + const { rerender } = renderHook(({ url }) => useImageUrl(url), { + initialProps: { url: 'http://example.com/img1.jpg' }, + }) + + await act(async () => { + await flushPromises() + }) + + // Change URL - should abort the first controller + rerender({ url: 'http://example.com/img2.jpg' }) + + expect(abortSpies[0]).toHaveBeenCalled() + }) + + it('should not set error on AbortError', async () => { + const abortError = new DOMException('Aborted', 'AbortError') + global.fetch = vi.fn(() => Promise.reject(abortError)) + + const { result } = renderHook(() => + useImageUrl('http://example.com/img.jpg'), + ) + + await act(async () => { + await flushPromises() + }) + + expect(result.current.error).toBe(false) + }) + + it('should use cached blob URL on remount without re-fetching', async () => { + const mockBlob = new Blob(['data'], { type: 'image/png' }) + global.fetch = vi.fn(() => + Promise.resolve({ + ok: true, + blob: () => Promise.resolve(mockBlob), + }), + ) + + // First mount — fetches and caches + const { unmount } = renderHook(() => + useImageUrl('http://example.com/img.jpg'), + ) + + await act(async () => { + await flushPromises() + }) + + expect(global.fetch).toHaveBeenCalledTimes(1) + + // Unmount (simulates React Admin refresh) + unmount() + + // Remount with same URL — should use cache + const { result: result2 } = renderHook(() => + useImageUrl('http://example.com/img.jpg'), + ) + + await act(async () => { + await flushPromises() + }) + + // Should NOT have fetched again + expect(global.fetch).toHaveBeenCalledTimes(1) + expect(result2.current.imgUrl).toBe('blob:mock-url') + expect(result2.current.loading).toBe(false) + }) + + it('should cache errors and not re-fetch broken URLs', async () => { + global.fetch = vi.fn(() => Promise.resolve({ ok: false, status: 404 })) + + // First mount — fetch fails and error is cached + const { unmount } = renderHook(() => + useImageUrl('http://example.com/broken.jpg'), + ) + + await act(async () => { + await flushPromises() + }) + + expect(global.fetch).toHaveBeenCalledTimes(1) + unmount() + + // Remount with same URL — should use cached error, not re-fetch + const { result: result2 } = renderHook(() => + useImageUrl('http://example.com/broken.jpg'), + ) + + await act(async () => { + await flushPromises() + }) + + expect(global.fetch).toHaveBeenCalledTimes(1) + expect(result2.current.error).toBe(true) + expect(result2.current.imgUrl).toBeNull() + expect(result2.current.loading).toBe(false) + }) +}) diff --git a/ui/src/radio/RadioList.jsx b/ui/src/radio/RadioList.jsx index 582fcaffc..945bac519 100644 --- a/ui/src/radio/RadioList.jsx +++ b/ui/src/radio/RadioList.jsx @@ -14,8 +14,12 @@ import { UrlField, useTranslate, } from 'react-admin' -import { List } from '../common' -import { ToggleFieldsMenu, useSelectedFields } from '../common' +import { + List, + useImageUrl, + ToggleFieldsMenu, + useSelectedFields, +} from '../common' import subsonic from '../subsonic' import { StreamField } from './StreamField' import { setTrack } from '../actions' @@ -78,10 +82,12 @@ const RadioListActions = ({ const avatarStyle = { width: 40, height: 40 } const CoverArtField = ({ record }) => { - if (!record) return null - const src = record.uploadedImage + const directUrl = record?.uploadedImage ? subsonic.getCoverArtUrl(record, 40, true) - : RADIO_PLACEHOLDER_IMAGE + : null + const { imgUrl } = useImageUrl(directUrl) + if (!record) return null + const src = imgUrl || RADIO_PLACEHOLDER_IMAGE return ( <Avatar src={src} variant="rounded" style={avatarStyle} alt={record.name} /> ) From ccee33f47410fcef245014c73117c7fe916ac900 Mon Sep 17 00:00:00 2001 From: Deluan <deluan@navidrome.org> Date: Thu, 26 Mar 2026 20:15:28 -0400 Subject: [PATCH 59/81] fix(search): use explicit AND in FTS5 queries to fix apostrophe search FTS5's implicit AND (space-separated tokens) silently fails when combined with parenthesized OR groups produced by processPunctuatedWords. For example, searching "you've got" generated the query `("you ve" OR youve*) got*` which returned no results. Using explicit AND (`("you ve" OR youve*) AND got*`) resolves this FTS5 quirk. Since implicit and explicit AND are semantically identical in FTS5, this change is safe for all queries unconditionally. --- persistence/sql_search_fts.go | 4 +++- persistence/sql_search_fts_test.go | 29 +++++++++++++++-------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/persistence/sql_search_fts.go b/persistence/sql_search_fts.go index 9eb01f0cf..1d4116b5d 100644 --- a/persistence/sql_search_fts.go +++ b/persistence/sql_search_fts.go @@ -178,7 +178,9 @@ func buildFTS5Query(userInput string) string { tokens[i] = t + "*" } - result = strings.Join(tokens, " ") + // Use explicit AND between tokens — FTS5's implicit AND (space-separated) + // doesn't work correctly with parenthesized OR groups from processPunctuatedWords. + result = strings.Join(tokens, " AND ") for i, phrase := range phrases { placeholder := fmt.Sprintf("\x00PHRASE%d\x00", i) diff --git a/persistence/sql_search_fts_test.go b/persistence/sql_search_fts_test.go index 337d54201..d0e26c8e3 100644 --- a/persistence/sql_search_fts_test.go +++ b/persistence/sql_search_fts_test.go @@ -17,32 +17,33 @@ var _ = DescribeTable("buildFTS5Query", Entry("returns empty string for empty input", "", ""), Entry("returns empty string for whitespace-only input", " ", ""), Entry("appends * to a single word for prefix matching", "beatles", "beatles*"), - Entry("appends * to each word for prefix matching", "abbey road", "abbey* road*"), + Entry("appends * to each word for prefix matching", "abbey road", "abbey* AND road*"), Entry("preserves quoted phrases without appending *", `"the beatles"`, `"the beatles"`), Entry("does not double-append * to existing prefix wildcard", "beat*", "beat*"), - Entry("strips FTS5 operators and appends * to lowercased words", "AND OR NOT NEAR", "and* or* not* near*"), - Entry("strips special FTS5 syntax characters and appends *", "test^col:val", "test* col* val*"), - Entry("handles mixed phrases and words", `"the beatles" abbey`, `"the beatles" abbey*`), - Entry("handles prefix with multiple words", "beat* abbey", "beat* abbey*"), - Entry("collapses multiple spaces", "abbey road", "abbey* road*"), + Entry("strips FTS5 operators and appends * to lowercased words", "AND OR NOT NEAR", "and* AND or* AND not* AND near*"), + Entry("strips special FTS5 syntax characters and appends *", "test^col:val", "test* AND col* AND val*"), + Entry("handles mixed phrases and words", `"the beatles" abbey`, `"the beatles" AND abbey*`), + Entry("handles prefix with multiple words", "beat* abbey", "beat* AND abbey*"), + Entry("collapses multiple spaces", "abbey road", "abbey* AND road*"), Entry("strips leading * from tokens and appends trailing *", "*livia", "livia*"), - Entry("strips leading * and preserves existing trailing *", "*livia oliv*", "livia* oliv*"), + Entry("strips leading * and preserves existing trailing *", "*livia oliv*", "livia* AND oliv*"), Entry("strips standalone *", "*", ""), - Entry("strips apostrophe from input", "Guns N' Roses", "Guns* N* Roses*"), + Entry("strips apostrophe from input", "Guns N' Roses", "Guns* AND N* AND Roses*"), Entry("converts slashed word to phrase+concat OR", "AC/DC", `("AC DC" OR ACDC*)`), Entry("converts hyphenated word to phrase+concat OR", "a-ha", `("a ha" OR aha*)`), Entry("converts partial hyphenated word to phrase+concat OR", "a-h", `("a h" OR ah*)`), Entry("converts hyphenated name to phrase+concat OR", "Jay-Z", `("Jay Z" OR JayZ*)`), Entry("converts contraction to phrase+concat OR", "it's", `("it s" OR its*)`), - Entry("handles punctuated word mixed with plain words", "best of a-ha", `best* of* ("a ha" OR aha*)`), - Entry("strips miscellaneous punctuation", "rock & roll, vol. 2", "rock* roll* vol* 2*"), - Entry("preserves unicode characters with diacritics", "Björk début", "Björk* début*"), + Entry("handles punctuated word mixed with plain words", "best of a-ha", `best* AND of* AND ("a ha" OR aha*)`), + Entry("handles contraction followed by plain words", "you've got", `("you ve" OR youve*) AND got*`), + Entry("strips miscellaneous punctuation", "rock & roll, vol. 2", "rock* AND roll* AND vol* AND 2*"), + Entry("preserves unicode characters with diacritics", "Björk début", "Björk* AND début*"), Entry("collapses dotted abbreviation into phrase", "R.E.M.", `"R E M"`), Entry("collapses abbreviation without trailing dot", "R.E.M", `"R E M"`), - Entry("collapses abbreviation mixed with words", "best of R.E.M.", `best* of* "R E M"`), + Entry("collapses abbreviation mixed with words", "best of R.E.M.", `best* AND of* AND "R E M"`), Entry("collapses two-letter abbreviation", "U.K.", `"U K"`), - Entry("does not collapse single letter surrounded by words", "I am fine", "I* am* fine*"), - Entry("does not collapse single standalone letter", "A test", "A* test*"), + Entry("does not collapse single letter surrounded by words", "I am fine", "I* AND am* AND fine*"), + Entry("does not collapse single standalone letter", "A test", "A* AND test*"), Entry("preserves quoted phrase with punctuation verbatim", `"ac/dc"`, `"ac/dc"`), Entry("preserves quoted abbreviation verbatim", `"R.E.M."`, `"R.E.M."`), Entry("returns empty string for punctuation-only input", "!!!!!!!", ""), From 79e1af7cd6c9a5d1b8e4d4cea7e33eeb5bd5d912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= <deluan@navidrome.org> Date: Fri, 27 Mar 2026 18:04:47 -0400 Subject: [PATCH 60/81] fix(ui): update Danish, German, Greek, Finnish, Galician, Portuguese (BR), Swedish, Ukrainian, Chinese (traditional) translations from POEditor (#5218) Co-authored-by: navidrome-bot <navidrome-bot@navidrome.org> --- resources/i18n/da.json | 16 +++++-- resources/i18n/de.json | 6 +-- resources/i18n/el.json | 16 +++++-- resources/i18n/fi.json | 4 +- resources/i18n/gl.json | 16 +++++-- resources/i18n/pt-br.json | 21 ++++---- resources/i18n/sv.json | 16 +++++-- resources/i18n/uk.json | 95 +++++++++++++++++++++++++++++++++++-- resources/i18n/zh-Hant.json | 4 +- 9 files changed, 153 insertions(+), 41 deletions(-) diff --git a/resources/i18n/da.json b/resources/i18n/da.json index a47b30bbc..edb1df183 100644 --- a/resources/i18n/da.json +++ b/resources/i18n/da.json @@ -38,7 +38,7 @@ "missing": "Manglende", "libraryName": "Bibliotek", "composer": "Komponist", - "disc": "" + "disc": "Disk %{discNumber}" }, "actions": { "addToQueue": "Afspil senere", @@ -355,7 +355,7 @@ "selectedUsers": "Valgte brugere", "allLibraries": "Tillad alle biblioteker", "selectedLibraries": "Valgte biblioteker", - "allowWriteAccess": "" + "allowWriteAccess": "Tillad skriveadgang" }, "sections": { "status": "Status", @@ -401,7 +401,7 @@ "requiredHosts": "Påkrævede hosts", "configValidationError": "Konfigurationsvalidering mislykkedes:", "schemaRenderError": "Kan ikke vise konfigurationsformularen. Pluginets skema er muligvis ugyldigt.", - "allowWriteAccessHelp": "" + "allowWriteAccessHelp": "Når aktiveret, kan denne plugin rette filer i biblioteksmapper. På forhånd har plugins kun læseadgang." }, "placeholders": { "configKey": "nøgle", @@ -591,7 +591,13 @@ "remove_all_missing_content": "Er du sikker på, at du vil fjerne alle manglende filer fra databasen? Dét vil permanent fjerne alle referencer til dem, inklusive deres afspilningstællere og vurderinger.", "noSimilarSongsFound": "Ingen lignende sange fundet", "noTopSongsFound": "Ingen topsange fundet", - "startingInstantMix": "Indlæser Instant Mix..." + "startingInstantMix": "Indlæser Instant Mix...", + "uploadCover": "Upload omslag", + "removeCover": "Fjern omslag", + "coverUploaded": "Omslagsbillede opdateret", + "coverRemoved": "Omslagsbillede fjernet", + "coverUploadError": "Fejl ved upload af omslagsbillede", + "coverRemoveError": "Fejl ved fjernelse af omslagsbillede" }, "menu": { "library": "Bibliotek", @@ -712,4 +718,4 @@ "empty": "Intet afspilles nu", "minutesAgo": "for %{smart_count} minut siden |||| for %{smart_count} minutter siden" } -} +} \ No newline at end of file diff --git a/resources/i18n/de.json b/resources/i18n/de.json index ab1760ed5..c540dee05 100644 --- a/resources/i18n/de.json +++ b/resources/i18n/de.json @@ -18,7 +18,7 @@ "size": "Dateigröße", "updatedAt": "Hochgeladen am", "bitRate": "Bitrate", - "discSubtitle": "CD Untertitel", + "discSubtitle": "Disc Untertitel", "starred": "Favorit", "comment": "Kommentar", "rating": "Bewertung", @@ -38,7 +38,7 @@ "missing": "Fehlend", "libraryName": "Bibliothek", "composer": "Komponist", - "disc": "" + "disc": "Disc %{discNumber}" }, "actions": { "addToQueue": "Später abspielen", @@ -718,4 +718,4 @@ "empty": "Keine Wiedergabe", "minutesAgo": "Vor %{smart_count} Minute |||| Vor %{smart_count} Minuten" } -} +} \ No newline at end of file diff --git a/resources/i18n/el.json b/resources/i18n/el.json index 019d05978..3876c2e33 100644 --- a/resources/i18n/el.json +++ b/resources/i18n/el.json @@ -38,7 +38,7 @@ "missing": "Απών", "libraryName": "Βιβλιοθήκη", "composer": "Συνθέτης", - "disc": "" + "disc": "Δίσκος %{discNumber}" }, "actions": { "addToQueue": "Αναπαραγωγη Μετα", @@ -355,7 +355,7 @@ "selectedUsers": "Επιλογή χρηστών", "allLibraries": "Επιτρέψτε όλες τις βιβλιοθήκες", "selectedLibraries": "Επιλεγμένες βιβλιοθήκες", - "allowWriteAccess": "" + "allowWriteAccess": "Επιτρέψτε την πρόσβαση εγγραφής" }, "sections": { "status": "Κατάσταση", @@ -401,7 +401,7 @@ "requiredHosts": "Απαιτούμενοι hosts", "configValidationError": "Η επικύρωση διαμόρφωσης απέτυχε:", "schemaRenderError": "Δεν είναι δυνατή η απόδοση της φόρμας διαμόρφωσης. Το σχήμα της προσθήκης ενδέχεται να μην είναι έγκυρο.", - "allowWriteAccessHelp": "" + "allowWriteAccessHelp": "Όταν είναι ενεργοποιημένο, το πρόσθετο μπορεί να τροποποιήσει αρχεία στους καταλόγους της βιβλιοθήκης. Από προεπιλογή, τα πρόσθετα έχουν πρόσβαση μόνο για ανάγνωση." }, "placeholders": { "configKey": "κλειδί", @@ -591,7 +591,13 @@ "remove_all_missing_content": "Είστε βέβαιοι ότι θέλετε να καταργήσετε όλα τα αρχεία που λείπουν από τη βάση δεδομένων? Αυτό θα καταργήσει οριστικά τυχόν αναφορές σε αυτά, συμπεριλαμβανομένου του αριθμού αναπαραγωγών και των αξιολογήσεών τους.", "noSimilarSongsFound": "Δεν βρέθηκαν παρόμοια τραγούδια", "noTopSongsFound": "Δεν βρέθηκαν κορυφαία τραγούδια", - "startingInstantMix": "Φόρτωση Άμεσης Μίξης..." + "startingInstantMix": "Φόρτωση Άμεσης Μίξης...", + "uploadCover": "Μεταφόρτωση εξωφύλλου", + "removeCover": "Αφαίρεση καλύμματος", + "coverUploaded": "Το εξώφυλλο ενημερώθηκε", + "coverRemoved": "Το εξώφυλλο αφαιρέθηκε", + "coverUploadError": "Σφάλμα κατά τη μεταφόρτωση του εξωφύλλου", + "coverRemoveError": "Σφάλμα κατά την αφαίρεση του εξωφύλλου" }, "menu": { "library": "Βιβλιοθήκη", @@ -712,4 +718,4 @@ "empty": "Δεν παίζει τίποτα", "minutesAgo": "%{smart_count} λεπτό πριν |||| %{smart_count} λεπτά πριν" } -} +} \ No newline at end of file diff --git a/resources/i18n/fi.json b/resources/i18n/fi.json index 59f353350..bbad47bd6 100644 --- a/resources/i18n/fi.json +++ b/resources/i18n/fi.json @@ -38,7 +38,7 @@ "missing": "Puuttuva", "libraryName": "Kirjasto", "composer": "Säveltäjä", - "disc": "" + "disc": "Levy %{discNumber}" }, "actions": { "addToQueue": "Lisää jonoon", @@ -718,4 +718,4 @@ "empty": "Ei soita mitään", "minutesAgo": "%{smart_count} minuutti sitten |||| %{smart_count} minuuttia sitten" } -} +} \ No newline at end of file diff --git a/resources/i18n/gl.json b/resources/i18n/gl.json index aba22b714..d62ca2ab2 100644 --- a/resources/i18n/gl.json +++ b/resources/i18n/gl.json @@ -38,7 +38,7 @@ "missing": "Falta", "libraryName": "Biblioteca", "composer": "Composición", - "disc": "" + "disc": "Disco %{discNumber}" }, "actions": { "addToQueue": "Ao final da cola", @@ -355,7 +355,7 @@ "selectedUsers": "Usuarias seleccionadas", "allLibraries": "Permitir todas as bibliotecas", "selectedLibraries": "Selecciona bibliotecas", - "allowWriteAccess": "" + "allowWriteAccess": "Conceder acceso de escritura" }, "sections": { "status": "Estado", @@ -401,7 +401,7 @@ "requiredHosts": "Servidores requeridos", "configValidationError": "Fallou a comprobación da configuración:", "schemaRenderError": "Non se puido aplicar a configuración. O esquema do complemento podería non ser válido.", - "allowWriteAccessHelp": "" + "allowWriteAccessHelp": "A activalo, este complemento pode modificar ficheiros nos directorios da biblioteca. Por defecto os complementos teñen acceso de só-lectura." }, "placeholders": { "configKey": "clave", @@ -591,7 +591,13 @@ "remove_all_missing_content": "Tes certeza de querer retirar da base de datos todos os ficheiros que faltan? Isto eliminará todas as referencias a eles, incluíndo o número de reproducións e valoracións.", "noSimilarSongsFound": "Sen cancións parecidas", "noTopSongsFound": "Sen cancións destacadas", - "startingInstantMix": "Cargando Mestura Súbita…" + "startingInstantMix": "Cargando Mestura Súbita…", + "uploadCover": "Subir capa", + "removeCover": "Retirar capa", + "coverUploaded": "Subiuse a capa", + "coverRemoved": "Retirouse a capa", + "coverUploadError": "Erro ao subir a capa", + "coverRemoveError": "Erro ao retirar a capa" }, "menu": { "library": "Biblioteca", @@ -712,4 +718,4 @@ "empty": "Sen reprodución", "minutesAgo": "hai %{smart_count} minuto |||| hai %{smart_count} minutos" } -} +} \ No newline at end of file diff --git a/resources/i18n/pt-br.json b/resources/i18n/pt-br.json index bc2a5f85e..2e4f517a9 100644 --- a/resources/i18n/pt-br.json +++ b/resources/i18n/pt-br.json @@ -37,7 +37,8 @@ "sampleRate": "Taxa de amostragem", "missing": "Ausente", "libraryName": "Biblioteca", - "composer": "Compositor" + "composer": "Compositor", + "disc": "Disco %{discNumber}" }, "actions": { "addToQueue": "Adicionar à fila", @@ -397,10 +398,10 @@ "allLibrariesHelp": "Quando habilitado, o plugin terá acesso a todas as bibliotecas, incluindo as criadas no futuro.", "noLibraries": "Nenhuma biblioteca selecionada", "librariesRequired": "Este plugin requer acesso a informações de bibliotecas. Selecione quais bibliotecas o plugin pode acessar, ou habilite 'Permitir todas as bibliotecas'.", - "allowWriteAccessHelp": "Quando habilitado, o plugin pode modificar arquivos nos diretórios das bibliotecas. Por padrão, plugins têm acesso somente leitura.", "requiredHosts": "Hosts necessários", "configValidationError": "Falha na validação da configuração:", - "schemaRenderError": "Não foi possível renderizar o formulário de configuração. O schema do plugin pode estar inválido." + "schemaRenderError": "Não foi possível renderizar o formulário de configuração. O schema do plugin pode estar inválido.", + "allowWriteAccessHelp": "Quando habilitado, o plugin pode modificar arquivos nos diretórios das bibliotecas. Por padrão, plugins têm acesso somente leitura." }, "placeholders": { "configKey": "chave", @@ -554,12 +555,6 @@ } }, "message": { - "uploadCover": "Enviar Capa", - "removeCover": "Remover Capa", - "coverUploaded": "Capa atualizada", - "coverRemoved": "Capa removida", - "coverUploadError": "Erro ao enviar capa", - "coverRemoveError": "Erro ao remover capa", "note": "ATENÇÃO", "transcodingDisabled": "Por questão de segurança, esta tela de configuração está desabilitada. Se você quiser alterar estas configurações, reinicie o servidor com a opção %{config}", "transcodingEnabled": "Navidrome está sendo executado com a opção %{config}. Isto permite que potencialmente se execute comandos do sistema pela interface Web. É recomendado que vc mantenha esta opção desabilitada, e só a habilite quando precisar configurar opções de Conversão", @@ -596,7 +591,13 @@ "remove_all_missing_content": "Você tem certeza que deseja remover todos os arquivos ausentes do banco de dados? Isso removerá permanentemente qualquer referência a eles, incluindo suas contagens de reprodução e classificações.", "noSimilarSongsFound": "Nenhuma música semelhante encontrada", "noTopSongsFound": "Nenhuma música mais tocada encontrada", - "startingInstantMix": "Carregando Mix Instantâneo..." + "startingInstantMix": "Carregando Mix Instantâneo...", + "uploadCover": "Enviar Capa", + "removeCover": "Remover Capa", + "coverUploaded": "Capa atualizada", + "coverRemoved": "Capa removida", + "coverUploadError": "Erro ao enviar capa", + "coverRemoveError": "Erro ao remover capa" }, "menu": { "library": "Biblioteca", diff --git a/resources/i18n/sv.json b/resources/i18n/sv.json index 23bd5fbc2..228cc2cf3 100644 --- a/resources/i18n/sv.json +++ b/resources/i18n/sv.json @@ -38,7 +38,7 @@ "missing": "Saknade", "libraryName": "Bibliotek", "composer": "Kompositör", - "disc": "" + "disc": "Disc %{discNumber}" }, "actions": { "addToQueue": "Lägg till i kön", @@ -355,7 +355,7 @@ "selectedUsers": "Valda användare", "allLibraries": "Tillåt alla bibliotek", "selectedLibraries": "Valda bibliotek", - "allowWriteAccess": "" + "allowWriteAccess": "Tillåt skrivrättigheter" }, "sections": { "status": "Status", @@ -401,7 +401,7 @@ "requiredHosts": "Krävda värdar", "configValidationError": "Validering av konfigurationen misslyckades:", "schemaRenderError": "Kunde inte rendera konfigurationsformuläret. Tilläggets schema kan vara ogiltigt.", - "allowWriteAccessHelp": "" + "allowWriteAccessHelp": "När detta är aktiverat kan tillägget ändra filer i bibliotekets kataloger. Som standard har tillägget endast läsrättigheter." }, "placeholders": { "configKey": "nyckel", @@ -591,7 +591,13 @@ "remove_all_missing_content": "Är du säker på att du vill ta bort alla saknade filer från databasen? Detta kommer permanent radera alla referenser till dem, inklusive antal spelningar och betyg.", "noSimilarSongsFound": "Hittade inga liknande låtar", "noTopSongsFound": "Hittade inga topplåtar", - "startingInstantMix": "Laddar direktmix..." + "startingInstantMix": "Laddar direktmix...", + "uploadCover": "Ladda upp omslagsbild", + "removeCover": "Ta bort omslagsbild", + "coverUploaded": "Omslagsbild uppdaterad", + "coverRemoved": "Omslagsbild borttagen", + "coverUploadError": "Fel vid uppladdning av omslagsbild", + "coverRemoveError": "Fel vid borttagning av omslagsbild" }, "menu": { "library": "Bibliotek", @@ -712,4 +718,4 @@ "empty": "Inget spelas", "minutesAgo": "%{smart_count} minut sedan |||| %{smart_count} minuter sedan" } -} +} \ No newline at end of file diff --git a/resources/i18n/uk.json b/resources/i18n/uk.json index 2c74c890a..c5644fde7 100644 --- a/resources/i18n/uk.json +++ b/resources/i18n/uk.json @@ -36,7 +36,9 @@ "bitDepth": "Глибина розрядності", "sampleRate": "Частота дискретизації", "missing": "Поле відсутнє", - "libraryName": "Бібліотека" + "libraryName": "Бібліотека", + "composer": "Композитор", + "disc": "Диск %{discNumber}" }, "actions": { "addToQueue": "Прослухати пізніше", @@ -46,7 +48,8 @@ "download": "Завантажити", "playNext": "Наступна", "info": "Отримати інформацію", - "showInPlaylist": "Показати у плейлісті" + "showInPlaylist": "Показати у плейлісті", + "instantMix": "Мікс" } }, "album": { @@ -328,6 +331,82 @@ "scanInProgress": "Сканування триває...", "noLibrariesAssigned": "Немає бібліотек, призначених цьому користувачеві" } + }, + "plugin": { + "name": "Плагін |||| Плагіни", + "fields": { + "id": "ІД", + "name": "Назва", + "description": "Опис", + "version": "Версія", + "author": "Автор", + "website": "Вебсайт", + "permissions": "Дозволи", + "enabled": "Увімкнено", + "status": "Стан", + "path": "Шлях", + "lastError": "Помилка", + "hasError": "Помилка", + "updatedAt": "Оновлено", + "createdAt": "Встановлено", + "configKey": "Ключ", + "configValue": "Значення", + "allUsers": "Дозволити усім користувачам", + "selectedUsers": "Вибрані користувачі", + "allLibraries": "Дозволити всі бібліотеки", + "selectedLibraries": "Вибрані бібліотеки", + "allowWriteAccess": "Дозволити доступ до запису" + }, + "sections": { + "status": "Стан", + "info": "Інформація про плагін", + "configuration": "Конфігурація", + "manifest": "Маніфест", + "usersPermission": "Дозволи користувачів", + "libraryPermission": "Дозволи бібліотеки" + }, + "status": { + "enabled": "Увімкнено", + "disabled": "Вимкнено" + }, + "actions": { + "enable": "Увімкнути", + "disable": "Вимкнути", + "disabledDueToError": "Виправте помилку перед увімкненням", + "disabledUsersRequired": "Виберіть користувачі перед увімкненням", + "disabledLibrariesRequired": "Виберіть бібліотеки перед увімкненням", + "addConfig": "Додати конфігурацію", + "rescan": "Пересканувати" + }, + "notifications": { + "enabled": "Увімкнути плагін", + "disabled": "Вимкнути плагін", + "updated": "Плагін оновлено", + "error": "Помилка оновлення плагіну" + }, + "validation": { + "invalidJson": "Конфігурація повинна має відповідати формату JSON" + }, + "messages": { + "configHelp": "Налаштуйте плагін використовуючи пару ключ-значення. Залиште порожнім, якщо плагін не вимагає конфігурації.", + "clickPermissions": "Натисніть дозволи для детальної інформації", + "noConfig": "Конфігурація не налаштована", + "allUsersHelp": "При увімкненні плагін матиме доступ до всіх користувачів, включно ті, які будуть створені в майбутньому.", + "noUsers": "Немає вибраних користувачів", + "permissionReason": "Причина", + "usersRequired": "Цей плагін вимагає доступу до інформації про користувача. Виберіть, до яких користувачів плагін може отримати доступ, або ввімкніть «Дозволити всім користувачам».", + "allLibrariesHelp": "Коли увімкнуто, плагін матиме доступ до всіх бібліотек, включаючи ті, які будуть створені в майбутньому.", + "noLibraries": "Немає виділених бібліотек", + "librariesRequired": "Цей плагін вимагає доступу до інформації бібліотеки. Виберіть, до яких бібліотек плагін може отримати доступ, або ввімкніть «Дозволити всі бібліотеки».", + "requiredHosts": "Обов'язкові хости", + "configValidationError": "Перевірка конфігурації зазнала невдачі:", + "schemaRenderError": "Неможливо відобразити форму конфігурації. Схема плагіна може бути недійсною.", + "allowWriteAccessHelp": "При включенні плагін може змінювати файли в каталогах бібліотеки. За замовчуванням плагіни мають доступ лише для читання." + }, + "placeholders": { + "configKey": "ключ", + "configValue": "значення" + } } }, "ra": { @@ -511,7 +590,14 @@ "remove_all_missing_title": "Видалити всі відсутні файли", "remove_all_missing_content": "Ви впевнені, що хочете видалити всі відсутні файли з бази даних? Це назавжди видалить будь-які посилання на них, включно з кількістю відтворень та рейтингами.", "noSimilarSongsFound": "Не знайдено схожих треків", - "noTopSongsFound": "Не знайдено ТОП-треків" + "noTopSongsFound": "Не знайдено ТОП-треків", + "startingInstantMix": "Завантаження міксу...", + "uploadCover": "Завантажити обкладинку", + "removeCover": "Видалити обкладинку", + "coverUploaded": "Обкладинку оновлено", + "coverRemoved": "Обкладинка видалена", + "coverUploadError": "Помилка завантаження обкладинки", + "coverRemoveError": "Помилка видалення обкладинки" }, "menu": { "library": "Бібліотека", @@ -597,7 +683,8 @@ "exportSuccess": "Конфігурацію експортовано в буфер обміну у форматі TOML", "exportFailed": "Не вдалося скопіювати конфігурацію", "devFlagsHeader": "Прапорці розробки (можуть бути змінені/видалені)", - "devFlagsComment": "Це експериментальні налаштування, які можуть бути видалені в майбутніх версіях." + "devFlagsComment": "Це експериментальні налаштування, які можуть бути видалені в майбутніх версіях.", + "downloadToml": "Завантажити конфігурацію (TOML)" } }, "activity": { diff --git a/resources/i18n/zh-Hant.json b/resources/i18n/zh-Hant.json index dabf61bdb..93951a311 100644 --- a/resources/i18n/zh-Hant.json +++ b/resources/i18n/zh-Hant.json @@ -38,7 +38,7 @@ "missing": "遺失", "libraryName": "媒體庫", "composer": "作曲者", - "disc": "" + "disc": "光碟 %{discNumber}" }, "actions": { "addToQueue": "加入至播放佇列", @@ -718,4 +718,4 @@ "empty": "無播放內容", "minutesAgo": "1 分鐘前 |||| %{smart_count} 分鐘前" } -} +} \ No newline at end of file From f33ca753781d36b3fbd9b137f830ed3ddc815ce8 Mon Sep 17 00:00:00 2001 From: Deluan <deluan@navidrome.org> Date: Fri, 27 Mar 2026 19:33:46 -0400 Subject: [PATCH 61/81] refactor: rename EnableCoverArtUpload to EnableArtworkUpload The config flag gates all image uploads (artists, radios, playlists), not just cover art. Rename it to accurately reflect its scope across the backend config, native API permission check, Subsonic CoverArtRole, serve_index JSON key, and frontend config. --- conf/configuration.go | 4 ++-- server/nativeapi/image_upload.go | 4 ++-- server/nativeapi/playlists_test.go | 8 ++++---- server/serve_index.go | 2 +- server/subsonic/users.go | 2 +- server/subsonic/users_test.go | 4 ++-- ui/src/common/ImageUploadOverlay.jsx | 2 +- ui/src/config.js | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/conf/configuration.go b/conf/configuration.go index 5f74d6db0..a9264d3b7 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -78,7 +78,7 @@ type configOptions struct { EnableFavourites bool EnableStarRating bool EnableUserEditing bool - EnableCoverArtUpload bool + EnableArtworkUpload bool EnableSharing bool ShareURL string DefaultShareExpiration time.Duration @@ -689,7 +689,7 @@ func setViperDefaults() { viper.SetDefault("enablereplaygain", true) viper.SetDefault("enablecoveranimation", true) viper.SetDefault("enablenowplaying", true) - viper.SetDefault("enablecoverartupload", true) + viper.SetDefault("enableartworkupload", true) viper.SetDefault("enablesharing", false) viper.SetDefault("shareurl", "") viper.SetDefault("defaultshareexpiration", 8760*time.Hour) diff --git a/server/nativeapi/image_upload.go b/server/nativeapi/image_upload.go index c29f14bdc..1f55e3851 100644 --- a/server/nativeapi/image_upload.go +++ b/server/nativeapi/image_upload.go @@ -24,8 +24,8 @@ const maxImageSize = 10 << 20 // 10MB func checkImageUploadPermission(w http.ResponseWriter, r *http.Request) bool { user, _ := request.UserFrom(r.Context()) - if !conf.Server.EnableCoverArtUpload && !user.IsAdmin { - http.Error(w, "cover art upload is disabled", http.StatusForbidden) + if !conf.Server.EnableArtworkUpload && !user.IsAdmin { + http.Error(w, "artwork upload is disabled", http.StatusForbidden) return false } return true diff --git a/server/nativeapi/playlists_test.go b/server/nativeapi/playlists_test.go index dfe6b9296..e1c933709 100644 --- a/server/nativeapi/playlists_test.go +++ b/server/nativeapi/playlists_test.go @@ -28,8 +28,8 @@ var _ = Describe("Playlist Image Endpoints", func() { }) DescribeTable("uploadPlaylistImage guard", - func(enableCoverArtUpload, isAdmin bool, expectedStatus int) { - conf.Server.EnableCoverArtUpload = enableCoverArtUpload + func(enableArtworkUpload, isAdmin bool, expectedStatus int) { + conf.Server.EnableArtworkUpload = enableArtworkUpload handler := uploadPlaylistImage(&mockPlaylistsService{}) req := httptest.NewRequest("POST", "/playlist/pls-1/image", nil) @@ -47,8 +47,8 @@ var _ = Describe("Playlist Image Endpoints", func() { ) DescribeTable("deletePlaylistImage guard", - func(enableCoverArtUpload, isAdmin bool, expectedStatus int) { - conf.Server.EnableCoverArtUpload = enableCoverArtUpload + func(enableArtworkUpload, isAdmin bool, expectedStatus int) { + conf.Server.EnableArtworkUpload = enableArtworkUpload handler := deletePlaylistImage(&mockPlaylistsService{}) req := httptest.NewRequest("DELETE", "/playlist/pls-1/image", nil) diff --git a/server/serve_index.go b/server/serve_index.go index 6b0c890a6..0d1a2f330 100644 --- a/server/serve_index.go +++ b/server/serve_index.go @@ -61,7 +61,7 @@ func serveIndex(ds model.DataStore, fs fs.FS, shareInfo *model.Share) http.Handl "losslessFormats": strings.ToUpper(strings.Join(mime.LosslessFormats, ",")), "devActivityPanel": conf.Server.DevActivityPanel, "enableUserEditing": conf.Server.EnableUserEditing, - "enableCoverArtUpload": conf.Server.EnableCoverArtUpload, + "enableArtworkUpload": conf.Server.EnableArtworkUpload, "enableSharing": conf.Server.EnableSharing, "shareURL": conf.Server.ShareURL, "defaultDownloadableShare": conf.Server.DefaultDownloadableShare, diff --git a/server/subsonic/users.go b/server/subsonic/users.go index 4f6dccaac..8b7406b60 100644 --- a/server/subsonic/users.go +++ b/server/subsonic/users.go @@ -22,7 +22,7 @@ func buildUserResponse(user model.User) responses.User { ScrobblingEnabled: true, DownloadRole: conf.Server.EnableDownloads, ShareRole: conf.Server.EnableSharing, - CoverArtRole: conf.Server.EnableCoverArtUpload || user.IsAdmin, + CoverArtRole: conf.Server.EnableArtworkUpload || user.IsAdmin, Folder: slice.Map(user.Libraries, func(lib model.Library) int32 { return int32(lib.ID) }), } diff --git a/server/subsonic/users_test.go b/server/subsonic/users_test.go index 1fd5dce71..2d08b3377 100644 --- a/server/subsonic/users_test.go +++ b/server/subsonic/users_test.go @@ -105,8 +105,8 @@ var _ = Describe("Users", func() { ) DescribeTable("CoverArt role permissions", - func(enableCoverArtUpload, isAdmin, expectedCoverArtRole bool) { - conf.Server.EnableCoverArtUpload = enableCoverArtUpload + func(enableArtworkUpload, isAdmin, expectedCoverArtRole bool) { + conf.Server.EnableArtworkUpload = enableArtworkUpload testUser.IsAdmin = isAdmin response := buildUserResponse(testUser) diff --git a/ui/src/common/ImageUploadOverlay.jsx b/ui/src/common/ImageUploadOverlay.jsx index e0d0d0a9a..a370e40fe 100644 --- a/ui/src/common/ImageUploadOverlay.jsx +++ b/ui/src/common/ImageUploadOverlay.jsx @@ -49,7 +49,7 @@ export const ImageUploadOverlay = ({ const fileInputRef = useRef(null) const canEdit = - config.enableCoverArtUpload || localStorage.getItem('role') === 'admin' + config.enableArtworkUpload || localStorage.getItem('role') === 'admin' const handleUploadClick = useCallback((e) => { e.stopPropagation() diff --git a/ui/src/config.js b/ui/src/config.js index 0672a58f4..bd11d7df6 100644 --- a/ui/src/config.js +++ b/ui/src/config.js @@ -22,7 +22,7 @@ const defaultConfig = { defaultUIVolume: 100, uiSearchDebounceMs: 200, enableUserEditing: true, - enableCoverArtUpload: true, + enableArtworkUpload: true, enableSharing: true, shareURL: '', defaultDownloadableShare: true, From 2588558946a21690e2d54a5d3c089be839aa1f51 Mon Sep 17 00:00:00 2001 From: Deluan <deluan@navidrome.org> Date: Fri, 27 Mar 2026 19:38:42 -0400 Subject: [PATCH 62/81] fix: resolve flaky ffmpeg context cancellation test Replaced single Read assertion with Eventually loop to drain buffered pipe data after context cancellation. The previous test assumed the first Read after cancel() would fail, but ffmpeg may have already written data into the pipe buffer before being killed, causing the Read to succeed from buffered content. --- core/ffmpeg/ffmpeg_test.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/ffmpeg/ffmpeg_test.go b/core/ffmpeg/ffmpeg_test.go index 04663828f..01b284172 100644 --- a/core/ffmpeg/ffmpeg_test.go +++ b/core/ffmpeg/ffmpeg_test.go @@ -584,9 +584,12 @@ var _ = Describe("ffmpeg", func() { // Cancel the context cancel() - // Next read should fail due to cancelled context - _, err = stream.Read(buf) - Expect(err).To(HaveOccurred()) + // Subsequent reads should eventually fail due to cancelled context. + // There may be buffered data in the pipe, so we drain until an error occurs. + Eventually(func() error { + _, err = stream.Read(buf) + return err + }).WithTimeout(5 * time.Second).WithPolling(10 * time.Millisecond).Should(HaveOccurred()) }) It("should handle immediate context cancellation", func() { From 2b041c02ad98fa8285d1400fd62ed9474f2cc11a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= <deluan@navidrome.org> Date: Sat, 28 Mar 2026 13:17:31 -0400 Subject: [PATCH 63/81] feat: accept ND_-prefixed env var names in config files (#5258) * feat: add toPascalCase helper for config key display Adds a toPascalCase helper that converts dotted lowercase Viper config keys (e.g. 'scanner.schedule') to PascalCase (e.g. 'Scanner.Schedule') for use in user-facing warning messages. Includes export_test.go binding and a full Ginkgo DescribeTable test suite covering simple, dotted, multi-segment, already-capitalized, and empty-string cases. * feat: remap ND_-prefixed env var names found in config files Detect when users mistakenly use environment variable names (like ND_ADDRESS) in config files, remap them to canonical keys, and warn. Fatal error if both ND_ and canonical versions of the same key exist. Closes #5242 --- conf/configuration.go | 52 +++++++++++++++++++++++++++ conf/configuration_test.go | 56 ++++++++++++++++++++++++++++++ conf/export_test.go | 8 +++++ conf/testdata/cfg_nd_conflict.toml | 2 ++ conf/testdata/cfg_nd_keys.toml | 3 ++ 5 files changed, 121 insertions(+) create mode 100644 conf/testdata/cfg_nd_conflict.toml create mode 100644 conf/testdata/cfg_nd_keys.toml diff --git a/conf/configuration.go b/conf/configuration.go index a9264d3b7..3d04e725c 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -258,6 +258,13 @@ type searchOptions struct { FullString bool } +// fatalFunc is called for fatal config errors. Defaults to printing + os.Exit(1). +// Overridden in tests to allow testing fatal paths. +var fatalFunc = func(msg string) { + _, _ = fmt.Fprintln(os.Stderr, "FATAL:", msg) + os.Exit(1) +} + var ( Server = &configOptions{} hooks []func() @@ -275,6 +282,7 @@ func LoadFromFile(confFile string) { func Load(noConfigDump bool) { parseIniFileConfiguration() + remapEnvVarKeysFromConfig() // Map deprecated options to their new names for backwards compatibility mapDeprecatedOption("ReverseProxyWhitelist", "ExtAuth.TrustedSources") @@ -466,6 +474,35 @@ func logRemovedOptions(options ...string) { } } +// remapEnvVarKeysFromConfig detects ND_-prefixed keys in the config file (users mistakenly +// using environment variable names) and remaps them to canonical Viper keys with a warning. +func remapEnvVarKeysFromConfig() { + for _, key := range viper.AllKeys() { + if !strings.HasPrefix(key, "nd_") || !viper.InConfig(key) { + continue + } + stripped := strings.TrimPrefix(key, "nd_") + canonicalKey := strings.ReplaceAll(stripped, "_", ".") + displayNDKey := "ND_" + strings.ToUpper(stripped) + displayCanonical := toPascalCase(canonicalKey) + + if viper.InConfig(canonicalKey) { + fatalFunc(fmt.Sprintf( + "Config file contains both '%s' and '%s'. Remove the ND_-prefixed version. "+ + "The 'ND_' prefix is only needed for environment variables, not config file keys.", + displayNDKey, displayCanonical, + )) + return + } + + viper.Set(canonicalKey, viper.Get(key)) + _, _ = fmt.Fprintf(os.Stderr, "WARNING: Config key '%s' uses environment variable naming. Use '%s' instead. "+ + "The 'ND_' prefix is only needed for environment variables.\n", + displayNDKey, displayCanonical, + ) + } +} + // mapDeprecatedOption is used to provide backwards compatibility for deprecated options. It should be called after // the config has been read by viper, but before unmarshalling it into the Config struct. func mapDeprecatedOption(legacyName, newName string) { @@ -617,6 +654,21 @@ func normalizeSearchBackend(value string) string { } } +// toPascalCase converts a dotted lowercase config key to PascalCase for display. +// Example: "scanner.schedule" → "Scanner.Schedule" +func toPascalCase(key string) string { + if key == "" { + return "" + } + parts := strings.Split(key, ".") + for i, part := range parts { + if len(part) > 0 { + parts[i] = strings.ToUpper(part[:1]) + part[1:] + } + } + return strings.Join(parts, ".") +} + // AddHook is used to register initialization code that should run as soon as the config is loaded func AddHook(hook func()) { hooks = append(hooks, hook) diff --git a/conf/configuration_test.go b/conf/configuration_test.go index 73fec4196..9aba04197 100644 --- a/conf/configuration_test.go +++ b/conf/configuration_test.go @@ -108,6 +108,62 @@ var _ = Describe("Configuration", func() { Entry("falls back to 'fts' for empty string", "", "fts"), ) + DescribeTable("ToPascalCase", + func(input, expected string) { + Expect(conf.ToPascalCase(input)).To(Equal(expected)) + }, + Entry("simple key", "address", "Address"), + Entry("dotted key", "scanner.schedule", "Scanner.Schedule"), + Entry("already capitalized", "Address", "Address"), + Entry("multi-segment", "lastfm.enabled", "Lastfm.Enabled"), + Entry("empty string", "", ""), + ) + + Describe("remapEnvVarKeysFromConfig", func() { + BeforeEach(func() { + viper.Reset() + conf.SetViperDefaults() + viper.SetDefault("datafolder", GinkgoT().TempDir()) + viper.SetDefault("loglevel", "error") + conf.ResetConf() + }) + + It("remaps ND_-prefixed keys to canonical keys", func() { + filename := filepath.Join("testdata", "cfg_nd_keys.toml") + conf.InitConfig(filename, false) + conf.Load(true) + + Expect(conf.Server.Address).To(Equal("127.0.0.1")) + Expect(conf.Server.Port).To(Equal(4531)) + Expect(conf.Server.Scanner.Schedule).To(Equal("@every 1h")) + }) + + It("exits with fatal error when both ND_ and canonical key exist", func() { + cleanup := conf.SetFatalFunc(func(msg string) { + panic(msg) + }) + defer cleanup() + + filename := filepath.Join("testdata", "cfg_nd_conflict.toml") + conf.InitConfig(filename, false) + + Expect(func() { conf.Load(true) }).To(PanicWith(And( + ContainSubstring("ND_ADDRESS"), + ContainSubstring("Address"), + ContainSubstring("only needed for environment variables"), + ))) + }) + + It("does nothing when no ND_ keys are present", func() { + filename := filepath.Join("testdata", "cfg.toml") + conf.InitConfig(filename, false) + conf.Load(true) + + // Verify normal config loading still works + Expect(conf.Server.MusicFolder).To(Equal("/toml/music")) + }) + }) + DescribeTable("should load configuration from", func(format string) { filename := filepath.Join("testdata", "cfg."+format) diff --git a/conf/export_test.go b/conf/export_test.go index d1d1bb3a9..6498215dc 100644 --- a/conf/export_test.go +++ b/conf/export_test.go @@ -11,3 +11,11 @@ var ParseLanguages = parseLanguages var ValidateURL = validateURL var NormalizeSearchBackend = normalizeSearchBackend + +var ToPascalCase = toPascalCase + +func SetFatalFunc(f func(string)) func() { + old := fatalFunc + fatalFunc = f + return func() { fatalFunc = old } +} diff --git a/conf/testdata/cfg_nd_conflict.toml b/conf/testdata/cfg_nd_conflict.toml new file mode 100644 index 000000000..2e8b94bc3 --- /dev/null +++ b/conf/testdata/cfg_nd_conflict.toml @@ -0,0 +1,2 @@ +ND_ADDRESS = "127.0.0.1" +Address = "0.0.0.0" diff --git a/conf/testdata/cfg_nd_keys.toml b/conf/testdata/cfg_nd_keys.toml new file mode 100644 index 000000000..de441ce66 --- /dev/null +++ b/conf/testdata/cfg_nd_keys.toml @@ -0,0 +1,3 @@ +ND_ADDRESS = "127.0.0.1" +ND_PORT = 4531 +ND_SCANNER_SCHEDULE = "@every 1h" From 049fc78177f797b711aab21d121a16b3ad6c6ba7 Mon Sep 17 00:00:00 2001 From: Deluan <deluan@navidrome.org> Date: Sat, 28 Mar 2026 13:23:03 -0400 Subject: [PATCH 64/81] refactor: extract logFatal helper for config error handling Replace 14 repeated fmt.Fprintln(os.Stderr, "FATAL:", ...)/os.Exit(1) patterns with a single logFatal function. This reduces duplication and makes all fatal config paths testable via SetLogFatal. Signed-off-by: Deluan <deluan@navidrome.org> --- conf/configuration.go | 47 ++++++++++----------------- conf/configuration_test.go | 65 +++++++++++++++++++++++++++++++++++--- conf/export_test.go | 8 ++--- 3 files changed, 81 insertions(+), 39 deletions(-) diff --git a/conf/configuration.go b/conf/configuration.go index 3d04e725c..653e3d8e1 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -258,10 +258,10 @@ type searchOptions struct { FullString bool } -// fatalFunc is called for fatal config errors. Defaults to printing + os.Exit(1). +// logFatal prints a fatal error message to stderr and exits. // Overridden in tests to allow testing fatal paths. -var fatalFunc = func(msg string) { - _, _ = fmt.Fprintln(os.Stderr, "FATAL:", msg) +var logFatal = func(args ...any) { + _, _ = fmt.Fprintln(os.Stderr, append([]any{"FATAL:"}, args...)...) os.Exit(1) } @@ -274,8 +274,7 @@ func LoadFromFile(confFile string) { viper.SetConfigFile(confFile) err := viper.ReadInConfig() if err != nil { - _, _ = fmt.Fprintln(os.Stderr, "FATAL: Error reading config file:", err) - os.Exit(1) + logFatal("Error reading config file:", err) } Load(true) } @@ -292,14 +291,12 @@ func Load(noConfigDump bool) { err := viper.Unmarshal(&Server) if err != nil { - _, _ = fmt.Fprintln(os.Stderr, "FATAL: Error parsing config:", err) - os.Exit(1) + logFatal("Error parsing config:", err) } err = os.MkdirAll(Server.DataFolder, os.ModePerm) if err != nil { - _, _ = fmt.Fprintln(os.Stderr, "FATAL: Error creating data path:", err) - os.Exit(1) + logFatal("Error creating data path:", err) } if Server.CacheFolder == "" { @@ -307,14 +304,12 @@ func Load(noConfigDump bool) { } err = os.MkdirAll(Server.CacheFolder, os.ModePerm) if err != nil { - _, _ = fmt.Fprintln(os.Stderr, "FATAL: Error creating cache path:", err) - os.Exit(1) + logFatal("Error creating cache path:", err) } err = os.MkdirAll(filepath.Join(Server.DataFolder, consts.ArtworkFolder), os.ModePerm) if err != nil { - _, _ = fmt.Fprintln(os.Stderr, "FATAL: Error creating artwork path:", err) - os.Exit(1) + logFatal("Error creating artwork path:", err) } if Server.Plugins.Enabled { @@ -323,8 +318,7 @@ func Load(noConfigDump bool) { } err = os.MkdirAll(Server.Plugins.Folder, 0700) if err != nil { - _, _ = fmt.Fprintln(os.Stderr, "FATAL: Error creating plugins path:", err) - os.Exit(1) + logFatal("Error creating plugins path:", err) } } @@ -336,8 +330,7 @@ func Load(noConfigDump bool) { if Server.Backup.Path != "" { err = os.MkdirAll(Server.Backup.Path, os.ModePerm) if err != nil { - _, _ = fmt.Fprintln(os.Stderr, "FATAL: Error creating backup path:", err) - os.Exit(1) + logFatal("Error creating backup path:", err) } } @@ -345,8 +338,7 @@ func Load(noConfigDump bool) { if Server.LogFile != "" { out, err = os.OpenFile(Server.LogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "FATAL: Error opening log file %s: %s\n", Server.LogFile, err.Error()) - os.Exit(1) + logFatal(fmt.Sprintf("Error opening log file %s: %s", Server.LogFile, err.Error())) } log.SetOutput(out) } else if os.Getenv("ND_SYSTEMD_PRIORITY_LOGGING") != "" && os.Getenv("JOURNAL_STREAM") != "" { @@ -378,8 +370,7 @@ func Load(noConfigDump bool) { if Server.BaseURL != "" { u, err := url.Parse(Server.BaseURL) if err != nil { - _, _ = fmt.Fprintln(os.Stderr, "FATAL: Invalid BaseURL:", err) - os.Exit(1) + logFatal("Invalid BaseURL:", err) } Server.BasePath = u.Path u.Path = "" @@ -487,7 +478,7 @@ func remapEnvVarKeysFromConfig() { displayCanonical := toPascalCase(canonicalKey) if viper.InConfig(canonicalKey) { - fatalFunc(fmt.Sprintf( + logFatal(fmt.Sprintf( "Config file contains both '%s' and '%s'. Remove the ND_-prefixed version. "+ "The 'ND_' prefix is only needed for environment variables, not config file keys.", displayNDKey, displayCanonical, @@ -520,18 +511,15 @@ func parseIniFileConfiguration() { var iniConfig map[string]any err := viper.Unmarshal(&iniConfig) if err != nil { - _, _ = fmt.Fprintln(os.Stderr, "FATAL: Error parsing config:", err) - os.Exit(1) + logFatal("Error parsing config:", err) } cfg, ok := iniConfig["default"].(map[string]any) if !ok { - _, _ = fmt.Fprintln(os.Stderr, "FATAL: Error parsing config: missing [default] section:", iniConfig) - os.Exit(1) + logFatal("Error parsing config: missing [default] section:", iniConfig) } err = viper.MergeConfigMap(cfg) if err != nil { - _, _ = fmt.Fprintln(os.Stderr, "FATAL: Error parsing config:", err) - os.Exit(1) + logFatal("Error parsing config:", err) } } } @@ -872,8 +860,7 @@ func InitConfig(cfgFile string, loadEnvVars bool) { err := viper.ReadInConfig() if viper.ConfigFileUsed() != "" && err != nil { - _, _ = fmt.Fprintln(os.Stderr, "FATAL: Navidrome could not open config file: ", err) - os.Exit(1) + logFatal("Navidrome could not open config file:", err) } } diff --git a/conf/configuration_test.go b/conf/configuration_test.go index 9aba04197..eb2176e83 100644 --- a/conf/configuration_test.go +++ b/conf/configuration_test.go @@ -2,6 +2,7 @@ package conf_test import ( "fmt" + "os" "path/filepath" "testing" @@ -24,6 +25,11 @@ var _ = Describe("Configuration", func() { viper.SetDefault("datafolder", GinkgoT().TempDir()) viper.SetDefault("loglevel", "error") conf.ResetConf() + + // Panic instead of exiting on fatal errors to allow testing error conditions + DeferCleanup(conf.SetLogFatal(func(args ...any) { + panic(fmt.Sprint(args...)) + })) }) Describe("ParseLanguages", func() { @@ -139,11 +145,6 @@ var _ = Describe("Configuration", func() { }) It("exits with fatal error when both ND_ and canonical key exist", func() { - cleanup := conf.SetFatalFunc(func(msg string) { - panic(msg) - }) - defer cleanup() - filename := filepath.Join("testdata", "cfg_nd_conflict.toml") conf.InitConfig(filename, false) @@ -164,6 +165,60 @@ var _ = Describe("Configuration", func() { }) }) + Describe("logFatal", func() { + var invalidPath string + BeforeEach(func() { + viper.Reset() + conf.SetViperDefaults() + viper.SetDefault("loglevel", "error") + conf.ResetConf() + + // Create a file so that any path under it is invalid on all OSes + f, err := os.CreateTemp(GinkgoT().TempDir(), "blocker") + Expect(err).ToNot(HaveOccurred()) + f.Close() + invalidPath = filepath.Join(f.Name(), "subdir") + }) + + It("is called when LoadFromFile gets an invalid config file", func() { + Expect(func() { + conf.LoadFromFile(filepath.Join(invalidPath, "file.toml")) + }).To(PanicWith(ContainSubstring("Error reading config file"))) + }) + + It("is called when DataFolder is not writable", func() { + viper.SetDefault("datafolder", invalidPath) + Expect(func() { + conf.Load(true) + }).To(PanicWith(ContainSubstring("Error creating data path"))) + }) + + It("is called when CacheFolder is not writable", func() { + viper.SetDefault("datafolder", GinkgoT().TempDir()) + viper.SetDefault("cachefolder", invalidPath) + Expect(func() { + conf.Load(true) + }).To(PanicWith(ContainSubstring("Error creating cache path"))) + }) + + It("is called when LogFile path is not writable", func() { + viper.SetDefault("datafolder", GinkgoT().TempDir()) + viper.SetDefault("logfile", filepath.Join(invalidPath, "log.txt")) + Expect(func() { + conf.Load(true) + }).To(PanicWith(ContainSubstring("Error opening log file"))) + }) + + It("is called when BaseURL is invalid", func() { + viper.SetDefault("datafolder", GinkgoT().TempDir()) + viper.SetDefault("baseurl", "://invalid") + Expect(func() { + conf.Load(true) + }).To(PanicWith(ContainSubstring("Invalid BaseURL"))) + }) + + }) + DescribeTable("should load configuration from", func(format string) { filename := filepath.Join("testdata", "cfg."+format) diff --git a/conf/export_test.go b/conf/export_test.go index 6498215dc..051f9bb65 100644 --- a/conf/export_test.go +++ b/conf/export_test.go @@ -14,8 +14,8 @@ var NormalizeSearchBackend = normalizeSearchBackend var ToPascalCase = toPascalCase -func SetFatalFunc(f func(string)) func() { - old := fatalFunc - fatalFunc = f - return func() { fatalFunc = old } +func SetLogFatal(f func(...any)) func() { + old := logFatal + logFatal = f + return func() { logFatal = old } } From dc99994bdd652765fa1e3d754dc2a4e4cfefaf15 Mon Sep 17 00:00:00 2001 From: Deluan <deluan@navidrome.org> Date: Sun, 29 Mar 2026 14:57:57 -0400 Subject: [PATCH 65/81] feat: add EnableArtworkUpload and CoverArtQuality to insights Signed-off-by: Deluan <deluan@navidrome.org> --- core/metrics/insights.go | 2 ++ core/metrics/insights/data.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/core/metrics/insights.go b/core/metrics/insights.go index 5f3f491ea..b87f1df5e 100644 --- a/core/metrics/insights.go +++ b/core/metrics/insights.go @@ -193,6 +193,8 @@ var staticData = sync.OnceValue(func() insights.Data { data.Config.TLSConfigured = conf.Server.TLSCert != "" && conf.Server.TLSKey != "" data.Config.DefaultBackgroundURLSet = conf.Server.UILoginBackgroundURL == consts.DefaultUILoginBackgroundURL data.Config.EnableArtworkPrecache = conf.Server.EnableArtworkPrecache + data.Config.EnableArtworkUpload = conf.Server.EnableArtworkUpload + data.Config.CoverArtQuality = conf.Server.CoverArtQuality data.Config.EnableCoverAnimation = conf.Server.EnableCoverAnimation data.Config.EnableNowPlaying = conf.Server.EnableNowPlaying data.Config.EnableDownloads = conf.Server.EnableDownloads diff --git a/core/metrics/insights/data.go b/core/metrics/insights/data.go index 27186e020..b316c866d 100644 --- a/core/metrics/insights/data.go +++ b/core/metrics/insights/data.go @@ -63,6 +63,8 @@ type Data struct { EnableMediaFileCoverArt bool `json:"enableMediaFileCoverArt,omitempty"` EnableJukebox bool `json:"enableJukebox,omitempty"` EnablePrometheus bool `json:"enablePrometheus,omitempty"` + EnableArtworkUpload bool `json:"enableArtworkUpload,omitempty"` + CoverArtQuality int `json:"coverArtQuality,omitempty"` EnableCoverAnimation bool `json:"enableCoverAnimation,omitempty"` EnableNowPlaying bool `json:"enableNowPlaying,omitempty"` SessionTimeout uint64 `json:"sessionTimeout,omitempty"` From a293d1203423f542c93ff58d371618fb99e9939c Mon Sep 17 00:00:00 2001 From: ChekeredList71 <66330496+ChekeredList71@users.noreply.github.com> Date: Sun, 29 Mar 2026 23:50:58 +0000 Subject: [PATCH 66/81] fix(ui): update Hungarian translation (#5263) * [ui] hungarian translation * Update resources/i18n/hu.json Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update resources/i18n/hu.json Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: ChekeredList71 <asd@asd.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- resources/i18n/hu.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/resources/i18n/hu.json b/resources/i18n/hu.json index 115b2d1a4..ce3d5ae87 100644 --- a/resources/i18n/hu.json +++ b/resources/i18n/hu.json @@ -22,6 +22,7 @@ "bitRate": "Bitráta", "bitDepth": "Bitmélység", "sampleRate": "Mintavételezési frekvencia", + "disc": "Lemez %{discNumber}", "discSubtitle": "Lemezfelirat", "starred": "Kedvenc", "comment": "Megjegyzés", @@ -350,7 +351,8 @@ "allUsers": "Összes felhasználó engedélyezése", "selectedUsers": "Kiválasztott felhasználók engedélyezése", "allLibraries": "Összes könyvtár engedélyezése", - "selectedLibraries": "Kiválasztott könyvtárak engedélyezése" + "selectedLibraries": "Kiválasztott könyvtárak engedélyezése", +"allowWriteAccess": "Írási hozzáférés engedélyezése" }, "sections": { "status": "Státusz", @@ -395,6 +397,7 @@ "allLibrariesHelp": "Engedélyezés esetén ez a kiegészítő hozzá fog férni minden jelenlegi és jövőben létrehozott könyvtárhoz.", "noLibraries": "Nincs kiválasztott könyvtár", "librariesRequired": "Ez a kiegészítő hozzáférést kér könyvtárinformációkhoz. Válaszd ki, melyik könyvtárakat érheti el, vagy az 'Összes könyvtár engedélyezése' opciót.", +"allowWriteAccessHelp": "Amikor ez engedélyezve van, a kiegészítő módosíthatja a könyvtár mappáit. Alapbeállításon a kiegészítőknek csak olvasási joguk van.", "requiredHosts": "Szükséges hostok" }, "placeholders": { @@ -549,6 +552,12 @@ } }, "message": { + "uploadCover": "Borítókép feltöltése", + "removeCover": "Borítókép törlése", + "coverUploaded": "Borítókép feltöltve", + "coverRemoved": "Borítókép eltávolítva", + "coverUploadError": "Borítókép feltöltése sikertelen", + "coverRemoveError": "Borítókép törlése sikertelen", "note": "MEGJEGYZÉS", "transcodingDisabled": "Az átkódolási konfiguráció módosítása a webes felületen keresztül biztonsági okokból nem lehetséges. Ha módosítani szeretnéd az átkódolási beállításokat, indítsd újra a kiszolgálót a %{config} konfigurációs opcióval.", "transcodingEnabled": "A Navidrome jelenleg a következőkkel fut %{config}, ez lehetővé teszi a rendszerparancsok futtatását az átkódolási beállításokból a webes felület segítségével. Javasoljuk, hogy biztonsági okokból tiltsd ezt le, és csak az átkódolási beállítások konfigurálásának idejére kapcsold be.", From 9fe9cf3ff6aaf8c1ac06b2f6cd2cef43616d7679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= <deluan@navidrome.org> Date: Sun, 29 Mar 2026 19:55:29 -0400 Subject: [PATCH 67/81] fix(ui): update Spanish, French translations from POEditor (#5260) Co-authored-by: navidrome-bot <navidrome-bot@navidrome.org> --- resources/i18n/es.json | 16 +++++++++++----- resources/i18n/fr.json | 16 +++++++++++----- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/resources/i18n/es.json b/resources/i18n/es.json index 29d1a367f..a018eda3d 100644 --- a/resources/i18n/es.json +++ b/resources/i18n/es.json @@ -38,7 +38,7 @@ "missing": "Faltante", "libraryName": "Biblioteca", "composer": "Compositor", - "disc": "" + "disc": "Disco %{discNumber}" }, "actions": { "addToQueue": "Reproducir después", @@ -355,7 +355,7 @@ "selectedUsers": "Usuarios seleccionados", "allLibraries": "Permitir todas las bibliotecas", "selectedLibraries": "Bibliotecas seleccionadas", - "allowWriteAccess": "" + "allowWriteAccess": "Permitir acceso de escritura" }, "sections": { "status": "Estado", @@ -401,7 +401,7 @@ "requiredHosts": "Hosts requeridos", "configValidationError": "La validación de la configuración falló:", "schemaRenderError": "No se pudo renderizar el formulario de configuración. Es posible que el esquema del complemento no sea válido.", - "allowWriteAccessHelp": "" + "allowWriteAccessHelp": "Cuando está activado, el plugin puede modificar archivos en los directorios de la biblioteca. Por defecto, los plugins tienen acceso de solo lectura." }, "placeholders": { "configKey": "clave", @@ -591,7 +591,13 @@ "remove_all_missing_content": "¿Realmente desea eliminar todos los archivos faltantes de la base de datos? Esto eliminará permanentemente cualquier referencia a ellos, incluidas sus reproducciones y valoraciones.", "noSimilarSongsFound": "No se encontraron canciones similares", "noTopSongsFound": "No se encontraron canciones destacadas", - "startingInstantMix": "Cargando la mezcla instantánea..." + "startingInstantMix": "Cargando la mezcla instantánea...", + "uploadCover": "Subir portada", + "removeCover": "Eliminar portada", + "coverUploaded": "Portada actualizada", + "coverRemoved": "Portada eliminada", + "coverUploadError": "Error al subir la portada", + "coverRemoveError": "Error al eliminar la portada" }, "menu": { "library": "Biblioteca", @@ -712,4 +718,4 @@ "empty": "Nada en reproducción", "minutesAgo": "Hace %{smart_count} minuto |||| Hace %{smart_count} minutos" } -} +} \ No newline at end of file diff --git a/resources/i18n/fr.json b/resources/i18n/fr.json index 891fde03a..dae9f47e7 100644 --- a/resources/i18n/fr.json +++ b/resources/i18n/fr.json @@ -38,7 +38,7 @@ "missing": "Manquant", "libraryName": "Bibliothèque", "composer": "Compositeur·e", - "disc": "" + "disc": "Disque %{discNumber}" }, "actions": { "addToQueue": "Ajouter à la file", @@ -355,7 +355,7 @@ "selectedUsers": "Utilisateur·rices sélectionné.e.s", "allLibraries": "Autoriser toutes les bibliothèques", "selectedLibraries": "Bibliothèques sélectionnées", - "allowWriteAccess": "" + "allowWriteAccess": "Autoriser l'accès en écriture" }, "sections": { "status": "Statut", @@ -401,7 +401,7 @@ "requiredHosts": "Hôtes requis", "configValidationError": "Erreur lors de la validation de la configuration", "schemaRenderError": "Impossible de processer la configuration. Le schéma de l'extension n'est peut-être pas valide.", - "allowWriteAccessHelp": "" + "allowWriteAccessHelp": "Lorsque cette option est activée, le plugin peut modifier les fichiers dans les répertoires de la bibliothèque. Par défaut, les plugins ont un accès en lecture seule." }, "placeholders": { "configKey": "clef", @@ -591,7 +591,13 @@ "remove_all_missing_content": "Êtes-vous sûr(e) de vouloir supprimer tous les fichiers manquants de la base de données ? Cette action est permanente et supprimera leurs nombres d'écoutes, leur notations et tout ce qui y fait référence.", "noSimilarSongsFound": "Aucun titre similaire n'a été trouvé", "noTopSongsFound": "Aucun meilleur titre n'a été trouvé", - "startingInstantMix": "Chargement du mix instantanné..." + "startingInstantMix": "Chargement du mix instantané...", + "uploadCover": "Téléverser la pochette", + "removeCover": "Supprimer la pochette", + "coverUploaded": "Pochette mise à jour", + "coverRemoved": "Pochette supprimée", + "coverUploadError": "Erreur lors du téléversement de la pochette", + "coverRemoveError": "Erreur lors de la suppression de la pochette" }, "menu": { "library": "Bibliothèque", @@ -712,4 +718,4 @@ "empty": "Aucun titre en cours de lecture", "minutesAgo": "Il y a %{smart_count} minute |||| Il y a %{smart_count} minutes" } -} +} \ No newline at end of file From 420d2c8e5ae147b11dc4fb1e6b9734d3ab7c137f Mon Sep 17 00:00:00 2001 From: Deluan <deluan@navidrome.org> Date: Mon, 30 Mar 2026 07:01:38 -0400 Subject: [PATCH 68/81] fix(artwork): validate ffmpeg pipe before returning in cover art fallback ffmpeg.ExtractImage returns a pipe-based reader immediately, before ffmpeg finishes processing. When the audio file has no embedded image stream (e.g. a plain MP3), ffmpeg exits with an error that closes the pipe asynchronously. The selectImageReader function saw the non-nil reader as a success and returned it instead of falling through to the next source in the chain (album art). This caused getCoverArt to return an error response for tracks on albums where the disc artwork reader was invoked but no embedded art existed. Fixed by reading one byte from the pipe to validate the stream delivers data before returning it. If the read fails, the reader is closed and nil is returned, allowing the fallback chain to continue to album artwork. Closes #5265 --- core/artwork/sources.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/core/artwork/sources.go b/core/artwork/sources.go index 0628461e0..d830593fc 100644 --- a/core/artwork/sources.go +++ b/core/artwork/sources.go @@ -130,10 +130,25 @@ func fromFFmpegTag(ctx context.Context, ffmpeg ffmpeg.FFmpeg, path string) sourc if err != nil { return nil, "", err } - return r, path, nil + // Validate that the stream actually contains image data by reading the first byte. + // ffmpeg.ExtractImage returns a pipe reader that may fail asynchronously if the + // file has no video/image stream (e.g., an MP3 without embedded art). + buf := make([]byte, 1) + n, err := r.Read(buf) + if n == 0 || err != nil { + r.Close() + return nil, "", fmt.Errorf("ffmpeg produced no image data for %s: %w", path, err) + } + return readCloser{Reader: io.MultiReader(bytes.NewReader(buf[:n]), r), Closer: r}, path, nil } } +// readCloser combines a Reader and a Closer into an io.ReadCloser. +type readCloser struct { + io.Reader + io.Closer +} + func fromAlbum(ctx context.Context, a *artwork, id model.ArtworkID) sourceFunc { return func() (io.ReadCloser, string, error) { r, _, err := a.Get(ctx, id, 0, false) From 0f6a076dcae679d2cc4407b905a0786e9c923336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= <deluan@navidrome.org> Date: Mon, 30 Mar 2026 09:35:02 -0400 Subject: [PATCH 69/81] fix(artwork): refresh stale artist image URLs on expiry (#5267) * fix(external): refresh stale artist image URLs on expiry ArtistImage() was serving cached image URLs from the database indefinitely, ignoring ExternalInfoUpdatedAt. When users changed agent configuration (e.g. disabling Deezer), old URLs persisted because only the UpdateArtistInfo code path checked the TTL. Now ArtistImage() checks the expiry and enqueues a background refresh when the cached info is stale, matching the pattern used by refreshArtistInfo(). The stale URL is still returned immediately to avoid blocking clients. Fixes #5266 * test: add expired artist image info test with log assertion Verify that ArtistImage() enqueues a background refresh when cached info is expired, by capturing log output and checking for the expected debug message. Also asserts the stale URL is returned immediately without calling the agent. Signed-off-by: Deluan <deluan@navidrome.org> * fix: only enqueue refresh when returning a stale cached URL Move the expiry check to the else branch so we only enqueue a background refresh when a cached image URL exists and is being returned. This avoids doubling external API calls when the URL is empty (synchronous fetch) but ExternalInfoUpdatedAt is old. --------- Signed-off-by: Deluan <deluan@navidrome.org> --- core/external/provider.go | 10 +++- core/external/provider_artistimage_test.go | 65 ++++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/core/external/provider.go b/core/external/provider.go index a30afed15..40ca34069 100644 --- a/core/external/provider.go +++ b/core/external/provider.go @@ -374,8 +374,6 @@ func (e *provider) ArtistImage(ctx context.Context, id string) (*url.URL, error) return nil, err } - // Use already-stored image URL if available, avoiding expensive external API calls. - // If the info is expired, the background refresh (via UpdateArtistInfo/artistQueue) will update it. imageUrl := artist.ArtistImageUrl() if imageUrl == "" { // No cached URL — must fetch from external source synchronously @@ -385,6 +383,14 @@ func (e *provider) ArtistImage(ctx context.Context, id string) (*url.URL, error) return nil, ctx.Err() } imageUrl = artist.ArtistImageUrl() + } else { + // If cached info is expired, enqueue a background refresh so that config changes + // (e.g. disabling an agent) take effect without waiting for a full artist info refresh. + updatedAt := V(artist.ExternalInfoUpdatedAt) + if !updatedAt.IsZero() && time.Since(updatedAt) > conf.Server.DevArtistInfoTimeToLive { + log.Debug(ctx, "Artist image info expired, enqueuing background refresh", "artist", artist.Name(), "updatedAt", updatedAt) + e.artistQueue.enqueue(&artist) + } } if imageUrl == "" { diff --git a/core/external/provider_artistimage_test.go b/core/external/provider_artistimage_test.go index 11290bb66..529289ed3 100644 --- a/core/external/provider_artistimage_test.go +++ b/core/external/provider_artistimage_test.go @@ -1,14 +1,17 @@ package external_test import ( + "bytes" "context" "errors" "net/url" + "time" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf/configtest" "github.com/navidrome/navidrome/core/agents" . "github.com/navidrome/navidrome/core/external" + "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/tests" . "github.com/onsi/ginkgo/v2" @@ -266,6 +269,68 @@ var _ = Describe("Provider - ArtistImage", func() { mockImageAgent.AssertCalled(GinkgoT(), "GetArtistImages", ctx, "artist-1", "Artist One", "") }) + It("returns cached URL and does not call agent when info is not expired", func() { + // Arrange: artist has a cached image URL with recent ExternalInfoUpdatedAt + recentTime := time.Now().Add(-1 * time.Minute) + cachedArtist := &model.Artist{ + ID: "artist-cached", + Name: "Cached Artist", + LargeImageUrl: "http://example.com/cached-large.jpg", + ExternalInfoUpdatedAt: &recentTime, + } + mockArtistRepo.On("Get", "artist-cached").Return(cachedArtist, nil).Maybe() + expectedURL, _ := url.Parse("http://example.com/cached-large.jpg") + + // Capture log output + var logBuf bytes.Buffer + log.SetOutput(&logBuf) + defer log.SetOutput(GinkgoWriter) + log.SetLevel(log.LevelDebug) + + // Act + imgURL, err := provider.ArtistImage(ctx, "artist-cached") + + // Assert + Expect(err).ToNot(HaveOccurred()) + Expect(imgURL).To(Equal(expectedURL)) + mockImageAgent.AssertNotCalled(GinkgoT(), "GetArtistImages", mock.Anything, "artist-cached", mock.Anything, mock.Anything) + + // Assert: background refresh was NOT enqueued + Expect(logBuf.String()).ToNot(ContainSubstring("Artist image info expired, enqueuing background refresh")) + + }) + + It("returns stale URL and enqueues refresh when info is expired", func() { + // Arrange + conf.Server.DevArtistInfoTimeToLive = 1 * time.Nanosecond + expiredTime := time.Now().Add(-1 * time.Hour) + staleArtist := &model.Artist{ + ID: "artist-expired", + Name: "Expired Artist", + LargeImageUrl: "http://example.com/expired-large.jpg", + ExternalInfoUpdatedAt: &expiredTime, + } + mockArtistRepo.On("Get", "artist-expired").Return(staleArtist, nil).Maybe() + expectedURL, _ := url.Parse("http://example.com/expired-large.jpg") + + // Capture log output + var logBuf bytes.Buffer + log.SetOutput(&logBuf) + defer log.SetOutput(GinkgoWriter) + log.SetLevel(log.LevelDebug) + + // Act + imgURL, err := provider.ArtistImage(ctx, "artist-expired") + + // Assert: returns stale URL immediately, no agent call + Expect(err).ToNot(HaveOccurred()) + Expect(imgURL).To(Equal(expectedURL)) + mockImageAgent.AssertNotCalled(GinkgoT(), "GetArtistImages", mock.Anything, "artist-expired", mock.Anything, mock.Anything) + + // Assert: background refresh was enqueued + Expect(logBuf.String()).To(ContainSubstring("Artist image info expired, enqueuing background refresh")) + }) + Context("Unicode handling in artist names", func() { var artistWithEnDash *model.Artist var expectedURL *url.URL From c5bb920b88c1e7a397fbd3509fa5cbede92c8300 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:57:43 -0400 Subject: [PATCH 70/81] chore(deps): bump golang.org/x/image from 0.37.0 to 0.38.0 (#5268) Bumps [golang.org/x/image](https://github.com/golang/image) from 0.37.0 to 0.38.0. - [Commits](https://github.com/golang/image/compare/v0.37.0...v0.38.0) --- updated-dependencies: - dependency-name: golang.org/x/image dependency-version: 0.38.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 487b57ef7..3c6f7dc69 100644 --- a/go.mod +++ b/go.mod @@ -58,7 +58,7 @@ require ( github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 go.senan.xyz/taglib v0.11.1 go.uber.org/goleak v1.3.0 - golang.org/x/image v0.37.0 + golang.org/x/image v0.38.0 golang.org/x/net v0.52.0 golang.org/x/sync v0.20.0 golang.org/x/sys v0.42.0 diff --git a/go.sum b/go.sum index d7b16d9d3..eb6e6bf27 100644 --- a/go.sum +++ b/go.sum @@ -323,8 +323,8 @@ golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= -golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA= -golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= +golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE= +golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= From 4030bfe06f808573b568c47d3d272a563e10f027 Mon Sep 17 00:00:00 2001 From: Deluan <deluan@navidrome.org> Date: Wed, 1 Apr 2026 08:38:29 -0400 Subject: [PATCH 71/81] fix(artwork): preserve animation for square thumbnails with animated images Signed-off-by: Deluan <deluan@navidrome.org> --- core/artwork/reader_resized.go | 24 ++++++++-------- core/artwork/reader_resized_test.go | 44 ++++++++++++++++------------- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/core/artwork/reader_resized.go b/core/artwork/reader_resized.go index 72baad434..88ca8b83b 100644 --- a/core/artwork/reader_resized.go +++ b/core/artwork/reader_resized.go @@ -98,21 +98,19 @@ func (a *resizedArtworkReader) resizeImage(ctx context.Context, reader io.Reader return nil, 0, fmt.Errorf("reading image data: %w", err) } - // Preserve animation for animated images (skip for square thumbnails) - if !a.square { - if isAnimatedGIF(data) { - if a.a.ffmpeg.IsAvailable() { - // Animated GIF: convert to animated WebP via ffmpeg (with optional resize) - r, err := a.a.ffmpeg.ConvertAnimatedImage(ctx, bytes.NewReader(data), a.size, conf.Server.CoverArtQuality) - if err == nil { - return r, 0, nil - } - log.Warn(ctx, "Could not convert animated GIF, falling back to static", err) + // Preserve animation for animated images + if isAnimatedGIF(data) { + if a.a.ffmpeg.IsAvailable() { + // Animated GIF: convert to animated WebP via ffmpeg (with optional resize) + r, err := a.a.ffmpeg.ConvertAnimatedImage(ctx, bytes.NewReader(data), a.size, conf.Server.CoverArtQuality) + if err == nil { + return r, 0, nil } - } else if isAnimatedWebP(data) || isAnimatedPNG(data) { - // Animated WebP/APNG: return original as-is (ffmpeg can't re-encode these) - return bytes.NewReader(data), 0, nil + log.Warn(ctx, "Could not convert animated GIF, falling back to static", err) } + } else if isAnimatedWebP(data) || isAnimatedPNG(data) { + // Animated WebP/APNG: return original as-is (ffmpeg can't re-encode these) + return bytes.NewReader(data), 0, nil } return resizeStaticImage(data, a.size, a.square) diff --git a/core/artwork/reader_resized_test.go b/core/artwork/reader_resized_test.go index 7c9e21c0a..7c14f5e44 100644 --- a/core/artwork/reader_resized_test.go +++ b/core/artwork/reader_resized_test.go @@ -54,17 +54,17 @@ var _ = Describe("resizeImage", func() { Expect(len(output)).To(BeNumerically(">", 0)) }) - It("skips animation for square thumbnails even with animated GIF", func() { + It("preserves animation for square thumbnails with animated GIF", func() { r.square = true data := createAnimatedGIF(3) result, _, err := r.resizeImage(context.Background(), bytes.NewReader(data)) - // Should fall through to static resize (not ffmpeg conversion) - // The minimal test GIF may or may not resize successfully, - // but ffmpeg should NOT have been called for animated conversion - _ = result - _ = err - // Verify by checking the mock wasn't used for animated conversion: - // If ffmpeg was called, it would return mock data, not static resize result + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(BeNil()) + + // Should have been processed by ffmpeg (mock returns input data) + output, err := io.ReadAll(result) + Expect(err).ToNot(HaveOccurred()) + Expect(output).To(Equal(data)) }) }) @@ -81,13 +81,17 @@ var _ = Describe("resizeImage", func() { Expect(output).To(Equal(data)) }) - It("does not passthrough animated WebP for square thumbnails", func() { + It("preserves animated WebP for square thumbnails", func() { r.square = true data := createAnimatedWebPBytes() - // Should fall through to static resize, which will fail on fake WebP data - _, _, err := r.resizeImage(context.Background(), bytes.NewReader(data)) - // Static decode will fail on our minimal test WebP bytes (not a real image) - Expect(err).To(HaveOccurred()) + result, _, err := r.resizeImage(context.Background(), bytes.NewReader(data)) + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(BeNil()) + + // Should return original data unchanged + output, err := io.ReadAll(result) + Expect(err).ToNot(HaveOccurred()) + Expect(output).To(Equal(data)) }) }) @@ -104,15 +108,17 @@ var _ = Describe("resizeImage", func() { Expect(output).To(Equal(data)) }) - It("does not passthrough animated PNG for square thumbnails", func() { + It("preserves animated PNG for square thumbnails", func() { r.square = true data := createAPNGBytes() - // Should fall through to static resize result, _, err := r.resizeImage(context.Background(), bytes.NewReader(data)) - // Static PNG decode should succeed on our APNG (it's a valid PNG) - if err == nil { - Expect(result).ToNot(BeNil()) - } + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(BeNil()) + + // Should return original data unchanged + output, err := io.ReadAll(result) + Expect(err).ToNot(HaveOccurred()) + Expect(output).To(Equal(data)) }) }) From 6109bf519228b6cd6ae6b6a8b06a6a8c4e7a6cae Mon Sep 17 00:00:00 2001 From: Deluan <deluan@navidrome.org> Date: Wed, 1 Apr 2026 08:51:10 -0400 Subject: [PATCH 72/81] chore(deps): update go-sqlite3 to v1.14.38 and go-toml to v2.3.0 Signed-off-by: Deluan <deluan@navidrome.org> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 3c6f7dc69..fcee08c7e 100644 --- a/go.mod +++ b/go.mod @@ -36,12 +36,12 @@ require ( github.com/kardianos/service v1.2.4 github.com/kr/pretty v0.3.1 github.com/lestrrat-go/jwx/v3 v3.0.13 - github.com/mattn/go-sqlite3 v1.14.37 + github.com/mattn/go-sqlite3 v1.14.38 github.com/microcosm-cc/bluemonday v1.0.27 github.com/mileusna/useragent v1.3.5 github.com/onsi/ginkgo/v2 v2.28.1 github.com/onsi/gomega v1.39.1 - github.com/pelletier/go-toml/v2 v2.2.4 + github.com/pelletier/go-toml/v2 v2.3.0 github.com/pocketbase/dbx v1.12.0 github.com/pressly/goose/v3 v3.27.0 github.com/prometheus/client_golang v1.23.2 @@ -104,7 +104,7 @@ require ( github.com/lestrrat-go/dsig v1.0.0 // indirect github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect - github.com/lestrrat-go/httprc/v3 v3.0.4 // indirect + github.com/lestrrat-go/httprc/v3 v3.0.5 // indirect github.com/lestrrat-go/option/v2 v2.0.0 // indirect github.com/maruel/natural v1.3.0 // indirect github.com/mfridman/interpolate v0.0.2 // indirect diff --git a/go.sum b/go.sum index eb6e6bf27..e0671367a 100644 --- a/go.sum +++ b/go.sum @@ -167,8 +167,8 @@ github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7 github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= -github.com/lestrrat-go/httprc/v3 v3.0.4 h1:pXyH2ppK8GYYggygxJ3TvxpCZnbEUWc9qSwRTTApaLA= -github.com/lestrrat-go/httprc/v3 v3.0.4/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0= +github.com/lestrrat-go/httprc/v3 v3.0.5 h1:S+Mb4L2I+bM6JGTibLmxExhyTOqnXjqx+zi9MoXw/TM= +github.com/lestrrat-go/httprc/v3 v3.0.5/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0= github.com/lestrrat-go/jwx/v3 v3.0.13 h1:AdHKiPIYeCSnOJtvdpipPg/0SuFh9rdkN+HF3O0VdSk= github.com/lestrrat-go/jwx/v3 v3.0.13/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU= github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= @@ -177,8 +177,8 @@ github.com/maruel/natural v1.3.0 h1:VsmCsBmEyrR46RomtgHs5hbKADGRVtliHTyCOLFBpsg= github.com/maruel/natural v1.3.0/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg= -github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.38 h1:tDUzL85kMvOrvpCt8P64SbGgVFtJB11GPi2AdmITgb4= +github.com/mattn/go-sqlite3 v1.14.38/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= @@ -199,8 +199,8 @@ github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= +github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= From 220019a9f196bb146d1c8df407944afe2be709ff Mon Sep 17 00:00:00 2001 From: Deluan <deluan@navidrome.org> Date: Wed, 1 Apr 2026 18:19:06 -0400 Subject: [PATCH 73/81] fix: add missing viper defaults for mpvpath, artistimagefolder, and plugins.loglevel Fix #5284 Several configOptions struct fields were missing corresponding viper.SetDefault entries, making them invisible to environment variable overrides and config file parsing. Added defaults for mpvpath (consistent with ffmpegpath), artistimagefolder, and plugins.loglevel. Signed-off-by: Deluan <deluan@navidrome.org> --- conf/configuration.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/conf/configuration.go b/conf/configuration.go index 653e3d8e1..fce5e0b2f 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -712,10 +712,12 @@ func setViperDefaults() { viper.SetDefault("ignoredarticles", "The El La Los Las Le Les Os As O A") viper.SetDefault("indexgroups", "A B C D E F G H I J K L M N O P Q R S T U V W X-Z(XYZ) [Unknown]([)") viper.SetDefault("ffmpegpath", "") + viper.SetDefault("mpvpath", "") viper.SetDefault("mpvcmdtemplate", "mpv --audio-device=%d --no-audio-display %f --input-ipc-server=%s") viper.SetDefault("coverartpriority", "cover.*, folder.*, front.*, embedded, external") viper.SetDefault("coverartquality", 75) viper.SetDefault("artistartpriority", "artist.*, album/artist.*, external") + viper.SetDefault("artistimagefolder", "") viper.SetDefault("discartpriority", "disc*.*, cd*.*, cover.*, folder.*, front.*, discsubtitle, embedded") viper.SetDefault("lyricspriority", ".lrc,.txt,embedded") viper.SetDefault("enablegravatar", false) @@ -794,6 +796,7 @@ func setViperDefaults() { viper.SetDefault("plugins.enabled", true) viper.SetDefault("plugins.cachesize", "200MB") viper.SetDefault("plugins.autoreload", false) + viper.SetDefault("plugins.loglevel", "") // DevFlags. These are used to enable/disable debugging and incomplete features viper.SetDefault("devlogsourceline", false) From c60637de249d52266121bbf08d4d8192acf1b478 Mon Sep 17 00:00:00 2001 From: Deluan <deluan@navidrome.org> Date: Thu, 2 Apr 2026 15:44:20 -0400 Subject: [PATCH 74/81] fix(subsonic): return proper artwork ID format in getInternetRadioStations The coverArt field was returning the raw uploaded image filename instead of the standard ra-{id} artwork ID format. This caused getCoverArt to fail when clients passed the coverArt value directly. Now uses CoverArtID().String() consistent with how albums, artists, and playlists return their coverArt values. Fixes #5293. --- server/subsonic/radio.go | 6 +++++- server/subsonic/radio_test.go | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/server/subsonic/radio.go b/server/subsonic/radio.go index 7121566f9..4fbd6a53d 100644 --- a/server/subsonic/radio.go +++ b/server/subsonic/radio.go @@ -75,8 +75,12 @@ func (api *Router) GetInternetRadios(r *http.Request) (*responses.Subsonic, erro continue } // Add coverArt if not legacy client + var coverArt string + if g.UploadedImage != "" { + coverArt = g.CoverArtID().String() + } res[i].OpenSubsonicRadio = &responses.OpenSubsonicRadio{ - CoverArt: g.UploadedImage, + CoverArt: coverArt, } } diff --git a/server/subsonic/radio_test.go b/server/subsonic/radio_test.go index d5b764f60..e959ebe29 100644 --- a/server/subsonic/radio_test.go +++ b/server/subsonic/radio_test.go @@ -71,7 +71,7 @@ var _ = Describe("Radio", func() { 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[0].CoverArt).To(Equal("ra-rd-1_0")) Expect(response.InternetRadioStations.Radios[1].OpenSubsonicRadio).ToNot(BeNil()) Expect(response.InternetRadioStations.Radios[1].CoverArt).To(BeEmpty()) }) @@ -129,7 +129,7 @@ var _ = Describe("Radio", func() { Expect(err).ToNot(HaveOccurred()) Expect(response.InternetRadioStations.Radios[0].OpenSubsonicRadio).ToNot(BeNil()) - Expect(response.InternetRadioStations.Radios[0].CoverArt).To(Equal("rd-1_cover.jpg")) + Expect(response.InternetRadioStations.Radios[0].CoverArt).To(Equal("ra-rd-1_0")) }) }) From 23f3556371321faf199866989b906f2ef06a8034 Mon Sep 17 00:00:00 2001 From: Deluan <deluan@navidrome.org> Date: Thu, 2 Apr 2026 16:37:52 -0400 Subject: [PATCH 75/81] fix(subsonic): strip OpenSubsonic extensions from playlists for legacy clients buildOSPlaylist was the only OpenSubsonic builder function missing the LegacyClients guard, causing attributes like `validUntil` and `readonly` to appear in playlist XML responses for legacy clients like DSub2000. This caused a crash when DSub2000 tried to parse evaluated smart playlists containing the `validUntil` attribute. --- server/subsonic/playlists.go | 4 ++++ server/subsonic/playlists_test.go | 34 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/server/subsonic/playlists.go b/server/subsonic/playlists.go index baae7514b..a8c3da68c 100644 --- a/server/subsonic/playlists.go +++ b/server/subsonic/playlists.go @@ -159,6 +159,10 @@ func (api *Router) buildPlaylist(ctx context.Context, p model.Playlist) response } func buildOSPlaylist(ctx context.Context, p model.Playlist) *responses.OpenSubsonicPlaylist { + player, ok := request.PlayerFrom(ctx) + if ok && isClientInList(conf.Server.Subsonic.LegacyClients, player.Client) { + return nil + } pls := responses.OpenSubsonicPlaylist{} if p.IsSmartPlaylist() { diff --git a/server/subsonic/playlists_test.go b/server/subsonic/playlists_test.go index 41701b4de..3f2a2068e 100644 --- a/server/subsonic/playlists_test.go +++ b/server/subsonic/playlists_test.go @@ -128,6 +128,23 @@ var _ = Describe("buildPlaylist", func() { }) }) + Context("with legacy client", func() { + BeforeEach(func() { + conf.Server.Subsonic.LegacyClients = "legacy-client" + player := model.Player{Client: "legacy-client"} + ctx = request.WithPlayer(ctx, player) + }) + + It("returns all standard fields but no OpenSubsonic extensions", func() { + result := router.buildPlaylist(ctx, playlist) + + Expect(result.Comment).To(Equal("Test comment")) + Expect(result.Owner).To(Equal("admin")) + Expect(result.Public).To(BeTrue()) + Expect(result.OpenSubsonicPlaylist).To(BeNil()) + }) + }) + Context("when no player in context", func() { It("returns all fields", func() { result := router.buildPlaylist(ctx, playlist) @@ -213,6 +230,23 @@ var _ = Describe("buildPlaylist", func() { Expect(result.ValidUntil).To(Equal(&validUntil)) }) }) + + Context("with legacy client", func() { + BeforeEach(func() { + conf.Server.Subsonic.LegacyClients = "legacy-client" + player := model.Player{Client: "legacy-client"} + ctx = request.WithPlayer(ctx, player) + }) + + It("returns standard fields but no OpenSubsonic extensions", func() { + result := router.buildPlaylist(ctx, playlist) + + Expect(result.Comment).To(Equal("Test comment")) + Expect(result.Owner).To(Equal("admin")) + Expect(result.Public).To(BeTrue()) + Expect(result.OpenSubsonicPlaylist).To(BeNil()) + }) + }) }) }) From 80c1e602593dc20c4d72255324acaa8403a4a524 Mon Sep 17 00:00:00 2001 From: Deluan <deluan@navidrome.org> Date: Sat, 4 Apr 2026 10:37:28 -0400 Subject: [PATCH 76/81] feat(playlists): add sampleRate, codec, and missing fields for smart playlists Closes #5302 --- model/criteria/fields.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/model/criteria/fields.go b/model/criteria/fields.go index b9d91f087..bc3c7a3d3 100644 --- a/model/criteria/fields.go +++ b/model/criteria/fields.go @@ -35,6 +35,7 @@ var fieldMap = map[string]*mappedField{ "releasedate": {field: "media_file.release_date"}, "size": {field: "media_file.size"}, "compilation": {field: "media_file.compilation"}, + "missing": {field: "media_file.missing"}, "explicitstatus": {field: "media_file.explicit_status"}, "dateadded": {field: "media_file.created_at"}, "datemodified": {field: "media_file.updated_at"}, @@ -49,9 +50,11 @@ var fieldMap = map[string]*mappedField{ "catalognumber": {field: "media_file.catalog_num"}, "filepath": {field: "media_file.path"}, "filetype": {field: "media_file.suffix"}, + "codec": {field: "media_file.codec"}, "duration": {field: "media_file.duration"}, "bitrate": {field: "media_file.bit_rate"}, "bitdepth": {field: "media_file.bit_depth"}, + "samplerate": {field: "media_file.sample_rate"}, "bpm": {field: "media_file.bpm"}, "channels": {field: "media_file.channels"}, "loved": {field: "COALESCE(annotation.starred, false)"}, From c87db92cee70fa5aa78e781b9a2ef93c46000d20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= <deluan@navidrome.org> Date: Sat, 4 Apr 2026 15:17:01 -0400 Subject: [PATCH 77/81] fix(artwork): address WebP performance regression on low-power hardware (#5286) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(artwork): rename DevJpegCoverArt to EnableWebPEncoding Replaced the internal DevJpegCoverArt flag with a user-facing EnableWebPEncoding config option (defaults to true). When disabled, the fallback encoding now preserves the original image format — PNG sources stay PNG for non-square resizes, matching v0.60.3 behavior. The previous implementation incorrectly re-encoded PNG sources as JPEG in non-square mode. Also added EnableWebPEncoding to the insights data. * feat: add configurable UICoverArtSize option Converted the hardcoded UICoverArtSize constant (600px) into a configurable option, allowing users to reduce the cover art size requested by the UI to mitigate slow image encoding. The value is served to the frontend via the app config and used by all components that request cover art. Also simplified the cache warmer by removing a single-iteration loop in favor of direct code. * style: fix prettier formatting in subsonic test * feat: log WebP encoder/decoder selection Signed-off-by: Deluan <deluan@navidrome.org> * fix(artwork): address PR review feedback - Add DevJpegCoverArt to logRemovedOptions so users with the old config key get a clear warning instead of a silent ignore. - Include EnableWebPEncoding in the resized artwork cache key to prevent stale WebP responses after toggling the setting. - Skip animated GIF to WebP conversion via ffmpeg when EnableWebPEncoding is false, so the setting is consistent across all image types. - Fix data race in cache warmer by reading UICoverArtSize at construction time instead of per-image, avoiding concurrent access with config cleanup in tests. - Clarify cache warmer docstring to accurately describe caching behavior. * Revert "fix(artwork): address PR review feedback" This reverts commit 3a213ef03e401930977138afe0e84c83290df683. * fix(artwork): avoid data race in cache warmer config access Capture UICoverArtSize at construction time instead of reading from conf.Server on each doCacheImage call. The background goroutine could race with test config cleanup, causing intermittent race detector failures in CI. * fix(configuration): clamp UICoverArtSize to be within 200 and 1200 Signed-off-by: Deluan <deluan@navidrome.org> * fix(artwork): preserve album cache key compatibility with v0.60.3 Restored the v0.60.3 hash input order for album artwork cache keys (Agents + CoverArtPriority) so that existing caches remain valid on upgrade when EnableExternalServices is true. Also ensures CoverArtPriority is always part of the hash even when external services are disabled, fixing a v0.60.3 bug where changing CoverArtPriority had no effect on cache invalidation. Signed-off-by: Deluan <deluan@navidrome.org> * fix: default EnableWebPEncoding to false and reduce artwork parallelism Changed EnableWebPEncoding default to false so that upgrading users get the same JPEG/PNG encoding behavior as v0.60.3 out of the box, avoiding the WebP WASM overhead until native libwebp is available. Users can opt in to WebP by setting EnableWebPEncoding=true. Also reduced the default DevArtworkMaxRequests to half the CPU count (min 2) to lower resource pressure during artwork processing. * fix(configuration): update DefaultUICoverArtSize to 300 Signed-off-by: Deluan <deluan@navidrome.org> * fix(Makefile): append EXTRA_BUILD_TAGS to GO_BUILD_TAGS Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org> --- Makefile | 4 +- conf/configuration.go | 15 +++++-- consts/consts.go | 4 +- core/artwork/artwork_internal_test.go | 55 +++++++++++++++++++------- core/artwork/cache_warmer.go | 39 +++++++++--------- core/artwork/cache_warmer_test.go | 3 +- core/artwork/reader_album.go | 2 +- core/artwork/reader_resized.go | 24 +++++++---- core/metrics/insights.go | 2 + core/metrics/insights/data.go | 2 + server/public/handle_shares.go | 3 +- server/serve_index.go | 1 + server/serve_index_test.go | 1 + ui/src/album/AlbumDetails.jsx | 5 +-- ui/src/album/AlbumGridView.jsx | 5 ++- ui/src/artist/DesktopArtistDetails.jsx | 3 +- ui/src/artist/MobileArtistDetails.jsx | 3 +- ui/src/common/CoverArtAvatar.jsx | 4 +- ui/src/config.js | 1 + ui/src/consts.js | 2 - ui/src/playlist/PlaylistDetails.jsx | 4 +- ui/src/radio/RadioEdit.jsx | 5 ++- ui/src/radio/helper.jsx | 5 ++- ui/src/subsonic/index.test.js | 28 ++++++++++--- 24 files changed, 142 insertions(+), 78 deletions(-) diff --git a/Makefile b/Makefile index 3bad5b620..0673838c2 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,8 @@ GO_VERSION=$(shell grep "^go " go.mod | cut -f 2 -d ' ') NODE_VERSION=$(shell cat .nvmrc) -GO_BUILD_TAGS=netgo,sqlite_fts5 + +comma:=, +GO_BUILD_TAGS=netgo,sqlite_fts5$(if $(EXTRA_BUILD_TAGS),$(comma)$(EXTRA_BUILD_TAGS)) # Set global environment variables, required for most targets export CGO_CFLAGS_ALLOW=--define-prefix diff --git a/conf/configuration.go b/conf/configuration.go index fce5e0b2f..58239884a 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -70,6 +70,7 @@ type configOptions struct { MPVCmdTemplate string CoverArtPriority string CoverArtQuality int + EnableWebPEncoding bool ArtistArtPriority string ArtistImageFolder string DiscArtPriority string @@ -87,6 +88,7 @@ type configOptions struct { DefaultLanguage string DefaultUIVolume int UISearchDebounceMs int + UICoverArtSize int EnableReplayGain bool EnableCoverAnimation bool EnableNowPlaying bool @@ -141,7 +143,6 @@ type configOptions struct { DevOptimizeDB bool DevPreserveUnicodeInExternalCalls bool DevEnableMediaFileProbe bool - DevJpegCoverArt bool } type scannerOptions struct { @@ -424,6 +425,13 @@ func Load(noConfigDump bool) { // Removed options logRemovedOptions("Spotify.ID", "Spotify.Secret") + // Validate other options + if Server.UICoverArtSize < 200 || Server.UICoverArtSize > 1200 { + newValue := max(200, min(1200, Server.UICoverArtSize)) + log.Warn("UICoverArtSize must be between 200 and 1200, clamping", "value", Server.UICoverArtSize, "newValue", newValue) + Server.UICoverArtSize = newValue + } + // Call init hooks for _, hook := range hooks { hook() @@ -716,6 +724,7 @@ func setViperDefaults() { viper.SetDefault("mpvcmdtemplate", "mpv --audio-device=%d --no-audio-display %f --input-ipc-server=%s") viper.SetDefault("coverartpriority", "cover.*, folder.*, front.*, embedded, external") viper.SetDefault("coverartquality", 75) + viper.SetDefault("enablewebpencoding", false) viper.SetDefault("artistartpriority", "artist.*, album/artist.*, external") viper.SetDefault("artistimagefolder", "") viper.SetDefault("discartpriority", "disc*.*, cd*.*, cover.*, folder.*, front.*, discsubtitle, embedded") @@ -728,6 +737,7 @@ func setViperDefaults() { viper.SetDefault("defaultlanguage", "") viper.SetDefault("defaultuivolume", consts.DefaultUIVolume) viper.SetDefault("uisearchdebouncems", consts.DefaultUISearchDebounceMs) + viper.SetDefault("uicoverartsize", consts.DefaultUICoverArtSize) viper.SetDefault("enablereplaygain", true) viper.SetDefault("enablecoveranimation", true) viper.SetDefault("enablenowplaying", true) @@ -810,7 +820,7 @@ func setViperDefaults() { viper.SetDefault("devuishowconfig", true) viper.SetDefault("devneweventstream", true) viper.SetDefault("devoffsetoptimize", 50000) - viper.SetDefault("devartworkmaxrequests", max(4, runtime.NumCPU())) + viper.SetDefault("devartworkmaxrequests", max(2, runtime.NumCPU()/2)) viper.SetDefault("devartworkthrottlebackloglimit", consts.RequestThrottleBacklogLimit) viper.SetDefault("devartworkthrottlebacklogtimeout", consts.RequestThrottleBacklogTimeout) viper.SetDefault("devartistinfotimetolive", consts.ArtistInfoTimeToLive) @@ -826,7 +836,6 @@ func setViperDefaults() { viper.SetDefault("devoptimizedb", true) viper.SetDefault("devpreserveunicodeinexternalcalls", false) viper.SetDefault("devenablemediafileprobe", true) - viper.SetDefault("devjpegcoverart", false) } func init() { diff --git a/consts/consts.go b/consts/consts.go index f1010a872..ff5dedc2b 100644 --- a/consts/consts.go +++ b/consts/consts.go @@ -85,11 +85,9 @@ const ( ) const ( - UICoverArtSize = 600 + DefaultUICoverArtSize = 300 ) -var CacheWarmerImageSizes = []int{UICoverArtSize} - // Prometheus options const ( PrometheusDefaultPath = "/metrics" diff --git a/core/artwork/artwork_internal_test.go b/core/artwork/artwork_internal_test.go index 4b2359898..380352d3f 100644 --- a/core/artwork/artwork_internal_test.go +++ b/core/artwork/artwork_internal_test.go @@ -380,24 +380,24 @@ var _ = Describe("Artwork", func() { }) }) When("Square is false", func() { - It("returns WebP even if original image is a PNG", func() { + It("returns PNG if original image is a PNG", func() { conf.Server.CoverArtPriority = "front.png" r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID(), 15, false) Expect(err).ToNot(HaveOccurred()) img, format, err := image.Decode(r) Expect(err).ToNot(HaveOccurred()) - Expect(format).To(Equal("webp")) + Expect(format).To(Equal("png")) Expect(img.Bounds().Size().X).To(Equal(15)) Expect(img.Bounds().Size().Y).To(Equal(15)) }) - It("returns WebP if original image is not a PNG", func() { + It("returns JPEG if original image is not a PNG", func() { conf.Server.CoverArtPriority = "cover.jpg" r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID(), 200, false) Expect(err).ToNot(HaveOccurred()) img, format, err := image.Decode(r) - Expect(format).To(Equal("webp")) + Expect(format).To(Equal("jpeg")) Expect(err).ToNot(HaveOccurred()) Expect(img.Bounds().Size().X).To(Equal(200)) Expect(img.Bounds().Size().Y).To(Equal(200)) @@ -430,24 +430,51 @@ var _ = Describe("Artwork", func() { Expect(img.Bounds().Size().X).To(Equal(size)) Expect(img.Bounds().Size().Y).To(Equal(size)) }, - Entry("portrait png image", "png", "webp", false, 200), - Entry("landscape png image", "png", "webp", true, 200), - Entry("portrait jpg image", "jpg", "webp", false, 200), - Entry("landscape jpg image", "jpg", "webp", true, 200), + Entry("portrait png image", "png", "png", false, 200), + Entry("landscape png image", "png", "png", true, 200), + Entry("portrait jpg image", "jpg", "png", false, 200), + Entry("landscape jpg image", "jpg", "png", true, 200), ) }) - When("DevJpegCoverArt is true and square is false", func() { + When("EnableWebPEncoding is true and square is false", func() { BeforeEach(func() { - conf.Server.DevJpegCoverArt = true + conf.Server.EnableWebPEncoding = true }) - It("returns JPEG even if original image is a PNG", func() { + It("returns WebP even if original image is a PNG", func() { conf.Server.CoverArtPriority = "front.png" r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID(), 15, false) Expect(err).ToNot(HaveOccurred()) img, format, err := image.Decode(r) Expect(err).ToNot(HaveOccurred()) - Expect(format).To(Equal("jpeg")) + Expect(format).To(Equal("webp")) + Expect(img.Bounds().Size().X).To(Equal(15)) + Expect(img.Bounds().Size().Y).To(Equal(15)) + }) + It("returns WebP if original image is not a PNG", func() { + conf.Server.CoverArtPriority = "cover.jpg" + r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID(), 200, false) + Expect(err).ToNot(HaveOccurred()) + + img, format, err := image.Decode(r) + Expect(format).To(Equal("webp")) + Expect(err).ToNot(HaveOccurred()) + Expect(img.Bounds().Size().X).To(Equal(200)) + Expect(img.Bounds().Size().Y).To(Equal(200)) + }) + }) + When("EnableWebPEncoding is false and square is false", func() { + BeforeEach(func() { + conf.Server.EnableWebPEncoding = false + }) + It("returns PNG if original image is a PNG", func() { + conf.Server.CoverArtPriority = "front.png" + r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID(), 15, false) + Expect(err).ToNot(HaveOccurred()) + + img, format, err := image.Decode(r) + Expect(err).ToNot(HaveOccurred()) + Expect(format).To(Equal("png")) Expect(img.Bounds().Size().X).To(Equal(15)) Expect(img.Bounds().Size().Y).To(Equal(15)) }) @@ -463,11 +490,11 @@ var _ = Describe("Artwork", func() { Expect(img.Bounds().Size().Y).To(Equal(200)) }) }) - When("DevJpegCoverArt is true and square is true", func() { + When("EnableWebPEncoding is false and square is true", func() { var alCover model.Album BeforeEach(func() { - conf.Server.DevJpegCoverArt = true + conf.Server.EnableWebPEncoding = false }) It("returns PNG for square mode", func() { dirName := createImage("png", false, 200) diff --git a/core/artwork/cache_warmer.go b/core/artwork/cache_warmer.go index bd1359b74..5090d638e 100644 --- a/core/artwork/cache_warmer.go +++ b/core/artwork/cache_warmer.go @@ -10,7 +10,6 @@ import ( "time" "github.com/navidrome/navidrome/conf" - "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" @@ -24,7 +23,7 @@ type CacheWarmer interface { // NewCacheWarmer creates a new CacheWarmer instance. The CacheWarmer will pre-cache Artwork images in the background // to speed up the response time when the image is requested by the UI. The cache is pre-populated with the original -// image size, as well as the size defined in the UICoverArtSize constant. +// image size, as well as the size defined by the UICoverArtSize config option. func NewCacheWarmer(artwork Artwork, cache cache.FileCache) CacheWarmer { // If image cache is disabled, return a NOOP implementation if conf.Server.ImageCacheSize == "0" || !conf.Server.EnableArtworkPrecache { @@ -38,10 +37,11 @@ func NewCacheWarmer(artwork Artwork, cache cache.FileCache) CacheWarmer { } a := &cacheWarmer{ - artwork: artwork, - cache: cache, - buffer: make(map[model.ArtworkID]struct{}), - wakeSignal: make(chan struct{}, 1), + artwork: artwork, + cache: cache, + buffer: make(map[model.ArtworkID]struct{}), + wakeSignal: make(chan struct{}, 1), + coverArtSize: conf.Server.UICoverArtSize, } // Create a context with a fake admin user, to be able to pre-cache Playlist CoverArts @@ -51,11 +51,12 @@ func NewCacheWarmer(artwork Artwork, cache cache.FileCache) CacheWarmer { } type cacheWarmer struct { - artwork Artwork - buffer map[model.ArtworkID]struct{} - mutex sync.Mutex - cache cache.FileCache - wakeSignal chan struct{} + artwork Artwork + buffer map[model.ArtworkID]struct{} + mutex sync.Mutex + cache cache.FileCache + wakeSignal chan struct{} + coverArtSize int } func (a *cacheWarmer) PreCache(artID model.ArtworkID) { @@ -142,16 +143,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 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) - } - _, err = io.Copy(io.Discard, r) - r.Close() - return err + size := a.coverArtSize + r, _, err := a.artwork.Get(ctx, id, size, true) + if err != nil { + return fmt.Errorf("caching id='%s', size=%d: %w", id, size, err) } - return nil + _, err = io.Copy(io.Discard, r) + r.Close() + return err } func NoopCacheWarmer() CacheWarmer { diff --git a/core/artwork/cache_warmer_test.go b/core/artwork/cache_warmer_test.go index 9798ea8d6..a5da2004c 100644 --- a/core/artwork/cache_warmer_test.go +++ b/core/artwork/cache_warmer_test.go @@ -12,7 +12,6 @@ import ( "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf/configtest" - "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/utils/cache" . "github.com/onsi/ginkgo/v2" @@ -182,7 +181,7 @@ var _ = Describe("CacheWarmer", func() { Eventually(func() []int { return aw.getCachedSizes() - }).Should(ContainElements(consts.UICoverArtSize)) + }).Should(ContainElements(conf.Server.UICoverArtSize)) }) }) }) diff --git a/core/artwork/reader_album.go b/core/artwork/reader_album.go index 6de1d31d1..641b12b33 100644 --- a/core/artwork/reader_album.go +++ b/core/artwork/reader_album.go @@ -61,7 +61,7 @@ func newAlbumArtworkReader(ctx context.Context, artwork *artwork, artID model.Ar func (a *albumArtworkReader) Key() string { hashInput := conf.Server.CoverArtPriority if conf.Server.EnableExternalServices { - hashInput += conf.Server.Agents + hashInput = conf.Server.Agents + hashInput } hash := md5.Sum([]byte(hashInput)) return fmt.Sprintf( diff --git a/core/artwork/reader_resized.go b/core/artwork/reader_resized.go index 88ca8b83b..85a19a4c3 100644 --- a/core/artwork/reader_resized.go +++ b/core/artwork/reader_resized.go @@ -19,6 +19,16 @@ import ( xdraw "golang.org/x/image/draw" ) +func init() { + conf.AddHook(func() { + if err := webp.Dynamic(); err != nil { + log.Debug("Using WASM WebP encoder/decoder", "reason", err) + } else { + log.Debug("Using native libwebp for WebP encoding/decoding") + } + }) +} + var bufPool = sync.Pool{ New: func() any { return new(bytes.Buffer) @@ -117,7 +127,7 @@ func (a *resizedArtworkReader) resizeImage(ctx context.Context, reader io.Reader } func resizeStaticImage(data []byte, size int, square bool) (io.Reader, int, error) { - original, _, err := image.Decode(bytes.NewReader(data)) + original, format, err := image.Decode(bytes.NewReader(data)) if err != nil { return nil, 0, err } @@ -157,14 +167,12 @@ func resizeStaticImage(data []byte, size int, square bool) (io.Reader, int, erro buf := bufPool.Get().(*bytes.Buffer) buf.Reset() - if conf.Server.DevJpegCoverArt { - if square { - err = png.Encode(buf, dst) - } else { - err = jpeg.Encode(buf, dst, &jpeg.Options{Quality: conf.Server.CoverArtQuality}) - } - } else { + if conf.Server.EnableWebPEncoding { err = webp.Encode(buf, dst, webp.Options{Quality: conf.Server.CoverArtQuality}) + } else if format == "png" || square { + err = png.Encode(buf, dst) + } else { + err = jpeg.Encode(buf, dst, &jpeg.Options{Quality: conf.Server.CoverArtQuality}) } if err != nil { bufPool.Put(buf) diff --git a/core/metrics/insights.go b/core/metrics/insights.go index b87f1df5e..f069d3fb6 100644 --- a/core/metrics/insights.go +++ b/core/metrics/insights.go @@ -195,6 +195,8 @@ var staticData = sync.OnceValue(func() insights.Data { data.Config.EnableArtworkPrecache = conf.Server.EnableArtworkPrecache data.Config.EnableArtworkUpload = conf.Server.EnableArtworkUpload data.Config.CoverArtQuality = conf.Server.CoverArtQuality + data.Config.EnableWebPEncoding = conf.Server.EnableWebPEncoding + data.Config.UICoverArtSize = conf.Server.UICoverArtSize data.Config.EnableCoverAnimation = conf.Server.EnableCoverAnimation data.Config.EnableNowPlaying = conf.Server.EnableNowPlaying data.Config.EnableDownloads = conf.Server.EnableDownloads diff --git a/core/metrics/insights/data.go b/core/metrics/insights/data.go index b316c866d..34648a49b 100644 --- a/core/metrics/insights/data.go +++ b/core/metrics/insights/data.go @@ -65,6 +65,8 @@ type Data struct { EnablePrometheus bool `json:"enablePrometheus,omitempty"` EnableArtworkUpload bool `json:"enableArtworkUpload,omitempty"` CoverArtQuality int `json:"coverArtQuality,omitempty"` + EnableWebPEncoding bool `json:"enableWebPEncoding,omitempty"` + UICoverArtSize int `json:"uiCoverArtSize,omitempty"` EnableCoverAnimation bool `json:"enableCoverAnimation,omitempty"` EnableNowPlaying bool `json:"enableNowPlaying,omitempty"` SessionTimeout uint64 `json:"sessionTimeout,omitempty"` diff --git a/server/public/handle_shares.go b/server/public/handle_shares.go index 15e63d4db..24ecff1d6 100644 --- a/server/public/handle_shares.go +++ b/server/public/handle_shares.go @@ -6,6 +6,7 @@ import ( "net/http" "path" + "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/core/auth" "github.com/navidrome/navidrome/core/publicurl" @@ -81,7 +82,7 @@ func checkShareError(ctx context.Context, w http.ResponseWriter, err error, id s func (pub *Router) mapShareInfo(r *http.Request, s model.Share) *model.Share { s.URL = ShareURL(r, s.ID) - s.ImageURL = publicurl.ImageURL(r, s.CoverArtID(), consts.UICoverArtSize) + s.ImageURL = publicurl.ImageURL(r, s.CoverArtID(), conf.Server.UICoverArtSize) for i := range s.Tracks { s.Tracks[i].ID = encodeMediafileShare(s, s.Tracks[i].ID) } diff --git a/server/serve_index.go b/server/serve_index.go index 0d1a2f330..bd5be44f5 100644 --- a/server/serve_index.go +++ b/server/serve_index.go @@ -55,6 +55,7 @@ func serveIndex(ds model.DataStore, fs fs.FS, shareInfo *model.Share) http.Handl "defaultLanguage": conf.Server.DefaultLanguage, "defaultUIVolume": conf.Server.DefaultUIVolume, "uiSearchDebounceMs": conf.Server.UISearchDebounceMs, + "uiCoverArtSize": conf.Server.UICoverArtSize, "enableCoverAnimation": conf.Server.EnableCoverAnimation, "enableNowPlaying": conf.Server.EnableNowPlaying, "gaTrackingId": conf.Server.GATrackingID, diff --git a/server/serve_index_test.go b/server/serve_index_test.go index e08a42643..7515e7276 100644 --- a/server/serve_index_test.go +++ b/server/serve_index_test.go @@ -86,6 +86,7 @@ var _ = Describe("serveIndex", func() { Entry("defaultLanguage", func() { conf.Server.DefaultLanguage = "pt" }, "defaultLanguage", "pt"), Entry("defaultUIVolume", func() { conf.Server.DefaultUIVolume = 45 }, "defaultUIVolume", float64(45)), Entry("uiSearchDebounceMs", func() { conf.Server.UISearchDebounceMs = 500 }, "uiSearchDebounceMs", float64(500)), + Entry("uiCoverArtSize", func() { conf.Server.UICoverArtSize = 300 }, "uiCoverArtSize", float64(300)), Entry("enableCoverAnimation", func() { conf.Server.EnableCoverAnimation = true }, "enableCoverAnimation", true), Entry("enableNowPlaying", func() { conf.Server.EnableNowPlaying = true }, "enableNowPlaying", true), Entry("gaTrackingId", func() { conf.Server.GATrackingID = "UA-12345" }, "gaTrackingId", "UA-12345"), diff --git a/ui/src/album/AlbumDetails.jsx b/ui/src/album/AlbumDetails.jsx index 2411b8611..cec66eb8b 100644 --- a/ui/src/album/AlbumDetails.jsx +++ b/ui/src/album/AlbumDetails.jsx @@ -18,7 +18,7 @@ import { useTranslate, } from 'react-admin' import Lightbox from 'react-image-lightbox' -import { COVER_ART_SIZE } from '../consts' +import config from '../config' import 'react-image-lightbox/style.css' import subsonic from '../subsonic' import { @@ -32,7 +32,6 @@ import { useAlbumsPerPage, useImageLoadingState, } from '../common' -import config from '../config' import { formatFullDate, intersperse } from '../utils' import AlbumExternalLinks from './AlbumExternalLinks' import { SafeHTML } from '../common/SafeHTML' @@ -255,7 +254,7 @@ const AlbumDetails = (props) => { }) }, [record]) - const imageUrl = subsonic.getCoverArtUrl(record, COVER_ART_SIZE) + const imageUrl = subsonic.getCoverArtUrl(record, config.uiCoverArtSize) const fullImageUrl = subsonic.getCoverArtUrl(record) return ( diff --git a/ui/src/album/AlbumGridView.jsx b/ui/src/album/AlbumGridView.jsx index c8a161571..9717618fa 100644 --- a/ui/src/album/AlbumGridView.jsx +++ b/ui/src/album/AlbumGridView.jsx @@ -20,7 +20,8 @@ import { OverflowTooltip, useImageUrl, } from '../common' -import { COVER_ART_SIZE, DraggableTypes } from '../consts' +import config from '../config' +import { DraggableTypes } from '../consts' import clsx from 'clsx' import { AlbumDatesField } from './AlbumDatesField.jsx' @@ -135,7 +136,7 @@ const Cover = withContentRect('bounds')(({ [record], ) - const url = subsonic.getCoverArtUrl(record, COVER_ART_SIZE, true) + const url = subsonic.getCoverArtUrl(record, config.uiCoverArtSize, true) const { imgUrl, loading: imageLoading } = useImageUrl(url) return ( diff --git a/ui/src/artist/DesktopArtistDetails.jsx b/ui/src/artist/DesktopArtistDetails.jsx index bc2312477..dda761097 100644 --- a/ui/src/artist/DesktopArtistDetails.jsx +++ b/ui/src/artist/DesktopArtistDetails.jsx @@ -15,7 +15,6 @@ import { import Lightbox from 'react-image-lightbox' import ExpandInfoDialog from '../dialogs/ExpandInfoDialog' import AlbumInfo from '../album/AlbumInfo' -import { COVER_ART_SIZE } from '../consts' import subsonic from '../subsonic' import { SafeHTML } from '../common/SafeHTML' @@ -110,7 +109,7 @@ const DesktopArtistDetails = ({ artistInfo, record, biography }) => { <CardMedia key={record.id} component="img" - src={subsonic.getCoverArtUrl(record, COVER_ART_SIZE)} + src={subsonic.getCoverArtUrl(record, config.uiCoverArtSize)} className={`${classes.cover} ${imageLoading ? classes.coverLoading : ''}`} onClick={handleOpenLightbox} onLoad={handleImageLoad} diff --git a/ui/src/artist/MobileArtistDetails.jsx b/ui/src/artist/MobileArtistDetails.jsx index 9c6cd88ae..e82d4c28a 100644 --- a/ui/src/artist/MobileArtistDetails.jsx +++ b/ui/src/artist/MobileArtistDetails.jsx @@ -11,7 +11,6 @@ import { useImageLoadingState, } from '../common' import Lightbox from 'react-image-lightbox' -import { COVER_ART_SIZE } from '../consts' import subsonic from '../subsonic' import { SafeHTML } from '../common/SafeHTML' @@ -113,7 +112,7 @@ const MobileArtistDetails = ({ artistInfo, biography, record }) => { <CardMedia key={record.id} component="img" - src={subsonic.getCoverArtUrl(record, COVER_ART_SIZE)} + src={subsonic.getCoverArtUrl(record, config.uiCoverArtSize)} className={`${classes.cover} ${imageLoading ? classes.coverLoading : ''}`} onClick={handleOpenLightbox} onLoad={handleImageLoad} diff --git a/ui/src/common/CoverArtAvatar.jsx b/ui/src/common/CoverArtAvatar.jsx index bc6dd8a3c..f70403774 100644 --- a/ui/src/common/CoverArtAvatar.jsx +++ b/ui/src/common/CoverArtAvatar.jsx @@ -2,7 +2,7 @@ import { useRecordContext } from 'react-admin' import { Avatar } from '@material-ui/core' import { makeStyles } from '@material-ui/core/styles' import clsx from 'clsx' -import { COVER_ART_SIZE } from '../consts' +import config from '../config' import subsonic from '../subsonic' import { useImageUrl } from './useImageUrl' @@ -28,7 +28,7 @@ export const CoverArtAvatar = ({ const record = recordProp || recordContext const square = variant !== 'circular' const url = record - ? subsonic.getCoverArtUrl(record, COVER_ART_SIZE, square) + ? subsonic.getCoverArtUrl(record, config.uiCoverArtSize, square) : null const { imgUrl } = useImageUrl(url) if (!record) return null diff --git a/ui/src/config.js b/ui/src/config.js index bd11d7df6..f4d60dfba 100644 --- a/ui/src/config.js +++ b/ui/src/config.js @@ -21,6 +21,7 @@ const defaultConfig = { defaultLanguage: '', defaultUIVolume: 100, uiSearchDebounceMs: 200, + uiCoverArtSize: 600, enableUserEditing: true, enableArtworkUpload: true, enableSharing: true, diff --git a/ui/src/consts.js b/ui/src/consts.js index 68ec478f1..472cd4940 100644 --- a/ui/src/consts.js +++ b/ui/src/consts.js @@ -26,8 +26,6 @@ DraggableTypes.ALL.push( export const RADIO_PLACEHOLDER_IMAGE = 'internet-radio-icon.svg' -export const COVER_ART_SIZE = 600 - export const DEFAULT_SHARE_BITRATE = 128 export const BITRATE_CHOICES = [ diff --git a/ui/src/playlist/PlaylistDetails.jsx b/ui/src/playlist/PlaylistDetails.jsx index f2f528d4b..c396cbbeb 100644 --- a/ui/src/playlist/PlaylistDetails.jsx +++ b/ui/src/playlist/PlaylistDetails.jsx @@ -18,7 +18,7 @@ import { OverflowTooltip, useImageLoadingState, } from '../common' -import { COVER_ART_SIZE } from '../consts' +import config from '../config' import subsonic from '../subsonic' const useStyles = makeStyles( @@ -107,7 +107,7 @@ const PlaylistDetails = (props) => { handleCloseLightbox, } = useImageLoadingState(record.id) - const imageUrl = subsonic.getCoverArtUrl(record, COVER_ART_SIZE, true) + const imageUrl = subsonic.getCoverArtUrl(record, config.uiCoverArtSize, true) const fullImageUrl = subsonic.getCoverArtUrl(record) return ( diff --git a/ui/src/radio/RadioEdit.jsx b/ui/src/radio/RadioEdit.jsx index 5f804535a..bbe001e6f 100644 --- a/ui/src/radio/RadioEdit.jsx +++ b/ui/src/radio/RadioEdit.jsx @@ -11,7 +11,8 @@ import { makeStyles } from '@material-ui/core/styles' import { urlValidate } from '../utils/validations' import { Title, ImageUploadOverlay, useImageLoadingState } from '../common' import subsonic from '../subsonic' -import { COVER_ART_SIZE, RADIO_PLACEHOLDER_IMAGE } from '../consts' +import config from '../config' +import { RADIO_PLACEHOLDER_IMAGE } from '../consts' const useStyles = makeStyles({ coverParent: { @@ -83,7 +84,7 @@ const RadioCoverArt = ({ record }) => { {record.uploadedImage ? ( <CardMedia component="img" - src={subsonic.getCoverArtUrl(record, COVER_ART_SIZE, true)} + src={subsonic.getCoverArtUrl(record, config.uiCoverArtSize, true)} className={`${classes.cover} ${imageLoading ? classes.coverLoading : ''}`} onLoad={handleImageLoad} onError={handleImageError} diff --git a/ui/src/radio/helper.jsx b/ui/src/radio/helper.jsx index b278c0d7d..b451dae9a 100644 --- a/ui/src/radio/helper.jsx +++ b/ui/src/radio/helper.jsx @@ -1,5 +1,6 @@ import subsonic from '../subsonic' -import { COVER_ART_SIZE, RADIO_PLACEHOLDER_IMAGE } from '../consts' +import config from '../config' +import { RADIO_PLACEHOLDER_IMAGE } from '../consts' export async function songFromRadio(radio) { if (!radio) { @@ -8,7 +9,7 @@ export async function songFromRadio(radio) { let cover = RADIO_PLACEHOLDER_IMAGE if (radio.uploadedImage) { - cover = subsonic.getCoverArtUrl(radio, COVER_ART_SIZE, true) + cover = subsonic.getCoverArtUrl(radio, config.uiCoverArtSize, true) } else { // Try favicon as fallback try { diff --git a/ui/src/subsonic/index.test.js b/ui/src/subsonic/index.test.js index 38b910b08..a750694f4 100644 --- a/ui/src/subsonic/index.test.js +++ b/ui/src/subsonic/index.test.js @@ -1,5 +1,5 @@ import { vi } from 'vitest' -import { COVER_ART_SIZE } from '../consts' +import config from '../config' import subsonic from './index' describe('getCoverArtUrl', () => { @@ -31,7 +31,11 @@ describe('getCoverArtUrl', () => { updatedAt: '2023-01-01T00:00:00Z', } - const url = subsonic.getCoverArtUrl(playlistRecord, COVER_ART_SIZE, true) + const url = subsonic.getCoverArtUrl( + playlistRecord, + config.uiCoverArtSize, + true, + ) expect(url).toContain('pl-playlist-123') expect(url).toContain('size=600') @@ -45,7 +49,11 @@ describe('getCoverArtUrl', () => { sync: true, } - const url = subsonic.getCoverArtUrl(playlistRecord, COVER_ART_SIZE, true) + const url = subsonic.getCoverArtUrl( + playlistRecord, + config.uiCoverArtSize, + true, + ) expect(url).toContain('pl-playlist-123') expect(url).toContain('size=600') @@ -60,7 +68,11 @@ describe('getCoverArtUrl', () => { updatedAt: '2023-01-01T00:00:00Z', } - const url = subsonic.getCoverArtUrl(albumRecord, COVER_ART_SIZE, true) + const url = subsonic.getCoverArtUrl( + albumRecord, + config.uiCoverArtSize, + true, + ) expect(url).toContain('al-album-123') expect(url).toContain('size=600') @@ -74,7 +86,7 @@ describe('getCoverArtUrl', () => { updatedAt: '2023-01-01T00:00:00Z', } - const url = subsonic.getCoverArtUrl(songRecord, COVER_ART_SIZE, true) + const url = subsonic.getCoverArtUrl(songRecord, config.uiCoverArtSize, true) expect(url).toContain('mf-song-123') expect(url).toContain('size=600') @@ -87,7 +99,11 @@ describe('getCoverArtUrl', () => { updatedAt: '2023-01-01T00:00:00Z', } - const url = subsonic.getCoverArtUrl(artistRecord, COVER_ART_SIZE, true) + const url = subsonic.getCoverArtUrl( + artistRecord, + config.uiCoverArtSize, + true, + ) expect(url).toContain('ar-artist-123') expect(url).toContain('size=600') From 93631cdee99ecfec713ff0721277022c6528eb50 Mon Sep 17 00:00:00 2001 From: Xabi <888924+xabirequejo@users.noreply.github.com> Date: Sat, 4 Apr 2026 21:17:40 +0200 Subject: [PATCH 78/81] fix(ui): update Basque localisation (#5278) Added missing strings --- resources/i18n/eu.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/resources/i18n/eu.json b/resources/i18n/eu.json index 58954c9dc..6bfd09d0e 100644 --- a/resources/i18n/eu.json +++ b/resources/i18n/eu.json @@ -23,6 +23,7 @@ "bitDepth": "Bit-sakonera", "sampleRate": "Lagin-tasa", "channels": "Kanalak", + "disc": "%{discNumber}. diskoa", "discSubtitle": "Diskoaren azpititulua", "starred": "Gogokoa", "comment": "Iruzkina", @@ -355,7 +356,8 @@ "allUsers": "Baimendu erabiltzaile guztiak", "selectedUsers": "Hautatutako erabiltzaileak", "allLibraries": "Baimendu liburutegi guztiak", - "selectedLibraries": "Hautatutako liburutegiak" + "selectedLibraries": "Hautatutako liburutegiak", + "allowWriteAccess": "Eman idazteko baimena" }, "sections": { "status": "Egoera", @@ -400,6 +402,7 @@ "allLibrariesHelp": "Gaituta dagoenean, pluginak liburutegi guztietara izango du sarbidea, baita etorkizunean sortuko direnetara ere.", "noLibraries": "Ez da liburutegirik hautatu", "librariesRequired": "Plugin honek liburutegien informaziora sarbidea behar du. Hautatu zein liburutegi atzitu dezakeen pluginak, edo gaitu 'Baimendu liburutegi guztiak'.", + "allowWriteAccessHelp": "Gaituta dagoenean, pluginak liburutegien direktorioko fitxategiak moldatu ditzake. Defektuz, pluginek bakarrik irakurtzeko baimena dute.", "requiredHosts": "Beharrezko ostatatzaileak" }, "placeholders": { @@ -554,6 +557,12 @@ } }, "message": { + "uploadCover": "Igo azala", + "removeCover": "Kendu azala", + "coverUploaded": "Diskoaren azala eguneratu da", + "coverRemoved": "Diskoaren azala kendu da", + "coverUploadError": "Errorea diskoaren azala igotzean", + "coverRemoveError": "Errorea diskoaren azala kentzean", "note": "OHARRA", "transcodingDisabled": "Segurtasun arrazoiak direla-eta, transkodeketaren ezarpenak web-interfazearen bidez aldatzea ezgaituta dago. Transkodeketa-aukerak aldatu (editatu edo gehitu) nahi badituzu, berrabiarazi zerbitzaria konfigurazio-aukeraren %{config}-arekin.", "transcodingEnabled": "Navidrome %{config}-ekin martxan dago eta, beraz, web-interfazeko transkodeketa-ataletik sistema-komandoak exekuta daitezke. Segurtasun arrazoiak tarteko, ezgaitzea gomendatzen dugu, eta transkodeketa-aukerak konfiguratzen ari zarenean bakarrik gaitzea.", @@ -673,6 +682,7 @@ "currentValue": "Uneko balioa", "configurationFile": "Konfigurazio-fitxategia", "exportToml": "Esportatu konfigurazioa (TOML)", + "downloadToml": "Deskargatu konfigurazioa (TOML)", "exportSuccess": "Konfigurazioa arbelera esportatu da TOML formatuan", "exportFailed": "Konfigurazioa kopiatzeak huts egin du", "devFlagsHeader": "Garapen-adierazleak (aldatu/kendu litezke)", From e7c7cba87374ebe1bace57271bc5e8cf731b7a6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= <deluan@navidrome.org> Date: Sat, 4 Apr 2026 15:18:00 -0400 Subject: [PATCH 79/81] fix(ui): update Esperanto, Dutch translations from POEditor (#5301) Co-authored-by: navidrome-bot <navidrome-bot@navidrome.org> --- resources/i18n/eo.json | 95 ++++++++++++++++++++++++++++++++++++++++-- resources/i18n/nl.json | 40 +++++++++++------- 2 files changed, 116 insertions(+), 19 deletions(-) diff --git a/resources/i18n/eo.json b/resources/i18n/eo.json index 7a13c471d..60eaa6d7c 100644 --- a/resources/i18n/eo.json +++ b/resources/i18n/eo.json @@ -36,7 +36,9 @@ "bitDepth": "Bitprofundo", "sampleRate": "Elprena rapido", "missing": "Mankaj", - "libraryName": "Biblioteko" + "libraryName": "Biblioteko", + "composer": "", + "disc": "" }, "actions": { "addToQueue": "Ludi Poste", @@ -46,7 +48,8 @@ "download": "Elŝuti", "playNext": "Ludu Poste", "info": "Akiri Informon", - "showInPlaylist": "Montri en Ludlisto" + "showInPlaylist": "Montri en Ludlisto", + "instantMix": "" } }, "album": { @@ -328,6 +331,82 @@ "scanInProgress": "Skano progresas...", "noLibrariesAssigned": "Neniuj bibliotekoj asignitaj por ĉi tiu uzanto" } + }, + "plugin": { + "name": "", + "fields": { + "id": "", + "name": "", + "description": "", + "version": "Versio", + "author": "Aŭtoro", + "website": "Retejo", + "permissions": "Permesoj", + "enabled": "Ebligite", + "status": "", + "path": "Vojo", + "lastError": "Eraro", + "hasError": "Eraro", + "updatedAt": "Ĝisdatigite", + "createdAt": "", + "configKey": "Ŝlosilo", + "configValue": "", + "allUsers": "", + "selectedUsers": "", + "allLibraries": "", + "selectedLibraries": "", + "allowWriteAccess": "" + }, + "sections": { + "status": "", + "info": "", + "configuration": "", + "manifest": "", + "usersPermission": "", + "libraryPermission": "" + }, + "status": { + "enabled": "", + "disabled": "" + }, + "actions": { + "enable": "", + "disable": "", + "disabledDueToError": "", + "disabledUsersRequired": "", + "disabledLibrariesRequired": "", + "addConfig": "", + "rescan": "" + }, + "notifications": { + "enabled": "", + "disabled": "", + "updated": "", + "error": "" + }, + "validation": { + "invalidJson": "" + }, + "messages": { + "configHelp": "", + "clickPermissions": "", + "noConfig": "", + "allUsersHelp": "", + "noUsers": "", + "permissionReason": "", + "usersRequired": "", + "allLibrariesHelp": "", + "noLibraries": "", + "librariesRequired": "", + "requiredHosts": "", + "configValidationError": "", + "schemaRenderError": "", + "allowWriteAccessHelp": "" + }, + "placeholders": { + "configKey": "", + "configValue": "" + } } }, "ra": { @@ -511,7 +590,14 @@ "remove_all_missing_title": "Forigi ĉiujn mankajn dosierojn", "remove_all_missing_content": "Ĉu vi certas, ke vi volas forigi ĉiujn mankajn dosierojn de la datumbazo? Ĉi tio permanante forigos ĉiujn referencojn al ili, inkluzive iliajn ludnombrojn kaj taksojn.", "noSimilarSongsFound": "Neniuj similaj kantoj trovitaj", - "noTopSongsFound": "Neniuj plej luditaj kantoj trovitaj" + "noTopSongsFound": "Neniuj plej luditaj kantoj trovitaj", + "startingInstantMix": "", + "uploadCover": "", + "removeCover": "", + "coverUploaded": "", + "coverRemoved": "", + "coverUploadError": "", + "coverRemoveError": "" }, "menu": { "library": "Biblioteko", @@ -597,7 +683,8 @@ "exportSuccess": "Agordoj eksportiĝis al la tondujo en TOML-a formato", "exportFailed": "Malsukcesis kopii agordojn", "devFlagsHeader": "Programadaj Flagoj (povas ŝanĝiĝi/foriĝi)", - "devFlagsComment": "Ĉi tiuj estas eksperimentaj agordoj kaj eble foriĝos en estontaj versioj" + "devFlagsComment": "Ĉi tiuj estas eksperimentaj agordoj kaj eble foriĝos en estontaj versioj", + "downloadToml": "" } }, "activity": { diff --git a/resources/i18n/nl.json b/resources/i18n/nl.json index 86793ee19..3f638c13c 100644 --- a/resources/i18n/nl.json +++ b/resources/i18n/nl.json @@ -37,7 +37,8 @@ "sampleRate": "Sample waarde", "missing": "Ontbrekend", "libraryName": "Bibliotheek", - "composer": "" + "composer": "Componist", + "disc": "Schijf %{discNumber}" }, "actions": { "addToQueue": "Voeg toe aan wachtrij", @@ -48,7 +49,7 @@ "playNext": "Volgende", "info": "Meer info", "showInPlaylist": "Toon in afspeellijst", - "instantMix": "" + "instantMix": "Instant mix" } }, "album": { @@ -350,10 +351,11 @@ "createdAt": "Geinstalleerd", "configKey": "Sleutel", "configValue": "Waarde", - "allUsers": "Alle gebruikers toelaten", + "allUsers": "Sta toe voor alle gebruikers", "selectedUsers": "Geselecteerde gebruikers", - "allLibraries": "Alle bibliotheken toestaan", - "selectedLibraries": "Geselecteerde bibliotheken" + "allLibraries": "Sta toe voor alle bibliotheken", + "selectedLibraries": "Geselecteerde bibliotheken", + "allowWriteAccess": "Sta schrijftoegang toe" }, "sections": { "status": "Status", @@ -379,26 +381,27 @@ "notifications": { "enabled": "Plugin actief", "disabled": "Plugin niet actief", - "updated": "Plugin geupdate", + "updated": "Plugin bijgewerkt", "error": "Fout bij updaten plugin" }, "validation": { "invalidJson": "Configuratie moet geldige JSON zijn" }, "messages": { - "configHelp": "", + "configHelp": "Configureer de plug-in met key-value paren. Leeglaten als de plug-in niet geconfigueerd hoeft te worden.", "clickPermissions": "Klik op permissie voor details", "noConfig": "Geen configuratie ingesteld", - "allUsersHelp": "", + "allUsersHelp": "Als dit aanstaat heeft de plug-in toegang tot alle gebruikers, inclusief toekomstige.", "noUsers": "Geen gebruikers geselecteerd", "permissionReason": "Reden", - "usersRequired": "", - "allLibrariesHelp": "", + "usersRequired": "Deze plug-in heeft toegang nodig tot gebruikersinformatie. Selecteer welke gebruikers de plug-in toegang toe heeft, of schakel 'sta toe voor alle gebruikers' in.", + "allLibrariesHelp": "Als dit aanstaat, heeft de plug-in toegang tot alle bibliotheken, inclusief toekomstige.", "noLibraries": "Geen bibliotheken geselecteerd", - "librariesRequired": "", + "librariesRequired": "Deze plug-in heeft toegang nodig tot bibliotheek informatie. Selecteer welke bibliotheken de plug-in toegang to heeft, of schakel 'sta toe voor alle bibliotheken' in.", "requiredHosts": "Benodigde hosts", - "configValidationError": "", - "schemaRenderError": "" + "configValidationError": "Configuratiecheck mislukt", + "schemaRenderError": "Kan het configuratieformulier niet verwerken. Het plugin schema is wellicht ongeldig.", + "allowWriteAccessHelp": "Met dit ingeschakeld, kan de plug-in bestanden bewerken in de bibliotheekmappen. Standaard kunnen plug-ins alleen lezen." }, "placeholders": { "configKey": "Sleutel", @@ -588,7 +591,13 @@ "remove_all_missing_content": "Weet je zeker dat je alle ontbrekende bestanden van de database wil verwijderen? Dit wist permanent al hun referenties inclusief afspeel tellers en beoordelingen.", "noSimilarSongsFound": "Geen vergelijkbare nummers gevonden", "noTopSongsFound": "Geen beste nummers gevonden", - "startingInstantMix": "" + "startingInstantMix": "Laden van Instant mix...", + "uploadCover": "Albumhoes toevoegen", + "removeCover": "Verwijder albumhoes", + "coverUploaded": "Albumhoes bijgewerkt", + "coverRemoved": "Albumhoes verwijderd", + "coverUploadError": "Fout bij het toevoegen albumhoes", + "coverRemoveError": "Fout bij verwijderen albumhoes" }, "menu": { "library": "Bibliotheek", @@ -674,7 +683,8 @@ "exportSuccess": "Configuratie geëxporteerd naar klembord in TOML formaat", "exportFailed": "Kopiëren van configuratie mislukt", "devFlagsHeader": "Ontwikkelaarsinstellingen (onder voorbehoud)", - "devFlagsComment": "Dit zijn experimentele instellingen en worden mogelijk in latere versies verwijderd" + "devFlagsComment": "Dit zijn experimentele instellingen en worden mogelijk in latere versies verwijderd", + "downloadToml": "Download configuratie (TOML)" } }, "activity": { From 2018979bc34e7bdc3f119c095a70e39fa26c524d Mon Sep 17 00:00:00 2001 From: Chris M <821688+tebriel@users.noreply.github.com> Date: Sun, 5 Apr 2026 11:37:50 -0400 Subject: [PATCH 80/81] chore(ui): regenerate package-lock.json to have integrity fields (#5276) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(ui): regenerate package-lock.json to have integrity fields * chore(deps): update esbuild and related packages to version 0.27.7 Signed-off-by: Deluan <deluan@navidrome.org> * chore(lint): exclude node_modules from golangci-lint Prevents lint errors from Go files inside npm packages under ui/node_modules from being picked up by golangci-lint. --------- Signed-off-by: Deluan <deluan@navidrome.org> Co-authored-by: Deluan Quintão <deluan@navidrome.org> --- .golangci.yml | 2 + ui/package-lock.json | 2793 +++++++++++++++++++++++++++++++++++------- 2 files changed, 2348 insertions(+), 447 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index b6c632dee..28eb375a5 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -55,6 +55,7 @@ linters: - third_party$ - builtin$ - examples$ + - node_modules formatters: exclusions: generated: lax @@ -62,3 +63,4 @@ formatters: - third_party$ - builtin$ - examples$ + - node_modules diff --git a/ui/package-lock.json b/ui/package-lock.json index a9b83d76e..2dd91a674 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -75,11 +75,15 @@ }, "node_modules/@adobe/css-tools": { "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", "dev": true, "license": "MIT" }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", "dev": true, "license": "MIT", "dependencies": { @@ -92,11 +96,15 @@ }, "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, "license": "ISC" }, "node_modules/@babel/code-frame": { - "version": "7.28.6", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", @@ -108,25 +116,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.6", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.28.6", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", + "peer": true, "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.28.6", + "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -144,17 +157,21 @@ }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { - "version": "7.28.6", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -165,6 +182,8 @@ }, "node_modules/@babel/helper-annotate-as-pure": { "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "license": "MIT", "dependencies": { "@babel/types": "^7.27.3" @@ -175,6 +194,8 @@ }, "node_modules/@babel/helper-compilation-targets": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "license": "MIT", "dependencies": { "@babel/compat-data": "^7.28.6", @@ -189,6 +210,8 @@ }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -196,6 +219,8 @@ }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -215,6 +240,8 @@ }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -222,6 +249,8 @@ }, "node_modules/@babel/helper-create-regexp-features-plugin": { "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -237,20 +266,24 @@ }, "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.5", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "debug": "^4.4.1", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", "lodash.debounce": "^4.0.8", - "resolve": "^1.22.10" + "resolve": "^1.22.11" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -258,6 +291,8 @@ }, "node_modules/@babel/helper-define-polyfill-provider/node_modules/resolve": { "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "license": "MIT", "dependencies": { "is-core-module": "^2.16.1", @@ -276,6 +311,8 @@ }, "node_modules/@babel/helper-globals": { "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -283,6 +320,8 @@ }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.28.5", @@ -294,6 +333,8 @@ }, "node_modules/@babel/helper-module-imports": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.28.6", @@ -305,6 +346,8 @@ }, "node_modules/@babel/helper-module-transforms": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.28.6", @@ -320,6 +363,8 @@ }, "node_modules/@babel/helper-optimise-call-expression": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "license": "MIT", "dependencies": { "@babel/types": "^7.27.1" @@ -330,6 +375,8 @@ }, "node_modules/@babel/helper-plugin-utils": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -337,6 +384,8 @@ }, "node_modules/@babel/helper-remap-async-to-generator": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", @@ -352,6 +401,8 @@ }, "node_modules/@babel/helper-replace-supers": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", "license": "MIT", "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", @@ -367,6 +418,8 @@ }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -378,6 +431,8 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -385,6 +440,8 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -392,6 +449,8 @@ }, "node_modules/@babel/helper-validator-option": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -399,6 +458,8 @@ }, "node_modules/@babel/helper-wrap-function": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", + "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", "license": "MIT", "dependencies": { "@babel/template": "^7.28.6", @@ -410,21 +471,25 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.6", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "license": "MIT", "dependencies": { "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.6", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -435,6 +500,8 @@ }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -449,6 +516,8 @@ }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -462,6 +531,8 @@ }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -475,6 +546,8 @@ }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -490,6 +563,8 @@ }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", + "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", @@ -504,6 +579,8 @@ }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -514,6 +591,8 @@ }, "node_modules/@babel/plugin-syntax-import-assertions": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", + "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -527,6 +606,8 @@ }, "node_modules/@babel/plugin-syntax-import-attributes": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -540,6 +621,8 @@ }, "node_modules/@babel/plugin-syntax-unicode-sets-regex": { "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", @@ -554,6 +637,8 @@ }, "node_modules/@babel/plugin-transform-arrow-functions": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -566,12 +651,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.28.6", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", + "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.28.6" + "@babel/traverse": "^7.29.0" }, "engines": { "node": ">=6.9.0" @@ -582,6 +669,8 @@ }, "node_modules/@babel/plugin-transform-async-to-generator": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", + "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.28.6", @@ -597,6 +686,8 @@ }, "node_modules/@babel/plugin-transform-block-scoped-functions": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -610,6 +701,8 @@ }, "node_modules/@babel/plugin-transform-block-scoping": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", + "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -623,6 +716,8 @@ }, "node_modules/@babel/plugin-transform-class-properties": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", @@ -637,6 +732,8 @@ }, "node_modules/@babel/plugin-transform-class-static-block": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", @@ -651,6 +748,8 @@ }, "node_modules/@babel/plugin-transform-classes": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", + "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -669,6 +768,8 @@ }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", + "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", @@ -683,6 +784,8 @@ }, "node_modules/@babel/plugin-transform-destructuring": { "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -697,6 +800,8 @@ }, "node_modules/@babel/plugin-transform-dotall-regex": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", + "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", @@ -711,6 +816,8 @@ }, "node_modules/@babel/plugin-transform-duplicate-keys": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -723,7 +830,9 @@ } }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.28.6", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", @@ -738,6 +847,8 @@ }, "node_modules/@babel/plugin-transform-dynamic-import": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -751,6 +862,8 @@ }, "node_modules/@babel/plugin-transform-explicit-resource-management": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", + "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", @@ -765,6 +878,8 @@ }, "node_modules/@babel/plugin-transform-exponentiation-operator": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", + "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -778,6 +893,8 @@ }, "node_modules/@babel/plugin-transform-export-namespace-from": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -791,6 +908,8 @@ }, "node_modules/@babel/plugin-transform-for-of": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -805,6 +924,8 @@ }, "node_modules/@babel/plugin-transform-function-name": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.27.1", @@ -820,6 +941,8 @@ }, "node_modules/@babel/plugin-transform-json-strings": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", + "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -833,6 +956,8 @@ }, "node_modules/@babel/plugin-transform-literals": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -846,6 +971,8 @@ }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", + "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -859,6 +986,8 @@ }, "node_modules/@babel/plugin-transform-member-expression-literals": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -872,6 +1001,8 @@ }, "node_modules/@babel/plugin-transform-modules-amd": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.27.1", @@ -886,6 +1017,8 @@ }, "node_modules/@babel/plugin-transform-modules-commonjs": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.28.6", @@ -899,13 +1032,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.28.5", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", + "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.5" + "@babel/traverse": "^7.29.0" }, "engines": { "node": ">=6.9.0" @@ -916,6 +1051,8 @@ }, "node_modules/@babel/plugin-transform-modules-umd": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.27.1", @@ -929,11 +1066,13 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.27.1", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -944,6 +1083,8 @@ }, "node_modules/@babel/plugin-transform-new-target": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -957,6 +1098,8 @@ }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -970,6 +1113,8 @@ }, "node_modules/@babel/plugin-transform-numeric-separator": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", + "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -983,6 +1128,8 @@ }, "node_modules/@babel/plugin-transform-object-rest-spread": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", + "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", @@ -1000,6 +1147,8 @@ }, "node_modules/@babel/plugin-transform-object-super": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1014,6 +1163,8 @@ }, "node_modules/@babel/plugin-transform-optional-catch-binding": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", + "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1027,6 +1178,8 @@ }, "node_modules/@babel/plugin-transform-optional-chaining": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", @@ -1041,6 +1194,8 @@ }, "node_modules/@babel/plugin-transform-parameters": { "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1054,6 +1209,8 @@ }, "node_modules/@babel/plugin-transform-private-methods": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", @@ -1068,6 +1225,8 @@ }, "node_modules/@babel/plugin-transform-private-property-in-object": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", + "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -1083,6 +1242,8 @@ }, "node_modules/@babel/plugin-transform-property-literals": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1096,6 +1257,8 @@ }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", "dev": true, "license": "MIT", "dependencies": { @@ -1110,6 +1273,8 @@ }, "node_modules/@babel/plugin-transform-react-jsx-source": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", "dev": true, "license": "MIT", "dependencies": { @@ -1123,7 +1288,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.6", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", + "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1137,6 +1304,8 @@ }, "node_modules/@babel/plugin-transform-regexp-modifiers": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", + "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", @@ -1151,6 +1320,8 @@ }, "node_modules/@babel/plugin-transform-reserved-words": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1164,6 +1335,8 @@ }, "node_modules/@babel/plugin-transform-shorthand-properties": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1177,6 +1350,8 @@ }, "node_modules/@babel/plugin-transform-spread": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", + "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", @@ -1191,6 +1366,8 @@ }, "node_modules/@babel/plugin-transform-sticky-regex": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1204,6 +1381,8 @@ }, "node_modules/@babel/plugin-transform-template-literals": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1217,6 +1396,8 @@ }, "node_modules/@babel/plugin-transform-typeof-symbol": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1230,6 +1411,8 @@ }, "node_modules/@babel/plugin-transform-unicode-escapes": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1243,6 +1426,8 @@ }, "node_modules/@babel/plugin-transform-unicode-property-regex": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", + "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", @@ -1257,6 +1442,8 @@ }, "node_modules/@babel/plugin-transform-unicode-regex": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", @@ -1271,6 +1458,8 @@ }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", + "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", @@ -1284,10 +1473,12 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.28.6", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.2.tgz", + "integrity": "sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", + "@babel/compat-data": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", @@ -1301,7 +1492,7 @@ "@babel/plugin-syntax-import-attributes": "^7.28.6", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.28.6", + "@babel/plugin-transform-async-generator-functions": "^7.29.0", "@babel/plugin-transform-async-to-generator": "^7.28.6", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", "@babel/plugin-transform-block-scoping": "^7.28.6", @@ -1312,7 +1503,7 @@ "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-dotall-regex": "^7.28.6", "@babel/plugin-transform-duplicate-keys": "^7.27.1", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.28.6", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", "@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-explicit-resource-management": "^7.28.6", "@babel/plugin-transform-exponentiation-operator": "^7.28.6", @@ -1325,9 +1516,9 @@ "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.28.6", - "@babel/plugin-transform-modules-systemjs": "^7.28.5", + "@babel/plugin-transform-modules-systemjs": "^7.29.0", "@babel/plugin-transform-modules-umd": "^7.27.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", "@babel/plugin-transform-numeric-separator": "^7.28.6", @@ -1339,7 +1530,7 @@ "@babel/plugin-transform-private-methods": "^7.28.6", "@babel/plugin-transform-private-property-in-object": "^7.28.6", "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.6", + "@babel/plugin-transform-regenerator": "^7.29.0", "@babel/plugin-transform-regexp-modifiers": "^7.28.6", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", @@ -1352,10 +1543,10 @@ "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.14", - "babel-plugin-polyfill-corejs3": "^0.13.0", - "babel-plugin-polyfill-regenerator": "^0.6.5", - "core-js-compat": "^3.43.0", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", "semver": "^6.3.1" }, "engines": { @@ -1367,6 +1558,8 @@ }, "node_modules/@babel/preset-env/node_modules/semver": { "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1374,6 +1567,8 @@ }, "node_modules/@babel/preset-modules": { "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -1385,18 +1580,22 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.6", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/runtime-corejs3": { - "version": "7.28.6", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.2.tgz", + "integrity": "sha512-Lc94FOD5+0aXhdb0Tdg3RUtqT6yWbI/BbFWvlaSJ3gAb9Ks+99nHRDKADVqC37er4eCB0fHyWT+y+K3QOvJKbw==", "dev": true, "license": "MIT", "dependencies": { - "core-js-pure": "^3.43.0" + "core-js-pure": "^3.48.0" }, "engines": { "node": ">=6.9.0" @@ -1404,6 +1603,8 @@ }, "node_modules/@babel/template": { "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.28.6", @@ -1415,15 +1616,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.6", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.6", + "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -1431,7 +1634,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.6", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -1443,6 +1648,8 @@ }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, "license": "MIT", "engines": { @@ -1451,6 +1658,8 @@ }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", "dev": true, "funding": [ { @@ -1469,6 +1678,8 @@ }, "node_modules/@csstools/css-calc": { "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", "dev": true, "funding": [ { @@ -1491,6 +1702,8 @@ }, "node_modules/@csstools/css-color-parser": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", "dev": true, "funding": [ { @@ -1517,6 +1730,8 @@ }, "node_modules/@csstools/css-parser-algorithms": { "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", "dev": true, "funding": [ { @@ -1529,6 +1744,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -1538,6 +1754,8 @@ }, "node_modules/@csstools/css-tokenizer": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", "dev": true, "funding": [ { @@ -1550,16 +1768,21 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } }, "node_modules/@date-io/core": { "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz", + "integrity": "sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA==", "license": "MIT" }, "node_modules/@date-io/moment": { "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-1.3.11.tgz", + "integrity": "sha512-pLEkqp8+P1DfC+QU8StaIANXoiadjJjoImLQCy0rhFAo0RVcJB9cM7mWr7fVgM49EjCwpA8a1JekNhuRLIJVwQ==", "license": "MIT", "dependencies": { "@date-io/core": "^1.3.11" @@ -1570,12 +1793,14 @@ }, "node_modules/@emotion/hash": { "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "cpu": [ "ppc64" ], @@ -1590,9 +1815,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "cpu": [ "arm" ], @@ -1607,9 +1832,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "cpu": [ "arm64" ], @@ -1624,9 +1849,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "cpu": [ "x64" ], @@ -1641,7 +1866,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "cpu": [ "arm64" ], @@ -1656,9 +1883,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "cpu": [ "x64" ], @@ -1673,9 +1900,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "cpu": [ "arm64" ], @@ -1690,9 +1917,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "cpu": [ "x64" ], @@ -1707,9 +1934,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "cpu": [ "arm" ], @@ -1724,9 +1951,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "cpu": [ "arm64" ], @@ -1741,9 +1968,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "cpu": [ "ia32" ], @@ -1758,9 +1985,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "cpu": [ "loong64" ], @@ -1775,9 +2002,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "cpu": [ "mips64el" ], @@ -1792,9 +2019,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "cpu": [ "ppc64" ], @@ -1809,9 +2036,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "cpu": [ "riscv64" ], @@ -1826,9 +2053,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "cpu": [ "s390x" ], @@ -1843,9 +2070,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "cpu": [ "x64" ], @@ -1860,9 +2087,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", "cpu": [ "arm64" ], @@ -1877,9 +2104,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "cpu": [ "x64" ], @@ -1894,9 +2121,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", "cpu": [ "arm64" ], @@ -1911,9 +2138,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "cpu": [ "x64" ], @@ -1928,9 +2155,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", "cpu": [ "arm64" ], @@ -1945,9 +2172,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "cpu": [ "x64" ], @@ -1962,9 +2189,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "cpu": [ "arm64" ], @@ -1979,9 +2206,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "cpu": [ "ia32" ], @@ -1996,9 +2223,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "cpu": [ "x64" ], @@ -2014,6 +2241,8 @@ }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2031,6 +2260,8 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -2039,6 +2270,8 @@ }, "node_modules/@eslint/eslintrc": { "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2060,7 +2293,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -2069,7 +2304,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -2081,6 +2318,8 @@ }, "node_modules/@eslint/js": { "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, "license": "MIT", "engines": { @@ -2089,6 +2328,9 @@ }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2101,7 +2343,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.12", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -2110,7 +2354,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -2122,6 +2368,8 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2134,106 +2382,25 @@ }, "node_modules/@humanwhocodes/object-schema": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true, "license": "BSD-3-Clause" }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.1", - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=18" } }, "node_modules/@jest/types": { "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2249,6 +2416,8 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -2257,6 +2426,8 @@ }, "node_modules/@jridgewell/remapping": { "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -2265,6 +2436,8 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "license": "MIT", "engines": { "node": ">=6.0.0" @@ -2272,6 +2445,8 @@ }, "node_modules/@jridgewell/source-map": { "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -2280,10 +2455,14 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2292,7 +2471,10 @@ }, "node_modules/@jsonforms/core": { "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@jsonforms/core/-/core-2.5.2.tgz", + "integrity": "sha512-tl64cLC2dUrGvu2nTHRDEA5Yv3RfwzMCIlVaoSUSq44LakKLGJdkPl8j/fb07llpFqz0a7gEAmy/8gLdmwgaLQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.3", "ajv": "^6.10.2", @@ -2304,6 +2486,9 @@ }, "node_modules/@jsonforms/core/node_modules/uuid": { "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", "license": "MIT", "bin": { "uuid": "bin/uuid" @@ -2311,6 +2496,8 @@ }, "node_modules/@jsonforms/material-renderers": { "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@jsonforms/material-renderers/-/material-renderers-2.5.2.tgz", + "integrity": "sha512-0C6MVyhLoMOf1Byhgs9ZNvV4NWHdbMce6m7hW2LF1Mt9mK1wDfTTst+xM3g7K5b8FSiBiF5kMnTggJcRweAEBA==", "license": "MIT", "dependencies": { "@date-io/moment": "1.3.11", @@ -2328,6 +2515,9 @@ }, "node_modules/@jsonforms/material-renderers/node_modules/uuid": { "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", "license": "MIT", "bin": { "uuid": "bin/uuid" @@ -2335,7 +2525,10 @@ }, "node_modules/@jsonforms/react": { "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@jsonforms/react/-/react-2.5.2.tgz", + "integrity": "sha512-kZf2fq4urIBlFTCiBX95eKg8uojkyJj7FVDtIV739aVkJjE5+ihn1+kG1qLxYSxlGC7S24i12BZJzRetSRihBQ==", "license": "MIT", + "peer": true, "dependencies": { "lodash": "^4.17.15", "object-hash": "^2.0.0" @@ -2347,7 +2540,11 @@ }, "node_modules/@material-ui/core": { "version": "4.12.4", + "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.4.tgz", + "integrity": "sha512-tr7xekNlM9LjA6pagJmL8QCgZXaubWUwkJnoYcMKd4gw/t4XiyvnTkjdGrUVicyB2BsdaAv1tvow45bPM4sSwQ==", + "deprecated": "Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.4.4", "@material-ui/styles": "^4.11.5", @@ -2382,6 +2579,8 @@ }, "node_modules/@material-ui/core/node_modules/clsx": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", "license": "MIT", "engines": { "node": ">=6" @@ -2389,7 +2588,10 @@ }, "node_modules/@material-ui/icons": { "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.3.tgz", + "integrity": "sha512-IKHlyx6LDh8n19vzwH5RtHIOHl9Tu90aAAxcbWME6kp4dmvODM3UvOHJeMIDzUbd4muuJKHmlNoBN+mDY4XkBA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.4.4" }, @@ -2410,6 +2612,9 @@ }, "node_modules/@material-ui/lab": { "version": "4.0.0-alpha.61", + "resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.61.tgz", + "integrity": "sha512-rSzm+XKiNUjKegj8bzt5+pygZeckNLOr+IjykH8sYdVk7dE9y2ZuUSofiMV2bJk3qU+JHwexmw+q0RyNZB9ugg==", + "deprecated": "Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.", "license": "MIT", "dependencies": { "@babel/runtime": "^7.4.4", @@ -2435,6 +2640,8 @@ }, "node_modules/@material-ui/lab/node_modules/clsx": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", "license": "MIT", "engines": { "node": ">=6" @@ -2442,6 +2649,9 @@ }, "node_modules/@material-ui/pickers": { "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.3.11.tgz", + "integrity": "sha512-pDYjbjUeabapijS2FpSwK/ruJdk7IGeAshpLbKDa3PRRKRy7Nv6sXxAvUg2F+lID/NwUKgBmCYS5bzrl7Xxqzw==", + "deprecated": "This package no longer supported. It has been relaced by @mui/x-date-pickers", "license": "MIT", "dependencies": { "@babel/runtime": "^7.6.0", @@ -2461,6 +2671,8 @@ }, "node_modules/@material-ui/pickers/node_modules/clsx": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", "license": "MIT", "engines": { "node": ">=6" @@ -2468,7 +2680,11 @@ }, "node_modules/@material-ui/styles": { "version": "4.11.5", + "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.5.tgz", + "integrity": "sha512-o/41ot5JJiUsIETME9wVLAJrmIWL3j0R0Bj2kCOLbSfqEkKf0fmaPt+5vtblUh5eXr2S+J/8J3DaCb10+CzPGA==", + "deprecated": "Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.4.4", "@emotion/hash": "^0.8.0", @@ -2507,6 +2723,8 @@ }, "node_modules/@material-ui/styles/node_modules/clsx": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", "license": "MIT", "engines": { "node": ">=6" @@ -2514,6 +2732,8 @@ }, "node_modules/@material-ui/system": { "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.12.2.tgz", + "integrity": "sha512-6CSKu2MtmiJgcCGf6nBQpM8fLkuB9F55EKfbdTC80NND5wpTmKzwdhLYLH3zL4cLlK0gVaaltW7/wMuyTnN0Lw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.4.4", @@ -2541,6 +2761,8 @@ }, "node_modules/@material-ui/types": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", + "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==", "license": "MIT", "peerDependencies": { "@types/react": "*" @@ -2553,6 +2775,8 @@ }, "node_modules/@material-ui/utils": { "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.3.tgz", + "integrity": "sha512-ZuQPV4rBK/V1j2dIkSSEcH5uT6AaHuKWFfotADHsC0wVL1NLd2WkFCm4ZZbX33iO4ydl6V0GPngKm8HZQ2oujg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.4.4", @@ -2569,6 +2793,8 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "license": "MIT", "dependencies": { @@ -2581,6 +2807,8 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", "engines": { @@ -2589,6 +2817,8 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", "dependencies": { @@ -2601,6 +2831,8 @@ }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", "license": "MIT", "engines": { "node": ">=12.22.0" @@ -2608,6 +2840,8 @@ }, "node_modules/@pnpm/network.ca-file": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", "license": "MIT", "dependencies": { "graceful-fs": "4.2.10" @@ -2618,10 +2852,14 @@ }, "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "license": "ISC" }, "node_modules/@pnpm/npm-conf": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-3.0.2.tgz", + "integrity": "sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA==", "license": "MIT", "dependencies": { "@pnpm/config.env-replace": "^1.1.0", @@ -2634,14 +2872,20 @@ }, "node_modules/@react-dnd/asap": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.1.tgz", + "integrity": "sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==", "license": "MIT" }, "node_modules/@react-dnd/invariant": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz", + "integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==", "license": "MIT" }, "node_modules/@react-dnd/shallowequal": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz", + "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==", "license": "MIT" }, "node_modules/@react-icons/all-files": { @@ -2655,6 +2899,8 @@ }, "node_modules/@redux-saga/core": { "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.4.2.tgz", + "integrity": "sha512-nIMLGKo6jV6Wc1sqtVQs1iqbB3Kq20udB/u9XEaZQisT6YZ0NRB8+4L6WqD/E+YziYutd27NJbG8EWUPkb7c6Q==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", @@ -2672,10 +2918,14 @@ }, "node_modules/@redux-saga/deferred": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.3.1.tgz", + "integrity": "sha512-0YZ4DUivWojXBqLB/TmuRRpDDz7tyq1I0AuDV7qi01XlLhM5m51W7+xYtIckH5U2cMlv9eAuicsfRAi1XHpXIg==", "license": "MIT" }, "node_modules/@redux-saga/delay-p": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.3.1.tgz", + "integrity": "sha512-597I7L5MXbD/1i3EmcaOOjL/5suxJD7p5tnbV1PiWnE28c2cYiIHqmSMK2s7us2/UrhOL2KTNBiD0qBg6KnImg==", "license": "MIT", "dependencies": { "@redux-saga/symbols": "^1.2.1" @@ -2683,6 +2933,8 @@ }, "node_modules/@redux-saga/is": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.2.1.tgz", + "integrity": "sha512-x3aWtX3GmQfEvn8dh0ovPbsXgK9JjpiR24wKztpGbZP8JZUWWvUgKrvnWZ/T/4iphOBftyVc9VrIwhAnsM+OFA==", "license": "MIT", "dependencies": { "@redux-saga/symbols": "^1.2.1", @@ -2691,19 +2943,27 @@ }, "node_modules/@redux-saga/symbols": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.2.1.tgz", + "integrity": "sha512-3dh+uDvpBXi7EUp/eO+N7eFM4xKaU4yuGBXc50KnZGzIrR/vlvkTFQsX13zsY8PB6sCFYAgROfPSRUj8331QSA==", "license": "MIT" }, "node_modules/@redux-saga/types": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.3.1.tgz", + "integrity": "sha512-YRCrJdhQLobGIQ8Cj1sta3nn6DrZDTSUnrIYhS2e5V590BmfVDleKoAquclAiKSBKWJwmuXTb+b4BL6rSHnahw==", "license": "MIT" }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.53", + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", "dev": true, "license": "MIT" }, "node_modules/@rollup/plugin-node-resolve": { "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", "license": "MIT", "dependencies": { "@rollup/pluginutils": "^5.0.1", @@ -2726,6 +2986,8 @@ }, "node_modules/@rollup/plugin-node-resolve/node_modules/resolve": { "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "license": "MIT", "dependencies": { "is-core-module": "^2.16.1", @@ -2744,6 +3006,8 @@ }, "node_modules/@rollup/plugin-terser": { "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", "license": "MIT", "dependencies": { "serialize-javascript": "^6.0.1", @@ -2764,6 +3028,8 @@ }, "node_modules/@rollup/pluginutils": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -2784,6 +3050,8 @@ }, "node_modules/@rollup/pluginutils/node_modules/estree-walker": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, "node_modules/@rollup/pluginutils/node_modules/picomatch": { @@ -2800,11 +3068,15 @@ }, "node_modules/@standard-schema/spec": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, "license": "MIT" }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", + "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", "license": "Apache-2.0", "dependencies": { "ejs": "^3.1.6", @@ -2815,6 +3087,8 @@ }, "node_modules/@surma/rollup-plugin-off-main-thread/node_modules/magic-string": { "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", "license": "MIT", "dependencies": { "sourcemap-codec": "^1.4.8" @@ -2822,6 +3096,8 @@ }, "node_modules/@testing-library/dom": { "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", "peer": true, @@ -2841,6 +3117,8 @@ }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", "dev": true, "license": "MIT", "dependencies": { @@ -2859,11 +3137,15 @@ }, "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", "dev": true, "license": "MIT" }, "node_modules/@testing-library/react": { "version": "12.1.5", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.1.5.tgz", + "integrity": "sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg==", "dev": true, "license": "MIT", "dependencies": { @@ -2881,6 +3163,8 @@ }, "node_modules/@testing-library/react-hooks": { "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-7.0.2.tgz", + "integrity": "sha512-dYxpz8u9m4q1TuzfcUApqi8iFfR6R0FaMbr2hjZJy1uC8z+bO/K4v8Gs9eogGKYQop7QsrBTFkv/BCF7MzD2Cg==", "dev": true, "license": "MIT", "dependencies": { @@ -2909,6 +3193,8 @@ }, "node_modules/@testing-library/react/node_modules/@testing-library/dom": { "version": "8.20.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", + "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==", "dev": true, "license": "MIT", "dependencies": { @@ -2927,6 +3213,8 @@ }, "node_modules/@testing-library/react/node_modules/aria-query": { "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2935,6 +3223,8 @@ }, "node_modules/@testing-library/user-event": { "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", "dev": true, "license": "MIT", "engines": { @@ -2947,11 +3237,15 @@ }, "node_modules/@types/aria-query": { "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -2964,6 +3258,8 @@ }, "node_modules/@types/babel__generator": { "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -2972,6 +3268,8 @@ }, "node_modules/@types/babel__template": { "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "devOptional": true, "license": "MIT", "dependencies": { @@ -2981,6 +3279,8 @@ }, "node_modules/@types/babel__traverse": { "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "devOptional": true, "license": "MIT", "dependencies": { @@ -2989,6 +3289,8 @@ }, "node_modules/@types/chai": { "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { @@ -2998,16 +3300,23 @@ }, "node_modules/@types/deep-eql": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", "license": "MIT", + "peer": true, "dependencies": { "hoist-non-react-statics": "^3.3.0" }, @@ -3017,11 +3326,15 @@ }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true, "license": "MIT" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dev": true, "license": "MIT", "dependencies": { @@ -3030,6 +3343,8 @@ }, "node_modules/@types/istanbul-reports": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3038,31 +3353,45 @@ }, "node_modules/@types/json-schema": { "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, "node_modules/@types/minimist": { "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.9", + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "license": "MIT" }, "node_modules/@types/prop-types": { "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", "license": "MIT" }, "node_modules/@types/react": { - "version": "17.0.90", + "version": "17.0.91", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.91.tgz", + "integrity": "sha512-xauZca6qMeCU3Moy0KxCM9jtf1vyk6qRYK39Ryf3afUqwgNUjRIGoDdS9BcGWgAMGSg1hvP4XcmlYrM66PtqeA==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "^0.16", @@ -3071,6 +3400,8 @@ }, "node_modules/@types/react-dom": { "version": "17.0.26", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.26.tgz", + "integrity": "sha512-Z+2VcYXJwOqQ79HreLU/1fyQ88eXSSFh6I3JdrEHQIfYSI0kCQpTGvOrbE6jFGGYXKsHuwY9tBa/w5Uo6KzrEg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -3079,6 +3410,8 @@ }, "node_modules/@types/react-redux": { "version": "7.1.34", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz", + "integrity": "sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==", "license": "MIT", "dependencies": { "@types/hoist-non-react-statics": "^3.3.0", @@ -3089,6 +3422,8 @@ }, "node_modules/@types/react-test-renderer": { "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz", + "integrity": "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3097,6 +3432,8 @@ }, "node_modules/@types/react-transition-group": { "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", "license": "MIT", "peerDependencies": { "@types/react": "*" @@ -3104,23 +3441,33 @@ }, "node_modules/@types/react/node_modules/csstype": { "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/@types/resolve": { "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "license": "MIT" }, "node_modules/@types/scheduler": { "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", "license": "MIT" }, "node_modules/@types/semver": { "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", "dev": true, "license": "MIT" }, "node_modules/@types/styled-jsx": { "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@types/styled-jsx/-/styled-jsx-2.2.9.tgz", + "integrity": "sha512-W/iTlIkGEyTBGTEvZCey8EgQlQ5l0DwMqi3iOXlLs2kyBwYTXHKEiU6IZ5EwoRwngL8/dGYuzezSup89ttVHLw==", "license": "MIT", "dependencies": { "@types/react": "*" @@ -3128,19 +3475,27 @@ }, "node_modules/@types/trusted-types": { "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, "node_modules/@types/uuid": { "version": "3.4.13", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.13.tgz", + "integrity": "sha512-pAeZeUbLE4Z9Vi9wsWV2bYPTweEHeJJy0G4pEjOA/FSvy1Ad5U5Km8iDV6TKre1mjBiVNfAdVHKruP8bAh4Q5A==", "license": "MIT" }, "node_modules/@types/whatwg-mimetype": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", "dev": true, "license": "MIT" }, "node_modules/@types/ws": { "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "dev": true, "license": "MIT", "dependencies": { @@ -3149,6 +3504,8 @@ }, "node_modules/@types/yargs": { "version": "15.0.20", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.20.tgz", + "integrity": "sha512-KIkX+/GgfFitlASYCGoSF+T4XRXhOubJLhkLVtSfsRTe9jWMmuM2g28zQ41BtPTG7TRBb2xHW+LCNVE9QR/vsg==", "dev": true, "license": "MIT", "dependencies": { @@ -3157,11 +3514,15 @@ }, "node_modules/@types/yargs-parser": { "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", "dev": true, "license": "MIT", "dependencies": { @@ -3196,8 +3557,11 @@ }, "node_modules/@typescript-eslint/parser": { "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -3223,6 +3587,8 @@ }, "node_modules/@typescript-eslint/scope-manager": { "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", "dev": true, "license": "MIT", "dependencies": { @@ -3239,6 +3605,8 @@ }, "node_modules/@typescript-eslint/type-utils": { "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", "dev": true, "license": "MIT", "dependencies": { @@ -3265,6 +3633,8 @@ }, "node_modules/@typescript-eslint/types": { "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", "dev": true, "license": "MIT", "engines": { @@ -3277,6 +3647,8 @@ }, "node_modules/@typescript-eslint/typescript-estree": { "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3304,6 +3676,8 @@ }, "node_modules/@typescript-eslint/utils": { "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3328,6 +3702,8 @@ }, "node_modules/@typescript-eslint/visitor-keys": { "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", "dev": true, "license": "MIT", "dependencies": { @@ -3344,18 +3720,22 @@ }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true, "license": "ISC" }, "node_modules/@vitejs/plugin-react": { - "version": "5.1.2", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.28.5", + "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.53", + "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, @@ -3363,31 +3743,33 @@ "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/@vitest/coverage-v8": { - "version": "4.0.17", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.2.tgz", + "integrity": "sha512-sPK//PHO+kAkScb8XITeB1bf7fsk85Km7+rt4eeuRR3VS1/crD47cmV5wicisJmjNdfeokTZwjMk4Mj2d58Mgg==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.0.17", - "ast-v8-to-istanbul": "^0.3.10", + "@vitest/utils": "4.1.2", + "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", - "magicast": "^0.5.1", + "magicast": "^0.5.2", "obug": "^2.1.1", - "std-env": "^3.10.0", - "tinyrainbow": "^3.0.3" + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.0.17", - "vitest": "4.0.17" + "@vitest/browser": "4.1.2", + "vitest": "4.1.2" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -3396,27 +3778,31 @@ } }, "node_modules/@vitest/expect": { - "version": "4.0.17", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", "dev": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.0.0", + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.17", - "@vitest/utils": "4.0.17", - "chai": "^6.2.1", - "tinyrainbow": "^3.0.3" + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "4.0.17", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.17", + "@vitest/spy": "4.1.2", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -3425,7 +3811,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -3437,22 +3823,26 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.17", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "4.0.17", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.17", + "@vitest/utils": "4.1.2", "pathe": "^2.0.3" }, "funding": { @@ -3460,11 +3850,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.17", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.17", + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -3473,7 +3866,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.17", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", "dev": true, "license": "MIT", "funding": { @@ -3481,20 +3876,26 @@ } }, "node_modules/@vitest/utils": { - "version": "4.0.17", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.17", - "tinyrainbow": "^3.0.3" + "@vitest/pretty-format": "4.1.2", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/acorn": { - "version": "8.15.0", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3504,6 +3905,8 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -3512,6 +3915,8 @@ }, "node_modules/agent-base": { "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", "engines": { @@ -3520,6 +3925,8 @@ }, "node_modules/ajv": { "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -3534,6 +3941,8 @@ }, "node_modules/ansi-align": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", "license": "ISC", "dependencies": { "string-width": "^4.1.0" @@ -3541,6 +3950,8 @@ }, "node_modules/ansi-escapes": { "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "license": "MIT", "dependencies": { "type-fest": "^0.21.3" @@ -3554,6 +3965,8 @@ }, "node_modules/ansi-escapes/node_modules/type-fest": { "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" @@ -3564,6 +3977,8 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" @@ -3571,6 +3986,8 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3584,6 +4001,8 @@ }, "node_modules/anymatch": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -3595,11 +4014,15 @@ }, "node_modules/argparse": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3608,6 +4031,8 @@ }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -3622,6 +4047,8 @@ }, "node_modules/array-includes": { "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3643,6 +4070,8 @@ }, "node_modules/array-union": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, "license": "MIT", "engines": { @@ -3651,6 +4080,8 @@ }, "node_modules/array.prototype.findlast": { "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3670,6 +4101,8 @@ }, "node_modules/array.prototype.flat": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, "license": "MIT", "dependencies": { @@ -3687,6 +4120,8 @@ }, "node_modules/array.prototype.flatmap": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, "license": "MIT", "dependencies": { @@ -3704,6 +4139,8 @@ }, "node_modules/array.prototype.tosorted": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, "license": "MIT", "dependencies": { @@ -3719,6 +4156,8 @@ }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", @@ -3738,6 +4177,8 @@ }, "node_modules/arrify": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3745,6 +4186,8 @@ }, "node_modules/assertion-error": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", "engines": { @@ -3753,30 +4196,40 @@ }, "node_modules/ast-types-flow": { "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", "dev": true, "license": "MIT" }, "node_modules/ast-v8-to-istanbul": { - "version": "0.3.10", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", - "js-tokens": "^9.0.1" + "js-tokens": "^10.0.0" } }, "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { - "version": "9.0.1", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", "dev": true, "license": "MIT" }, "node_modules/async": { "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, "node_modules/async-function": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -3784,13 +4237,17 @@ }, "node_modules/at-least-node": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "license": "ISC", "engines": { "node": ">= 4.0.0" } }, "node_modules/atomically": { - "version": "2.1.0", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.1.tgz", + "integrity": "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==", "license": "MIT", "dependencies": { "stubborn-fs": "^2.0.0", @@ -3799,6 +4256,8 @@ }, "node_modules/attr-accept": { "version": "2.2.5", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", "license": "MIT", "engines": { "node": ">=4" @@ -3806,6 +4265,8 @@ }, "node_modules/autosuggest-highlight": { "version": "3.3.4", + "resolved": "https://registry.npmjs.org/autosuggest-highlight/-/autosuggest-highlight-3.3.4.tgz", + "integrity": "sha512-j6RETBD2xYnrVcoV1S5R4t3WxOlWZKyDQjkwnggDPSjF5L4jV98ZltBpvPvbkM1HtoSe5o+bNrTHyjPbieGeYA==", "license": "MIT", "dependencies": { "remove-accents": "^0.4.2" @@ -3813,6 +4274,8 @@ }, "node_modules/available-typed-arrays": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" @@ -3825,7 +4288,9 @@ } }, "node_modules/axe-core": { - "version": "4.11.1", + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.2.tgz", + "integrity": "sha512-byD6KPdvo72y/wj2T/4zGEvvlis+PsZsn/yPS3pEO+sFpcrqRpX/TJCxvVaEsNeMrfQbCr7w163YqoD9IYwHXw==", "dev": true, "license": "MPL-2.0", "engines": { @@ -3834,6 +4299,8 @@ }, "node_modules/axobject-query": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3841,11 +4308,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.14", + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.7", - "@babel/helper-define-polyfill-provider": "^0.6.5", + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", "semver": "^6.3.1" }, "peerDependencies": { @@ -3854,27 +4323,33 @@ }, "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.13.0", + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5", - "core-js-compat": "^3.43.0" + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.5", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5" + "@babel/helper-define-polyfill-provider": "^0.6.8" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -3882,6 +4357,8 @@ }, "node_modules/babel-runtime": { "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", "license": "MIT", "dependencies": { "core-js": "^2.4.0", @@ -3890,10 +4367,14 @@ }, "node_modules/balanced-match": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "funding": [ { "type": "github", @@ -3911,14 +4392,21 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.15", + "version": "2.10.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.15.tgz", + "integrity": "sha512-1nfKCq9wuAZFTkA2ey/3OXXx7GzFjLdkTiFVNwlJ9WqdI706CZRIhEqjuwanjMIja+84jDLa9rcyZDPDiVkASQ==", "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/binary-extensions": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "license": "MIT", "engines": { "node": ">=8" @@ -3929,6 +4417,8 @@ }, "node_modules/bl": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -3938,10 +4428,14 @@ }, "node_modules/blueimp-md5": { "version": "2.19.0", + "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", + "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", "license": "MIT" }, "node_modules/boxen": { "version": "8.0.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", + "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", "license": "MIT", "dependencies": { "ansi-align": "^3.0.1", @@ -3962,6 +4456,8 @@ }, "node_modules/boxen/node_modules/ansi-regex": { "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { "node": ">=12" @@ -3972,6 +4468,8 @@ }, "node_modules/boxen/node_modules/camelcase": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", "license": "MIT", "engines": { "node": ">=16" @@ -3982,6 +4480,8 @@ }, "node_modules/boxen/node_modules/chalk": { "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -3992,10 +4492,14 @@ }, "node_modules/boxen/node_modules/emoji-regex": { "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "license": "MIT" }, "node_modules/boxen/node_modules/string-width": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", @@ -4010,10 +4514,12 @@ } }, "node_modules/boxen/node_modules/strip-ansi": { - "version": "7.1.2", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -4024,6 +4530,8 @@ }, "node_modules/boxen/node_modules/type-fest": { "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" @@ -4033,7 +4541,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -4041,6 +4551,8 @@ }, "node_modules/braces": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -4050,7 +4562,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "funding": [ { "type": "opencollective", @@ -4066,12 +4580,13 @@ } ], "license": "MIT", + "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -4082,6 +4597,8 @@ }, "node_modules/buffer": { "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "funding": [ { "type": "github", @@ -4104,10 +4621,14 @@ }, "node_modules/buffer-from": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, "node_modules/call-bind": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", @@ -4124,6 +4645,8 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4135,6 +4658,8 @@ }, "node_modules/call-bound": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -4149,10 +4674,14 @@ }, "node_modules/call-me-maybe": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", "license": "MIT" }, "node_modules/callsites": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "engines": { @@ -4161,6 +4690,8 @@ }, "node_modules/camelcase": { "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "license": "MIT", "engines": { "node": ">=6" @@ -4168,6 +4699,8 @@ }, "node_modules/camelcase-keys": { "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", "license": "MIT", "dependencies": { "camelcase": "^5.3.1", @@ -4182,7 +4715,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001765", + "version": "1.0.30001785", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001785.tgz", + "integrity": "sha512-blhOL/WNR+Km1RI/LCVAvA73xplXA7ZbjzI4YkMK9pa6T/P3F2GxjNpEkyw5repTw9IvkyrjyHpwjnhZ5FOvYQ==", "funding": [ { "type": "opencollective", @@ -4201,6 +4736,8 @@ }, "node_modules/chai": { "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", "engines": { @@ -4209,6 +4746,8 @@ }, "node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -4223,10 +4762,14 @@ }, "node_modules/chardet": { "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "license": "MIT" }, "node_modules/chokidar": { "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -4249,6 +4792,8 @@ }, "node_modules/chokidar/node_modules/glob-parent": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -4265,6 +4810,8 @@ }, "node_modules/cli-boxes": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", "license": "MIT", "engines": { "node": ">=10" @@ -4275,6 +4822,8 @@ }, "node_modules/cli-cursor": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "license": "MIT", "dependencies": { "restore-cursor": "^3.1.0" @@ -4285,6 +4834,8 @@ }, "node_modules/cli-spinners": { "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "license": "MIT", "engines": { "node": ">=6" @@ -4295,6 +4846,8 @@ }, "node_modules/cli-width": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", "license": "ISC", "engines": { "node": ">= 10" @@ -4302,6 +4855,8 @@ }, "node_modules/clone": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", "license": "MIT", "engines": { "node": ">=0.8" @@ -4309,6 +4864,8 @@ }, "node_modules/clsx": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", "engines": { "node": ">=6" @@ -4316,6 +4873,8 @@ }, "node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4326,14 +4885,20 @@ }, "node_modules/color-name": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/commander": { "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, "node_modules/common-tags": { "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", "license": "MIT", "engines": { "node": ">=4.0.0" @@ -4341,15 +4906,21 @@ }, "node_modules/compute-scroll-into-view": { "version": "1.0.20", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", + "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==", "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, "node_modules/config-chain": { "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", "license": "MIT", "dependencies": { "ini": "^1.3.4", @@ -4358,10 +4929,14 @@ }, "node_modules/config-chain/node_modules/ini": { "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, "node_modules/configstore": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-7.1.0.tgz", + "integrity": "sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg==", "license": "BSD-2-Clause", "dependencies": { "atomically": "^2.0.3", @@ -4378,7 +4953,10 @@ }, "node_modules/connected-react-router": { "version": "6.9.3", + "resolved": "https://registry.npmjs.org/connected-react-router/-/connected-react-router-6.9.3.tgz", + "integrity": "sha512-4ThxysOiv/R2Dc4Cke1eJwjKwH1Y51VDwlOrOfs1LjpdYOVvCNjNkZDayo7+sx42EeGJPQUNchWkjAIJdXGIOQ==", "license": "MIT", + "peer": true, "dependencies": { "lodash.isequalwith": "^4.4.0", "prop-types": "^15.7.2" @@ -4397,18 +4975,25 @@ }, "node_modules/convert-source-map": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, "node_modules/core-js": { "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", "hasInstallScript": true, "license": "MIT" }, "node_modules/core-js-compat": { - "version": "3.47.0", + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", "license": "MIT", "dependencies": { - "browserslist": "^4.28.0" + "browserslist": "^4.28.1" }, "funding": { "type": "opencollective", @@ -4416,7 +5001,9 @@ } }, "node_modules/core-js-pure": { - "version": "3.47.0", + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.49.0.tgz", + "integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4427,6 +5014,8 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -4439,6 +5028,8 @@ }, "node_modules/crypto-random-string": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "license": "MIT", "engines": { "node": ">=8" @@ -4446,10 +5037,14 @@ }, "node_modules/css-mediaquery": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", + "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==", "license": "BSD" }, "node_modules/css-vendor": { "version": "2.0.8", + "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", + "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.3", @@ -4458,11 +5053,15 @@ }, "node_modules/css.escape": { "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", "dev": true, "license": "MIT" }, "node_modules/cssstyle": { "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", "dev": true, "license": "MIT", "dependencies": { @@ -4475,15 +5074,21 @@ }, "node_modules/csstype": { "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", "license": "MIT" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/data-urls": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, "license": "MIT", "dependencies": { @@ -4496,6 +5101,8 @@ }, "node_modules/data-urls/node_modules/whatwg-mimetype": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "license": "MIT", "engines": { @@ -4504,6 +5111,8 @@ }, "node_modules/data-view-buffer": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -4519,6 +5128,8 @@ }, "node_modules/data-view-byte-length": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -4534,6 +5145,8 @@ }, "node_modules/data-view-byte-offset": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -4549,10 +5162,14 @@ }, "node_modules/date-fns": { "version": "1.30.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", + "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4568,6 +5185,8 @@ }, "node_modules/decamelize": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4575,6 +5194,8 @@ }, "node_modules/decamelize-keys": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", "license": "MIT", "dependencies": { "decamelize": "^1.1.0", @@ -4589,6 +5210,8 @@ }, "node_modules/decamelize-keys/node_modules/map-obj": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4596,11 +5219,15 @@ }, "node_modules/decimal.js": { "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "dev": true, "license": "MIT" }, "node_modules/decode-uri-component": { "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "license": "MIT", "engines": { "node": ">=0.10" @@ -4608,6 +5235,8 @@ }, "node_modules/deep-equal": { "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", "dev": true, "license": "MIT", "dependencies": { @@ -4639,11 +5268,15 @@ }, "node_modules/deep-equal/node_modules/isarray": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, "license": "MIT" }, "node_modules/deep-extend": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "license": "MIT", "engines": { "node": ">=4.0.0" @@ -4651,11 +5284,15 @@ }, "node_modules/deep-is": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, "node_modules/deepmerge": { "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4663,6 +5300,8 @@ }, "node_modules/defaults": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", "license": "MIT", "dependencies": { "clone": "^1.0.2" @@ -4673,6 +5312,8 @@ }, "node_modules/define-data-property": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -4688,6 +5329,8 @@ }, "node_modules/define-properties": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", @@ -4703,6 +5346,8 @@ }, "node_modules/dequal": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, "license": "MIT", "engines": { @@ -4711,6 +5356,8 @@ }, "node_modules/dir-glob": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, "license": "MIT", "dependencies": { @@ -4722,6 +5369,8 @@ }, "node_modules/dnd-core": { "version": "14.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-14.0.1.tgz", + "integrity": "sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==", "license": "MIT", "dependencies": { "@react-dnd/asap": "^4.0.0", @@ -4731,6 +5380,8 @@ }, "node_modules/doctrine": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4742,6 +5393,8 @@ }, "node_modules/dom-accessibility-api": { "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, "license": "MIT" }, @@ -4753,6 +5406,8 @@ }, "node_modules/dom-helpers": { "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.7", @@ -4761,20 +5416,23 @@ }, "node_modules/dom-helpers/node_modules/csstype": { "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/dompurify": { - "version": "3.3.2", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", + "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==", "license": "(MPL-2.0 OR Apache-2.0)", - "engines": { - "node": ">=20" - }, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "node_modules/dot-prop": { "version": "9.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", + "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", "license": "MIT", "dependencies": { "type-fest": "^4.18.2" @@ -4788,6 +5446,8 @@ }, "node_modules/dot-prop/node_modules/type-fest": { "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" @@ -4804,6 +5464,8 @@ }, "node_modules/downshift": { "version": "3.2.7", + "resolved": "https://registry.npmjs.org/downshift/-/downshift-3.2.7.tgz", + "integrity": "sha512-mbUO9ZFhMGtksIeVWRFFjNOPN237VsUqZSEYi0VS0Wj38XNLzpgOBTUcUjdjFeB8KVgmrcRa6GGFkTbACpG6FA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.1.2", @@ -4817,10 +5479,14 @@ }, "node_modules/downshift/node_modules/react-is": { "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, "node_modules/dunder-proto": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -4831,12 +5497,10 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "license": "MIT" - }, "node_modules/ejs": { "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "license": "Apache-2.0", "dependencies": { "jake": "^10.8.5" @@ -4849,15 +5513,22 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.267", + "version": "1.5.331", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", + "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==", "license": "ISC" }, "node_modules/emoji-regex": { "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, "license": "MIT" }, "node_modules/entities": { - "version": "4.5.0", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -4869,6 +5540,8 @@ }, "node_modules/error-ex": { "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -4876,6 +5549,8 @@ }, "node_modules/es-abstract": { "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.2", @@ -4942,6 +5617,8 @@ }, "node_modules/es-define-property": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -4949,6 +5626,8 @@ }, "node_modules/es-errors": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -4956,6 +5635,8 @@ }, "node_modules/es-get-iterator": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", "dev": true, "license": "MIT", "dependencies": { @@ -4975,11 +5656,15 @@ }, "node_modules/es-get-iterator/node_modules/isarray": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, "license": "MIT" }, "node_modules/es-iterator-helpers": { - "version": "1.2.2", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.1.tgz", + "integrity": "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4998,6 +5683,7 @@ "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0", "safe-array-concat": "^1.1.3" }, "engines": { @@ -5005,12 +5691,16 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -5021,6 +5711,8 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5034,6 +5726,8 @@ }, "node_modules/es-shim-unscopables": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, "license": "MIT", "dependencies": { @@ -5045,6 +5739,8 @@ }, "node_modules/es-to-primitive": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "license": "MIT", "dependencies": { "is-callable": "^1.2.7", @@ -5059,7 +5755,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.2", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5070,36 +5768,38 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" } }, "node_modules/escalade": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", "engines": { "node": ">=6" @@ -5107,6 +5807,8 @@ }, "node_modules/escape-goat": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", + "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", "license": "MIT", "engines": { "node": ">=12" @@ -5117,6 +5819,8 @@ }, "node_modules/escape-string-regexp": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { @@ -5128,8 +5832,12 @@ }, "node_modules/eslint": { "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -5182,6 +5890,8 @@ }, "node_modules/eslint-config-prettier": { "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", "bin": { @@ -5196,6 +5906,8 @@ }, "node_modules/eslint-plugin-jsx-a11y": { "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5224,6 +5936,8 @@ }, "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5231,7 +5945,9 @@ } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { - "version": "1.1.12", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -5240,7 +5956,9 @@ } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { - "version": "3.1.2", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -5252,6 +5970,8 @@ }, "node_modules/eslint-plugin-react": { "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, "license": "MIT", "dependencies": { @@ -5283,6 +6003,8 @@ }, "node_modules/eslint-plugin-react-hooks": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", "dev": true, "license": "MIT", "engines": { @@ -5294,6 +6016,8 @@ }, "node_modules/eslint-plugin-react-refresh": { "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -5301,7 +6025,9 @@ } }, "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.12", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -5311,6 +6037,8 @@ }, "node_modules/eslint-plugin-react/node_modules/doctrine": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5321,7 +6049,9 @@ } }, "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -5333,6 +6063,8 @@ }, "node_modules/eslint-plugin-react/node_modules/semver": { "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -5341,6 +6073,8 @@ }, "node_modules/eslint-scope": { "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5356,6 +6090,8 @@ }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5366,7 +6102,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -5375,7 +6113,9 @@ } }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -5387,6 +6127,8 @@ }, "node_modules/espree": { "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5403,6 +6145,8 @@ }, "node_modules/esprima": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -5414,6 +6158,8 @@ }, "node_modules/esquery": { "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5425,6 +6171,8 @@ }, "node_modules/esrecurse": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5436,6 +6184,8 @@ }, "node_modules/estraverse": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5444,6 +6194,8 @@ }, "node_modules/estree-walker": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { @@ -5452,6 +6204,8 @@ }, "node_modules/esutils": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -5459,14 +6213,20 @@ }, "node_modules/eventemitter3": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", "license": "MIT" }, "node_modules/exenv": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==", "license": "BSD-3-Clause" }, "node_modules/expect-type": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5475,6 +6235,8 @@ }, "node_modules/external-editor": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", "license": "MIT", "dependencies": { "chardet": "^0.7.0", @@ -5487,6 +6249,8 @@ }, "node_modules/external-editor/node_modules/iconv-lite": { "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -5497,10 +6261,14 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -5516,6 +6284,8 @@ }, "node_modules/fast-glob/node_modules/glob-parent": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "license": "ISC", "dependencies": { @@ -5527,15 +6297,21 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, "node_modules/fast-uri": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "funding": [ { "type": "github", @@ -5550,6 +6326,8 @@ }, "node_modules/fastq": { "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -5558,6 +6336,8 @@ }, "node_modules/figures": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "license": "MIT", "dependencies": { "escape-string-regexp": "^1.0.5" @@ -5571,6 +6351,8 @@ }, "node_modules/figures/node_modules/escape-string-regexp": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "license": "MIT", "engines": { "node": ">=0.8.0" @@ -5578,6 +6360,8 @@ }, "node_modules/file-entry-cache": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "license": "MIT", "dependencies": { @@ -5589,6 +6373,8 @@ }, "node_modules/file-selector": { "version": "0.1.19", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.19.tgz", + "integrity": "sha512-kCWw3+Aai8Uox+5tHCNgMFaUdgidxvMnLWO6fM5sZ0hA2wlHP5/DHGF0ECe84BiB95qdJbKNEJhWKVDvMN+JDQ==", "license": "MIT", "dependencies": { "tslib": "^2.0.1" @@ -5598,14 +6384,18 @@ } }, "node_modules/filelist": { - "version": "1.0.4", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", "license": "Apache-2.0", "dependencies": { "minimatch": "^5.0.1" } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -5616,6 +6406,8 @@ }, "node_modules/fill-range": { "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -5626,7 +6418,10 @@ }, "node_modules/final-form": { "version": "4.20.10", + "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.10.tgz", + "integrity": "sha512-TL48Pi1oNHeMOHrKv1bCJUrWZDcD3DIG6AGYVNOnyZPr7Bd/pStN0pL+lfzF5BNoj/FclaoiaLenk4XUIFVYng==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.10.0" }, @@ -5640,13 +6435,18 @@ }, "node_modules/final-form-arrays": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/final-form-arrays/-/final-form-arrays-3.1.0.tgz", + "integrity": "sha512-TWBvun+AopgBLw9zfTFHBllnKMVNEwCEyDawphPuBGGqNsuhGzhT7yewHys64KFFwzIs6KEteGLpKOwvTQEscQ==", "license": "MIT", + "peer": true, "peerDependencies": { "final-form": "^4.20.8" } }, "node_modules/find-up": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -5662,6 +6462,8 @@ }, "node_modules/flat-cache": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "license": "MIT", "dependencies": { @@ -5674,12 +6476,16 @@ } }, "node_modules/flatted": { - "version": "3.3.3", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, "node_modules/for-each": { "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "license": "MIT", "dependencies": { "is-callable": "^1.2.7" @@ -5693,6 +6499,8 @@ }, "node_modules/foreground-child": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -5707,6 +6515,8 @@ }, "node_modules/foreground-child/node_modules/signal-exit": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "license": "ISC", "engines": { "node": ">=14" @@ -5717,6 +6527,8 @@ }, "node_modules/fs-extra": { "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "license": "MIT", "dependencies": { "at-least-node": "^1.0.0", @@ -5730,11 +6542,16 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true, "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -5746,6 +6563,8 @@ }, "node_modules/function-bind": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5753,6 +6572,8 @@ }, "node_modules/function.prototype.name": { "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -5771,6 +6592,8 @@ }, "node_modules/functions-have-names": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5778,6 +6601,8 @@ }, "node_modules/generator-function": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -5785,13 +6610,17 @@ }, "node_modules/gensync": { "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/get-east-asian-width": { - "version": "1.4.0", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", "license": "MIT", "engines": { "node": ">=18" @@ -5802,6 +6631,8 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -5824,14 +6655,20 @@ }, "node_modules/get-node-dimensions": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-node-dimensions/-/get-node-dimensions-1.2.1.tgz", + "integrity": "sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ==", "license": "MIT" }, "node_modules/get-own-enumerable-property-symbols": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", "license": "ISC" }, "node_modules/get-proto": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -5843,6 +6680,8 @@ }, "node_modules/get-symbol-description": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -5858,6 +6697,9 @@ }, "node_modules/glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -5877,6 +6719,8 @@ }, "node_modules/glob-parent": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { @@ -5887,7 +6731,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -5896,7 +6742,9 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -5908,6 +6756,8 @@ }, "node_modules/global-directory": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", "license": "MIT", "dependencies": { "ini": "4.1.1" @@ -5921,6 +6771,8 @@ }, "node_modules/globals": { "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5935,6 +6787,8 @@ }, "node_modules/globalthis": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "license": "MIT", "dependencies": { "define-properties": "^1.2.1", @@ -5949,6 +6803,8 @@ }, "node_modules/globby": { "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, "license": "MIT", "dependencies": { @@ -5968,6 +6824,8 @@ }, "node_modules/gopd": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -5978,22 +6836,29 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true, "license": "MIT" }, "node_modules/happy-dom": { - "version": "20.3.3", + "version": "20.8.9", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.8.9.tgz", + "integrity": "sha512-Tz23LR9T9jOGVZm2x1EPdXqwA37G/owYMxRwU0E4miurAtFsPMQ1d2Jc2okUaSjZqAFz2oEn3FLXC5a0a+siyA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", - "entities": "^4.5.0", + "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" }, @@ -6003,6 +6868,8 @@ }, "node_modules/hard-rejection": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", "license": "MIT", "engines": { "node": ">=6" @@ -6010,6 +6877,8 @@ }, "node_modules/has-bigints": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -6020,6 +6889,8 @@ }, "node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "license": "MIT", "engines": { "node": ">=8" @@ -6027,6 +6898,8 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -6037,6 +6910,8 @@ }, "node_modules/has-proto": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.0" @@ -6050,6 +6925,8 @@ }, "node_modules/has-symbols": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -6060,6 +6937,8 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -6073,6 +6952,8 @@ }, "node_modules/hasown": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -6083,7 +6964,10 @@ }, "node_modules/history": { "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.1.2", "loose-envify": "^1.2.0", @@ -6095,6 +6979,8 @@ }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", "license": "BSD-3-Clause", "dependencies": { "react-is": "^16.7.0" @@ -6102,14 +6988,20 @@ }, "node_modules/hoist-non-react-statics/node_modules/react-is": { "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, "node_modules/hosted-git-info": { "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "license": "ISC" }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6121,11 +7013,15 @@ }, "node_modules/html-escaper": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, "license": "MIT" }, "node_modules/http-proxy-agent": { "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", "dependencies": { @@ -6138,6 +7034,8 @@ }, "node_modules/https-proxy-agent": { "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { @@ -6150,10 +7048,14 @@ }, "node_modules/hyphenate-style-name": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", "license": "BSD-3-Clause" }, "node_modules/iconv-lite": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -6165,10 +7067,14 @@ }, "node_modules/idb": { "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", "license": "ISC" }, "node_modules/ieee754": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "funding": [ { "type": "github", @@ -6187,6 +7093,8 @@ }, "node_modules/ignore": { "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -6195,11 +7103,15 @@ }, "node_modules/immutable": { "version": "4.3.8", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.8.tgz", + "integrity": "sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==", "license": "MIT", "optional": true }, "node_modules/import-fresh": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6215,6 +7127,8 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { @@ -6223,6 +7137,8 @@ }, "node_modules/indent-string": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "license": "MIT", "engines": { "node": ">=8" @@ -6230,6 +7146,8 @@ }, "node_modules/inflection": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-3.0.2.tgz", + "integrity": "sha512-+Bg3+kg+J6JUWn8J6bzFmOWkTQ6L/NHfDRSYU+EVvuKHDxUDHAXgqixHfVlzuBQaPOTac8hn43aPhMNk6rMe3g==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -6237,6 +7155,9 @@ }, "node_modules/inflight": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, "license": "ISC", "dependencies": { @@ -6246,10 +7167,14 @@ }, "node_modules/inherits": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, "node_modules/ini": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -6257,6 +7182,8 @@ }, "node_modules/inquirer": { "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", "license": "MIT", "dependencies": { "ansi-escapes": "^4.2.1", @@ -6279,6 +7206,8 @@ }, "node_modules/internal-slot": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6291,6 +7220,8 @@ }, "node_modules/is-arguments": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", "dev": true, "license": "MIT", "dependencies": { @@ -6306,6 +7237,8 @@ }, "node_modules/is-array-buffer": { "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -6321,10 +7254,14 @@ }, "node_modules/is-arrayish": { "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, "node_modules/is-async-function": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "license": "MIT", "dependencies": { "async-function": "^1.0.0", @@ -6342,6 +7279,8 @@ }, "node_modules/is-bigint": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "license": "MIT", "dependencies": { "has-bigints": "^1.0.2" @@ -6355,6 +7294,8 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -6365,6 +7306,8 @@ }, "node_modules/is-boolean-object": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -6379,6 +7322,8 @@ }, "node_modules/is-callable": { "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -6389,6 +7334,8 @@ }, "node_modules/is-core-module": { "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -6402,6 +7349,8 @@ }, "node_modules/is-data-view": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6417,6 +7366,8 @@ }, "node_modules/is-date-object": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6431,6 +7382,8 @@ }, "node_modules/is-extglob": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6438,6 +7391,8 @@ }, "node_modules/is-finalizationregistry": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -6451,6 +7406,8 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", "engines": { "node": ">=8" @@ -6458,6 +7415,8 @@ }, "node_modules/is-generator-function": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "license": "MIT", "dependencies": { "call-bound": "^1.0.4", @@ -6475,6 +7434,8 @@ }, "node_modules/is-glob": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -6485,10 +7446,14 @@ }, "node_modules/is-in-browser": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", + "integrity": "sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==", "license": "MIT" }, "node_modules/is-in-ci": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz", + "integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==", "license": "MIT", "bin": { "is-in-ci": "cli.js" @@ -6502,6 +7467,8 @@ }, "node_modules/is-installed-globally": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", + "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", "license": "MIT", "dependencies": { "global-directory": "^4.0.1", @@ -6516,6 +7483,8 @@ }, "node_modules/is-installed-globally/node_modules/is-path-inside": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", "license": "MIT", "engines": { "node": ">=12" @@ -6526,6 +7495,8 @@ }, "node_modules/is-interactive": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", "license": "MIT", "engines": { "node": ">=8" @@ -6533,6 +7504,8 @@ }, "node_modules/is-map": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -6549,10 +7522,14 @@ }, "node_modules/is-module": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", "license": "MIT" }, "node_modules/is-negative-zero": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -6563,6 +7540,8 @@ }, "node_modules/is-npm": { "version": "6.1.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.1.0.tgz", + "integrity": "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==", "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" @@ -6573,6 +7552,8 @@ }, "node_modules/is-number": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "license": "MIT", "engines": { "node": ">=0.12.0" @@ -6580,6 +7561,8 @@ }, "node_modules/is-number-object": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -6594,6 +7577,8 @@ }, "node_modules/is-obj": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6601,6 +7586,8 @@ }, "node_modules/is-path-inside": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, "license": "MIT", "engines": { @@ -6609,6 +7596,8 @@ }, "node_modules/is-plain-obj": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6616,11 +7605,15 @@ }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true, "license": "MIT" }, "node_modules/is-regex": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6637,6 +7630,8 @@ }, "node_modules/is-regexp": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6644,6 +7639,8 @@ }, "node_modules/is-set": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -6654,6 +7651,8 @@ }, "node_modules/is-shared-array-buffer": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -6667,6 +7666,8 @@ }, "node_modules/is-stream": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "license": "MIT", "engines": { "node": ">=8" @@ -6677,6 +7678,8 @@ }, "node_modules/is-string": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -6691,6 +7694,8 @@ }, "node_modules/is-symbol": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6706,6 +7711,8 @@ }, "node_modules/is-typed-array": { "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" @@ -6719,6 +7726,8 @@ }, "node_modules/is-unicode-supported": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "license": "MIT", "engines": { "node": ">=10" @@ -6729,6 +7738,8 @@ }, "node_modules/is-weakmap": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -6739,6 +7750,8 @@ }, "node_modules/is-weakref": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -6752,6 +7765,8 @@ }, "node_modules/is-weakset": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -6766,14 +7781,20 @@ }, "node_modules/isarray": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -6782,6 +7803,8 @@ }, "node_modules/istanbul-lib-report": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6795,6 +7818,8 @@ }, "node_modules/istanbul-reports": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6807,6 +7832,8 @@ }, "node_modules/iterator.prototype": { "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, "license": "MIT", "dependencies": { @@ -6822,10 +7849,12 @@ } }, "node_modules/jackspeak": { - "version": "4.1.1", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/cliui": "^8.0.2" + "@isaacs/cliui": "^9.0.0" }, "engines": { "node": "20 || >=22" @@ -6836,6 +7865,8 @@ }, "node_modules/jake": { "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", "license": "Apache-2.0", "dependencies": { "async": "^3.2.6", @@ -6851,10 +7882,14 @@ }, "node_modules/js-tokens": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -6866,6 +7901,8 @@ }, "node_modules/jsdom": { "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", "dependencies": { @@ -6904,6 +7941,8 @@ }, "node_modules/jsdom/node_modules/whatwg-mimetype": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "license": "MIT", "engines": { @@ -6912,6 +7951,8 @@ }, "node_modules/jsesc": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -6922,19 +7963,22 @@ }, "node_modules/json-buffer": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "license": "MIT" }, - "node_modules/json-schema": { - "version": "0.4.0", - "license": "(AFL-2.1 OR BSD-3-Clause)" - }, "node_modules/json-schema-ref-parser": { "version": "7.1.3", + "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-7.1.3.tgz", + "integrity": "sha512-/Lmyl0PW27dOmCO03PI339+1gs4Z2PlqIyUgzIOtoRp08zkkMCB30TRbdppbPO7WWzZX0uT98HqkDiZSujkmbA==", + "deprecated": "Please switch to @apidevtools/json-schema-ref-parser", "license": "MIT", "dependencies": { "call-me-maybe": "^1.0.1", @@ -6944,6 +7988,8 @@ }, "node_modules/json-schema-ref-parser/node_modules/argparse": { "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" @@ -6951,6 +7997,8 @@ }, "node_modules/json-schema-ref-parser/node_modules/js-yaml": { "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -6962,15 +8010,21 @@ }, "node_modules/json-schema-traverse": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -6981,6 +8035,8 @@ }, "node_modules/jsonexport": { "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsonexport/-/jsonexport-2.5.2.tgz", + "integrity": "sha512-4joNLCxxUAmS22GN3GA5os/MYFnq8oqXOKvoCymmcT0MPz/QPZ5eA+Fh5sIPxUji45RKq8DdQ1yoKq91p4E9VA==", "license": "Apache-2.0", "bin": { "jsonexport": "bin/jsonexport.js" @@ -6988,6 +8044,8 @@ }, "node_modules/jsonfile": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -6998,6 +8056,8 @@ }, "node_modules/jsonpointer": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7005,6 +8065,8 @@ }, "node_modules/jss": { "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss/-/jss-10.10.0.tgz", + "integrity": "sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", @@ -7019,6 +8081,8 @@ }, "node_modules/jss-plugin-camel-case": { "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.10.0.tgz", + "integrity": "sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", @@ -7028,6 +8092,8 @@ }, "node_modules/jss-plugin-default-unit": { "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.10.0.tgz", + "integrity": "sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", @@ -7036,6 +8102,8 @@ }, "node_modules/jss-plugin-global": { "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.10.0.tgz", + "integrity": "sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", @@ -7044,6 +8112,8 @@ }, "node_modules/jss-plugin-nested": { "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.10.0.tgz", + "integrity": "sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", @@ -7053,6 +8123,8 @@ }, "node_modules/jss-plugin-props-sort": { "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.10.0.tgz", + "integrity": "sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", @@ -7061,6 +8133,8 @@ }, "node_modules/jss-plugin-rule-value-function": { "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.10.0.tgz", + "integrity": "sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", @@ -7070,6 +8144,8 @@ }, "node_modules/jss-plugin-vendor-prefixer": { "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.10.0.tgz", + "integrity": "sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", @@ -7079,10 +8155,14 @@ }, "node_modules/jss/node_modules/csstype": { "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/jsx-ast-utils": { "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7097,6 +8177,8 @@ }, "node_modules/jwt-decode": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", "license": "MIT", "engines": { "node": ">=18" @@ -7104,6 +8186,8 @@ }, "node_modules/keyv": { "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { @@ -7112,13 +8196,17 @@ }, "node_modules/kind-of": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/ky": { - "version": "1.14.2", + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/ky/-/ky-1.14.3.tgz", + "integrity": "sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==", "license": "MIT", "engines": { "node": ">=18" @@ -7129,11 +8217,15 @@ }, "node_modules/language-subtag-registry": { "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", "dev": true, "license": "CC0-1.0" }, "node_modules/language-tags": { "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", "dev": true, "license": "MIT", "dependencies": { @@ -7145,6 +8237,8 @@ }, "node_modules/latest-version": { "version": "9.0.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-9.0.0.tgz", + "integrity": "sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==", "license": "MIT", "dependencies": { "package-json": "^10.0.0" @@ -7158,6 +8252,8 @@ }, "node_modules/leven": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "license": "MIT", "engines": { "node": ">=6" @@ -7165,6 +8261,8 @@ }, "node_modules/levn": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7177,10 +8275,14 @@ }, "node_modules/lines-and-columns": { "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, "node_modules/locate-path": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -7194,32 +8296,46 @@ } }, "node_modules/lodash": { - "version": "4.17.23", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash.debounce": { "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "license": "MIT" }, "node_modules/lodash.isequalwith": { "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.isequalwith/-/lodash.isequalwith-4.4.0.tgz", + "integrity": "sha512-dcZON0IalGBpRmJBmMkaoV7d3I80R2O+FrzsZyHdNSFrANq/cgDqKQNmAHE8UEj4+QYWwwhkQOVdLHiAopzlsQ==", "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, "license": "MIT" }, "node_modules/lodash.sortby": { "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", "license": "MIT" }, "node_modules/lodash.throttle": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", "license": "MIT" }, "node_modules/log-symbols": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "license": "MIT", "dependencies": { "chalk": "^4.1.0", @@ -7234,6 +8350,8 @@ }, "node_modules/loose-envify": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -7244,6 +8362,8 @@ }, "node_modules/lru-cache": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -7251,6 +8371,8 @@ }, "node_modules/lz-string": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", "bin": { @@ -7259,6 +8381,8 @@ }, "node_modules/magic-string": { "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7266,17 +8390,21 @@ } }, "node_modules/magicast": { - "version": "0.5.1", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "node_modules/make-dir": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "license": "MIT", "dependencies": { @@ -7291,6 +8419,8 @@ }, "node_modules/map-obj": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", "license": "MIT", "engines": { "node": ">=8" @@ -7301,6 +8431,8 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -7308,6 +8440,8 @@ }, "node_modules/meow": { "version": "7.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-7.1.1.tgz", + "integrity": "sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA==", "license": "MIT", "dependencies": { "@types/minimist": "^1.2.0", @@ -7331,6 +8465,8 @@ }, "node_modules/meow/node_modules/type-fest": { "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" @@ -7341,6 +8477,8 @@ }, "node_modules/merge2": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", "engines": { @@ -7349,6 +8487,8 @@ }, "node_modules/micromatch": { "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { @@ -7361,6 +8501,8 @@ }, "node_modules/mimic-fn": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "license": "MIT", "engines": { "node": ">=6" @@ -7368,6 +8510,8 @@ }, "node_modules/min-indent": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "license": "MIT", "engines": { "node": ">=4" @@ -7375,6 +8519,8 @@ }, "node_modules/minimatch": { "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, "license": "ISC", "dependencies": { @@ -7389,6 +8535,8 @@ }, "node_modules/minimist": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7396,6 +8544,8 @@ }, "node_modules/minimist-options": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", "license": "MIT", "dependencies": { "arrify": "^1.0.1", @@ -7407,29 +8557,40 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "license": "ISC", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } }, "node_modules/moment": { "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "license": "MIT", + "peer": true, "engines": { "node": "*" } }, "node_modules/ms": { "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/mute-stream": { "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "license": "ISC" }, "node_modules/nanoid": { "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -7447,6 +8608,8 @@ }, "node_modules/natural-compare": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, @@ -7471,8 +8634,39 @@ "react-dom": ">=16.9.0" } }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/node-polyglot": { "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-polyglot/-/node-polyglot-2.6.0.tgz", + "integrity": "sha512-ZZFkaYzIfGfBvSM6QhA9dM8EEaUJOVewzGSRcXWbJELXDj0lajAtKaENCYxvF5yE+TgHg6NQb0CmgYMsMdcNJQ==", "license": "BSD-2-Clause", "dependencies": { "hasown": "^2.0.2", @@ -7484,11 +8678,15 @@ } }, "node_modules/node-releases": { - "version": "2.0.27", + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", "license": "MIT" }, "node_modules/normalize-package-data": { "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "license": "BSD-2-Clause", "dependencies": { "hosted-git-info": "^2.1.4", @@ -7499,6 +8697,8 @@ }, "node_modules/normalize-package-data/node_modules/resolve": { "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "license": "MIT", "dependencies": { "is-core-module": "^2.16.1", @@ -7517,6 +8717,8 @@ }, "node_modules/normalize-package-data/node_modules/semver": { "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "license": "ISC", "bin": { "semver": "bin/semver" @@ -7524,6 +8726,8 @@ }, "node_modules/normalize-path": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7531,11 +8735,15 @@ }, "node_modules/nwsapi": { "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", "dev": true, "license": "MIT" }, "node_modules/object-assign": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7543,6 +8751,8 @@ }, "node_modules/object-hash": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", "license": "MIT", "engines": { "node": ">= 6" @@ -7550,6 +8760,8 @@ }, "node_modules/object-inspect": { "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -7560,6 +8772,8 @@ }, "node_modules/object-is": { "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7575,6 +8789,8 @@ }, "node_modules/object-keys": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -7582,6 +8798,8 @@ }, "node_modules/object.assign": { "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -7600,6 +8818,8 @@ }, "node_modules/object.entries": { "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -7613,6 +8833,8 @@ }, "node_modules/object.fromentries": { "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7630,6 +8852,8 @@ }, "node_modules/object.values": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, "license": "MIT", "dependencies": { @@ -7647,6 +8871,8 @@ }, "node_modules/obug": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -7656,6 +8882,8 @@ }, "node_modules/once": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", "dependencies": { @@ -7664,6 +8892,8 @@ }, "node_modules/onetime": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -7677,10 +8907,14 @@ }, "node_modules/ono": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ono/-/ono-6.0.1.tgz", + "integrity": "sha512-5rdYW/106kHqLeG22GE2MHKq+FlsxMERZev9DCzQX1zwkxnFwBivSn5i17a5O/rDmOJOdf4Wyt80UZljzx9+DA==", "license": "MIT" }, "node_modules/optionator": { "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { @@ -7697,6 +8931,8 @@ }, "node_modules/ora": { "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", "license": "MIT", "dependencies": { "bl": "^4.1.0", @@ -7718,6 +8954,8 @@ }, "node_modules/os-tmpdir": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7725,6 +8963,8 @@ }, "node_modules/own-keys": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.6", @@ -7740,6 +8980,8 @@ }, "node_modules/p-limit": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7754,6 +8996,8 @@ }, "node_modules/p-locate": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -7768,6 +9012,8 @@ }, "node_modules/p-try": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "license": "MIT", "engines": { "node": ">=6" @@ -7775,6 +9021,8 @@ }, "node_modules/package-json": { "version": "10.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-10.0.1.tgz", + "integrity": "sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==", "license": "MIT", "dependencies": { "ky": "^1.2.0", @@ -7791,10 +9039,14 @@ }, "node_modules/package-json-from-dist": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { @@ -7806,6 +9058,8 @@ }, "node_modules/parse-json": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -7822,6 +9076,8 @@ }, "node_modules/parse5": { "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, "license": "MIT", "dependencies": { @@ -7833,6 +9089,8 @@ }, "node_modules/parse5/node_modules/entities": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -7844,6 +9102,8 @@ }, "node_modules/path-exists": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "license": "MIT", "engines": { "node": ">=8" @@ -7851,6 +9111,8 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "license": "MIT", "engines": { @@ -7859,6 +9121,8 @@ }, "node_modules/path-key": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", "engines": { "node": ">=8" @@ -7866,24 +9130,30 @@ }, "node_modules/path-parse": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, "node_modules/path-scurry": { - "version": "2.0.1", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.4", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -7891,6 +9161,8 @@ }, "node_modules/path-to-regexp": { "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", "license": "MIT", "dependencies": { "isarray": "0.0.1" @@ -7898,6 +9170,8 @@ }, "node_modules/path-type": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, "license": "MIT", "engines": { @@ -7906,11 +9180,15 @@ }, "node_modules/pathe": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { @@ -7927,17 +9205,23 @@ }, "node_modules/popper.js": { "version": "1.16.1-lts", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", + "integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==", "license": "MIT" }, "node_modules/possible-typed-array-names": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/postcss": { - "version": "8.5.6", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "dev": true, "funding": [ { @@ -7965,6 +9249,8 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { @@ -7972,7 +9258,9 @@ } }, "node_modules/prettier": { - "version": "3.8.0", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", "bin": { @@ -7987,6 +9275,8 @@ }, "node_modules/pretty-bytes": { "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", "dev": true, "license": "MIT", "engines": { @@ -7998,6 +9288,8 @@ }, "node_modules/pretty-format": { "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8011,6 +9303,8 @@ }, "node_modules/pretty-format/node_modules/ansi-styles": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", "engines": { @@ -8022,7 +9316,10 @@ }, "node_modules/prop-types": { "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -8031,14 +9328,20 @@ }, "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, "node_modules/proto-list": { "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", "license": "ISC" }, "node_modules/punycode": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "license": "MIT", "engines": { "node": ">=6" @@ -8046,6 +9349,8 @@ }, "node_modules/pupa": { "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.3.0.tgz", + "integrity": "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==", "license": "MIT", "dependencies": { "escape-goat": "^4.0.0" @@ -8059,6 +9364,8 @@ }, "node_modules/query-string": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", "license": "MIT", "dependencies": { "decode-uri-component": "^0.2.0", @@ -8071,6 +9378,8 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { @@ -8090,6 +9399,8 @@ }, "node_modules/quick-lru": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", "license": "MIT", "engines": { "node": ">=8" @@ -8097,7 +9408,10 @@ }, "node_modules/ra-core": { "version": "3.19.12", + "resolved": "https://registry.npmjs.org/ra-core/-/ra-core-3.19.12.tgz", + "integrity": "sha512-E0cM6OjEUtccaR+dR5mL1MLiVVYML0Yf7aPhpLEq4iue73X3+CKcLztInoBhWgeevPbFQwgAtsXhlpedeyrNNg==", "license": "MIT", + "peer": true, "dependencies": { "classnames": "~2.3.1", "date-fns": "^1.29.0", @@ -8124,17 +9438,29 @@ }, "node_modules/ra-core/node_modules/classnames": { "version": "2.3.3", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.3.tgz", + "integrity": "sha512-1inzZmicIFcmUya7PGtUQeXtcF7zZpPnxtQoYOrz0uiOBGlLFa4ik4361seYL2JCcRDIyfdFHiwQolESFlw+Og==", "license": "MIT" }, "node_modules/ra-core/node_modules/inflection": { "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", "engines": [ "node >= 0.4.0" ], "license": "MIT" }, + "node_modules/ra-core/node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, "node_modules/ra-data-json-server": { "version": "3.19.12", + "resolved": "https://registry.npmjs.org/ra-data-json-server/-/ra-data-json-server-3.19.12.tgz", + "integrity": "sha512-SEa0ueZd9LUG6iuPnHd+MHWf7BTgLKjx3Eky16VvTsqf6ueHkMU8AZiH1pHzrdxV6ku5VL34MCYWVSIbm2iDnw==", "license": "MIT", "dependencies": { "query-string": "^5.1.1", @@ -8143,6 +9469,8 @@ }, "node_modules/ra-i18n-polyglot": { "version": "3.19.12", + "resolved": "https://registry.npmjs.org/ra-i18n-polyglot/-/ra-i18n-polyglot-3.19.12.tgz", + "integrity": "sha512-7VkNybY+RYVL5aDf8MdefYpRMkaELOjSXx7rrRY7PzVwmQzVe5ESoKBcH4Cob2M8a52pAlXY32dwmA3dZ91l/Q==", "license": "MIT", "dependencies": { "node-polyglot": "^2.2.2", @@ -8151,6 +9479,8 @@ }, "node_modules/ra-language-english": { "version": "3.19.12", + "resolved": "https://registry.npmjs.org/ra-language-english/-/ra-language-english-3.19.12.tgz", + "integrity": "sha512-aYY0ma74eXLuflPT9iXEQtVEDZxebw1NiQZ5pPGiBCpsq+hoiDWuzerLU13OdBHbySD5FHLuk89SkyAdfMtUaQ==", "license": "MIT", "dependencies": { "ra-core": "^3.19.12" @@ -8158,6 +9488,8 @@ }, "node_modules/ra-test": { "version": "3.19.12", + "resolved": "https://registry.npmjs.org/ra-test/-/ra-test-3.19.12.tgz", + "integrity": "sha512-SX6oi+VPADIeQeQlGWUVj2kgEYgLbizpzYMq+oacCmnAqvHezwnQ2MXrLDRK6C56YIl+t8DyY/ipYBiRPZnHbA==", "dev": true, "license": "MIT", "dependencies": { @@ -8177,6 +9509,8 @@ }, "node_modules/ra-test/node_modules/@testing-library/dom": { "version": "7.31.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.31.2.tgz", + "integrity": "sha512-3UqjCpey6HiTZT92vODYLPxTBWlM8ZOOjr3LX5F37/VRipW2M1kX6I/Cm4VXzteZqfGfagg8yXywpcOgQBlNsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8195,6 +9529,8 @@ }, "node_modules/ra-test/node_modules/@testing-library/react": { "version": "11.2.7", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-11.2.7.tgz", + "integrity": "sha512-tzRNp7pzd5QmbtXNG/mhdcl7Awfu/Iz1RaVHY75zTdOkmHCuzMhRL83gWHSgOAcjS3CCbyfwUHMZgRJb4kAfpA==", "dev": true, "license": "MIT", "dependencies": { @@ -8211,11 +9547,15 @@ }, "node_modules/ra-test/node_modules/@types/aria-query": { "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==", "dev": true, "license": "MIT" }, "node_modules/ra-test/node_modules/aria-query": { "version": "4.2.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -8228,11 +9568,22 @@ }, "node_modules/ra-test/node_modules/classnames": { "version": "2.3.3", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.3.tgz", + "integrity": "sha512-1inzZmicIFcmUya7PGtUQeXtcF7zZpPnxtQoYOrz0uiOBGlLFa4ik4361seYL2JCcRDIyfdFHiwQolESFlw+Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/ra-test/node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, "license": "MIT" }, "node_modules/ra-test/node_modules/pretty-format": { "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", "dev": true, "license": "MIT", "dependencies": { @@ -8247,6 +9598,8 @@ }, "node_modules/ra-ui-materialui": { "version": "3.19.12", + "resolved": "https://registry.npmjs.org/ra-ui-materialui/-/ra-ui-materialui-3.19.12.tgz", + "integrity": "sha512-8Zz88r5yprmUxOw9/F0A/kjjVmFMb2n+sjpel8fuOWtS6y++JWonDsvTwo4yIuSF9mC0fht3f/hd2KEHQdmj6Q==", "license": "MIT", "dependencies": { "autosuggest-highlight": "^3.1.1", @@ -8282,21 +9635,35 @@ }, "node_modules/ra-ui-materialui/node_modules/classnames": { "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==", "license": "MIT" }, "node_modules/ra-ui-materialui/node_modules/dompurify": { "version": "2.5.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.9.tgz", + "integrity": "sha512-i6mvVmWN4xo9LrhCOZrDgSs9noW6nOahbrmzjRbPF36YPyj5Ue5lgok0MHDWkG7xzpWFO2OYttXdzM7rJxHvNA==", "license": "(MPL-2.0 OR Apache-2.0)" }, "node_modules/ra-ui-materialui/node_modules/inflection": { "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", "engines": [ "node >= 0.4.0" ], "license": "MIT" }, + "node_modules/ra-ui-materialui/node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, "node_modules/randombytes": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" @@ -8304,6 +9671,8 @@ }, "node_modules/rc": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", @@ -8439,10 +9808,14 @@ }, "node_modules/rc/node_modules/ini": { "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8450,7 +9823,10 @@ }, "node_modules/react": { "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -8461,6 +9837,8 @@ }, "node_modules/react-admin": { "version": "3.19.12", + "resolved": "https://registry.npmjs.org/react-admin/-/react-admin-3.19.12.tgz", + "integrity": "sha512-LanWS3Yjie7n5GZI8v7oP73DSvQyCeZD0dpkC65IC0+UOhkInxa1zedJc8CyD3+ZwlgVC+CGqi6jQ1fo73Cdqw==", "license": "MIT", "dependencies": { "@material-ui/core": "^4.12.1", @@ -8488,6 +9866,8 @@ }, "node_modules/react-dnd": { "version": "14.0.5", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-14.0.5.tgz", + "integrity": "sha512-9i1jSgbyVw0ELlEVt/NkCUkxy1hmhJOkePoCH713u75vzHGyXhPDm28oLfc2NMSBjZRM1Y+wRjHXJT3sPrTy+A==", "license": "MIT", "dependencies": { "@react-dnd/invariant": "^2.0.0", @@ -8516,6 +9896,8 @@ }, "node_modules/react-dnd-html5-backend": { "version": "14.1.0", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz", + "integrity": "sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw==", "license": "MIT", "dependencies": { "dnd-core": "14.0.1" @@ -8523,7 +9905,10 @@ }, "node_modules/react-dom": { "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -8535,6 +9920,8 @@ }, "node_modules/react-drag-listview": { "version": "0.1.9", + "resolved": "https://registry.npmjs.org/react-drag-listview/-/react-drag-listview-0.1.9.tgz", + "integrity": "sha512-/OsYevKtCUlw4FhJIfZPH7INHEmyl89sSC5COzonHW5Z2c8rHg4DNYFnUxOyqH+65o7sHweL13oaf6wr7dFvPA==", "license": "MIT", "dependencies": { "babel-runtime": "^6.26.0", @@ -8557,6 +9944,8 @@ }, "node_modules/react-dropzone": { "version": "10.2.2", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-10.2.2.tgz", + "integrity": "sha512-U5EKckXVt6IrEyhMMsgmHQiWTGLudhajPPG77KFSvgsMqNEHSyGpqWvOMc5+DhEah/vH4E1n+J5weBNLd5VtyA==", "license": "MIT", "dependencies": { "attr-accept": "^2.0.0", @@ -8572,6 +9961,8 @@ }, "node_modules/react-error-boundary": { "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", "dev": true, "license": "MIT", "dependencies": { @@ -8587,7 +9978,10 @@ }, "node_modules/react-final-form": { "version": "6.5.9", + "resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.9.tgz", + "integrity": "sha512-x3XYvozolECp3nIjly+4QqxdjSSWfcnpGEL5K8OBT6xmGrq5kBqbA6+/tOqoom9NwqIPPbxPNsOViFlbKgowbA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.15.4" }, @@ -8602,7 +9996,10 @@ }, "node_modules/react-final-form-arrays": { "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-final-form-arrays/-/react-final-form-arrays-3.1.4.tgz", + "integrity": "sha512-siVFAolUAe29rMR6u8VwepoysUcUdh6MLV2OWnCtKpsPRUdT9VUgECjAPaVMAH2GROZNiVB9On1H9MMrm9gdpg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.19.4" }, @@ -8615,6 +10012,8 @@ }, "node_modules/react-ga": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/react-ga/-/react-ga-3.3.1.tgz", + "integrity": "sha512-4Vc0W5EvXAXUN/wWyxvsAKDLLgtJ3oLmhYYssx+YzphJpejtOst6cbIHCIyF50Fdxuf5DDKqRYny24yJ2y7GFQ==", "license": "Apache-2.0", "peerDependencies": { "prop-types": "^15.6.0", @@ -8623,6 +10022,8 @@ }, "node_modules/react-hotkeys": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-hotkeys/-/react-hotkeys-2.0.0.tgz", + "integrity": "sha512-3n3OU8vLX/pfcJrR3xJ1zlww6KS1kEJt0Whxc4FiGV+MJrQ1mYSYI3qS/11d2MJDFm8IhOXMTFQirfu6AVOF6Q==", "license": "ISC", "dependencies": { "prop-types": "^15.6.1" @@ -8632,7 +10033,9 @@ } }, "node_modules/react-icons": { - "version": "5.5.0", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz", + "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==", "license": "MIT", "peerDependencies": { "react": "*" @@ -8640,6 +10043,9 @@ }, "node_modules/react-image-lightbox": { "version": "5.1.4", + "resolved": "https://registry.npmjs.org/react-image-lightbox/-/react-image-lightbox-5.1.4.tgz", + "integrity": "sha512-kTiAODz091bgT7SlWNHab0LSMZAPJtlNWDGKv7pLlLY1krmf7FuG1zxE0wyPpeA8gPdwfr3cu6sPwZRqWsc3Eg==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "dependencies": { "prop-types": "^15.7.2", @@ -8652,14 +10058,20 @@ }, "node_modules/react-is": { "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "license": "MIT" }, "node_modules/react-lifecycles-compat": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", "license": "MIT" }, "node_modules/react-measure": { "version": "2.5.2", + "resolved": "https://registry.npmjs.org/react-measure/-/react-measure-2.5.2.tgz", + "integrity": "sha512-M+rpbTLWJ3FD6FXvYV6YEGvQ5tMayQ3fGrZhRPHrE9bVlBYfDCLuDcgNttYfk8IqfOI03jz6cbpqMRTUclQnaA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.2.0", @@ -8674,6 +10086,8 @@ }, "node_modules/react-modal": { "version": "3.16.3", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.3.tgz", + "integrity": "sha512-yCYRJB5YkeQDQlTt17WGAgFJ7jr2QYcWa1SHqZ3PluDmnKJ/7+tVU+E6uKyZ0nODaeEj+xCpK4LcSnKXLMC0Nw==", "license": "MIT", "dependencies": { "exenv": "^1.2.0", @@ -8688,7 +10102,10 @@ }, "node_modules/react-redux": { "version": "7.2.9", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.15.4", "@types/react-redux": "^7.1.20", @@ -8711,6 +10128,8 @@ }, "node_modules/react-refresh": { "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", "dev": true, "license": "MIT", "engines": { @@ -8719,7 +10138,10 @@ }, "node_modules/react-router": { "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", + "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -8737,7 +10159,10 @@ }, "node_modules/react-router-dom": { "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", + "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -8753,10 +10178,14 @@ }, "node_modules/react-router/node_modules/react-is": { "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, "node_modules/react-transition-group": { "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", "license": "BSD-3-Clause", "dependencies": { "@babel/runtime": "^7.5.5", @@ -8771,6 +10200,8 @@ }, "node_modules/read-pkg": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", "license": "MIT", "dependencies": { "@types/normalize-package-data": "^2.4.0", @@ -8784,6 +10215,8 @@ }, "node_modules/read-pkg-up": { "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", "license": "MIT", "dependencies": { "find-up": "^4.1.0", @@ -8799,6 +10232,8 @@ }, "node_modules/read-pkg-up/node_modules/find-up": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -8810,6 +10245,8 @@ }, "node_modules/read-pkg-up/node_modules/locate-path": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -8820,6 +10257,8 @@ }, "node_modules/read-pkg-up/node_modules/p-limit": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -8833,6 +10272,8 @@ }, "node_modules/read-pkg-up/node_modules/p-locate": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -8843,6 +10284,8 @@ }, "node_modules/read-pkg-up/node_modules/type-fest": { "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=8" @@ -8850,6 +10293,8 @@ }, "node_modules/read-pkg/node_modules/type-fest": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=8" @@ -8857,6 +10302,8 @@ }, "node_modules/readable-stream": { "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -8869,6 +10316,8 @@ }, "node_modules/readdirp": { "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -8879,6 +10328,8 @@ }, "node_modules/redent": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", "license": "MIT", "dependencies": { "indent-string": "^4.0.0", @@ -8890,20 +10341,28 @@ }, "node_modules/redux": { "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.9.2" } }, "node_modules/redux-saga": { "version": "1.4.2", + "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.4.2.tgz", + "integrity": "sha512-QLIn/q+7MX/B+MkGJ/K6R3//60eJ4QNy65eqPsJrfGezbxdh1Jx+37VRKE2K4PsJnNET5JufJtgWdT30WBa+6w==", "license": "MIT", + "peer": true, "dependencies": { "@redux-saga/core": "^1.4.2" } }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -8924,10 +10383,14 @@ }, "node_modules/regenerate": { "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", "license": "MIT" }, "node_modules/regenerate-unicode-properties": { "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", "license": "MIT", "dependencies": { "regenerate": "^1.4.2" @@ -8938,10 +10401,14 @@ }, "node_modules/regenerator-runtime": { "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", "license": "MIT" }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -8960,6 +10427,8 @@ }, "node_modules/regexpu-core": { "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", "license": "MIT", "dependencies": { "regenerate": "^1.4.2", @@ -8975,6 +10444,8 @@ }, "node_modules/registry-auth-token": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.1.tgz", + "integrity": "sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==", "license": "MIT", "dependencies": { "@pnpm/npm-conf": "^3.0.2" @@ -8985,6 +10456,8 @@ }, "node_modules/registry-url": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", + "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", "license": "MIT", "dependencies": { "rc": "1.2.8" @@ -8998,10 +10471,14 @@ }, "node_modules/regjsgen": { "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.13.0", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz", + "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==", "license": "BSD-2-Clause", "dependencies": { "jsesc": "~3.1.0" @@ -9012,10 +10489,14 @@ }, "node_modules/remove-accents": { "version": "0.4.4", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.4.tgz", + "integrity": "sha512-EpFcOa/ISetVHEXqu+VwI96KZBmq+a8LJnGkaeFw45epGlxIZz5dhEEnNZMsQXgORu3qaMoLX4qJCzOik6ytAg==", "license": "MIT" }, "node_modules/require-from-string": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9023,30 +10504,44 @@ }, "node_modules/reselect": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-3.0.1.tgz", + "integrity": "sha512-b/6tFZCmRhtBMa4xGqiiRp9jh9Aqi2A687Lo265cN0/QohJQEBPiQ52f4QB6i0eF3yp3hmLL21LSGBcML2dlxA==", "license": "MIT" }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", "license": "MIT" }, "node_modules/resolve": { - "version": "2.0.0-next.5", + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/resolve-from": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { @@ -9055,10 +10550,14 @@ }, "node_modules/resolve-pathname": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==", "license": "MIT" }, "node_modules/restore-cursor": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "license": "MIT", "dependencies": { "onetime": "^5.1.0", @@ -9070,6 +10569,8 @@ }, "node_modules/reusify": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -9079,6 +10580,8 @@ }, "node_modules/rifm": { "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.7.0.tgz", + "integrity": "sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1" @@ -9089,6 +10592,9 @@ }, "node_modules/rimraf": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", "dependencies": { @@ -9103,9 +10609,12 @@ }, "node_modules/rollup": { "name": "@rollup/wasm-node", - "version": "4.55.2", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/wasm-node/-/wasm-node-4.60.1.tgz", + "integrity": "sha512-FAfGj5Ferzyna11iUwGdkYus/Y9d/H75PEpsseP5DZOsEsyPvP/Q7mJiSXhUYSEmyfHPaZyC8EsJCjqzDbtcfg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -9122,11 +10631,15 @@ }, "node_modules/rrweb-cssom": { "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", "dev": true, "license": "MIT" }, "node_modules/run-async": { "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", "license": "MIT", "engines": { "node": ">=0.12.0" @@ -9134,6 +10647,8 @@ }, "node_modules/run-parallel": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -9156,6 +10671,8 @@ }, "node_modules/rxjs": { "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^1.9.0" @@ -9166,10 +10683,14 @@ }, "node_modules/rxjs/node_modules/tslib": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "license": "0BSD" }, "node_modules/safe-array-concat": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -9187,10 +10708,14 @@ }, "node_modules/safe-array-concat/node_modules/isarray": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "license": "MIT" }, "node_modules/safe-buffer": { "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "funding": [ { "type": "github", @@ -9209,6 +10734,8 @@ }, "node_modules/safe-push-apply": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -9223,10 +10750,14 @@ }, "node_modules/safe-push-apply/node_modules/isarray": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "license": "MIT" }, "node_modules/safe-regex-test": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -9242,10 +10773,14 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, "node_modules/saxes": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, "license": "ISC", "dependencies": { @@ -9257,6 +10792,8 @@ }, "node_modules/scheduler": { "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", @@ -9265,11 +10802,15 @@ }, "node_modules/seamless-immutable": { "version": "7.1.4", + "resolved": "https://registry.npmjs.org/seamless-immutable/-/seamless-immutable-7.1.4.tgz", + "integrity": "sha512-XiUO1QP4ki4E2PHegiGAlu6r82o5A+6tRh7IkGGTVg/h+UoeX4nFBeCGPOhb4CYjvkqsfm/TUtvOMYC1xmV30A==", "license": "BSD-3-Clause", "optional": true }, "node_modules/semver": { - "version": "7.7.3", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -9280,6 +10821,8 @@ }, "node_modules/serialize-javascript": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" @@ -9287,6 +10830,8 @@ }, "node_modules/set-function-length": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -9302,6 +10847,8 @@ }, "node_modules/set-function-name": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -9315,6 +10862,8 @@ }, "node_modules/set-proto": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -9333,6 +10882,8 @@ }, "node_modules/shebang-command": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -9343,6 +10894,8 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "license": "MIT", "engines": { "node": ">=8" @@ -9350,6 +10903,8 @@ }, "node_modules/side-channel": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -9367,6 +10922,8 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -9381,6 +10938,8 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -9397,6 +10956,8 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -9414,15 +10975,21 @@ }, "node_modules/siginfo": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, "license": "ISC" }, "node_modules/signal-exit": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, "node_modules/slash": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", "engines": { @@ -9430,8 +10997,13 @@ } }, "node_modules/smob": { - "version": "1.5.0", - "license": "MIT" + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.1.tgz", + "integrity": "sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } }, "node_modules/sortablejs": { "version": "1.15.7", @@ -9441,6 +11013,9 @@ }, "node_modules/source-map": { "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", "license": "BSD-3-Clause", "dependencies": { "whatwg-url": "^7.0.0" @@ -9451,6 +11026,8 @@ }, "node_modules/source-map-js": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -9459,6 +11036,8 @@ }, "node_modules/source-map-support": { "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -9467,6 +11046,8 @@ }, "node_modules/source-map-support/node_modules/source-map": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -9474,6 +11055,8 @@ }, "node_modules/source-map/node_modules/tr46": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", "license": "MIT", "dependencies": { "punycode": "^2.1.0" @@ -9481,10 +11064,14 @@ }, "node_modules/source-map/node_modules/webidl-conversions": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", "license": "BSD-2-Clause" }, "node_modules/source-map/node_modules/whatwg-url": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", "license": "MIT", "dependencies": { "lodash.sortby": "^4.7.0", @@ -9494,10 +11081,15 @@ }, "node_modules/sourcemap-codec": { "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", "license": "MIT" }, "node_modules/spdx-correct": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "license": "Apache-2.0", "dependencies": { "spdx-expression-parse": "^3.0.0", @@ -9506,10 +11098,14 @@ }, "node_modules/spdx-exceptions": { "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", @@ -9517,25 +11113,35 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.22", + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", "license": "CC0-1.0" }, "node_modules/sprintf-js": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "license": "BSD-3-Clause" }, "node_modules/stackback": { "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", "dev": true, "license": "MIT" }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -9547,6 +11153,8 @@ }, "node_modules/strict-uri-encode": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9554,6 +11162,8 @@ }, "node_modules/string_decoder": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -9561,6 +11171,8 @@ }, "node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -9571,29 +11183,16 @@ "node": ">=8" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "license": "MIT" - }, "node_modules/string-width/node_modules/emoji-regex": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/string.prototype.includes": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", "dev": true, "license": "MIT", "dependencies": { @@ -9607,6 +11206,8 @@ }, "node_modules/string.prototype.matchall": { "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -9632,6 +11233,8 @@ }, "node_modules/string.prototype.repeat": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, "license": "MIT", "dependencies": { @@ -9641,6 +11244,8 @@ }, "node_modules/string.prototype.trim": { "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -9660,6 +11265,8 @@ }, "node_modules/string.prototype.trimend": { "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -9676,6 +11283,8 @@ }, "node_modules/string.prototype.trimstart": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -9691,6 +11300,8 @@ }, "node_modules/stringify-object": { "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", "license": "BSD-2-Clause", "dependencies": { "get-own-enumerable-property-symbols": "^3.0.0", @@ -9703,17 +11314,8 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -9724,6 +11326,8 @@ }, "node_modules/strip-comments": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", "license": "MIT", "engines": { "node": ">=10" @@ -9731,6 +11335,8 @@ }, "node_modules/strip-indent": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", "license": "MIT", "dependencies": { "min-indent": "^1.0.0" @@ -9741,6 +11347,8 @@ }, "node_modules/strip-json-comments": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", "engines": { @@ -9752,6 +11360,8 @@ }, "node_modules/stubborn-fs": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", + "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==", "license": "MIT", "dependencies": { "stubborn-utils": "^1.0.1" @@ -9759,10 +11369,14 @@ }, "node_modules/stubborn-utils": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz", + "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", "license": "MIT" }, "node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -9773,6 +11387,8 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -9783,11 +11399,15 @@ }, "node_modules/symbol-tree": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true, "license": "MIT" }, "node_modules/temp-dir": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", "license": "MIT", "engines": { "node": ">=8" @@ -9795,6 +11415,8 @@ }, "node_modules/tempy": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", "license": "MIT", "dependencies": { "is-stream": "^2.0.0", @@ -9811,6 +11433,8 @@ }, "node_modules/tempy/node_modules/type-fest": { "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" @@ -9820,7 +11444,9 @@ } }, "node_modules/terser": { - "version": "5.46.0", + "version": "5.46.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -9837,28 +11463,40 @@ }, "node_modules/text-table": { "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true, "license": "MIT" }, "node_modules/through": { "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "license": "MIT" }, "node_modules/tiny-invariant": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, "node_modules/tiny-warning": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", "license": "MIT" }, "node_modules/tinybench": { "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.0.2", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", "dev": true, "license": "MIT", "engines": { @@ -9867,6 +11505,8 @@ }, "node_modules/tinyglobby": { "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9882,6 +11522,8 @@ }, "node_modules/tinyglobby/node_modules/fdir": { "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -9902,6 +11544,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -9910,7 +11553,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.0.3", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -9919,6 +11564,8 @@ }, "node_modules/tldts": { "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9930,11 +11577,15 @@ }, "node_modules/tldts-core": { "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", "dev": true, "license": "MIT" }, "node_modules/tmp": { "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "license": "MIT", "dependencies": { "os-tmpdir": "~1.0.2" @@ -9945,6 +11596,8 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -9955,6 +11608,8 @@ }, "node_modules/tough-cookie": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -9966,6 +11621,8 @@ }, "node_modules/tr46": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "dev": true, "license": "MIT", "dependencies": { @@ -9977,6 +11634,8 @@ }, "node_modules/trim-newlines": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", "license": "MIT", "engines": { "node": ">=8" @@ -9984,6 +11643,8 @@ }, "node_modules/ts-api-utils": { "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", "dev": true, "license": "MIT", "engines": { @@ -9995,10 +11656,14 @@ }, "node_modules/tslib": { "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -10010,6 +11675,8 @@ }, "node_modules/type-fest": { "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -10021,6 +11688,8 @@ }, "node_modules/typed-array-buffer": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -10033,6 +11702,8 @@ }, "node_modules/typed-array-byte-length": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -10050,6 +11721,8 @@ }, "node_modules/typed-array-byte-offset": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -10069,6 +11742,8 @@ }, "node_modules/typed-array-length": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -10087,8 +11762,11 @@ }, "node_modules/typescript": { "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10099,6 +11777,8 @@ }, "node_modules/typescript-compare": { "version": "0.0.2", + "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", + "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==", "license": "MIT", "dependencies": { "typescript-logic": "^0.0.0" @@ -10106,10 +11786,14 @@ }, "node_modules/typescript-logic": { "version": "0.0.0", + "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz", + "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==", "license": "MIT" }, "node_modules/typescript-tuple": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz", + "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==", "license": "MIT", "dependencies": { "typescript-compare": "^0.0.2" @@ -10117,6 +11801,8 @@ }, "node_modules/unbox-primitive": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -10133,11 +11819,15 @@ }, "node_modules/undici-types": { "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "devOptional": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", "license": "MIT", "engines": { "node": ">=4" @@ -10145,6 +11835,8 @@ }, "node_modules/unicode-match-property-ecmascript": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", "license": "MIT", "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", @@ -10156,6 +11848,8 @@ }, "node_modules/unicode-match-property-value-ecmascript": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", "license": "MIT", "engines": { "node": ">=4" @@ -10163,6 +11857,8 @@ }, "node_modules/unicode-property-aliases-ecmascript": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", "license": "MIT", "engines": { "node": ">=4" @@ -10170,6 +11866,8 @@ }, "node_modules/unique-string": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", "license": "MIT", "dependencies": { "crypto-random-string": "^2.0.0" @@ -10180,6 +11878,8 @@ }, "node_modules/universalify": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "license": "MIT", "engines": { "node": ">= 10.0.0" @@ -10187,6 +11887,8 @@ }, "node_modules/upath": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", "license": "MIT", "engines": { "node": ">=4", @@ -10195,6 +11897,8 @@ }, "node_modules/update-browserslist-db": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "funding": [ { "type": "opencollective", @@ -10223,6 +11927,8 @@ }, "node_modules/update-notifier": { "version": "7.3.1", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-7.3.1.tgz", + "integrity": "sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA==", "license": "BSD-2-Clause", "dependencies": { "boxen": "^8.0.1", @@ -10245,6 +11951,8 @@ }, "node_modules/update-notifier/node_modules/chalk": { "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -10255,6 +11963,8 @@ }, "node_modules/uri-js": { "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -10262,10 +11972,14 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, "node_modules/uuid": { "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -10277,6 +11991,8 @@ }, "node_modules/validate-npm-package-license": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", @@ -10285,12 +12001,17 @@ }, "node_modules/value-equal": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==", "license": "MIT" }, "node_modules/vite": { "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -10362,6 +12083,8 @@ }, "node_modules/vite-plugin-pwa": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.2.0.tgz", + "integrity": "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw==", "dev": true, "license": "MIT", "dependencies": { @@ -10391,6 +12114,8 @@ }, "node_modules/vite/node_modules/fdir": { "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -10411,6 +12136,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10419,29 +12145,32 @@ } }, "node_modules/vitest": { - "version": "4.0.17", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@vitest/expect": "4.0.17", - "@vitest/mocker": "4.0.17", - "@vitest/pretty-format": "4.0.17", - "@vitest/runner": "4.0.17", - "@vitest/snapshot": "4.0.17", - "@vitest/spy": "4.0.17", - "@vitest/utils": "4.0.17", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", - "std-env": "^3.10.0", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -10457,12 +12186,13 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.17", - "@vitest/browser-preview": "4.0.17", - "@vitest/browser-webdriverio": "4.0.17", - "@vitest/ui": "4.0.17", + "@vitest/browser-playwright": "4.1.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -10491,6 +12221,9 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, @@ -10509,6 +12242,8 @@ }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, "license": "MIT", "dependencies": { @@ -10520,6 +12255,8 @@ }, "node_modules/warning": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", "license": "MIT", "dependencies": { "loose-envify": "^1.0.0" @@ -10527,6 +12264,8 @@ }, "node_modules/wcwidth": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", "license": "MIT", "dependencies": { "defaults": "^1.0.3" @@ -10534,6 +12273,8 @@ }, "node_modules/webidl-conversions": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -10542,6 +12283,9 @@ }, "node_modules/whatwg-encoding": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", "dev": true, "license": "MIT", "dependencies": { @@ -10553,6 +12297,8 @@ }, "node_modules/whatwg-mimetype": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", "dev": true, "license": "MIT", "engines": { @@ -10561,6 +12307,8 @@ }, "node_modules/whatwg-url": { "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "dev": true, "license": "MIT", "dependencies": { @@ -10573,10 +12321,14 @@ }, "node_modules/when-exit": { "version": "2.1.5", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", + "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==", "license": "MIT" }, "node_modules/which": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -10590,6 +12342,8 @@ }, "node_modules/which-boxed-primitive": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "license": "MIT", "dependencies": { "is-bigint": "^1.1.0", @@ -10607,6 +12361,8 @@ }, "node_modules/which-builtin-type": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -10632,10 +12388,14 @@ }, "node_modules/which-builtin-type/node_modules/isarray": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "license": "MIT" }, "node_modules/which-collection": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "license": "MIT", "dependencies": { "is-map": "^2.0.3", @@ -10652,6 +12412,8 @@ }, "node_modules/which-typed-array": { "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -10671,6 +12433,8 @@ }, "node_modules/why-is-node-running": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { @@ -10686,6 +12450,8 @@ }, "node_modules/widest-line": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", "license": "MIT", "dependencies": { "string-width": "^7.0.0" @@ -10699,6 +12465,8 @@ }, "node_modules/widest-line/node_modules/ansi-regex": { "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { "node": ">=12" @@ -10709,10 +12477,14 @@ }, "node_modules/widest-line/node_modules/emoji-regex": { "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "license": "MIT" }, "node_modules/widest-line/node_modules/string-width": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", @@ -10727,10 +12499,12 @@ } }, "node_modules/widest-line/node_modules/strip-ansi": { - "version": "7.1.2", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -10741,6 +12515,8 @@ }, "node_modules/word-wrap": { "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { @@ -10749,6 +12525,8 @@ }, "node_modules/workbox-background-sync": { "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.0.tgz", + "integrity": "sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w==", "license": "MIT", "dependencies": { "idb": "^7.0.1", @@ -10757,6 +12535,8 @@ }, "node_modules/workbox-broadcast-update": { "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.4.0.tgz", + "integrity": "sha512-+eZQwoktlvo62cI0b+QBr40v5XjighxPq3Fzo9AWMiAosmpG5gxRHgTbGGhaJv/q/MFVxwFNGh/UwHZ/8K88lA==", "license": "MIT", "dependencies": { "workbox-core": "7.4.0" @@ -10764,6 +12544,8 @@ }, "node_modules/workbox-build": { "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.4.0.tgz", + "integrity": "sha512-Ntk1pWb0caOFIvwz/hfgrov/OJ45wPEhI5PbTywQcYjyZiVhT3UrwwUPl6TRYbTm4moaFYithYnl1lvZ8UjxcA==", "license": "MIT", "dependencies": { "@apideck/better-ajv-errors": "^0.3.1", @@ -10809,11 +12591,12 @@ } }, "node_modules/workbox-build/node_modules/@apideck/better-ajv-errors": { - "version": "0.3.6", + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.7.tgz", + "integrity": "sha512-TajUJwGWbDwkCx/CZi7tRE8PVB7simCvKJfHUsSdvps+aTM/PDPP4gkLmKnc+x3CE//y9i/nj74GqdL/hwk7Iw==", "license": "MIT", "dependencies": { - "json-schema": "^0.4.0", - "jsonpointer": "^5.0.0", + "jsonpointer": "^5.0.1", "leven": "^3.1.0" }, "engines": { @@ -10825,6 +12608,8 @@ }, "node_modules/workbox-build/node_modules/@rollup/plugin-babel": { "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", + "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.10.4", @@ -10846,6 +12631,8 @@ }, "node_modules/workbox-build/node_modules/@rollup/plugin-replace": { "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", "license": "MIT", "dependencies": { "@rollup/pluginutils": "^3.1.0", @@ -10857,6 +12644,8 @@ }, "node_modules/workbox-build/node_modules/@rollup/pluginutils": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", "license": "MIT", "dependencies": { "@types/estree": "0.0.39", @@ -10872,11 +12661,16 @@ }, "node_modules/workbox-build/node_modules/@types/estree": { "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", "license": "MIT" }, "node_modules/workbox-build/node_modules/ajv": { "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -10888,12 +12682,38 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/workbox-build/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/workbox-build/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/workbox-build/node_modules/estree-walker": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", "license": "MIT" }, "node_modules/workbox-build/node_modules/glob": { "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "BlueOak-1.0.0", "dependencies": { "foreground-child": "^3.3.1", @@ -10915,23 +12735,29 @@ }, "node_modules/workbox-build/node_modules/json-schema-traverse": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, "node_modules/workbox-build/node_modules/magic-string": { "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", "license": "MIT", "dependencies": { "sourcemap-codec": "^1.4.8" } }, "node_modules/workbox-build/node_modules/minimatch": { - "version": "10.1.1", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -10939,6 +12765,8 @@ }, "node_modules/workbox-build/node_modules/pretty-bytes": { "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", "license": "MIT", "engines": { "node": ">=6" @@ -10948,8 +12776,11 @@ } }, "node_modules/workbox-build/node_modules/rollup": { - "version": "2.79.2", + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", + "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -10962,6 +12793,8 @@ }, "node_modules/workbox-cacheable-response": { "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.0.tgz", + "integrity": "sha512-0Fb8795zg/x23ISFkAc7lbWes6vbw34DGFIMw31cwuHPgDEC/5EYm6m/ZkylLX0EnEbbOyOCLjKgFS/Z5g0HeQ==", "license": "MIT", "dependencies": { "workbox-core": "7.4.0" @@ -10969,6 +12802,8 @@ }, "node_modules/workbox-cli": { "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-cli/-/workbox-cli-7.4.0.tgz", + "integrity": "sha512-BTc9CbW+aXMyIxBdW2mX+dLYHwTeCdKARX0zpjLvR/mZ2ho/7d9XWckwgFGLQRsJfcxml5WngNqp1PG7+qa9Ug==", "license": "MIT", "dependencies": { "chalk": "^4.1.0", @@ -10992,8 +12827,32 @@ "node": ">=20.0.0" } }, + "node_modules/workbox-cli/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/workbox-cli/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/workbox-cli/node_modules/glob": { "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "BlueOak-1.0.0", "dependencies": { "foreground-child": "^3.3.1", @@ -11014,13 +12873,15 @@ } }, "node_modules/workbox-cli/node_modules/minimatch": { - "version": "10.1.1", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -11028,6 +12889,8 @@ }, "node_modules/workbox-cli/node_modules/pretty-bytes": { "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", "license": "MIT", "engines": { "node": ">=6" @@ -11038,10 +12901,14 @@ }, "node_modules/workbox-core": { "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.0.tgz", + "integrity": "sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ==", "license": "MIT" }, "node_modules/workbox-expiration": { "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.4.0.tgz", + "integrity": "sha512-V50p4BxYhtA80eOvulu8xVfPBgZbkxJ1Jr8UUn0rvqjGhLDqKNtfrDfjJKnLz2U8fO2xGQJTx/SKXNTzHOjnHw==", "license": "MIT", "dependencies": { "idb": "^7.0.1", @@ -11050,6 +12917,8 @@ }, "node_modules/workbox-google-analytics": { "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.4.0.tgz", + "integrity": "sha512-MVPXQslRF6YHkzGoFw1A4GIB8GrKym/A5+jYDUSL+AeJw4ytQGrozYdiZqUW1TPQHW8isBCBtyFJergUXyNoWQ==", "license": "MIT", "dependencies": { "workbox-background-sync": "7.4.0", @@ -11060,6 +12929,8 @@ }, "node_modules/workbox-navigation-preload": { "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.4.0.tgz", + "integrity": "sha512-etzftSgdQfjMcfPgbfaZCfM2QuR1P+4o8uCA2s4rf3chtKTq/Om7g/qvEOcZkG6v7JZOSOxVYQiOu6PbAZgU6w==", "license": "MIT", "dependencies": { "workbox-core": "7.4.0" @@ -11067,6 +12938,8 @@ }, "node_modules/workbox-precaching": { "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.0.tgz", + "integrity": "sha512-VQs37T6jDqf1rTxUJZXRl3yjZMf5JX/vDPhmx2CPgDDKXATzEoqyRqhYnRoxl6Kr0rqaQlp32i9rtG5zTzIlNg==", "license": "MIT", "dependencies": { "workbox-core": "7.4.0", @@ -11076,6 +12949,8 @@ }, "node_modules/workbox-range-requests": { "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.4.0.tgz", + "integrity": "sha512-3Vq854ZNuP6Y0KZOQWLaLC9FfM7ZaE+iuQl4VhADXybwzr4z/sMmnLgTeUZLq5PaDlcJBxYXQ3U91V7dwAIfvw==", "license": "MIT", "dependencies": { "workbox-core": "7.4.0" @@ -11083,6 +12958,8 @@ }, "node_modules/workbox-recipes": { "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.4.0.tgz", + "integrity": "sha512-kOkWvsAn4H8GvAkwfJTbwINdv4voFoiE9hbezgB1sb/0NLyTG4rE7l6LvS8lLk5QIRIto+DjXLuAuG3Vmt3cxQ==", "license": "MIT", "dependencies": { "workbox-cacheable-response": "7.4.0", @@ -11095,6 +12972,8 @@ }, "node_modules/workbox-routing": { "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.0.tgz", + "integrity": "sha512-C/ooj5uBWYAhAqwmU8HYQJdOjjDKBp9MzTQ+otpMmd+q0eF59K+NuXUek34wbL0RFrIXe/KKT+tUWcZcBqxbHQ==", "license": "MIT", "dependencies": { "workbox-core": "7.4.0" @@ -11102,6 +12981,8 @@ }, "node_modules/workbox-strategies": { "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.0.tgz", + "integrity": "sha512-T4hVqIi5A4mHi92+5EppMX3cLaVywDp8nsyUgJhOZxcfSV/eQofcOA6/EMo5rnTNmNTpw0rUgjAI6LaVullPpg==", "license": "MIT", "dependencies": { "workbox-core": "7.4.0" @@ -11109,6 +12990,8 @@ }, "node_modules/workbox-streams": { "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.4.0.tgz", + "integrity": "sha512-QHPBQrey7hQbnTs5GrEVoWz7RhHJXnPT+12qqWM378orDMo5VMJLCkCM1cnCk+8Eq92lccx/VgRZ7WAzZWbSLg==", "license": "MIT", "dependencies": { "workbox-core": "7.4.0", @@ -11117,10 +13000,14 @@ }, "node_modules/workbox-sw": { "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.4.0.tgz", + "integrity": "sha512-ltU+Kr3qWR6BtbdlMnCjobZKzeV1hN+S6UvDywBrwM19TTyqA03X66dzw1tEIdJvQ4lYKkBFox6IAEhoSEZ8Xw==", "license": "MIT" }, "node_modules/workbox-window": { "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.0.tgz", + "integrity": "sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==", "license": "MIT", "dependencies": { "@types/trusted-types": "^2.0.2", @@ -11129,6 +13016,8 @@ }, "node_modules/wrap-ansi": { "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", @@ -11142,24 +13031,10 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { "node": ">=12" @@ -11170,6 +13045,8 @@ }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", "engines": { "node": ">=12" @@ -11180,10 +13057,14 @@ }, "node_modules/wrap-ansi/node_modules/emoji-regex": { "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "license": "MIT" }, "node_modules/wrap-ansi/node_modules/string-width": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", @@ -11198,10 +13079,12 @@ } }, "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.2", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -11212,11 +13095,15 @@ }, "node_modules/wrappy": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, "license": "ISC" }, "node_modules/ws": { - "version": "8.19.0", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "dev": true, "license": "MIT", "engines": { @@ -11237,6 +13124,8 @@ }, "node_modules/xdg-basedir": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", + "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", "license": "MIT", "engines": { "node": ">=12" @@ -11247,6 +13136,8 @@ }, "node_modules/xml-name-validator": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -11255,15 +13146,21 @@ }, "node_modules/xmlchars": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true, "license": "MIT" }, "node_modules/yallist": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "license": "ISC" }, "node_modules/yargs-parser": { "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "license": "ISC", "dependencies": { "camelcase": "^5.0.0", @@ -11275,6 +13172,8 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { From d7baf6ee7f9533f5fdd51a66f806f48a75fcbbf5 Mon Sep 17 00:00:00 2001 From: Deluan <deluan@navidrome.org> Date: Sun, 5 Apr 2026 12:12:15 -0400 Subject: [PATCH 81/81] fix(shares): honor path component of ShareURL config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PublicURL() copied only the scheme and host from conf.Server.ShareURL, silently dropping any path component. This broke OpenGraph image URLs (and other share links) when ShareURL was configured with a path prefix like https://example.com/navi — generated URLs pointed to /share/img/... at the root instead of /navi/share/img/... Now the ShareURL path is prepended to the resource path, with trailing slashes trimmed. When ShareURL has no path, behavior is unchanged. --- core/publicurl/publicurl.go | 3 +++ core/publicurl/publicurl_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/core/publicurl/publicurl.go b/core/publicurl/publicurl.go index c1b8e01c4..b0865e78b 100644 --- a/core/publicurl/publicurl.go +++ b/core/publicurl/publicurl.go @@ -45,6 +45,9 @@ func PublicURL(req *http.Request, u string, params url.Values) string { } buildUrl.Scheme = shareUrl.Scheme buildUrl.Host = shareUrl.Host + if basePath := strings.TrimRight(shareUrl.Path, "/"); basePath != "" { + buildUrl.Path = path.Join(basePath, buildUrl.Path) + } if len(params) > 0 { buildUrl.RawQuery = params.Encode() } diff --git a/core/publicurl/publicurl_test.go b/core/publicurl/publicurl_test.go index 18f8f8129..a195fb9cd 100644 --- a/core/publicurl/publicurl_test.go +++ b/core/publicurl/publicurl_test.go @@ -56,6 +56,31 @@ var _ = Describe("Public URL Utilities", func() { }) }) + When("ShareURL includes a path", func() { + BeforeEach(func() { + conf.Server.ShareURL = "https://example.com/navi" + }) + + It("prepends the ShareURL path to the resource", func() { + r, _ := http.NewRequest("GET", "http://localhost/test", nil) + result := publicurl.PublicURL(r, "/share/img/hash", nil) + Expect(result).To(Equal("https://example.com/navi/share/img/hash")) + }) + + It("prepends the ShareURL path and includes query parameters", func() { + r, _ := http.NewRequest("GET", "http://localhost/test", nil) + params := url.Values{"size": []string{"600"}} + result := publicurl.PublicURL(r, "/share/img/hash", params) + Expect(result).To(Equal("https://example.com/navi/share/img/hash?size=600")) + }) + + It("handles trailing slash in ShareURL path", func() { + conf.Server.ShareURL = "https://example.com/navi/" + result := publicurl.PublicURL(nil, "/share/img/hash", nil) + Expect(result).To(Equal("https://example.com/navi/share/img/hash")) + }) + }) + When("ShareURL is not set", func() { BeforeEach(func() { conf.Server.ShareURL = ""