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], }, },