diff --git a/adapters/gotaglib/gotaglib_test.go b/adapters/gotaglib/gotaglib_test.go
index 8fdf5b406..6756fb690 100644
--- a/adapters/gotaglib/gotaglib_test.go
+++ b/adapters/gotaglib/gotaglib_test.go
@@ -127,6 +127,17 @@ var _ = Describe("Extractor", func() {
Expect(m.Tags).To(HaveKeyWithValue("albumartist", []string{"Album Artist"}))
Expect(m.Tags).To(HaveKeyWithValue("genre", []string{"Rock"}))
Expect(m.Tags).To(HaveKeyWithValue("date", []string{"2014"}))
+ // Still as of TagLib v2.2.1, TagLib only maps values in ID3, MP4, and ASF tags
+ // to `originaldate`.
+ if strings.HasSuffix(file, ".mp3") || strings.HasSuffix(file, ".wav") || strings.HasSuffix(file, ".aiff") || strings.HasSuffix(file, ".m4a") || strings.HasSuffix(file, ".wma") {
+ Expect(m.Tags).To(HaveKeyWithValue("originaldate", []string{"1996-11-21"}))
+ }
+ // MP3Tag sets `ORIGYEAR` in several formats for which it has no built-in mapping
+ // for original release dates.
+ Expect(m.Tags).To(Or(
+ HaveKeyWithValue("origyear", []string{"1998-07-28"}),
+ HaveKeyWithValue("----:com.apple.itunes:origyear", []string{"1998-07-28"}),
+ ))
Expect(m.Tags).To(HaveKeyWithValue("bpm", []string{"123"}))
Expect(m.Tags).To(Or(
diff --git a/core/ffmpeg/ffmpeg.go b/core/ffmpeg/ffmpeg.go
index 33d6733c8..c034ca7d0 100644
--- a/core/ffmpeg/ffmpeg.go
+++ b/core/ffmpeg/ffmpeg.go
@@ -49,6 +49,7 @@ type FFmpeg interface {
ProbeAudioStream(ctx context.Context, filePath string) (*AudioProbeResult, error)
CmdPath() (string, error)
IsAvailable() bool
+ IsProbeAvailable() bool
Version() string
}
@@ -224,6 +225,19 @@ func (e *ffmpeg) IsAvailable() bool {
return err == nil
}
+func (e *ffmpeg) IsProbeAvailable() bool {
+ if _, err := ffmpegCmd(); err != nil {
+ return false
+ }
+ probeOnce.Do(func() {
+ probePath := ffprobePath(ffmpegPath)
+ if _, err := exec.LookPath(probePath); err == nil {
+ probeAvail = true
+ }
+ })
+ return probeAvail
+}
+
// Version executes ffmpeg -version and extracts the version from the output.
// Sample output: ffmpeg version 6.0 Copyright (c) 2000-2023 the FFmpeg developers
func (e *ffmpeg) Version() string {
@@ -533,4 +547,6 @@ var (
ffOnce sync.Once
ffmpegPath string
ffmpegErr error
+ probeOnce sync.Once
+ probeAvail bool
)
diff --git a/core/stream/decider.go b/core/stream/decider.go
index 6c1f06a06..713c779fe 100644
--- a/core/stream/decider.go
+++ b/core/stream/decider.go
@@ -44,10 +44,14 @@ func (s *deciderService) MakeDecision(ctx context.Context, mf *model.MediaFile,
var probe *ffmpeg.AudioProbeResult
if !opts.SkipProbe {
- var err error
- probe, err = s.ensureProbed(ctx, mf)
- if err != nil {
- return nil, err
+ if !s.ff.IsProbeAvailable() {
+ log.Debug(ctx, "ffprobe not available, using tag metadata for transcode decision", "mediaID", mf.ID)
+ } else {
+ var err error
+ probe, err = s.ensureProbed(ctx, mf)
+ if err != nil {
+ return nil, err
+ }
}
}
diff --git a/core/stream/decider_test.go b/core/stream/decider_test.go
index 9eaa00990..c776cbdc3 100644
--- a/core/stream/decider_test.go
+++ b/core/stream/decider_test.go
@@ -1164,6 +1164,7 @@ var _ = Describe("Decider", func() {
Expect(bitrate).To(Equal(fallbackBitrate))
})
})
+
})
Describe("ensureProbed", func() {
diff --git a/go.mod b/go.mod
index fcee08c7e..4f4ad0461 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/navidrome/navidrome
go 1.25.0
// Fork to implement raw tags support
-replace go.senan.xyz/taglib => github.com/deluan/go-taglib v0.0.0-20260307161927-168f6e74ada7
+replace go.senan.xyz/taglib => github.com/deluan/go-taglib v0.0.0-20260407173416-cf47afbaa67a
require (
github.com/Masterminds/squirrel v1.5.4
diff --git a/go.sum b/go.sum
index e0671367a..5a0761f15 100644
--- a/go.sum
+++ b/go.sum
@@ -34,8 +34,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
-github.com/deluan/go-taglib v0.0.0-20260307161927-168f6e74ada7 h1:RpRSTEsAdLHx3Ci0d3M5wtpjcBZiKzhnGfnNAxGXrAE=
-github.com/deluan/go-taglib v0.0.0-20260307161927-168f6e74ada7/go.mod h1:sKDN0U4qXDlq6LFK+aOAkDH4Me5nDV1V/A4B+B69xBA=
+github.com/deluan/go-taglib v0.0.0-20260407173416-cf47afbaa67a h1:ZPwh87Xa08FCg5MU5e0Did5WgapEWGxb5d4Je0pLjJw=
+github.com/deluan/go-taglib v0.0.0-20260407173416-cf47afbaa67a/go.mod h1:sKDN0U4qXDlq6LFK+aOAkDH4Me5nDV1V/A4B+B69xBA=
github.com/deluan/rest v0.0.0-20211102003136-6260bc399cbf h1:tb246l2Zmpt/GpF9EcHCKTtwzrd0HGfEmoODFA/qnk4=
github.com/deluan/rest v0.0.0-20211102003136-6260bc399cbf/go.mod h1:tSgDythFsl0QgS/PFWfIZqcJKnkADWneY80jaVRlqK8=
github.com/deluan/sanitize v0.0.0-20241120162836-fdfd8fdfaa55 h1:wSCnggTs2f2ji6nFwQmfwgINcmSMj0xF0oHnoyRSPe4=
diff --git a/plugins/host_kvstore.go b/plugins/host_kvstore.go
index 248e43c4d..c3f6ec734 100644
--- a/plugins/host_kvstore.go
+++ b/plugins/host_kvstore.go
@@ -9,6 +9,7 @@ import (
"path/filepath"
"slices"
"strings"
+ "sync"
"time"
"github.com/dustin/go-humanize"
@@ -35,6 +36,8 @@ type kvstoreServiceImpl struct {
pluginName string
db *sql.DB
maxSize int64
+ cancel context.CancelFunc
+ wg sync.WaitGroup
}
// newKVStoreService creates a new kvstoreServiceImpl instance with its own SQLite database.
@@ -74,12 +77,15 @@ func newKVStoreService(ctx context.Context, pluginName string, perm *KVStorePerm
log.Debug("Initialized plugin kvstore", "plugin", pluginName, "path", dbPath, "maxSize", humanize.Bytes(uint64(maxSize)))
+ cleanupCtx, cancel := context.WithCancel(ctx)
svc := &kvstoreServiceImpl{
pluginName: pluginName,
db: db,
maxSize: maxSize,
+ cancel: cancel,
}
- go svc.cleanupLoop(ctx)
+ svc.wg.Add(1)
+ go svc.cleanupLoop(cleanupCtx)
return svc, nil
}
@@ -335,6 +341,7 @@ func (s *kvstoreServiceImpl) GetMany(ctx context.Context, keys []string) (map[st
// cleanupLoop periodically removes expired keys from the database.
// It stops when the provided context is cancelled.
func (s *kvstoreServiceImpl) cleanupLoop(ctx context.Context) {
+ defer s.wg.Done()
ticker := time.NewTicker(cleanupInterval)
defer ticker.Stop()
for {
@@ -359,17 +366,12 @@ func (s *kvstoreServiceImpl) cleanupExpired(ctx context.Context) {
}
}
-// Close runs a final cleanup and closes the SQLite database connection.
-// The cleanup goroutine is stopped by the context passed to newKVStoreService.
+// Close stops the cleanup goroutine and closes the SQLite database connection.
func (s *kvstoreServiceImpl) Close() error {
- if s.db != nil {
- log.Debug("Closing plugin kvstore", "plugin", s.pluginName)
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- s.cleanupExpired(ctx)
- return s.db.Close()
- }
- return nil
+ log.Debug("Closing plugin kvstore", "plugin", s.pluginName)
+ s.cancel()
+ s.wg.Wait()
+ return s.db.Close()
}
// Compile-time verification
diff --git a/plugins/host_kvstore_test.go b/plugins/host_kvstore_test.go
index 4928825ef..e5d467f79 100644
--- a/plugins/host_kvstore_test.go
+++ b/plugins/host_kvstore_test.go
@@ -445,6 +445,36 @@ var _ = Describe("KVStoreService", func() {
})
})
+ Describe("Close", func() {
+ It("does not race with cleanupLoop goroutine", func() {
+ // Create a service with a dedicated context so we can verify
+ // that Close() properly waits for the cleanup goroutine.
+ closeCtx, closeCancel := context.WithCancel(ctx)
+ defer closeCancel()
+
+ maxSize := "1KB"
+ svc, err := newKVStoreService(closeCtx, "test_close_race", &KVStorePermission{MaxSize: &maxSize})
+ Expect(err).ToNot(HaveOccurred())
+
+ // Insert an expired key so cleanup has work to do
+ _, err = svc.db.Exec(`
+ INSERT INTO kvstore (key, value, size, expires_at)
+ VALUES ('cleanup_race', 'old', 3, datetime('now', '-1 seconds'))
+ `)
+ Expect(err).ToNot(HaveOccurred())
+
+ // Close should not panic or produce "database is closed" errors.
+ // Before the fix, the cleanup goroutine could race with db.Close().
+ err = svc.Close()
+ Expect(err).ToNot(HaveOccurred())
+
+ // Verify the database is actually closed (further queries should fail)
+ _, err = svc.db.Exec(`SELECT 1`)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("database is closed"))
+ })
+ })
+
Describe("SetWithTTL", func() {
It("stores value that is retrievable before expiry", func() {
err := service.SetWithTTL(ctx, "ttl_key", []byte("ttl_value"), 3600)
diff --git a/release/wix/build_msi.sh b/release/wix/build_msi.sh
index 7e595311e..a8781a965 100755
--- a/release/wix/build_msi.sh
+++ b/release/wix/build_msi.sh
@@ -43,8 +43,9 @@ FFMPEG_FILE="ffmpeg-n${FFMPEG_VERSION}-latest-${WIN_ARCH}-gpl-${FFMPEG_VERSION}"
wget --quiet --output-document="${DOWNLOAD_FOLDER}/ffmpeg.zip" \
"https://github.com/${FFMPEG_REPOSITORY}/releases/download/latest/${FFMPEG_FILE}.zip"
rm -rf "${DOWNLOAD_FOLDER}/extracted_ffmpeg"
-unzip -d "${DOWNLOAD_FOLDER}/extracted_ffmpeg" "${DOWNLOAD_FOLDER}/ffmpeg.zip" "*/ffmpeg.exe"
+unzip -d "${DOWNLOAD_FOLDER}/extracted_ffmpeg" "${DOWNLOAD_FOLDER}/ffmpeg.zip" "*/ffmpeg.exe" "*/ffprobe.exe"
cp "${DOWNLOAD_FOLDER}"/extracted_ffmpeg/${FFMPEG_FILE}/bin/ffmpeg.exe "$MSI_OUTPUT_DIR"
+cp "${DOWNLOAD_FOLDER}"/extracted_ffmpeg/${FFMPEG_FILE}/bin/ffprobe.exe "$MSI_OUTPUT_DIR"
cp "$WORKSPACE"/LICENSE "$WORKSPACE"/README.md "$MSI_OUTPUT_DIR"
cp "$BINARY" "$MSI_OUTPUT_DIR"
diff --git a/release/wix/navidrome.wxs b/release/wix/navidrome.wxs
index 8ebba4632..6d94bab9d 100644
--- a/release/wix/navidrome.wxs
+++ b/release/wix/navidrome.wxs
@@ -67,6 +67,10 @@
+
+
+
+
@@ -87,6 +91,7 @@
+
diff --git a/resources/i18n/zh-Hans.json b/resources/i18n/zh-Hans.json
index e26c2b664..63ea5cf60 100644
--- a/resources/i18n/zh-Hans.json
+++ b/resources/i18n/zh-Hans.json
@@ -23,6 +23,7 @@
"bitDepth": "位深度",
"sampleRate": "采样率",
"channels": "声道",
+ "disc": "碟片 %{discNumber}",
"discSubtitle": "碟片副标题",
"starred": "收藏",
"comment": "注释",
@@ -355,7 +356,8 @@
"allUsers": "允许所有用户",
"selectedUsers": "指定用户",
"allLibraries": "允许所有媒体库",
- "selectedLibraries": "指定媒体库"
+ "selectedLibraries": "指定媒体库",
+ "allowWriteAccess": "允许写入权限"
},
"sections": {
"status": "状态",
@@ -400,6 +402,7 @@
"allLibrariesHelp": "启用时,插件将可以访问所有媒体库,包括将来创建的。",
"noLibraries": "未选择媒体库",
"librariesRequired": "此插件需要访问媒体库信息。请选择允许此插件访问的媒体库, 或启用 '允许所有媒体库'。",
+ "allowWriteAccessHelp": "启用时,插件将可以修改媒体库目录中的文件。默认情况下,插件仅拥有只读权限。",
"requiredHosts": "必需的主机"
},
"placeholders": {
@@ -554,6 +557,12 @@
}
},
"message": {
+ "uploadCover": "上传封面",
+ "removeCover": "移除封面",
+ "coverUploaded": "封面已上传",
+ "coverRemoved": "封面已移除",
+ "coverUploadError": "上传封面时出错",
+ "coverRemoveError": "移除封面时出错",
"note": "注意",
"transcodingDisabled": "出于安全原因,从 Web 界面更改转码配置的功能已被禁用。要更改(编辑或新增)转码选项,请在启用 %{config} 选项的情况下重新启动服务器。",
"transcodingEnabled": "Navidrome 当前与 %{config} 一起使用,可以通过从 Web 界面配置转码选项来执行任意命令。建议禁用此选项,并且仅在需要配置转码选项时启用此功能。",
@@ -673,6 +682,7 @@
"currentValue": "当前值",
"configurationFile": "配置文件",
"exportToml": "导出配置(TOML)",
+ "downloadToml": "下载配置(TOML)",
"exportSuccess": "配置以 TOML 格式导出到剪贴板完成",
"exportFailed": "复制配置失败",
"devFlagsHeader": "开发标志(可能会更改/删除)",
diff --git a/resources/mappings.yaml b/resources/mappings.yaml
index 19ba0b090..16dddd504 100644
--- a/resources/mappings.yaml
+++ b/resources/mappings.yaml
@@ -116,7 +116,7 @@ main:
aliases: [ comm:description, comment, ©cmt, description, icmt ]
maxLength: 4096
originaldate:
- aliases: [ tdor, originaldate, ----:com.apple.itunes:originaldate, wm/originalreleasetime, tory, originalyear, ----:com.apple.itunes:originalyear, wm/originalreleaseyear ]
+ aliases: [ tdor, originaldate, ----:com.apple.itunes:originaldate, wm/originalreleasetime, tory, originalyear, ----:com.apple.itunes:originalyear, wm/originalreleaseyear, origyear, ----:com.apple.itunes:origyear ]
type: date
recordingdate:
aliases: [ tdrc, date, recordingdate, icrd, record date ]
diff --git a/server/e2e/e2e_suite_test.go b/server/e2e/e2e_suite_test.go
index 262a5ed36..03fa9bbef 100644
--- a/server/e2e/e2e_suite_test.go
+++ b/server/e2e/e2e_suite_test.go
@@ -337,6 +337,7 @@ func (n noopFFmpeg) ConvertAnimatedImage(context.Context, io.Reader, int, int) (
func (n noopFFmpeg) CmdPath() (string, error) { return "", nil }
func (n noopFFmpeg) IsAvailable() bool { return false }
+func (n noopFFmpeg) IsProbeAvailable() bool { return true }
func (n noopFFmpeg) Version() string { return "noop" }
// noopArchiver implements core.Archiver
diff --git a/server/initial_setup.go b/server/initial_setup.go
index d50f25958..7e974dc21 100644
--- a/server/initial_setup.go
+++ b/server/initial_setup.go
@@ -68,13 +68,16 @@ func createInitialAdminUser(ds model.DataStore, initialPassword string) error {
func checkFFmpegInstallation() {
f := ffmpeg.New()
_, err := f.CmdPath()
- if err == nil {
+ if err != nil {
+ log.Warn("Unable to find ffmpeg. Transcoding will fail if used", err)
+ if conf.Server.Scanner.Extractor == "ffmpeg" {
+ log.Warn("ffmpeg cannot be used for metadata extraction. Falling back to taglib")
+ conf.Server.Scanner.Extractor = "taglib"
+ }
return
}
- log.Warn("Unable to find ffmpeg. Transcoding will fail if used", err)
- if conf.Server.Scanner.Extractor == "ffmpeg" {
- log.Warn("ffmpeg cannot be used for metadata extraction. Falling back to taglib")
- conf.Server.Scanner.Extractor = "taglib"
+ if !f.IsProbeAvailable() {
+ log.Warn("Unable to find ffprobe. Transcoding decisions will be limited")
}
}
diff --git a/tests/fixtures/01 Invisible (RED) Edit Version.m4a b/tests/fixtures/01 Invisible (RED) Edit Version.m4a
index 005792eb5..76b61a2d4 100644
Binary files a/tests/fixtures/01 Invisible (RED) Edit Version.m4a and b/tests/fixtures/01 Invisible (RED) Edit Version.m4a differ
diff --git a/tests/fixtures/test.aiff b/tests/fixtures/test.aiff
index 1435115d9..d179f0714 100644
Binary files a/tests/fixtures/test.aiff and b/tests/fixtures/test.aiff differ
diff --git a/tests/fixtures/test.flac b/tests/fixtures/test.flac
index 6c1270fd5..50430f539 100644
Binary files a/tests/fixtures/test.flac and b/tests/fixtures/test.flac differ
diff --git a/tests/fixtures/test.m4a b/tests/fixtures/test.m4a
index c469dd9e4..e9b54d44d 100644
Binary files a/tests/fixtures/test.m4a and b/tests/fixtures/test.m4a differ
diff --git a/tests/fixtures/test.mp3 b/tests/fixtures/test.mp3
index 7333249ad..18cb90674 100644
Binary files a/tests/fixtures/test.mp3 and b/tests/fixtures/test.mp3 differ
diff --git a/tests/fixtures/test.ogg b/tests/fixtures/test.ogg
index 507da9050..2b1deea98 100644
Binary files a/tests/fixtures/test.ogg and b/tests/fixtures/test.ogg differ
diff --git a/tests/fixtures/test.opus b/tests/fixtures/test.opus
index 5052c0e6e..044947c70 100644
Binary files a/tests/fixtures/test.opus and b/tests/fixtures/test.opus differ
diff --git a/tests/fixtures/test.wav b/tests/fixtures/test.wav
index 155d88bdb..b8c1f9a65 100644
Binary files a/tests/fixtures/test.wav and b/tests/fixtures/test.wav differ
diff --git a/tests/fixtures/test.wma b/tests/fixtures/test.wma
index 2edb7260e..2e7341cf5 100644
Binary files a/tests/fixtures/test.wma and b/tests/fixtures/test.wma differ
diff --git a/tests/fixtures/test.wv b/tests/fixtures/test.wv
index 3722d28a2..7f4118bdb 100644
Binary files a/tests/fixtures/test.wv and b/tests/fixtures/test.wv differ
diff --git a/tests/mock_ffmpeg.go b/tests/mock_ffmpeg.go
index 346209b71..f9862767e 100644
--- a/tests/mock_ffmpeg.go
+++ b/tests/mock_ffmpeg.go
@@ -12,7 +12,7 @@ import (
)
func NewMockFFmpeg(data string) *MockFFmpeg {
- return &MockFFmpeg{Reader: strings.NewReader(data)}
+ return &MockFFmpeg{Reader: strings.NewReader(data), ProbeAvailable: true}
}
type MockFFmpeg struct {
@@ -21,12 +21,17 @@ type MockFFmpeg struct {
closed atomic.Bool
Error error
ProbeAudioResult *ffmpeg.AudioProbeResult
+ ProbeAvailable bool
}
func (ff *MockFFmpeg) IsAvailable() bool {
return true
}
+func (ff *MockFFmpeg) IsProbeAvailable() bool {
+ return ff.ProbeAvailable
+}
+
func (ff *MockFFmpeg) Transcode(_ context.Context, _ ffmpeg.TranscodeOptions) (io.ReadCloser, error) {
if ff.Error != nil {
return nil, ff.Error
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 2dd91a674..1f95f14f8 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -68,7 +68,7 @@
"prettier": "^3.6.2",
"ra-test": "^3.19.12",
"typescript": "^5.8.3",
- "vite": "^7.1.12",
+ "vite": "^7.3.2",
"vite-plugin-pwa": "^1.1.0",
"vitest": "^4.0.3"
}
@@ -129,7 +129,6 @@
"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.29.0",
"@babel/generator": "^7.29.0",
@@ -1744,7 +1743,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18"
},
@@ -1768,7 +1766,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -2474,7 +2471,6 @@
"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",
@@ -2528,7 +2524,6 @@
"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"
@@ -2544,7 +2539,6 @@
"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",
@@ -2591,7 +2585,6 @@
"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"
},
@@ -2684,7 +2677,6 @@
"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",
@@ -3316,7 +3308,6 @@
"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"
},
@@ -3369,7 +3360,6 @@
"integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -3391,7 +3381,6 @@
"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",
@@ -3561,7 +3550,6 @@
"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",
@@ -3895,7 +3883,6 @@
"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"
},
@@ -4580,7 +4567,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
@@ -4956,7 +4942,6 @@
"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"
@@ -5837,7 +5822,6 @@
"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",
@@ -6421,7 +6405,6 @@
"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"
},
@@ -6438,7 +6421,6 @@
"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"
}
@@ -6853,7 +6835,6 @@
"integrity": "sha512-Tz23LR9T9jOGVZm2x1EPdXqwA37G/owYMxRwU0E4miurAtFsPMQ1d2Jc2okUaSjZqAFz2oEn3FLXC5a0a+siyA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/node": ">=20.0.0",
"@types/whatwg-mimetype": "^3.0.2",
@@ -6967,7 +6948,6 @@
"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",
@@ -8570,7 +8550,6 @@
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
- "peer": true,
"engines": {
"node": "*"
}
@@ -9319,7 +9298,6 @@
"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",
@@ -9411,7 +9389,6 @@
"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",
@@ -9826,7 +9803,6 @@
"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"
@@ -9908,7 +9884,6 @@
"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",
@@ -9981,7 +9956,6 @@
"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"
},
@@ -9999,7 +9973,6 @@
"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"
},
@@ -10105,7 +10078,6 @@
"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",
@@ -10141,7 +10113,6 @@
"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",
@@ -10162,7 +10133,6 @@
"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",
@@ -10344,7 +10314,6 @@
"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"
}
@@ -10354,7 +10323,6 @@
"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"
}
@@ -10614,7 +10582,6 @@
"integrity": "sha512-FAfGj5Ferzyna11iUwGdkYus/Y9d/H75PEpsseP5DZOsEsyPvP/Q7mJiSXhUYSEmyfHPaZyC8EsJCjqzDbtcfg==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -11544,7 +11511,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -11766,7 +11732,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -12006,12 +11971,11 @@
"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==",
+ "version": "7.3.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz",
+ "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -12136,7 +12100,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -12150,7 +12113,6 @@
"integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@vitest/expect": "4.1.2",
"@vitest/mocker": "4.1.2",
@@ -12670,7 +12632,6 @@
"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",
@@ -12780,7 +12741,6 @@
"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"
},
diff --git a/ui/package.json b/ui/package.json
index d4c149b23..b440f0595 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -77,7 +77,7 @@
"prettier": "^3.6.2",
"ra-test": "^3.19.12",
"typescript": "^5.8.3",
- "vite": "^7.1.12",
+ "vite": "^7.3.2",
"vite-plugin-pwa": "^1.1.0",
"vitest": "^4.0.3"
},
diff --git a/ui/src/common/SongDatagrid.jsx b/ui/src/common/SongDatagrid.jsx
index 5d2ae3ed1..d2c98bbe7 100644
--- a/ui/src/common/SongDatagrid.jsx
+++ b/ui/src/common/SongDatagrid.jsx
@@ -51,7 +51,6 @@ const useStyles = makeStyles({
borderRadius: '4px',
flexShrink: 0,
cursor: 'pointer',
- filter: 'none !important',
},
row: {
cursor: 'pointer',
diff --git a/ui/src/themes/SquiddiesGlass.js b/ui/src/themes/SquiddiesGlass.js
index 5c3844074..880b0be20 100644
--- a/ui/src/themes/SquiddiesGlass.js
+++ b/ui/src/themes/SquiddiesGlass.js
@@ -208,11 +208,11 @@ export default {
borderBottom: `1px solid ${colors.gray[300]}`,
padding: '10px !important',
color: `${colors.gray[100]} !important`,
- '& img': {
+ '& img[alt="playing"], & img[alt="paused"]': {
filter:
- 'brightness(0) saturate(100%) invert(36%) sepia(93%) saturate(7463%) hue-rotate(289deg) brightness(95%) contrast(102%);',
+ 'brightness(0) saturate(100%) invert(36%) sepia(93%) saturate(7463%) hue-rotate(289deg) brightness(95%) contrast(102%)',
},
- '& img + span': {
+ '& img[alt="playing"] + span, & img[alt="paused"] + span': {
color: colors.pink[500],
},
},