Merge branch 'master' into subsonic-folder

This commit is contained in:
Patrik Wallström 2026-04-11 00:30:21 +02:00 committed by GitHub
commit 25b950126d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 124 additions and 76 deletions

View File

@ -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(

View File

@ -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
)

View File

@ -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
}
}
}

View File

@ -1164,6 +1164,7 @@ var _ = Describe("Decider", func() {
Expect(bitrate).To(Equal(fallbackBitrate))
})
})
})
Describe("ensureProbed", func() {

2
go.mod
View File

@ -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

4
go.sum
View File

@ -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=

View File

@ -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

View File

@ -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)

View File

@ -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"

View File

@ -67,6 +67,10 @@
<File Id='ffmpeg.exe' Name='ffmpeg.exe' DiskId='1' Source='ffmpeg.exe' KeyPath='yes' />
</Component>
<Component Id='FFProbeExecutable' Guid='f8a3b2c1-5d4e-4f6a-9b8c-7e2d1a0f3c5b' Win64="$(var.Win64)">
<File Id='ffprobe.exe' Name='ffprobe.exe' DiskId='1' Source='ffprobe.exe' KeyPath='yes' />
</Component>
</Directory>
</Directory>
@ -87,6 +91,7 @@
<ComponentRef Id='Configuration'/>
<ComponentRef Id='MainExecutable' />
<ComponentRef Id='FFMpegExecutable' />
<ComponentRef Id='FFProbeExecutable' />
<ComponentRef Id='PackageFile' />
</Feature>
</Product>

View File

@ -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": "开发标志(可能会更改/删除)",

View File

@ -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 ]

View File

@ -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

View File

@ -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")
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
tests/fixtures/test.wv vendored

Binary file not shown.

View File

@ -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

48
ui/package-lock.json generated
View File

@ -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"
},

View File

@ -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"
},

View File

@ -51,7 +51,6 @@ const useStyles = makeStyles({
borderRadius: '4px',
flexShrink: 0,
cursor: 'pointer',
filter: 'none !important',
},
row: {
cursor: 'pointer',

View File

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