From 7834674381d86648c2f33606bff7ace59a85585d Mon Sep 17 00:00:00 2001 From: obskyr Date: Tue, 7 Apr 2026 03:35:22 +0200 Subject: [PATCH 1/7] fix(scanner): map ORIGYEAR tag for VorbisComment and MP4 formats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use ORIGYEAR tag for original date As it is a default mapping in MP3Tag. https://docs.mp3tag.de/mapping/#origyear * Test parsing `originaldate` and `ORIGYEAR` tags `originaldate` is populated by TagLib’s mappings. https://taglib.org/api/p_propertymapping.html --- adapters/gotaglib/gotaglib_test.go | 11 +++++++++++ resources/mappings.yaml | 2 +- .../01 Invisible (RED) Edit Version.m4a | Bin 38962 -> 38962 bytes tests/fixtures/test.aiff | Bin 109766 -> 109766 bytes tests/fixtures/test.flac | Bin 31146 -> 31146 bytes tests/fixtures/test.m4a | Bin 45120 -> 45120 bytes tests/fixtures/test.mp3 | Bin 64223 -> 64223 bytes tests/fixtures/test.ogg | Bin 32177 -> 32200 bytes tests/fixtures/test.opus | Bin 14236 -> 13219 bytes tests/fixtures/test.wav | Bin 109954 -> 109954 bytes tests/fixtures/test.wma | Bin 40717 -> 40717 bytes tests/fixtures/test.wv | Bin 42882 -> 42909 bytes 12 files changed, 12 insertions(+), 1 deletion(-) 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/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/tests/fixtures/01 Invisible (RED) Edit Version.m4a b/tests/fixtures/01 Invisible (RED) Edit Version.m4a index 005792eb5f7f2447de71a3e6719f69697c3d21bd..76b61a2d4331923781ec3f342382b5661e5f3baa 100644 GIT binary patch delta 154 zcmdnAfoanQrVSikjMp}EdVOZ*4ATVy1_lP1$^Jgl))IM%xv4-NgMW~xyQiO{kBei7 zE08Odl30=mq!~eShL)CQx`u|jMutF65VC%45oG<5u8u)K73g{`bPddPjVw0r@T+y3 P{M&aw6I1v`gOARpq diff --git a/tests/fixtures/test.aiff b/tests/fixtures/test.aiff index 1435115d9ca65b2574798a29fe10d75f45dff76c..d179f0714d9b074becd26a169971147a5ad45600 100644 GIT binary patch delta 74 zcmX?hlkM0|wuUW?Gb2S@{DT-67Mm<=s0&2$Y7b&U+CPmg3wkqe24hyY570;T+e bJl!K*9fKHvYAkdO%yo?{w*QJ`oE`)K@_-Xe delta 27 jcmX?hlkM0|wuUW?Gb5+3k7P{RZV<&dEoi#HN=8!vu?!0& diff --git a/tests/fixtures/test.flac b/tests/fixtures/test.flac index 6c1270fd5091bde08bcdbcf147b9ac188dcc7367..50430f539ed2b9a4d350d46ec0e8f0fc719dcac7 100644 GIT binary patch delta 72 zcmZ4WnQ_%;#tFJY*LWBh7<>}T(##C>jP(o+3_>>Auj3XM28#LzdAdisItJMqT3TA@ b8kp-ES!{mGeMF5hZ1X{*Fp!0gx>czYG8X08C+aW;QS`GcGYOFjKKVt_!mS48$e@ZL`rbP85^i KG9R-aM~gDm5fQHd diff --git a/tests/fixtures/test.m4a b/tests/fixtures/test.m4a index c469dd9e411c678ec2b7548df4533c6f1fe4df0a..e9b54d44d26e997bae7a2cd2de3457f73663aa1e 100644 GIT binary patch delta 169 zcmX@`fa$;krVadIj7K*MgvBvV7K*mq{2}TR6Jyxqh6rV2iM+(zR0bgM5At;P^mFua zaSU+)JBS_BB($Y-V&`{UN5XcDv>6`p6oKp>?CDPR~2q=iI+(OsDT-V3~ pC=&xyzIk@I4D;lcB+JdeW2drh-jG`Dws}H|0wWW1{ziptW&qwoFMn*rmP MlMtN*v!R+kVBy*kkN^Mx delta 27 lcmV+$0ObGQ^#kAa1F)O{v%3M}n3JHEACnNB1hb)TBzmlT?~Jajr}~FGwgT#4|X=*0HE0v$%vEDBu*}Yinp^ED2;f`}_L3`i0m!=jZ08 z=9L(7K^aCMjX=o&Panq+Pk%pKLr$Q8i(`nZt&xGD2_J~#8SLyA>g(hhWNWBz#03;_ zcl8T$wGGNo&ej9+d?JH9Cm&~w5C=KUKgiQP($x{D)Y8&I*T7uY$YQcFV~;Y($swL0 qKCZSQnI$=?0w8XXqq8^EU}JqlgURwtw%m>gFgcy+_2xKsI|Bfb-beBP delta 334 zcmZ3SJ|};IEAQzy=Vo#IhXCP;eiFt)3=9n1Kpf&3;^S%?l39|I3gSBYIEDJ!I_4ym z=JJBLK_Q;OA-0Z1C7Hz~6Dwt6xqyQ1u6{wTwn6#H*_=S0i(`nZt&xGDi2#rj66EOY z?HB6n#exKVa; yGh+{<-eh}5waLbemXnQ`Y`I+>fxrnwxKFNPdOcat*l4nXu`-=8+vZeeI|BfT{91Pa diff --git a/tests/fixtures/test.wav b/tests/fixtures/test.wav index 155d88bdb51e4e8ea0e377305b02acf669d59e08..b8c1f9a650c2f414baacf4d4fa4fa3fae89906de 100644 GIT binary patch delta 79 zcmZp=&DM0Atzipef24?ue-Hx$0}lfOv!SJ>nXaLsu94yN-blt2xsZs62%wZGP|82Z g(>>DFF^B=E#zNP?T-V5A`-e!z=|R)?tYJI`04~%OZ~y=R delta 31 ncmZp=&DM0Atzipef8_L~k&H>()uI@u1x;JYXgYn*8pdM))jge z7050Il96Cm5m=)bgCT<@5SlUQ0$GMY(g;X0FbDz72x9PLa0jY(WpD(tK>()O0;tY_ l!5pmG0w_L_{m%C7E6;DP^>b!q+RVK9qW?nX&9{S%ya9K`A%Fk? delta 42 vcmeC($JD!zX@eFsXN)%k7)Wk5W_I@8{Lasdkx83n^TZjFESql!8+ii&0J02v diff --git a/tests/fixtures/test.wv b/tests/fixtures/test.wv index 3722d28a24f04c02edc0c73543796137debfbc99..7f4118bdb343988ff70f96dcbd8c5119d952e21f 100644 GIT binary patch delta 520 zcmYjO&o2W(6n^!iL?j|4BpfOc7s~Edv=W!aY9xeI>DFy_4JNj`&2$M4h(Ex=ySRw0 z#yXO?Ik-w3RQL=09}d=bCo#!;@0;(v@5`IadA;en-t_ofIIXV$j6!(4MgeG!!0hTW z$Vy6F=UK%Bc|vK4+C@q@D3UxWh%B`^g`8vV2`0~pxd*8US!#_$s%7k#Ak2Rh?t5Kn z(aST}q7dd-G(P8)%FH4zb8L{~!h^f4;sX8@29rXo**naplmjI|4Z$w6L$xu?32+0; zH+@oD0p79&9zf?qn~%>47WNP>W6;ECUl5Bk7BMfi5vg` delta 502 zcmbPxo~h|P(}rE)PFwsL7_=C`U;%_;U|@wZ9CMONa~Z%S53-PgV^K+FaS235VRC(h z%;bUyQGRBq8m9nX216s`$zLKBMH!)D&iT0onK_9infZAPhLd9>RVLq#l$7R#%jTt& zCYR(FF(l{bC4z{g&HoeX+$P_TkWhme>ylZV%wVW*#17Tul9iiTky^x%l9dR=lPe-+ zV_^<=%}dYBO9jfMCNiWZu|PGrr{)!Q6Z5bCy4{95++`p zUsS@7sH2brlAOFcDr)k^2ytPUExusua}ybIlO`{Wsh#W_lQH>FTo{loGFdJ*b8>xj z`sDW5OOqv|gEqIuS22PmBI7bAKZ_2YED@hOnLkp3k$rMPl*DAcD4xmjQEHQ|qAVuQ zh!PHiSrL+1l9S2+CSkq^DN0PvW-!(_G=PajrY06K7#SFvI0m?eIJ&z=TmbqHDP9|( HzG46XL`r_4 From 478845bc5dba78a65c065b64488db8ed945c3ef6 Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 6 Apr 2026 22:28:37 -0400 Subject: [PATCH 2/7] fix(plugins): fix race between KVStore cleanup goroutine and Close (navidrome/apple-music-plugin#7) The cleanupLoop goroutine could execute cleanupExpired against a closed database because Close() did not wait for the goroutine to exit before calling db.Close(). This caused 'sql: database is closed' errors during plugin unload or shutdown. Close() now cancels the cleanup goroutine's context and waits for it to finish via a sync.WaitGroup before running the final cleanup and closing the database. Signed-off-by: Deluan --- plugins/host_kvstore.go | 24 +++++++++++++----------- plugins/host_kvstore_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 11 deletions(-) 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) From 1044c173cb3759883351f9ebac547ae89386e0a0 Mon Sep 17 00:00:00 2001 From: fxj368 <62541194+fxj368@users.noreply.github.com> Date: Tue, 7 Apr 2026 23:11:05 +0800 Subject: [PATCH 3/7] fix(ui): update Chinese (Simplified) translation (#5323) --- resources/i18n/zh-Hans.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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": "开发标志(可能会更改/删除)", From 1de4e43d29cb0e3c1945641915eeda23916dde31 Mon Sep 17 00:00:00 2001 From: Deluan Date: Tue, 7 Apr 2026 15:30:21 -0400 Subject: [PATCH 4/7] fix(gotaglib): update go-taglib to fix issue with empty id3v2 frames Signed-off-by: Deluan --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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= From 9e2c6adffdf9cdff4a05c70e4b48f647032515b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:07:46 -0400 Subject: [PATCH 5/7] chore(deps-dev): bump vite from 7.3.1 to 7.3.2 in /ui (#5321) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.1 to 7.3.2. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.3.2/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 7.3.2 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ui/package-lock.json | 48 ++++---------------------------------------- ui/package.json | 2 +- 2 files changed, 5 insertions(+), 45 deletions(-) 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" }, From 36a7be9eaf822447ce531ef5ee06c87dae1f8698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Tue, 7 Apr 2026 20:11:38 -0400 Subject: [PATCH 6/7] fix(transcoding): include ffprobe in MSI and fall back gracefully when absent (#5326) * fix(msi): include ffprobe executable in MSI build Signed-off-by: Deluan * feat(ffmpeg): add IsProbeAvailable() to FFmpeg interface Add runtime check for ffprobe binary availability with cached result and startup logging. When ffprobe is missing, logs a warning at startup. * feat(stream): guard MakeDecision behind ffprobe availability When ffprobe is not available, MakeDecision returns a decision with ErrorReason set and both CanDirectPlay and CanTranscode false, instead of failing with an opaque exec error. * feat(subsonic): only advertise transcoding extension when ffprobe is available The OpenSubsonic transcoding extension is now conditionally included based on ffprobe availability, so clients know not to call getTranscodeDecision when ffprobe is missing. * refactor(ffmpeg): move ffprobe startup warning to initial_setup Move the ffprobe availability warning from the lazy IsProbeAvailable() check to checkFFmpegInstallation() in server/initial_setup.go, alongside the existing ffmpeg warning. This ensures the warning appears at startup rather than on first endpoint call. * fix(e2e): set noopFFmpeg.IsProbeAvailable to true The e2e tests use pre-populated probe data and don't need a real ffprobe binary. Setting IsProbeAvailable to true allows the transcode decision logic to proceed normally in e2e tests. * fix(stream): only guard on ffprobe when probing is needed Move the IsProbeAvailable() guard inside the SkipProbe check so that legacy stream requests (which pass SkipProbe: true) are not blocked when ffprobe is missing. The guard only applies when probing is actually required (i.e., getTranscodeDecision endpoint). * refactor(stream): fall back to tag metadata when ffprobe is unavailable Instead of blocking getTranscodeDecision when ffprobe is missing, fall back to tag-based metadata (same behavior as /rest/stream). The transcoding extension is always advertised. A startup warning still alerts admins when ffprobe is not found. * fix(stream): downgrade ffprobe-unavailable log to Debug Avoids log spam when clients call getTranscodeDecision repeatedly without ffprobe installed. The startup warning in initial_setup.go already alerts admins at Warn level. --------- Signed-off-by: Deluan --- core/ffmpeg/ffmpeg.go | 16 ++++++++++++++++ core/stream/decider.go | 12 ++++++++---- core/stream/decider_test.go | 1 + release/wix/build_msi.sh | 3 ++- release/wix/navidrome.wxs | 5 +++++ server/e2e/e2e_suite_test.go | 1 + server/initial_setup.go | 13 ++++++++----- tests/mock_ffmpeg.go | 7 ++++++- 8 files changed, 47 insertions(+), 11 deletions(-) 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/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/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/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 From 4570dec675f904aa0cecd09b2c842685b762527e Mon Sep 17 00:00:00 2001 From: Deluan Date: Wed, 8 Apr 2026 13:13:56 -0400 Subject: [PATCH 7/7] fix(ui): refine image filters for playing and paused states in SquiddiesGlass Signed-off-by: Deluan --- ui/src/common/SongDatagrid.jsx | 1 - ui/src/themes/SquiddiesGlass.js | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) 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], }, },