From 9a2eb483e8e9ed21c75ebadccab59fe35ad40cd2 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sat, 6 Jun 2026 10:58:13 -0400 Subject: [PATCH 01/18] fix(transcode): log warning for invalid or stale transcode tokens Signed-off-by: Deluan --- server/subsonic/transcode.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/subsonic/transcode.go b/server/subsonic/transcode.go index 4e494b324..578ad44fc 100644 --- a/server/subsonic/transcode.go +++ b/server/subsonic/transcode.go @@ -370,6 +370,7 @@ func (api *Router) GetTranscodeStream(w http.ResponseWriter, r *http.Request) (* if err != nil { switch { case errors.Is(err, stream.ErrTokenInvalid), errors.Is(err, stream.ErrTokenStale): + log.Warn(ctx, "Invalid or stale transcode token", "mediaID", mediaID, err) http.Error(w, "Gone", http.StatusGone) default: log.Error(ctx, "Error validating transcode params", err) From 5c387630ffc8042755cfdcd9edaecdb427592331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Sun, 7 Jun 2026 12:29:17 -0400 Subject: [PATCH 02/18] fix(ui): update Estonian translations from POEditor (#5573) Co-authored-by: navidrome-bot --- resources/i18n/et.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/resources/i18n/et.json b/resources/i18n/et.json index d511c1246..b0131f4ea 100644 --- a/resources/i18n/et.json +++ b/resources/i18n/et.json @@ -154,12 +154,12 @@ "currentPassword": "Senine salasõna", "newPassword": "Uus salasõna", "token": "Tunnusluba", - "lastAccessAt": "Viimasti avatud", + "lastAccessAt": "Viimati avatud", "libraries": "Kogumikud" }, "helperTexts": { "name": "Sinu nime muudatused on näha järgmisel sisselogimisel", - "libraries": "Vali selle kasutaja jaoks konkreetsed kogumikus või jäta vaikimisi väärtuse kasutamiseks tühjaks" + "libraries": "Vali selle kasutaja jaoks konkreetsed kogumikud või jäta vaikimisi väärtuse kasutamiseks tühjaks" }, "notifications": { "created": "Kasutaja on lisatud", @@ -413,10 +413,10 @@ }, "ra": { "auth": { - "welcome1": "Aitäh, et paigaldasite Navidrome'i!", + "welcome1": "Aitäh, et paigaldasid Navidrome'i!", "welcome2": "Alustamiseks lisa peakasutaja", "confirmPassword": "Korda salasõna", - "buttonCreateAdmin": "Loo admin", + "buttonCreateAdmin": "Lisa peakasutaja", "auth_check_error": "Jätkamiseks palun logi sisse", "user_menu": "Profiil", "username": "Kasutajanimi", @@ -427,7 +427,7 @@ "insightsCollectionNote": "Navidrome kogub anonüümset kasutustusstatistikat, mille alusel on võimalik projekti paremaks muuta. Klõpsides [siin], saad lugeda lisateavet ning soovi korral sellest kogumisest loobuda" }, "validation": { - "invalidChars": "Palun kasutage ainult tähti ja numbreid", + "invalidChars": "Palun kasuta ainult tähti ja numbreid", "passwordDoesNotMatch": "Salasõnad ei kattu", "required": "Nõutav", "minLength": "Pikkus peab olema vähemalt %{min} tähemärki", @@ -558,8 +558,8 @@ }, "message": { "note": "MÄRGE", - "transcodingDisabled": "Transkodeeringu seadistuse muutmine läbi veebiliidese ei ole turvariskide tõttu saadaval. Kui soovite muuta või lisada transkodeerimisega seotud seadistusi, taaskäivitage server %{config} valikuga.", - "transcodingEnabled": "Navidrome käivitati %{config} valikuga, mis lubab läbi veebiliidese transkodeerimisseadistuste jooksutada süsteemikäsklusi. Turvakaalutlustel on soovitatav kasutada seda valikut ainult transkodeerimisseadete muutmiseks.", + "transcodingDisabled": "Teisendusseadistuste muutmine läbi veebiliidese ei ole turvariskide tõttu saadaval. Kui soovid muuta või lisada teisendamisega seotud seadistusi, taaskäivita server %{config} valikuga.", + "transcodingEnabled": "Navidrome käivitati %{config} valikuga, mis lubab läbi veebiliidese teisendusseadistuste käivitada süsteemikäsklusi. Turvakaalutlustel on soovitatav kasutada seda valikut ainult teisendusvalikute muutmiseks.", "songsAddedToPlaylist": "Lisasin ühe loo esitusloendisse |||| Lisasin %{smart_count} lugu esitusloendisse", "noPlaylistsAvailable": "Pole saadaval", "delete_user_title": "Kustuta kasutaja „%{name}“", @@ -603,13 +603,13 @@ }, "menu": { "library": "Kogumik", - "settings": "Seaded", + "settings": "Seadistused", "version": "Versioon", - "theme": "Teema", + "theme": "Kujundus", "personal": { "name": "Isiklik", "options": { - "theme": "Teema", + "theme": "Kujundus", "language": "Keel", "defaultView": "Vaikimisi vaade", "desktop_notifications": "Teavitused töölaual", From 1b46b9771229ee903d094968337d3e2efcfcbf20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Mon, 8 Jun 2026 08:22:59 -0400 Subject: [PATCH 03/18] fix(ui): update Indonesian translations from POEditor (#5575) Co-authored-by: navidrome-bot --- resources/i18n/id.json | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/resources/i18n/id.json b/resources/i18n/id.json index cdba66663..762ce4ebb 100644 --- a/resources/i18n/id.json +++ b/resources/i18n/id.json @@ -37,7 +37,10 @@ "sampleRate": "Sample rate", "missing": "Hilang", "libraryName": "Pustaka", - "composer": "Komposer" + "composer": "Komposer", + "disc": "Disk %{discNumber}", + "albumGain": "Album gain", + "trackGain": "Trek gain" }, "actions": { "addToQueue": "Tambah ke antrean", @@ -353,7 +356,8 @@ "allUsers": "Izinkan semua pengguna", "selectedUsers": "Pengguna yang dipilih", "allLibraries": "Izinkan semua pustaka", - "selectedLibraries": "Pustaka dipilih" + "selectedLibraries": "Pustaka dipilih", + "allowWriteAccess": "Izinkan akses tulis" }, "sections": { "status": "Status", @@ -398,7 +402,8 @@ "librariesRequired": "Plugin ini membutuhkan akses ke informasi pustaka. Pilih beberapa pustaka yang bisa diakses, atau aktifkan 'Izinkan semua pustaka'.", "requiredHosts": "Hosts diperlukan", "configValidationError": "Validasi konfigurasi gagal:", - "schemaRenderError": "Tidak dapat menampilkan form konfigurasi. Skema plugin mungkin tidak valid." + "schemaRenderError": "Tidak dapat menampilkan form konfigurasi. Skema plugin mungkin tidak valid.", + "allowWriteAccessHelp": "Ketika diaktifkan, plugin dapat mengubah file di direktori pustaka. Bawaannya, plugin hanya memiliki akses read-only" }, "placeholders": { "configKey": "key", @@ -588,7 +593,13 @@ "remove_all_missing_content": "Apa kamu yakin ingin menghapus semua file dari database? Ini akan menghapus permanen dan apapun referensi ke mereka, termasuk hitungan pemutaran dan rating mereka.", "noSimilarSongsFound": "Tidak ada lagu yang serupa ditemukan", "noTopSongsFound": "Tidak ada lagu teratas ditemukan", - "startingInstantMix": "Memuat Mix Instan..." + "startingInstantMix": "Memuat Mix Instan...", + "uploadCover": "Unggah Sampul", + "removeCover": "Hapus Sampul", + "coverUploaded": "Sampul diperbarui", + "coverRemoved": "Sampul dihapus", + "coverUploadError": "Kesalahan mengunggah sampul", + "coverRemoveError": "Kesalahan menghapus sampul" }, "menu": { "library": "Pustaka", @@ -674,7 +685,8 @@ "exportSuccess": "Konfigurasi sudah diekspor ke papan klip dalam bentuk format TOML", "exportFailed": "Gagal menyalin konfigurasi", "devFlagsHeader": "Flag Pengembangan (subyek untuk perubahan/pemindahan)", - "devFlagsComment": "Ini adalan pengaturan eksperimen dan mungkin akan dihapus di versi mendatang" + "devFlagsComment": "Ini adalan pengaturan eksperimen dan mungkin akan dihapus di versi mendatang", + "downloadToml": "Unduh Konfigurasi (TOML)" } }, "activity": { From 9cd2cd0a8b8675a5229fe2ee79ec71b5fdde0b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Tue, 9 Jun 2026 19:27:15 -0400 Subject: [PATCH 04/18] fix(ui): load ND_DEFAULTLANGUAGE on app startup (#4000) * fix: load ND_DEFAULTLANGUAGE on app startup Added in to apply on initial mount, ensuring the locale is set even when the login page is skipped by reverse-proxy authentication. Removed the redundant language-init effect from . Fixes #3605. * style(ui): format App.jsx with Prettier Ran Prettier on ui/src/App.jsx to satisfy code style checks after adding default-language useEffect. * fix(ui): move default language initialization to Admin component Signed-off-by: Deluan * fix(ui): streamline locale setting in App component Signed-off-by: Deluan --------- Signed-off-by: Deluan --- ui/src/App.jsx | 28 ++++++++++++++++++++++++++-- ui/src/layout/Login.jsx | 24 +----------------------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/ui/src/App.jsx b/ui/src/App.jsx index 35eaee3eb..d10aa5a33 100644 --- a/ui/src/App.jsx +++ b/ui/src/App.jsx @@ -1,7 +1,12 @@ import ReactGA from 'react-ga' import { Provider } from 'react-redux' import { createHashHistory } from 'history' -import { Admin as RAAdmin, Resource } from 'react-admin' +import { + Admin as RAAdmin, + Resource, + useSetLocale, + useRefresh, +} from 'react-admin' import { HotKeys } from 'react-hotkeys' import dataProvider from './dataProvider' import authProvider from './authProvider' @@ -36,7 +41,7 @@ import { transcodingReducer, } from './reducers' import createAdminStore from './store/createAdminStore' -import { i18nProvider } from './i18n' +import { i18nProvider, retrieveTranslation } from './i18n' import config, { shareInfo } from './config' import { keyMap } from './hotkeys' import useChangeThemeColor from './useChangeThemeColor' @@ -44,6 +49,7 @@ import SharePlayer from './share/SharePlayer' import { HTML5Backend } from 'react-dnd-html5-backend' import { DndProvider } from 'react-dnd' import missing from './missing/index.js' +import { useEffect } from 'react' const history = createHashHistory() @@ -84,6 +90,24 @@ const App = () => ( ) const Admin = (props) => { + const setLocale = useSetLocale() + const refresh = useRefresh() + useEffect(() => { + if (config.defaultLanguage !== '' && !localStorage.getItem('locale')) { + retrieveTranslation(config.defaultLanguage) + .then(() => setLocale(config.defaultLanguage)) + .then(() => { + localStorage.setItem('locale', config.defaultLanguage) + refresh(true) + }) + .catch((e) => { + // eslint-disable-next-line no-console + console.error( + 'Cannot load language "' + config.defaultLanguage + '": ' + e, + ) + }) + } + }, [setLocale, refresh]) useChangeThemeColor() /* eslint-disable react/jsx-key */ return ( diff --git a/ui/src/layout/Login.jsx b/ui/src/layout/Login.jsx index a84e01f4d..a7763cff3 100644 --- a/ui/src/layout/Login.jsx +++ b/ui/src/layout/Login.jsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, useEffect } from 'react' +import React, { useState, useCallback } from 'react' import PropTypes from 'prop-types' import { Field, Form } from 'react-final-form' import { useDispatch } from 'react-redux' @@ -13,8 +13,6 @@ import { createMuiTheme, useLogin, useNotify, - useRefresh, - useSetLocale, useTranslate, useVersion, } from 'react-admin' @@ -24,7 +22,6 @@ import Notification from './Notification' import useCurrentTheme from '../themes/useCurrentTheme' import config from '../config' import { clearQueue } from '../actions' -import { retrieveTranslation } from '../i18n' import { INSIGHTS_DOC_URL } from '../consts.js' const useStyles = makeStyles( @@ -407,27 +404,8 @@ Login.propTypes = { // the right theme const LoginWithTheme = (props) => { const theme = useCurrentTheme() - const setLocale = useSetLocale() - const refresh = useRefresh() const version = useVersion() - useEffect(() => { - if (config.defaultLanguage !== '' && !localStorage.getItem('locale')) { - retrieveTranslation(config.defaultLanguage) - .then(() => { - setLocale(config.defaultLanguage).then(() => { - localStorage.setItem('locale', config.defaultLanguage) - }) - refresh(true) - }) - .catch((e) => { - throw new Error( - 'Cannot load language "' + config.defaultLanguage + '": ' + e, - ) - }) - } - }, [refresh, setLocale]) - return ( From b6fba33b1406fddd43c536e453a77164bff7c5d5 Mon Sep 17 00:00:00 2001 From: Deluan Date: Tue, 9 Jun 2026 08:50:09 -0400 Subject: [PATCH 05/18] chore(docs): add Danian hosting option to installation instructions Signed-off-by: Deluan --- release/goreleaser.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release/goreleaser.yml b/release/goreleaser.yml index e5035adda..103f2beaf 100644 --- a/release/goreleaser.yml +++ b/release/goreleaser.yml @@ -114,7 +114,7 @@ release: ## Where to go next? * Read installation instructions on our [website](https://www.navidrome.org/docs/installation/). - * Host Navidrome on [PikaPods](https://www.pikapods.com/pods/navidrome) for a simple cloud solution. + * Host Navidrome on [PikaPods](https://www.pikapods.com/pods/navidrome) or [Danian](https://danian.co/navidrome?nd) for a simple cloud solution. * Reach out on [Discord](https://discord.gg/xh7j7yF), [Reddit](https://www.reddit.com/r/navidrome/) and [Twitter](https://twitter.com/navidrome)! # Add the MSI installers to the release From bd3192be0b36cb63d0b01b618c9e79226db0e0a0 Mon Sep 17 00:00:00 2001 From: Deluan Date: Tue, 9 Jun 2026 19:29:51 -0400 Subject: [PATCH 06/18] fix(server): make DB `PRAGMA optimize` error non-fatal Signed-off-by: Deluan --- db/db.go | 1 - 1 file changed, 1 deletion(-) diff --git a/db/db.go b/db/db.go index 168c12122..6e5b2f569 100644 --- a/db/db.go +++ b/db/db.go @@ -51,7 +51,6 @@ func Db() *sql.DB { _, err = db.Exec("PRAGMA optimize=0x10002") if err != nil { log.Error("Error applying PRAGMA optimize", err) - return nil } } return db From 37e5a1d248cd2d4cc9f8d4ea95f4f6aa4a49cdda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Mrzyg=C5=82=C3=B3d?= <77896685+devBoi76@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:47:54 +0200 Subject: [PATCH 07/18] fix(ui): Fix Nautiline theme font and width on mobile devices (#5590) Signed-off-by: devBoi76 --- ui/public/fonts/Unbounded-Variable.woff2 | Bin 50892 -> 259144 bytes ui/src/themes/nautiline.js | 1 - 2 files changed, 1 deletion(-) diff --git a/ui/public/fonts/Unbounded-Variable.woff2 b/ui/public/fonts/Unbounded-Variable.woff2 index 96d8ff5fa1426b92047112b5c3c7b02279dd523e..a2f74491f07bcf8019a62424d62a78f568882ed8 100644 GIT binary patch literal 259144 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 }DJEOzL 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 z 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 Rh|iUC@JA|Y5V=`7;1Hzsj>7z=F22P&~` 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 Date: Wed, 10 Jun 2026 21:12:31 -0400 Subject: [PATCH 08/18] fix(smartplaylist): support isMissing/isPresent operators on ReplayGain fields (#5585) * fix(smartplaylist): support isMissing/isPresent operators on ReplayGain fields ReplayGain values are stored in dedicated nullable columns (rg_album_gain, rg_album_peak, rg_track_gain, rg_track_peak) rather than in the media_file.tags JSON blob. The isMissing/isPresent operators previously only supported tag and role fields, causing two failure modes: 1. Using the documented alias names (replaygain_album_gain etc.) from PR #5256: these got registered as JSON tags from mappings.yaml, so isMissing queried json_tree(media_file.tags, '$.replaygain_album_gain') which is always empty -> the playlist matched ALL songs. 2. Using the canonical field names (rgalbumgain etc.): not a tag/role, so SQL generation returned an error. Because refreshSmartPlaylist deletes old tracks before regenerating, the abort left the playlist empty. Fix: add Nullable bool to FieldInfo and mark the four ReplayGain fields. Add static alias entries (replaygain_album_gain -> rgalbumgain etc.) with Numeric+Nullable set; because AddTagNames skips names already in the field map, these static entries take precedence over the mappings.yaml tag registration. missingExpr now emits IS NULL / IS NOT NULL for nullable column fields instead of the json_tree lookup. Fixes #5584 * chore(smartplaylist): address code review feedback - Simplify the isMissing/isPresent unsupported-field error message, removing the internal "nullable fields" jargon - Standardize comments on mappings.yaml (the actual filename) - Clarify the alias precedence comment in the LookupField test --- model/criteria/fields.go | 31 +++++++++------ model/criteria/fields_test.go | 22 +++++++++++ persistence/criteria_sql.go | 18 ++++++++- persistence/criteria_sql_test.go | 20 ++++++++-- persistence/e2e/e2e_suite_test.go | 22 +++++++---- persistence/e2e/smartplaylist_test.go | 57 +++++++++++++++++++++++++++ 6 files changed, 146 insertions(+), 24 deletions(-) diff --git a/model/criteria/fields.go b/model/criteria/fields.go index 9eafff7ab..36206712f 100644 --- a/model/criteria/fields.go +++ b/model/criteria/fields.go @@ -4,13 +4,14 @@ import "strings" // FieldInfo contains semantic metadata about a criteria field. type FieldInfo struct { - Alias string // If set, this field is a backward-compat alias for another canonical name - IsTag bool - IsRole bool - Numeric bool - Boolean bool + Alias string // If set, this field is a backward-compat alias for another canonical name + IsTag bool + IsRole bool + Numeric bool + Boolean bool + Nullable bool // If set, this column field can be NULL, so isMissing/isPresent are supported on it - tagAlias string // If set, a tag name from mappings.yml that resolves to this field + tagAlias string // If set, a tag name from mappings.yaml that resolves to this field name string // Canonical name, populated by LookupField from the map key } @@ -80,15 +81,23 @@ var fieldMap = map[string]FieldInfo{ "mbz_recording_id": {}, "mbz_release_track_id": {}, "mbz_release_group_id": {}, - "rgalbumgain": {Numeric: true}, - "rgalbumpeak": {Numeric: true}, - "rgtrackgain": {Numeric: true}, - "rgtrackpeak": {Numeric: true}, + "rgalbumgain": {Numeric: true, Nullable: true}, + "rgalbumpeak": {Numeric: true, Nullable: true}, + "rgtrackgain": {Numeric: true, Nullable: true}, + "rgtrackpeak": {Numeric: true, Nullable: true}, "library_id": {Numeric: true}, // Backward compatibility: albumtype is an alias for the releasetype tag. "albumtype": {Alias: "releasetype", IsTag: true}, + // Backward compatibility: the replaygain_* tag names (as written in metadata and in the + // PR #5256 example) are aliases for the canonical rg* column fields. Without these, the tag + // names would be registered as empty tags from mappings.yaml and isMissing would always match. + "replaygain_album_gain": {Alias: "rgalbumgain", Numeric: true, Nullable: true}, + "replaygain_album_peak": {Alias: "rgalbumpeak", Numeric: true, Nullable: true}, + "replaygain_track_gain": {Alias: "rgtrackgain", Numeric: true, Nullable: true}, + "replaygain_track_peak": {Alias: "rgtrackpeak", Numeric: true, Nullable: true}, + // Pseudo-field for random sorting "random": {}, } @@ -128,7 +137,7 @@ func AddRoles(roles []string) { } } -// AddTagNames adds tag names to the field map. This is used to add all tags mapped in the `mappings.yml` +// AddTagNames adds tag names to the field map. This is used to add all tags mapped in the `mappings.yaml` // configuration file. func AddTagNames(tagNames []string) { for _, tagName := range tagNames { diff --git a/model/criteria/fields_test.go b/model/criteria/fields_test.go index 5b6f53341..3dc0c7b90 100644 --- a/model/criteria/fields_test.go +++ b/model/criteria/fields_test.go @@ -53,5 +53,27 @@ var _ = Describe("fields", func() { gomega.Expect(field.IsRole).To(gomega.BeTrue()) }) + It("marks ReplayGain column fields as nullable", func() { + field, ok := LookupField("rgAlbumGain") + + gomega.Expect(ok).To(gomega.BeTrue()) + gomega.Expect(field.Name()).To(gomega.Equal("rgalbumgain")) + gomega.Expect(field.Nullable).To(gomega.BeTrue()) + gomega.Expect(field.IsTag).To(gomega.BeFalse()) + }) + + It("resolves replaygain_* tag names as aliases to nullable column fields", func() { + // AddTagNames skips names already in the field map, so the startup tag registration + // (from mappings.yaml) must not convert the pre-registered alias into a tag field. + AddTagNames([]string{"replaygain_album_gain"}) + + field, ok := LookupField("replaygain_album_gain") + + gomega.Expect(ok).To(gomega.BeTrue()) + gomega.Expect(field.Name()).To(gomega.Equal("rgalbumgain")) + gomega.Expect(field.Nullable).To(gomega.BeTrue()) + gomega.Expect(field.IsTag).To(gomega.BeFalse()) + }) + }) }) diff --git a/persistence/criteria_sql.go b/persistence/criteria_sql.go index 37e4ae340..ee0baca18 100644 --- a/persistence/criteria_sql.go +++ b/persistence/criteria_sql.go @@ -218,8 +218,8 @@ func missingExpr(values map[string]any, checkAbsence bool) (squirrel.Sqlizer, er } return nil, fmt.Errorf("invalid field in criteria: %s", field) } - if !info.IsTag && !info.IsRole { - return nil, fmt.Errorf("isMissing/isPresent operator is only supported for tag and role fields, got: %s", field) + if !info.IsTag && !info.IsRole && !info.Nullable { + return nil, fmt.Errorf("isMissing/isPresent operator is not supported for field: %s", field) } b, ok := value.(bool) @@ -227,6 +227,20 @@ func missingExpr(values map[string]any, checkAbsence bool) (squirrel.Sqlizer, er return nil, fmt.Errorf("invalid boolean value for 'missing' expression: %s: %v", field, value) } negate := checkAbsence == b + + // Nullable column fields (e.g. ReplayGain) are stored in dedicated columns, not in the tags + // JSON, so "missing" maps to a NULL check on the column rather than a json_tree lookup. + if info.Nullable && !info.IsTag && !info.IsRole { + col, ok := fieldExpr(info.Name()) + if !ok || col == "" { + return nil, fmt.Errorf("invalid field in criteria: %s", field) + } + if negate { + return squirrel.Eq{col: nil}, nil + } + return squirrel.NotEq{col: nil}, nil + } + return jsonExpr(info, nil, negate), nil } diff --git a/persistence/criteria_sql_test.go b/persistence/criteria_sql_test.go index 5c8909e1c..8e801a703 100644 --- a/persistence/criteria_sql_test.go +++ b/persistence/criteria_sql_test.go @@ -14,7 +14,7 @@ import ( var _ = Describe("Smart playlist criteria SQL", func() { BeforeEach(func() { criteria.AddRoles([]string{"artist", "composer", "producer"}) - criteria.AddTagNames([]string{"genre", "mood", "releasetype", "recordingdate"}) + criteria.AddTagNames([]string{"genre", "mood", "releasetype", "recordingdate", "replaygain_album_gain"}) criteria.AddNumericTags([]string{"rate"}) }) @@ -85,6 +85,20 @@ var _ = Describe("Smart playlist criteria SQL", func() { "exists (select 1 from media_file_artists mfa where mfa.media_file_id = media_file.id and mfa.role = ?)", "composer"), Entry("isPresent role [false]", criteria.IsPresent{"composer": false}, "not exists (select 1 from media_file_artists mfa where mfa.media_file_id = media_file.id and mfa.role = ?)", "composer"), + // isMissing/isPresent — nullable column fields (ReplayGain) + Entry("isMissing rgAlbumGain [true]", criteria.IsMissing{"rgAlbumGain": true}, + "media_file.rg_album_gain IS NULL"), + Entry("isMissing rgAlbumGain [false]", criteria.IsMissing{"rgAlbumGain": false}, + "media_file.rg_album_gain IS NOT NULL"), + Entry("isPresent rgTrackPeak [true]", criteria.IsPresent{"rgTrackPeak": true}, + "media_file.rg_track_peak IS NOT NULL"), + Entry("isPresent rgTrackPeak [false]", criteria.IsPresent{"rgTrackPeak": false}, + "media_file.rg_track_peak IS NULL"), + // isMissing — replaygain_* tag-name alias resolves to the nullable column (issue #5584) + Entry("isMissing replaygain_album_gain alias [true]", criteria.IsMissing{"replaygain_album_gain": true}, + "media_file.rg_album_gain IS NULL"), + Entry("isPresent replaygain_album_gain alias [true]", criteria.IsPresent{"replaygain_album_gain": true}, + "media_file.rg_album_gain IS NOT NULL"), ) Describe("playlist permissions", func() { @@ -143,12 +157,12 @@ var _ = Describe("Smart playlist criteria SQL", func() { It("returns an error when isMissing is used with a regular field", func() { _, err := newSmartPlaylistCriteria(criteria.Criteria{Expression: criteria.IsMissing{"year": true}}).Where() - Expect(err).To(MatchError(ContainSubstring("isMissing/isPresent operator is only supported for tag and role fields"))) + Expect(err).To(MatchError(ContainSubstring("isMissing/isPresent operator is not supported for field"))) }) It("returns an error when isPresent is used with a regular field", func() { _, err := newSmartPlaylistCriteria(criteria.Criteria{Expression: criteria.IsPresent{"title": true}}).Where() - Expect(err).To(MatchError(ContainSubstring("isMissing/isPresent operator is only supported for tag and role fields"))) + Expect(err).To(MatchError(ContainSubstring("isMissing/isPresent operator is not supported for field"))) }) It("returns an error when isMissing has a non-boolean value", func() { diff --git a/persistence/e2e/e2e_suite_test.go b/persistence/e2e/e2e_suite_test.go index f42292f02..1ff2139e6 100644 --- a/persistence/e2e/e2e_suite_test.go +++ b/persistence/e2e/e2e_suite_test.go @@ -70,11 +70,13 @@ var ( func buildTestFS() { abbeyRoad := template(_t{ - "albumartist": "The Beatles", - "artist": "The Beatles", - "album": "Abbey Road", - "year": 1969, - "genre": "Rock;Blues", + "albumartist": "The Beatles", + "artist": "The Beatles", + "album": "Abbey Road", + "year": 1969, + "genre": "Rock;Blues", + "replaygain_album_gain": "-6.5 dB", + "replaygain_album_peak": "0.98", }) ledZepIV := template(_t{ "albumartist": "Led Zeppelin", @@ -116,12 +118,16 @@ func buildTestFS() { fs := storagetest.FakeFS{} fs.SetFiles(fstest.MapFS{ "Rock/The Beatles/Abbey Road/01 - Come Together.mp3": abbeyRoad(track(1, "Come Together", - _t{"genre": "Rock;Blues", "composer": "Lennon/McCartney", "bpm": 120, "grouping": "Beatles Tracks"})), + _t{"genre": "Rock;Blues", "composer": "Lennon/McCartney", "bpm": 120, "grouping": "Beatles Tracks", + "replaygain_track_gain": "-7.1 dB", "replaygain_track_peak": "0.95"})), "Rock/The Beatles/Abbey Road/02 - Something.mp3": abbeyRoad(track(2, "Something", - _t{"genre": "Rock", "composer": "Harrison", "bpm": 100, "grouping": "Beatles Tracks"})), + _t{"genre": "Rock", "composer": "Harrison", "bpm": 100, "grouping": "Beatles Tracks", + "replaygain_track_gain": "-6.0 dB", "replaygain_track_peak": "0.92"})), + // Stairway To Heaven has track gain but no album gain, to distinguish the two fields "Rock/Led Zeppelin/IV/01 - Stairway To Heaven.flac": ledZepIV(track(1, "Stairway To Heaven", _t{"genre": "Rock;Folk", "composer": "Page/Plant", "bpm": 82, "suffix": "flac", - "bitrate": 900, "samplerate": 44100, "bitdepth": 16})), + "bitrate": 900, "samplerate": 44100, "bitdepth": 16, + "replaygain_track_gain": "-8.25 dB", "replaygain_track_peak": "0.99"})), "Rock/Led Zeppelin/IV/02 - Black Dog.flac": ledZepIV(track(2, "Black Dog", _t{"genre": "Rock;Blues", "composer": "Page/Plant/Jones", "bpm": 150, "suffix": "flac", "bitrate": 900, "samplerate": 44100, "bitdepth": 16})), diff --git a/persistence/e2e/smartplaylist_test.go b/persistence/e2e/smartplaylist_test.go index a844dc982..4966d3b8a 100644 --- a/persistence/e2e/smartplaylist_test.go +++ b/persistence/e2e/smartplaylist_test.go @@ -371,4 +371,61 @@ var _ = Describe("Smart Playlists", func() { Expect(results).To(ConsistOf("Black Dog", "All Along the Watchtower")) }) }) + + // ReplayGain values are stored in nullable media_file columns (not in the tags JSON), so + // isMissing/isPresent translate to IS [NOT] NULL checks on those columns (issue #5584). + Describe("isMissing/isPresent on ReplayGain fields", func() { + It("isMissing finds tracks without album gain", func() { + results := evaluateRule(`{"all":[{"isMissing":{"rgalbumgain":true}}]}`) + Expect(results).To(ConsistOf("Stairway To Heaven", "Black Dog", "So What", + "Bohemian Rhapsody", "All Along the Watchtower", "We Are the Champions")) + }) + + It("isMissing false finds tracks with album gain", func() { + results := evaluateRule(`{"all":[{"isMissing":{"rgalbumgain":false}}]}`) + Expect(results).To(ConsistOf("Come Together", "Something")) + }) + + It("isPresent finds tracks with album gain", func() { + results := evaluateRule(`{"all":[{"isPresent":{"rgalbumgain":true}}]}`) + Expect(results).To(ConsistOf("Come Together", "Something")) + }) + + It("isPresent finds tracks with album peak", func() { + results := evaluateRule(`{"all":[{"isPresent":{"rgalbumpeak":true}}]}`) + Expect(results).To(ConsistOf("Come Together", "Something")) + }) + + It("isMissing distinguishes track gain from album gain", func() { + results := evaluateRule(`{"all":[{"isMissing":{"rgtrackgain":true}}]}`) + Expect(results).To(ConsistOf("Black Dog", "So What", "Bohemian Rhapsody", + "All Along the Watchtower", "We Are the Champions")) + }) + + It("isPresent finds tracks with track gain", func() { + results := evaluateRule(`{"all":[{"isPresent":{"rgtrackgain":true}}]}`) + Expect(results).To(ConsistOf("Come Together", "Something", "Stairway To Heaven")) + }) + + It("resolves the replaygain_album_gain alias to the rgalbumgain column", func() { + results := evaluateRule(`{"all":[{"isMissing":{"replaygain_album_gain":true}}]}`) + Expect(results).To(ConsistOf("Stairway To Heaven", "Black Dog", "So What", + "Bohemian Rhapsody", "All Along the Watchtower", "We Are the Champions")) + }) + + It("resolves the replaygain_track_gain alias to the rgtrackgain column", func() { + results := evaluateRule(`{"all":[{"isPresent":{"replaygain_track_gain":true}}]}`) + Expect(results).To(ConsistOf("Come Together", "Something", "Stairway To Heaven")) + }) + + It("supports numeric comparisons through the replaygain_* alias", func() { + results := evaluateRule(`{"all":[{"gt":{"replaygain_track_gain":-7.5}}]}`) + Expect(results).To(ConsistOf("Come Together", "Something")) + }) + + It("combines isMissing on ReplayGain with other operators", func() { + results := evaluateRule(`{"all":[{"isMissing":{"rgalbumgain":true}},{"is":{"genre":"Blues"}}]}`) + Expect(results).To(ConsistOf("Black Dog", "All Along the Watchtower")) + }) + }) }) From 3b958dd6a725b10d227ada78d7840c968d240972 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Wed, 10 Jun 2026 23:15:58 -0400 Subject: [PATCH 09/18] refactor(stream): remove dead type branches from getIntClaim (#5594) The jwx library always deserializes numeric claims from a parsed token as float64, so the int and int64 branches in getIntClaim could never succeed and were dead code. Keep only the float64 path, which is the one actually exercised by the token round-trip, and update the comment to document the library behavior. --- core/stream/token.go | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/core/stream/token.go b/core/stream/token.go index 24a154b54..21a26ca93 100644 --- a/core/stream/token.go +++ b/core/stream/token.go @@ -92,17 +92,10 @@ func paramsFromToken(token jwt.Token) (*params, error) { return &p, nil } -// getIntClaim extracts an int claim from a JWT token, handling the case where -// the value may be stored as int64 or float64 (common in JSON-based JWT libraries). +// getIntClaim extracts a numeric claim from a JWT token. Numeric claims in a +// parsed token are always deserialized as float64, regardless of the type used +// when encoding. func getIntClaim(token jwt.Token, key string) int { - var v int - if err := token.Get(key, &v); err == nil { - return v - } - var v64 int64 - if err := token.Get(key, &v64); err == nil { - return int(v64) - } var f float64 if err := token.Get(key, &f); err == nil { return int(f) From 5ec6e6a8d48f5534be79bc6ee0064fa5a61e5c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Fri, 12 Jun 2026 15:53:37 -0400 Subject: [PATCH 10/18] fix(opensubsonic): make search3 empty-query pagination fast at large offsets (#5601) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(subsonic): make search3 empty-query pagination fast at large offsets Empty-query search3 (used by clients like Symfonium to sync the whole library) degraded linearly with songOffset: the offset optimization in optimizePagination keeps the original query's LEFT JOINs (annotation, bookmark, library) inside its rowid NOT IN subquery, making it as slow as plain OFFSET (~5s per page at offset 900K on a 920K-track library). Rewrite the empty-query branch of doSearch to use the same two-phase approach as the FTS search: Phase 1 paginates rowids on the bare main table, which SQLite satisfies with a covering index at any offset; Phase 2 hydrates only the page's rows with all JOINs. The Phase 2 hydration logic is extracted into hydrateRowidPage, now shared with ftsSearch.execute. Also replace the media_file_missing index with a composite covering index on (missing, library_id), so Phase 1 stays covering for non-admin users, whose queries include a library_id filter. The composite serves all missing-only lookups via its prefix. With a 920K-track / 85K-album test library, search3 empty-query responses are now flat (~0.1s) at every offset, for both admin and non-admin users (previously 3-5s at offsets above 600K). * refactor(persistence): share search Phase 1 contract and dedup junction fan-out Extract the Phase 1 query assembly that was duplicated between the FTS search and the empty-query search into executeTwoPhase: both paths now supply only their strategy-specific FROM/JOINs and ORDER BY, while the shared contract (missing filter, library access, options.Filters, and Max/Offset semantics) lives in one place. Also fix a pagination integrity bug: the artist library filter joins the library_artist junction table, so an artist present in multiple libraries produced duplicate rowids in Phase 1, corrupting offset-based pagination (short pages and repeated artists during full-library syncs). Phase 1 now applies DISTINCT whenever a junction-based LibraryFilter is set. DISTINCT is used instead of GROUP BY because bm25() cannot be evaluated in a grouped query; plain-filter tables (media_file, album) skip the dedup so their Phase 1 keeps the streaming covering-index plan. This also fixes the same duplication in the pre-existing FTS search path. * fix(persistence): pin artist search Phase 1 join order with CROSS JOIN search3 always filters artists by library (library_artist.library_id IN ...), and with the junction JOIN in the search Phase 1 rowid query SQLite chose to drive from library_artist, sorting every junction row with a temp b-tree on each page — a flat ~200ms penalty per request at 405K artists, even at offset 0 (the previous code avoided this by accident: its GROUP BY artist.id pinned an artist-driven plan). Use CROSS JOIN (SQLite's explicit join-order override) in a search-only variant of the artist library filter, keeping artist as the outer table so Phase 1 streams rowids in artist.id order from the primary key index and LIMIT/OFFSET short-circuits. The DISTINCT dedup stays and costs nothing under the streaming plan. Other artist queries keep the planner's freedom. With 405K artists, empty-query artist search is now 0.07s at offset 0 and 0.25s at offset 399K end-to-end (was 0.31s/0.34s before this fix, and up to 1.2s on master at deep offsets). Artist FTS text search is unaffected. --- ...dd_media_file_missing_library_id_index.sql | 13 ++++ persistence/artist_repository.go | 17 +++++- persistence/artist_repository_test.go | 61 +++++++++++++++++++ persistence/mediafile_repository_test.go | 43 +++++++++++++ persistence/sql_search.go | 58 ++++++++++++++++-- persistence/sql_search_fts.go | 46 ++------------ 6 files changed, 192 insertions(+), 46 deletions(-) create mode 100644 db/migrations/20260612171826_add_media_file_missing_library_id_index.sql diff --git a/db/migrations/20260612171826_add_media_file_missing_library_id_index.sql b/db/migrations/20260612171826_add_media_file_missing_library_id_index.sql new file mode 100644 index 000000000..8bd8ad5b8 --- /dev/null +++ b/db/migrations/20260612171826_add_media_file_missing_library_id_index.sql @@ -0,0 +1,13 @@ +-- +goose Up +-- Covering index for the rowid-only pagination query used by search3 with an empty query +-- (full library sync). It must cover both `missing` and `library_id` so SQLite never touches +-- the (wide) media_file rows while skipping over large offsets. +-- Replaces media_file_missing: the composite serves all `missing = ?` lookups via its prefix. +create index if not exists media_file_missing_library_id + on media_file(missing, library_id); +drop index if exists media_file_missing; + +-- +goose Down +create index if not exists media_file_missing + on media_file(missing); +drop index if exists media_file_missing_library_id; diff --git a/persistence/artist_repository.go b/persistence/artist_repository.go index cfdc499e0..aa3bc0776 100644 --- a/persistence/artist_repository.go +++ b/persistence/artist_repository.go @@ -540,13 +540,28 @@ func (r *artistRepository) RefreshStats(allArtists bool) (int64, error) { return totalRowsAffected, nil } +// applyLibraryFilterToSearchQuery is applyLibraryFilterToArtistQuery with the join order +// pinned via CROSS JOIN (SQLite's explicit join-order override): the search Phase 1 paginates +// rowids by artist.id, and when the planner drives from library_artist it must sort every +// junction row on every page (temp b-tree over the whole table). Keeping artist as the outer +// table streams rows in artist.id order from its primary key index, so LIMIT/OFFSET +// short-circuits. Search-only: other artist queries keep the planner's freedom. +func (r *artistRepository) applyLibraryFilterToSearchQuery(query SelectBuilder) SelectBuilder { + user := loggedUser(r.ctx) + query = query.CrossJoin("library_artist on library_artist.artist_id = artist.id") + if user.ID != invalidUserId && !user.IsAdmin { + query = query.Join("user_library on user_library.library_id = library_artist.library_id AND user_library.user_id = ?", user.ID) + } + return query +} + func (r *artistRepository) searchCfg() searchConfig { return searchConfig{ // Natural order for artists is more performant by ID, due to GROUP BY clause in selectArtist NaturalOrder: "artist.id", OrderBy: []string{"sum(json_extract(stats, '$.total.m')) desc", "name"}, MBIDFields: []string{"mbz_artist_id"}, - LibraryFilter: r.applyLibraryFilterToArtistQuery, + LibraryFilter: r.applyLibraryFilterToSearchQuery, } } diff --git a/persistence/artist_repository_test.go b/persistence/artist_repository_test.go index 076a9da3b..7003efec3 100644 --- a/persistence/artist_repository_test.go +++ b/persistence/artist_repository_test.go @@ -3,6 +3,7 @@ package persistence import ( "context" "encoding/json" + "fmt" "os" "path/filepath" @@ -594,6 +595,66 @@ var _ = Describe("ArtistRepository", func() { }) }) + Context("Empty Query (sync pagination)", func() { + It("does not duplicate artists that belong to multiple libraries", func() { + // An artist in two libraries has two library_artist rows; pagination + // must still enumerate it exactly once, at a stable offset. + Expect(lr.AddArtist(lib2.ID, artistBeatles.ID)).To(Succeed()) + + all, err := repo.Search("", model.QueryOptions{Max: 1000}) + Expect(err).ToNot(HaveOccurred()) + + seen := map[string]bool{} + var paged model.Artists + for offset := range len(all) { + page, err := repo.Search("", model.QueryOptions{Max: 1, Offset: offset}) + Expect(err).ToNot(HaveOccurred()) + for _, a := range page { + Expect(seen[a.ID]).To(BeFalse(), fmt.Sprintf("artist %s returned twice", a.ID)) + seen[a.ID] = true + } + paged = append(paged, page...) + } + Expect(paged).To(HaveLen(len(all))) + }) + + It("paginates all artists in natural order without overlaps or gaps", func() { + all, err := repo.Search("", model.QueryOptions{Max: 1000}) + Expect(err).ToNot(HaveOccurred()) + Expect(len(all)).To(BeNumerically(">", 1)) + + var paged model.Artists + pageSize := 2 + for offset := 0; offset < len(all); offset += pageSize { + page, err := repo.Search("", model.QueryOptions{Max: pageSize, Offset: offset}) + Expect(err).ToNot(HaveOccurred()) + paged = append(paged, page...) + } + Expect(paged).To(HaveLen(len(all))) + for i := range all { + Expect(paged[i].ID).To(Equal(all[i].ID)) + } + }) + + It("respects library filtering for restricted users", func() { + // Create an artist only in library 2 (not accessible to restricted user) + lib2Artist := model.Artist{ID: "empty-query-lib2-artist", Name: "Empty Query Lib2 Artist"} + Expect(repo.Put(&lib2Artist)).To(Succeed()) + Expect(lr.AddArtist(lib2.ID, lib2Artist.ID)).To(Succeed()) + + results, err := restrictedRepo.Search("", model.QueryOptions{Max: 1000}) + Expect(err).ToNot(HaveOccurred()) + for _, a := range results { + Expect(a.ID).ToNot(Equal(lib2Artist.ID), "Empty query search should respect library filtering") + } + + // Clean up + if raw, ok := repo.(*artistRepository); ok { + _, _ = raw.executeSQL(squirrel.Delete(raw.tableName).Where(squirrel.Eq{"id": lib2Artist.ID})) + } + }) + }) + Context("Headless Processes (No User Context)", func() { It("should see all artists from all libraries when no user is in context", func() { // Add artists to different libraries diff --git a/persistence/mediafile_repository_test.go b/persistence/mediafile_repository_test.go index 2bc9d0267..532e9c10f 100644 --- a/persistence/mediafile_repository_test.go +++ b/persistence/mediafile_repository_test.go @@ -652,6 +652,49 @@ var _ = Describe("MediaRepository", func() { _, _ = raw.executeSQL(squirrel.Delete(raw.tableName).Where(squirrel.Eq{"id": missingMediaFile.ID})) }) }) + + Context("empty query (natural order pagination)", func() { + It("returns all non-missing files in natural order", func() { + results, err := mr.Search("", model.QueryOptions{Max: 1000}) + Expect(err).ToNot(HaveOccurred()) + Expect(results).ToNot(BeEmpty()) + for _, result := range results { + Expect(result.Missing).To(BeFalse()) + } + }) + + It(`treats quoted empty query ("") the same as empty`, func() { + all, err := mr.Search("", model.QueryOptions{Max: 1000}) + Expect(err).ToNot(HaveOccurred()) + quoted, err := mr.Search(`""`, model.QueryOptions{Max: 1000}) + Expect(err).ToNot(HaveOccurred()) + Expect(quoted).To(HaveLen(len(all))) + }) + + It("paginates without overlaps or gaps", func() { + all, err := mr.Search("", model.QueryOptions{Max: 1000}) + Expect(err).ToNot(HaveOccurred()) + Expect(len(all)).To(BeNumerically(">", 3)) + + var paged model.MediaFiles + pageSize := 3 + for offset := 0; offset < len(all); offset += pageSize { + page, err := mr.Search("", model.QueryOptions{Max: pageSize, Offset: offset}) + Expect(err).ToNot(HaveOccurred()) + paged = append(paged, page...) + } + Expect(paged).To(HaveLen(len(all))) + for i := range all { + Expect(paged[i].ID).To(Equal(all[i].ID), fmt.Sprintf("row %d differs", i)) + } + }) + + It("returns empty page when offset is beyond the total", func() { + results, err := mr.Search("", model.QueryOptions{Max: 10, Offset: 100000}) + Expect(err).ToNot(HaveOccurred()) + Expect(results).To(BeEmpty()) + }) + }) }) Describe("FindByPaths", func() { diff --git a/persistence/sql_search.go b/persistence/sql_search.go index 43965ebb7..19cbaf24f 100644 --- a/persistence/sql_search.go +++ b/persistence/sql_search.go @@ -1,6 +1,7 @@ package persistence import ( + "fmt" "strings" . "github.com/Masterminds/squirrel" @@ -20,8 +21,10 @@ type searchConfig struct { NaturalOrder string // ORDER BY for empty-query results (e.g. "album.rowid") OrderBy []string // ORDER BY for text search results (e.g. ["name"]) MBIDFields []string // columns to match when query is a UUID - // LibraryFilter overrides the default applyLibraryFilter for FTS Phase 1. - // Needed when library access requires a junction table (e.g. artist → library_artist). + // LibraryFilter overrides the default applyLibraryFilter for the rowid Phase 1 of + // two-phase searches (FTS and empty-query). Needed when library access goes through a + // junction table (e.g. artist → library_artist), whose JOIN can fan out rowids for + // entities in multiple libraries — Phase 1 dedups whenever this is set. LibraryFilter func(sq SelectBuilder) SelectBuilder } @@ -57,8 +60,8 @@ func (r sqlRepository) doSearch(sq SelectBuilder, q string, results any, cfg sea // Empty query (OpenSubsonic `search3?query=""`) — return all in natural order. if q == "" || q == `""` { - sq = sq.OrderBy(cfg.NaturalOrder) - return r.queryAll(sq, results, options) + rowidCore := Select(r.tableName + ".rowid").From(r.tableName).OrderBy(cfg.NaturalOrder) + return r.executeTwoPhase(sq, results, rowidCore, cfg, options) } // MBID search: if query is a valid UUID, search by MBID fields instead @@ -82,6 +85,53 @@ func (r sqlRepository) doSearch(sq SelectBuilder, q string, results any, cfg sea return strategy.execute(r, sq, results, cfg, options) } +// executeTwoPhase runs a search in two phases: +// - Phase 1: rowidCore (strategy-specific FROM/JOINs and ORDER BY) plus the shared search +// contract applied here — non-missing rows only, library access, options.Filters, and +// pagination. Keeping Phase 1 free of the full SELECT's JOINs lets SQLite paginate via a +// covering index; with those JOINs, large offsets degrade to O(offset) join probes — +// multi-second responses on 100k+ libraries. +// - Phase 2: full SELECT with all JOINs, scoped to Phase 1's rowid page. +func (r sqlRepository) executeTwoPhase(sq SelectBuilder, results any, rowidCore SelectBuilder, cfg searchConfig, options model.QueryOptions) error { + rowidQuery := rowidCore. + Where(Eq{r.tableName + ".missing": false}) + if options.Max > 0 { + rowidQuery = rowidQuery.Limit(uint64(options.Max)) + } + if options.Offset > 0 { + rowidQuery = rowidQuery.Offset(uint64(options.Offset)) + } + if cfg.LibraryFilter != nil { + // Junction-table library filters can repeat rowids for entities in multiple + // libraries, which would corrupt offset-based pagination — dedup before paginating. + // (DISTINCT, not GROUP BY: bm25() can't be evaluated in a grouped query.) + rowidQuery = cfg.LibraryFilter(rowidQuery).Distinct() + } else { + rowidQuery = r.applyLibraryFilter(rowidQuery) + } + if options.Filters != nil { + rowidQuery = rowidQuery.Where(options.Filters) + } + return r.hydrateRowidPage(sq, rowidQuery, results) +} + +// hydrateRowidPage joins sq to the ordered rowid set produced by rowidQuery, preserving its +// ordering. rowidQuery must handle pagination itself; sq's LIMIT/OFFSET are stripped. +func (r sqlRepository) hydrateRowidPage(sq SelectBuilder, rowidQuery SelectBuilder, results any) error { + rowidSQL, rowidArgs, err := rowidQuery.ToSql() + if err != nil { + return fmt.Errorf("building rowid query: %w", err) + } + sq = sq.RemoveLimit().RemoveOffset() + rankedSubquery := fmt.Sprintf( + "(SELECT rowid as _rid, row_number() OVER () AS _rn FROM (%s)) AS _ranked", + rowidSQL, + ) + sq = sq.Join(rankedSubquery+" ON "+r.tableName+".rowid = _ranked._rid", rowidArgs...) + sq = sq.OrderBy("_ranked._rn") + return r.queryAll(sq, results) +} + func mbidExpr(tableName, mbid string, mbidFields ...string) Sqlizer { if uuid.Validate(mbid) != nil || len(mbidFields) == 0 { return nil diff --git a/persistence/sql_search_fts.go b/persistence/sql_search_fts.go index b90dc937b..bbae47fe8 100644 --- a/persistence/sql_search_fts.go +++ b/persistence/sql_search_fts.go @@ -284,11 +284,9 @@ func (s *ftsSearch) ToSql() (string, []any, error) { return sql, []any{s.matchExpr}, nil } -// execute runs a two-phase FTS5 search: -// - Phase 1: lightweight rowid query (main table + FTS + library filter) for ranking and pagination. -// - Phase 2: full SELECT with all JOINs, scoped to Phase 1's rowid set. -// -// Complex ORDER BY (function calls, aggregations) are dropped from Phase 1. +// execute runs a two-phase FTS5 search (see executeTwoPhase): Phase 1 here contributes the +// FTS MATCH join and BM25 rank ordering. Complex ORDER BY (function calls, aggregations) are +// dropped from Phase 1. func (s *ftsSearch) execute(r sqlRepository, sq SelectBuilder, dest any, cfg searchConfig, options model.QueryOptions) error { qualifiedOrderBys := []string{s.rankExpr} for _, ob := range cfg.OrderBy { @@ -297,45 +295,11 @@ func (s *ftsSearch) execute(r sqlRepository, sq SelectBuilder, dest any, cfg sea } } - // Phase 1: fresh query — must set LIMIT/OFFSET from options explicitly. - // Mirror applyOptions behavior: Max=0 means no limit, not LIMIT 0. - rowidQuery := Select(s.tableName+".rowid"). + rowidCore := Select(s.tableName+".rowid"). From(s.tableName). Join(s.ftsTable+" ON "+s.ftsTable+".rowid = "+s.tableName+".rowid AND "+s.ftsTable+" MATCH ?", s.matchExpr). - Where(Eq{s.tableName + ".missing": false}). OrderBy(qualifiedOrderBys...) - if options.Max > 0 { - rowidQuery = rowidQuery.Limit(uint64(options.Max)) - } - if options.Offset > 0 { - rowidQuery = rowidQuery.Offset(uint64(options.Offset)) - } - - // Library filter + musicFolderId must be applied here, before pagination. - if cfg.LibraryFilter != nil { - rowidQuery = cfg.LibraryFilter(rowidQuery) - } else { - rowidQuery = r.applyLibraryFilter(rowidQuery) - } - if options.Filters != nil { - rowidQuery = rowidQuery.Where(options.Filters) - } - - rowidSQL, rowidArgs, err := rowidQuery.ToSql() - if err != nil { - return fmt.Errorf("building FTS rowid query: %w", err) - } - - // Phase 2: strip LIMIT/OFFSET from sq (Phase 1 handled pagination), - // join on the ranked rowid set to hydrate with full columns. - sq = sq.RemoveLimit().RemoveOffset() - rankedSubquery := fmt.Sprintf( - "(SELECT rowid as _rid, row_number() OVER () AS _rn FROM (%s)) AS _ranked", - rowidSQL, - ) - sq = sq.Join(rankedSubquery+" ON "+s.tableName+".rowid = _ranked._rid", rowidArgs...) - sq = sq.OrderBy("_ranked._rn") - return r.queryAll(sq, dest) + return r.executeTwoPhase(sq, dest, rowidCore, cfg, options) } // qualifyOrderBy prepends tableName to a simple column name. Returns empty string for From da56df3160aa0d0c452d5ad66b7154919e335c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Sat, 13 Jun 2026 13:15:20 -0400 Subject: [PATCH 11/18] feat(smartplaylist): extend isMissing/isPresent to bpm, bitDepth and many text fields (#5603) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(smartplaylist): support isMissing/isPresent on mbz_* and lyrics fields Mark the six mbz_* MusicBrainz ID columns and the lyrics column as Nullable in the criteria field map, then extend missingExpr to handle string columns where absence is encoded as NULL or empty string (plus '[]' for lyrics). The Numeric/Boolean path (ReplayGain) is preserved via an explicit type check. * refactor(model): make MediaFile BPM and BitDepth nullable pointers Convert BPM and BitDepth fields in model.MediaFile from int to *int so that 'tag absent' is distinguishable from zero. The metadata mapper now uses NullableFloat for BPM (nil when absent or zero/unparseable) and only sets BitDepth when the audio property is non-zero (lossy codecs report 0). All read sites use gg.V() for zero-fallback deref so Subsonic API output and transcoding behaviour are byte-identical to before. The persistence layer bridges the existing NOT NULL DB columns by coercing nil to 0 on write and 0 back to nil on read in PostMapArgs/PostScan; a later migration task will drop those constraints. Hash upgrade safety is verified by a new MediaFile.Hash describe block: nil *int hashes identically to the old int(0) default via ZeroNil+IgnoreZeroValue, so no files will be spuriously re-imported after this change. Extra files touched beyond the plan's list: core/stream/legacy_client_test.go (BitDepth in model.MediaFile literals), persistence/mediafile_repository.go (NOT NULL bridge). * test(model): pin pre-conversion golden hashes for BPM/BitDepth * feat(smartplaylist): support isMissing/isPresent on bpm and bitDepth * feat(db): make bpm and bit_depth columns nullable, backfill 0 to NULL Drop the NOT NULL constraint on media_file.bpm and bit_depth via a lossless migration that converts legacy 0-means-absent values to real NULL. Remove the temporary shim in PostScan/PostMapArgs that was bridging the old NOT NULL columns to the *int model fields. Add round-trip persistence tests asserting NULL storage for nil pointers and correct value round-trip for non-nil pointers. * test(e2e): verify isMissing/isPresent partition for nullable fields Add DescribeTable covering bpm, bitdepth, lyrics, and mbz_recording_id: for each field, isMissing + isPresent song counts must equal the total library count, proving the nullable-column SQL is exhaustive and correct. * test(e2e): seed bpm tag so isMissing/isPresent partition is non-trivial * fix(model): omit bitDepth from JSON when absent instead of emitting null * feat(smartplaylist): support isMissing/isPresent on more string fields Enable isMissing/isPresent operators for album, comment, catalognumber, discsubtitle, albumcomment, sorttitle, sortalbum, sortartist, sortalbumartist, and explicitstatus by marking them Nullable in fieldMap. * refactor(smartplaylist): unify missingExpr column logic into one flow Collapse the numeric/string fork in missingExpr into a single empties-driven loop (numeric/boolean fields simply have no empties), and replace the duplicated IsTag/IsRole guard with a three-way switch that expresses the dispatch model once. No SQL semantics change for string fields; numeric/boolean fields now emit a single-element Or/And which squirrel parenthesizes (e.g. `(col IS NULL)` instead of bare `col IS NULL`) — update the affected test expectations accordingly. --- core/stream/decider.go | 3 +- core/stream/decider_test.go | 65 ++++++++++--------- core/stream/legacy_client_test.go | 14 ++-- ...60612222838_make_bpm_bitdepth_nullable.sql | 35 ++++++++++ model/criteria/fields.go | 52 ++++++++------- model/criteria/fields_test.go | 12 ++++ model/mediafile.go | 7 +- model/mediafile_test.go | 21 +++++- model/metadata/map_mediafile.go | 10 ++- model/metadata/map_mediafile_test.go | 28 ++++++++ persistence/criteria_sql.go | 51 ++++++++++----- persistence/criteria_sql_test.go | 62 ++++++++++++++++-- persistence/mediafile_repository_test.go | 45 +++++++++++++ server/e2e/e2e_suite_test.go | 2 +- server/e2e/subsonic_playlists_test.go | 38 +++++++++++ server/subsonic/helpers.go | 5 +- server/subsonic/transcode_test.go | 2 +- 17 files changed, 352 insertions(+), 100 deletions(-) create mode 100644 db/migrations/20260612222838_make_bpm_bitdepth_nullable.sql diff --git a/core/stream/decider.go b/core/stream/decider.go index d6e48497c..7940c6862 100644 --- a/core/stream/decider.go +++ b/core/stream/decider.go @@ -12,6 +12,7 @@ import ( "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" + "github.com/navidrome/navidrome/utils/gg" ) const fallbackBitrate = 256 // kbps @@ -142,7 +143,7 @@ func buildSourceStream(mf *model.MediaFile, probe *ffmpeg.AudioProbeResult) Deta sd.Codec = mf.AudioCodec() sd.Bitrate = mf.BitRate sd.SampleRate = mf.SampleRate - sd.BitDepth = mf.BitDepth + sd.BitDepth = gg.V(mf.BitDepth) sd.Channels = mf.Channels } sd.IsLossless = isLosslessFormat(sd.Codec) diff --git a/core/stream/decider_test.go b/core/stream/decider_test.go index f74953258..03c4ea437 100644 --- a/core/stream/decider_test.go +++ b/core/stream/decider_test.go @@ -12,6 +12,7 @@ import ( "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/tests" + "github.com/navidrome/navidrome/utils/gg" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -23,7 +24,7 @@ func withProbe(mf *model.MediaFile) *model.MediaFile { Codec: mf.AudioCodec(), BitRate: mf.BitRate, SampleRate: mf.SampleRate, - BitDepth: mf.BitDepth, + BitDepth: gg.V(mf.BitDepth), Channels: mf.Channels, } data, _ := json.Marshal(probe) @@ -243,7 +244,7 @@ var _ = Describe("Decider", func() { Context("Transcoding", func() { It("selects transcoding when direct play isn't possible", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)}) ci := &ClientInfo{ MaxTranscodingAudioBitrate: 256, // kbps DirectPlayProfiles: []DirectPlayProfile{ @@ -278,7 +279,7 @@ var _ = Describe("Decider", func() { }) It("uses default bitrate when client doesn't specify", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, BitDepth: new(16)}) ci := &ClientInfo{ TranscodingProfiles: []Profile{ {Container: "mp3", Protocol: ProtocolHTTP}, @@ -331,7 +332,7 @@ var _ = Describe("Decider", func() { }) It("selects first valid transcoding profile in order", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 48000, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 48000, BitDepth: new(16)}) ci := &ClientInfo{ MaxTranscodingAudioBitrate: 320, DirectPlayProfiles: []DirectPlayProfile{ @@ -351,7 +352,7 @@ var _ = Describe("Decider", func() { Context("Lossless to lossless transcoding", func() { It("allows lossless to lossless when samplerate needs downsampling", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "dsf", Codec: "DSD", BitRate: 5644, Channels: 2, SampleRate: 176400, BitDepth: 1}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "dsf", Codec: "DSD", BitRate: 5644, Channels: 2, SampleRate: 176400, BitDepth: new(1)}) ci := &ClientInfo{ MaxAudioBitrate: 1000, DirectPlayProfiles: []DirectPlayProfile{ @@ -369,7 +370,7 @@ var _ = Describe("Decider", func() { It("sets IsLossless=true on transcoded stream when target is lossless", func() { // Transcoding to mp3 (lossy) should result in IsLossless=false. - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: new(24)}) ci := &ClientInfo{ MaxTranscodingAudioBitrate: 320, TranscodingProfiles: []Profile{ @@ -526,7 +527,7 @@ var _ = Describe("Decider", func() { }) It("rejects direct play due to samplerate limitation", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: new(24)}) ci := &ClientInfo{ DirectPlayProfiles: []DirectPlayProfile{ {Containers: []string{"flac"}, Protocols: []string{ProtocolHTTP}}, @@ -573,7 +574,7 @@ var _ = Describe("Decider", func() { }) It("applies channel limitation to transcoded stream", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6, SampleRate: 48000, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6, SampleRate: 48000, BitDepth: new(16)}) ci := &ClientInfo{ MaxTranscodingAudioBitrate: 320, TranscodingProfiles: []Profile{ @@ -596,7 +597,7 @@ var _ = Describe("Decider", func() { }) It("applies samplerate limitation to transcoded stream", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: new(24)}) ci := &ClientInfo{ MaxTranscodingAudioBitrate: 320, TranscodingProfiles: []Profile{ @@ -619,7 +620,7 @@ var _ = Describe("Decider", func() { }) It("applies bitdepth limitation to transcoded stream", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: new(24)}) ci := &ClientInfo{ TranscodingProfiles: []Profile{ {Container: "flac", AudioCodec: "flac", Protocol: ProtocolHTTP}, @@ -642,7 +643,7 @@ var _ = Describe("Decider", func() { }) It("preserves source bit depth when no limitation applies", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 24}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(24)}) ci := &ClientInfo{ TranscodingProfiles: []Profile{ {Container: "flac", AudioCodec: "flac", Protocol: ProtocolHTTP}, @@ -656,7 +657,7 @@ var _ = Describe("Decider", func() { }) It("rejects transcoding profile when GreaterThanEqual cannot be satisfied", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)}) ci := &ClientInfo{ MaxTranscodingAudioBitrate: 320, TranscodingProfiles: []Profile{ @@ -680,7 +681,7 @@ var _ = Describe("Decider", func() { Context("DSD sample rate conversion", func() { It("converts DSD sample rate to PCM-equivalent in decision", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "dsf", Codec: "DSD", BitRate: 5644, Channels: 2, SampleRate: 2822400, BitDepth: 1}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "dsf", Codec: "DSD", BitRate: 5644, Channels: 2, SampleRate: 2822400, BitDepth: new(1)}) ci := &ClientInfo{ MaxTranscodingAudioBitrate: 320, TranscodingProfiles: []Profile{ @@ -700,7 +701,7 @@ var _ = Describe("Decider", func() { }) It("converts DSD sample rate for FLAC target without codec limit", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "dsf", Codec: "DSD", BitRate: 5644, Channels: 2, SampleRate: 2822400, BitDepth: 1}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "dsf", Codec: "DSD", BitRate: 5644, Channels: 2, SampleRate: 2822400, BitDepth: new(1)}) ci := &ClientInfo{ TranscodingProfiles: []Profile{ {Container: "flac", AudioCodec: "flac", Protocol: ProtocolHTTP}, @@ -719,7 +720,7 @@ var _ = Describe("Decider", func() { }) It("applies codec profile limit to DSD-converted FLAC sample rate", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "dsf", Codec: "DSD", BitRate: 5644, Channels: 2, SampleRate: 2822400, BitDepth: 1}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "dsf", Codec: "DSD", BitRate: 5644, Channels: 2, SampleRate: 2822400, BitDepth: new(1)}) ci := &ClientInfo{ TranscodingProfiles: []Profile{ {Container: "flac", AudioCodec: "flac", Protocol: ProtocolHTTP}, @@ -746,7 +747,7 @@ var _ = Describe("Decider", func() { }) It("applies audioBitdepth limitation to DSD-converted bit depth", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "dsf", Codec: "DSD", BitRate: 5644, Channels: 2, SampleRate: 2822400, BitDepth: 1}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "dsf", Codec: "DSD", BitRate: 5644, Channels: 2, SampleRate: 2822400, BitDepth: new(1)}) ci := &ClientInfo{ TranscodingProfiles: []Profile{ {Container: "flac", AudioCodec: "flac", Protocol: ProtocolHTTP}, @@ -775,7 +776,7 @@ var _ = Describe("Decider", func() { // Regression test for #5336: ffmpeg's mp3 encoder rejects >2 channels. // The decider must clamp to the codec's hard limit even when no // transcoding profile MaxAudioChannels is configured. - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6, SampleRate: 44100, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6, SampleRate: 44100, BitDepth: new(16)}) ci := &ClientInfo{ MaxTranscodingAudioBitrate: 320, TranscodingProfiles: []Profile{ @@ -791,7 +792,7 @@ var _ = Describe("Decider", func() { }) It("honors a stricter profile MaxAudioChannels over the codec clamp", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6, SampleRate: 44100, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6, SampleRate: 44100, BitDepth: new(16)}) ci := &ClientInfo{ MaxTranscodingAudioBitrate: 320, TranscodingProfiles: []Profile{ @@ -806,7 +807,7 @@ var _ = Describe("Decider", func() { }) It("applies the codec clamp when the profile limit is looser", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6, SampleRate: 44100, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6, SampleRate: 44100, BitDepth: new(16)}) ci := &ClientInfo{ MaxTranscodingAudioBitrate: 320, TranscodingProfiles: []Profile{ @@ -821,7 +822,7 @@ var _ = Describe("Decider", func() { }) It("passes channels through unchanged for codecs with no hard limit", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6, SampleRate: 44100, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6, SampleRate: 44100, BitDepth: new(16)}) ci := &ClientInfo{ MaxTranscodingAudioBitrate: 320, TranscodingProfiles: []Profile{ @@ -840,7 +841,7 @@ var _ = Describe("Decider", func() { Context("Probe-based lossless detection", func() { It("uses probe codec name for lossless detection", func() { // WavPack files: ffprobe reports codec as "wavpack", suffix is ".wv" - mf := &model.MediaFile{ID: "1", Suffix: "wv", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16} + mf := &model.MediaFile{ID: "1", Suffix: "wv", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)} probe := ffmpeg.AudioProbeResult{ Codec: "wavpack", BitRate: 1000, SampleRate: 44100, BitDepth: 16, Channels: 2, } @@ -884,7 +885,7 @@ var _ = Describe("Decider", func() { Context("Opus fixed sample rate", func() { It("sets Opus output to 48000Hz regardless of input", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)}) ci := &ClientInfo{ MaxTranscodingAudioBitrate: 128, TranscodingProfiles: []Profile{ @@ -901,7 +902,7 @@ var _ = Describe("Decider", func() { }) It("sets Opus output to 48000Hz even for 96kHz input", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1500, Channels: 2, SampleRate: 96000, BitDepth: 24}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1500, Channels: 2, SampleRate: 96000, BitDepth: new(24)}) ci := &ClientInfo{ MaxTranscodingAudioBitrate: 128, TranscodingProfiles: []Profile{ @@ -917,7 +918,7 @@ var _ = Describe("Decider", func() { Context("Container vs format separation", func() { It("preserves mp4 container when falling back to aac format", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)}) ci := &ClientInfo{ MaxTranscodingAudioBitrate: 256, TranscodingProfiles: []Profile{ @@ -935,7 +936,7 @@ var _ = Describe("Decider", func() { }) It("uses container as format when container matches transcoding config", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)}) ci := &ClientInfo{ MaxTranscodingAudioBitrate: 256, TranscodingProfiles: []Profile{ @@ -952,7 +953,7 @@ var _ = Describe("Decider", func() { Context("MP3 max sample rate", func() { It("caps sample rate at 48000 for MP3", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1500, Channels: 2, SampleRate: 96000, BitDepth: 24}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1500, Channels: 2, SampleRate: 96000, BitDepth: new(24)}) ci := &ClientInfo{ MaxTranscodingAudioBitrate: 320, TranscodingProfiles: []Profile{ @@ -966,7 +967,7 @@ var _ = Describe("Decider", func() { }) It("preserves sample rate at 44100 for MP3", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)}) ci := &ClientInfo{ MaxTranscodingAudioBitrate: 320, TranscodingProfiles: []Profile{ @@ -982,7 +983,7 @@ var _ = Describe("Decider", func() { Context("AAC max sample rate", func() { It("caps sample rate at 96000 for AAC", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "dsf", Codec: "DSD", BitRate: 5644, Channels: 2, SampleRate: 2822400, BitDepth: 1}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "dsf", Codec: "DSD", BitRate: 5644, Channels: 2, SampleRate: 2822400, BitDepth: new(1)}) ci := &ClientInfo{ MaxTranscodingAudioBitrate: 320, TranscodingProfiles: []Profile{ @@ -1025,7 +1026,7 @@ var _ = Describe("Decider", func() { Context("Source stream details", func() { It("populates source stream correctly with kbps bitrate", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24, Duration: 300.5, Size: 50000000}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: new(24), Duration: 300.5, Size: 50000000}) ci := &ClientInfo{ DirectPlayProfiles: []DirectPlayProfile{ {Containers: []string{"flac"}, Protocols: []string{ProtocolHTTP}}, @@ -1058,7 +1059,7 @@ var _ = Describe("Decider", func() { }) It("ignores player MaxBitRate in context", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)}) ci := &ClientInfo{ Name: "TestClient", DirectPlayProfiles: []DirectPlayProfile{ @@ -1074,7 +1075,7 @@ var _ = Describe("Decider", func() { Context("Format-aware default bitrate", func() { It("uses opus default bitrate from DB", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 48000, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 48000, BitDepth: new(16)}) ci := &ClientInfo{ TranscodingProfiles: []Profile{ {Container: "opus", AudioCodec: "opus", Protocol: ProtocolHTTP}, @@ -1087,7 +1088,7 @@ var _ = Describe("Decider", func() { }) It("uses aac default bitrate from DB", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)}) ci := &ClientInfo{ TranscodingProfiles: []Profile{ {Container: "aac", AudioCodec: "aac", Protocol: ProtocolHTTP}, diff --git a/core/stream/legacy_client_test.go b/core/stream/legacy_client_test.go index ce7b38650..2ddd74ce0 100644 --- a/core/stream/legacy_client_test.go +++ b/core/stream/legacy_client_test.go @@ -138,7 +138,7 @@ var _ = Describe("ResolveRequest", func() { }) It("transcodes to requested format", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)}) decider := svc.(*deciderService) req := decider.ResolveRequest(ctx, mf, "opus", 0, 0) @@ -147,7 +147,7 @@ var _ = Describe("ResolveRequest", func() { }) It("transcodes to requested format with bitrate limit", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)}) decider := svc.(*deciderService) req := decider.ResolveRequest(ctx, mf, "mp3", 128, 0) @@ -169,7 +169,7 @@ var _ = Describe("ResolveRequest", func() { DeferCleanup(configtest.SetupConfig()) conf.Server.DefaultDownsamplingFormat = "opus" - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)}) decider := svc.(*deciderService) req := decider.ResolveRequest(ctx, mf, "", 128, 0) @@ -179,7 +179,7 @@ var _ = Describe("ResolveRequest", func() { }) It("passes offset through", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)}) decider := svc.(*deciderService) req := decider.ResolveRequest(ctx, mf, "opus", 128, 30) @@ -259,7 +259,7 @@ var _ = Describe("ResolveRequest", func() { Context("Player MaxBitRate cap", func() { It("applies player MaxBitRate cap when client has no limit", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)}) playerCtx := request.WithPlayer(ctx, model.Player{MaxBitRate: 320}) decider := svc.(*deciderService) @@ -270,7 +270,7 @@ var _ = Describe("ResolveRequest", func() { }) It("uses client limit when it is more restrictive than player MaxBitRate", func() { - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)}) playerCtx := request.WithPlayer(ctx, model.Player{MaxBitRate: 500}) decider := svc.(*deciderService) @@ -332,7 +332,7 @@ var _ = Describe("ResolveRequest", func() { DeferCleanup(configtest.SetupConfig()) conf.Server.DefaultDownsamplingFormat = "opus" - mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16}) + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)}) decider := svc.(*deciderService) req := decider.ResolveRequest(ctx, mf, "xyz", 128, 0) diff --git a/db/migrations/20260612222838_make_bpm_bitdepth_nullable.sql b/db/migrations/20260612222838_make_bpm_bitdepth_nullable.sql new file mode 100644 index 000000000..c84323158 --- /dev/null +++ b/db/migrations/20260612222838_make_bpm_bitdepth_nullable.sql @@ -0,0 +1,35 @@ +-- +goose Up +drop index if exists media_file_bpm; + +alter table media_file add column bpm_new integer; +alter table media_file add column bit_depth_new integer; + +update media_file set + bpm_new = nullif(bpm, 0), + bit_depth_new = nullif(bit_depth, 0); + +alter table media_file drop column bpm; +alter table media_file drop column bit_depth; + +alter table media_file rename column bpm_new to bpm; +alter table media_file rename column bit_depth_new to bit_depth; + +create index if not exists media_file_bpm on media_file (bpm); + +-- +goose Down +drop index if exists media_file_bpm; + +alter table media_file add column bpm_old integer default 0 not null; +alter table media_file add column bit_depth_old integer default 0 not null; + +update media_file set + bpm_old = coalesce(bpm, 0), + bit_depth_old = coalesce(bit_depth, 0); + +alter table media_file drop column bpm; +alter table media_file drop column bit_depth; + +alter table media_file rename column bpm_old to bpm; +alter table media_file rename column bit_depth_old to bit_depth; + +create index if not exists media_file_bpm on media_file (bpm); diff --git a/model/criteria/fields.go b/model/criteria/fields.go index 36206712f..5c9ec898d 100644 --- a/model/criteria/fields.go +++ b/model/criteria/fields.go @@ -4,12 +4,14 @@ import "strings" // FieldInfo contains semantic metadata about a criteria field. type FieldInfo struct { - Alias string // If set, this field is a backward-compat alias for another canonical name - IsTag bool - IsRole bool - Numeric bool - Boolean bool - Nullable bool // If set, this column field can be NULL, so isMissing/isPresent are supported on it + Alias string // If set, this field is a backward-compat alias for another canonical name + IsTag bool + IsRole bool + Numeric bool + Boolean bool + // Nullable: isMissing/isPresent are supported on this column field. For numeric/boolean + // fields, missing means NULL; for string fields it means NULL or empty string. + Nullable bool tagAlias string // If set, a tag name from mappings.yaml that resolves to this field name string // Canonical name, populated by LookupField from the map key @@ -22,7 +24,7 @@ func (f FieldInfo) Name() string { var fieldMap = map[string]FieldInfo{ "title": {}, - "album": {}, + "album": {Nullable: true}, "hascoverart": {Boolean: true}, "tracknumber": {}, "discnumber": {}, @@ -35,26 +37,26 @@ var fieldMap = map[string]FieldInfo{ "size": {}, "compilation": {Boolean: true}, "missing": {Boolean: true}, - "explicitstatus": {}, + "explicitstatus": {Nullable: true}, "dateadded": {}, "datemodified": {}, - "discsubtitle": {}, - "comment": {}, - "lyrics": {}, - "sorttitle": {}, - "sortalbum": {}, - "sortartist": {}, - "sortalbumartist": {}, - "albumcomment": {}, - "catalognumber": {}, + "discsubtitle": {Nullable: true}, + "comment": {Nullable: true}, + "lyrics": {Nullable: true}, + "sorttitle": {Nullable: true}, + "sortalbum": {Nullable: true}, + "sortartist": {Nullable: true}, + "sortalbumartist": {Nullable: true}, + "albumcomment": {Nullable: true}, + "catalognumber": {Nullable: true}, "filepath": {}, "filetype": {}, "codec": {}, "duration": {}, "bitrate": {}, - "bitdepth": {}, + "bitdepth": {Numeric: true, Nullable: true}, "samplerate": {}, - "bpm": {}, + "bpm": {Numeric: true, Nullable: true}, "channels": {}, "loved": {Boolean: true}, "dateloved": {}, @@ -75,12 +77,12 @@ var fieldMap = map[string]FieldInfo{ "artistlastplayed": {}, "artistdateloved": {}, "artistdaterated": {}, - "mbz_album_id": {}, - "mbz_album_artist_id": {}, - "mbz_artist_id": {}, - "mbz_recording_id": {}, - "mbz_release_track_id": {}, - "mbz_release_group_id": {}, + "mbz_album_id": {Nullable: true}, + "mbz_album_artist_id": {Nullable: true}, + "mbz_artist_id": {Nullable: true}, + "mbz_recording_id": {Nullable: true}, + "mbz_release_track_id": {Nullable: true}, + "mbz_release_group_id": {Nullable: true}, "rgalbumgain": {Numeric: true, Nullable: true}, "rgalbumpeak": {Numeric: true, Nullable: true}, "rgtrackgain": {Numeric: true, Nullable: true}, diff --git a/model/criteria/fields_test.go b/model/criteria/fields_test.go index 3dc0c7b90..2367101a8 100644 --- a/model/criteria/fields_test.go +++ b/model/criteria/fields_test.go @@ -75,5 +75,17 @@ var _ = Describe("fields", func() { gomega.Expect(field.IsTag).To(gomega.BeFalse()) }) + It("marks mbz_* and lyrics string fields as nullable (empty means missing)", func() { + for _, name := range []string{"mbz_album_id", "mbz_album_artist_id", "mbz_artist_id", + "mbz_recording_id", "mbz_release_track_id", "mbz_release_group_id", "lyrics", + "album", "comment", "catalognumber", "discsubtitle", "albumcomment", + "sorttitle", "sortalbum", "sortartist", "sortalbumartist", "explicitstatus"} { + field, ok := LookupField(name) + gomega.Expect(ok).To(gomega.BeTrue(), name) + gomega.Expect(field.Nullable).To(gomega.BeTrue(), name) + gomega.Expect(field.Numeric).To(gomega.BeFalse(), name) + } + }) + }) }) diff --git a/model/mediafile.go b/model/mediafile.go index 6be8402ae..718f0443d 100644 --- a/model/mediafile.go +++ b/model/mediafile.go @@ -16,6 +16,7 @@ import ( "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/utils" + "github.com/navidrome/navidrome/utils/gg" "github.com/navidrome/navidrome/utils/slice" ) @@ -54,7 +55,7 @@ type MediaFile struct { Duration float32 `structs:"duration" json:"duration"` BitRate int `structs:"bit_rate" json:"bitRate"` SampleRate int `structs:"sample_rate" json:"sampleRate"` - BitDepth int `structs:"bit_depth" json:"bitDepth"` + BitDepth *int `structs:"bit_depth" json:"bitDepth,omitempty"` Channels int `structs:"channels" json:"channels"` Codec string `structs:"codec" json:"codec"` ProbeData string `structs:"probe_data" json:"-" hash:"ignore"` @@ -71,7 +72,7 @@ type MediaFile struct { Compilation bool `structs:"compilation" json:"compilation"` Comment string `structs:"comment" json:"comment,omitempty"` Lyrics string `structs:"lyrics" json:"lyrics"` - BPM int `structs:"bpm" json:"bpm,omitempty"` + BPM *int `structs:"bpm" json:"bpm,omitempty"` ExplicitStatus string `structs:"explicit_status" json:"explicitStatus"` CatalogNum string `structs:"catalog_num" json:"catalogNum,omitempty"` MbzRecordingID string `structs:"mbz_recording_id" json:"mbzRecordingID,omitempty"` @@ -225,7 +226,7 @@ func (mf MediaFile) inferCodecFromSuffix() string { return "dsd" case "m4a": // AAC if BitDepth==0, ALAC if BitDepth>0 - if mf.BitDepth > 0 { + if gg.V(mf.BitDepth) > 0 { return "alac" } return "aac" diff --git a/model/mediafile_test.go b/model/mediafile_test.go index 3547ec4ef..65c5a0652 100644 --- a/model/mediafile_test.go +++ b/model/mediafile_test.go @@ -564,7 +564,7 @@ var _ = Describe("MediaFile", func() { DescribeTable("infers codec from suffix when Codec field is empty", func(suffix string, bitDepth int, expected string) { - mf := MediaFile{Suffix: suffix, BitDepth: bitDepth} + mf := MediaFile{Suffix: suffix, BitDepth: new(bitDepth)} Expect(mf.AudioCodec()).To(Equal(expected)) }, Entry("mp3", "mp3", 0, "mp3"), @@ -597,13 +597,30 @@ var _ = Describe("MediaFile", func() { ) It("prefers stored codec over suffix inference", func() { - mf := MediaFile{Codec: "ALAC", Suffix: "m4a", BitDepth: 0} + mf := MediaFile{Codec: "ALAC", Suffix: "m4a"} Expect(mf.AudioCodec()).To(Equal("alac")) }) }) }) +var _ = Describe("MediaFile.Hash", func() { + // Guards the upgrade guarantee: converting BPM/BitDepth from int to *int must not change hashes, + // or every file would be spuriously re-imported on the next scan. + // Golden hashes were captured at 46221d516 when those fields were plain ints. + It("keeps hashes identical to the pre-pointer-conversion values", func() { + // Golden hashes computed at 46221d516, when BPM/BitDepth were plain ints — pinning + // them guarantees the pointer conversion cannot trigger a full-library re-import. + Expect(MediaFile{Title: "Song"}.Hash()).To(Equal("1d856ced42cb96db39e354a4bac9a622")) + Expect(MediaFile{Title: "Song", BPM: new(120), BitDepth: new(16)}.Hash()).To(Equal("b2b0b1d1dd7fd767093588e4af3a0689")) + }) + It("changes the hash when a pointer field has a value", func() { + base := MediaFile{Title: "Song"} + Expect(base.Equals(MediaFile{Title: "Song", BPM: new(120)})).To(BeFalse()) + Expect(base.Equals(MediaFile{Title: "Song", BitDepth: new(24)})).To(BeFalse()) + }) +}) + func t(v string) time.Time { var timeFormats = []string{"2006-01-02", "2006-01-02 15:04", "2006-01-02 15:04:05", "2006-01-02T15:04:05", "2006-01-02T15:04", "2006-01-02 15:04:05.999999999 -0700 MST"} for _, f := range timeFormats { diff --git a/model/metadata/map_mediafile.go b/model/metadata/map_mediafile.go index 824cad7c2..966a545be 100644 --- a/model/metadata/map_mediafile.go +++ b/model/metadata/map_mediafile.go @@ -35,7 +35,11 @@ func (md Metadata) ToMediaFile(libID int, folderID string) model.MediaFile { mf.DiscSubtitle = md.String(model.TagDiscSubtitle) mf.CatalogNum = md.String(model.TagCatalogNumber) mf.Comment = md.String(model.TagComment) - mf.BPM = int(math.Round(md.Float(model.TagBPM))) + if f := md.NullableFloat(model.TagBPM); f != nil { + if v := int(math.Round(*f)); v != 0 { + mf.BPM = new(v) + } + } mf.Lyrics = md.mapLyrics() mf.ExplicitStatus = md.mapExplicitStatusTag() @@ -63,7 +67,9 @@ func (md Metadata) ToMediaFile(libID int, folderID string) model.MediaFile { mf.Duration = md.Length() mf.BitRate = md.AudioProperties().BitRate mf.SampleRate = md.AudioProperties().SampleRate - mf.BitDepth = md.AudioProperties().BitDepth + if bd := md.AudioProperties().BitDepth; bd > 0 { + mf.BitDepth = new(bd) + } mf.Channels = md.AudioProperties().Channels mf.Codec = md.AudioProperties().Codec mf.Path = md.FilePath() diff --git a/model/metadata/map_mediafile_test.go b/model/metadata/map_mediafile_test.go index 16142f526..75a7ed358 100644 --- a/model/metadata/map_mediafile_test.go +++ b/model/metadata/map_mediafile_test.go @@ -117,4 +117,32 @@ var _ = Describe("ToMediaFile", func() { Expect(actual).To(Equal(expected)) }) }) + + Describe("BPM", func() { + It("maps the BPM tag rounded to the nearest integer", func() { + mf = toMediaFile(model.RawTags{"BPM": {"120.6"}}) + Expect(mf.BPM).To(Equal(new(121))) + }) + It("leaves BPM nil when the tag is absent", func() { + mf = toMediaFile(model.RawTags{}) + Expect(mf.BPM).To(BeNil()) + }) + It("leaves BPM nil when the tag is zero or unparseable", func() { + Expect(toMediaFile(model.RawTags{"BPM": {"0"}}).BPM).To(BeNil()) + Expect(toMediaFile(model.RawTags{"BPM": {"fast"}}).BPM).To(BeNil()) + }) + }) + + Describe("BitDepth", func() { + It("maps the bit depth when present", func() { + props.AudioProperties = metadata.AudioProperties{BitDepth: 24} + mf = toMediaFile(model.RawTags{}) + Expect(mf.BitDepth).To(Equal(new(24))) + }) + It("leaves BitDepth nil when zero (lossy codecs have no bit depth)", func() { + props.AudioProperties = metadata.AudioProperties{BitDepth: 0} + mf = toMediaFile(model.RawTags{}) + Expect(mf.BitDepth).To(BeNil()) + }) + }) }) diff --git a/persistence/criteria_sql.go b/persistence/criteria_sql.go index ee0baca18..fa769ef40 100644 --- a/persistence/criteria_sql.go +++ b/persistence/criteria_sql.go @@ -28,9 +28,10 @@ func (j smartPlaylistJoinType) has(other smartPlaylistJoinType) bool { } type smartPlaylistField struct { - expr string - order string - joinType smartPlaylistJoinType + expr string + order string + joinType smartPlaylistJoinType + emptyValues []string // additional values that encode "missing" for string columns (e.g. '[]' for lyrics) } type smartPlaylistCriteria struct { @@ -72,7 +73,7 @@ var smartPlaylistFields = map[string]smartPlaylistField{ "datemodified": {expr: "media_file.updated_at"}, "discsubtitle": {expr: "media_file.disc_subtitle"}, "comment": {expr: "media_file.comment"}, - "lyrics": {expr: "media_file.lyrics"}, + "lyrics": {expr: "media_file.lyrics", emptyValues: []string{"[]"}}, "sorttitle": {expr: "media_file.sort_title"}, "sortalbum": {expr: "media_file.sort_album_name"}, "sortartist": {expr: "media_file.sort_artist_name"}, @@ -218,30 +219,44 @@ func missingExpr(values map[string]any, checkAbsence bool) (squirrel.Sqlizer, er } return nil, fmt.Errorf("invalid field in criteria: %s", field) } - if !info.IsTag && !info.IsRole && !info.Nullable { - return nil, fmt.Errorf("isMissing/isPresent operator is not supported for field: %s", field) - } - b, ok := value.(bool) if !ok { return nil, fmt.Errorf("invalid boolean value for 'missing' expression: %s: %v", field, value) } negate := checkAbsence == b - // Nullable column fields (e.g. ReplayGain) are stored in dedicated columns, not in the tags - // JSON, so "missing" maps to a NULL check on the column rather than a json_tree lookup. - if info.Nullable && !info.IsTag && !info.IsRole { - col, ok := fieldExpr(info.Name()) - if !ok || col == "" { + switch { + case info.IsTag || info.IsRole: + return jsonExpr(info, nil, negate), nil + case info.Nullable: + // Nullable column fields are stored in dedicated columns, not in the tags JSON, so + // "missing" maps to a column check rather than a json_tree lookup. Numeric/boolean + // columns (e.g. ReplayGain, BPM) encode absence as NULL only; string columns (e.g. + // mbz_* IDs, lyrics) additionally treat empty string — and any field-specific empty + // encodings (e.g. '[]' for lyrics) — as missing. The unified flow below handles both: + // numeric/boolean fields simply have no empties, so the loops are no-ops. + f, ok := smartPlaylistFields[info.Name()] + if !ok || f.expr == "" { return nil, fmt.Errorf("invalid field in criteria: %s", field) } - if negate { - return squirrel.Eq{col: nil}, nil + col := f.expr + var empties []string + if !info.Numeric && !info.Boolean { + empties = append([]string{""}, f.emptyValues...) } - return squirrel.NotEq{col: nil}, nil + missing := squirrel.Or{squirrel.Eq{col: nil}} + present := squirrel.And{squirrel.NotEq{col: nil}} + for _, e := range empties { + missing = append(missing, squirrel.Eq{col: e}) + present = append(present, squirrel.NotEq{col: e}) + } + if negate { + return missing, nil + } + return present, nil + default: + return nil, fmt.Errorf("isMissing/isPresent operator is not supported for field: %s", field) } - - return jsonExpr(info, nil, negate), nil } func mapExpr(values map[string]any, makeCond func(map[string]any) squirrel.Sqlizer, negateJSON bool) (squirrel.Sqlizer, error) { diff --git a/persistence/criteria_sql_test.go b/persistence/criteria_sql_test.go index 8e801a703..0257fa0cf 100644 --- a/persistence/criteria_sql_test.go +++ b/persistence/criteria_sql_test.go @@ -87,18 +87,68 @@ var _ = Describe("Smart playlist criteria SQL", func() { "not exists (select 1 from media_file_artists mfa where mfa.media_file_id = media_file.id and mfa.role = ?)", "composer"), // isMissing/isPresent — nullable column fields (ReplayGain) Entry("isMissing rgAlbumGain [true]", criteria.IsMissing{"rgAlbumGain": true}, - "media_file.rg_album_gain IS NULL"), + "(media_file.rg_album_gain IS NULL)"), Entry("isMissing rgAlbumGain [false]", criteria.IsMissing{"rgAlbumGain": false}, - "media_file.rg_album_gain IS NOT NULL"), + "(media_file.rg_album_gain IS NOT NULL)"), Entry("isPresent rgTrackPeak [true]", criteria.IsPresent{"rgTrackPeak": true}, - "media_file.rg_track_peak IS NOT NULL"), + "(media_file.rg_track_peak IS NOT NULL)"), Entry("isPresent rgTrackPeak [false]", criteria.IsPresent{"rgTrackPeak": false}, - "media_file.rg_track_peak IS NULL"), + "(media_file.rg_track_peak IS NULL)"), // isMissing — replaygain_* tag-name alias resolves to the nullable column (issue #5584) Entry("isMissing replaygain_album_gain alias [true]", criteria.IsMissing{"replaygain_album_gain": true}, - "media_file.rg_album_gain IS NULL"), + "(media_file.rg_album_gain IS NULL)"), Entry("isPresent replaygain_album_gain alias [true]", criteria.IsPresent{"replaygain_album_gain": true}, - "media_file.rg_album_gain IS NOT NULL"), + "(media_file.rg_album_gain IS NOT NULL)"), + // isMissing/isPresent — string column fields (empty string means missing) + Entry("isMissing mbz_recording_id [true]", criteria.IsMissing{"mbz_recording_id": true}, + "(media_file.mbz_recording_id IS NULL OR media_file.mbz_recording_id = ?)", ""), + Entry("isMissing mbz_recording_id [false]", criteria.IsMissing{"mbz_recording_id": false}, + "(media_file.mbz_recording_id IS NOT NULL AND media_file.mbz_recording_id <> ?)", ""), + Entry("isPresent mbz_album_id [true]", criteria.IsPresent{"mbz_album_id": true}, + "(media_file.mbz_album_id IS NOT NULL AND media_file.mbz_album_id <> ?)", ""), + Entry("isPresent mbz_album_id [false]", criteria.IsPresent{"mbz_album_id": false}, + "(media_file.mbz_album_id IS NULL OR media_file.mbz_album_id = ?)", ""), + // lyrics: absence is encoded as '' or '[]' (empty serialized LyricList) + Entry("isMissing lyrics [true]", criteria.IsMissing{"lyrics": true}, + "(media_file.lyrics IS NULL OR media_file.lyrics = ? OR media_file.lyrics = ?)", "", "[]"), + Entry("isPresent lyrics [true]", criteria.IsPresent{"lyrics": true}, + "(media_file.lyrics IS NOT NULL AND media_file.lyrics <> ? AND media_file.lyrics <> ?)", "", "[]"), + Entry("isMissing lyrics [false]", criteria.IsMissing{"lyrics": false}, + "(media_file.lyrics IS NOT NULL AND media_file.lyrics <> ? AND media_file.lyrics <> ?)", "", "[]"), + Entry("isPresent lyrics [false]", criteria.IsPresent{"lyrics": false}, + "(media_file.lyrics IS NULL OR media_file.lyrics = ? OR media_file.lyrics = ?)", "", "[]"), + // isMissing/isPresent — nullable numeric columns (BPM, BitDepth) + Entry("isMissing bpm [true]", criteria.IsMissing{"bpm": true}, + "(media_file.bpm IS NULL)"), + Entry("isPresent bpm [true]", criteria.IsPresent{"bpm": true}, + "(media_file.bpm IS NOT NULL)"), + Entry("isMissing bitdepth [true]", criteria.IsMissing{"bitdepth": true}, + "(media_file.bit_depth IS NULL)"), + Entry("isPresent bitdepth [false]", criteria.IsPresent{"bitdepth": false}, + "(media_file.bit_depth IS NULL)"), + // isMissing/isPresent — more string column fields (empty string means missing) + Entry("isMissing album [true]", criteria.IsMissing{"album": true}, + "(media_file.album IS NULL OR media_file.album = ?)", ""), + Entry("isMissing comment [true]", criteria.IsMissing{"comment": true}, + "(media_file.comment IS NULL OR media_file.comment = ?)", ""), + Entry("isMissing catalognumber [true]", criteria.IsMissing{"catalognumber": true}, + "(media_file.catalog_num IS NULL OR media_file.catalog_num = ?)", ""), + Entry("isMissing discsubtitle [true]", criteria.IsMissing{"discsubtitle": true}, + "(media_file.disc_subtitle IS NULL OR media_file.disc_subtitle = ?)", ""), + Entry("isMissing albumcomment [true]", criteria.IsMissing{"albumcomment": true}, + "(media_file.mbz_album_comment IS NULL OR media_file.mbz_album_comment = ?)", ""), + Entry("isMissing sorttitle [true]", criteria.IsMissing{"sorttitle": true}, + "(media_file.sort_title IS NULL OR media_file.sort_title = ?)", ""), + Entry("isMissing sortalbum [true]", criteria.IsMissing{"sortalbum": true}, + "(media_file.sort_album_name IS NULL OR media_file.sort_album_name = ?)", ""), + Entry("isMissing sortartist [true]", criteria.IsMissing{"sortartist": true}, + "(media_file.sort_artist_name IS NULL OR media_file.sort_artist_name = ?)", ""), + Entry("isMissing sortalbumartist [true]", criteria.IsMissing{"sortalbumartist": true}, + "(media_file.sort_album_artist_name IS NULL OR media_file.sort_album_artist_name = ?)", ""), + Entry("isMissing explicitstatus [true]", criteria.IsMissing{"explicitstatus": true}, + "(media_file.explicit_status IS NULL OR media_file.explicit_status = ?)", ""), + Entry("isPresent comment [true]", criteria.IsPresent{"comment": true}, + "(media_file.comment IS NOT NULL AND media_file.comment <> ?)", ""), ) Describe("playlist permissions", func() { diff --git a/persistence/mediafile_repository_test.go b/persistence/mediafile_repository_test.go index 532e9c10f..4c2363e43 100644 --- a/persistence/mediafile_repository_test.go +++ b/persistence/mediafile_repository_test.go @@ -824,4 +824,49 @@ var _ = Describe("MediaRepository", func() { Expect(mediafiles[0].ID).To(Equal("mf1")) }) }) + + Describe("BPM and BitDepth nullable round-trip", func() { + It("stores nil BPM and BitDepth as NULL and retrieves them as nil", func() { + newID := id.NewRandom() + mf := model.MediaFile{LibraryID: 1, ID: newID, Path: "test/bpm-nil.mp3"} + Expect(mr.Put(&mf)).To(Succeed()) + + retrieved, err := mr.Get(newID) + Expect(err).ToNot(HaveOccurred()) + Expect(retrieved.BPM).To(BeNil()) + Expect(retrieved.BitDepth).To(BeNil()) + + // Also verify via raw SQL that the columns are truly NULL (not 0) + db := GetDBXBuilder() + var row struct { + BPM *int `db:"bpm"` + BitDepth *int `db:"bit_depth"` + } + err = db.NewQuery("SELECT bpm, bit_depth FROM media_file WHERE id={:id}"). + Bind(dbx.Params{"id": newID}). + One(&row) + Expect(err).ToNot(HaveOccurred()) + Expect(row.BPM).To(BeNil(), "bpm should be stored as NULL in the database") + Expect(row.BitDepth).To(BeNil(), "bit_depth should be stored as NULL in the database") + + _ = mr.Delete(newID) + }) + + It("stores non-nil BPM and BitDepth and retrieves correct values", func() { + newID := id.NewRandom() + bpm := 120 + bitDepth := 24 + mf := model.MediaFile{LibraryID: 1, ID: newID, Path: "test/bpm-set.mp3", BPM: &bpm, BitDepth: &bitDepth} + Expect(mr.Put(&mf)).To(Succeed()) + + retrieved, err := mr.Get(newID) + Expect(err).ToNot(HaveOccurred()) + Expect(retrieved.BPM).ToNot(BeNil()) + Expect(*retrieved.BPM).To(Equal(120)) + Expect(retrieved.BitDepth).ToNot(BeNil()) + Expect(*retrieved.BitDepth).To(Equal(24)) + + _ = mr.Delete(newID) + }) + }) }) diff --git a/server/e2e/e2e_suite_test.go b/server/e2e/e2e_suite_test.go index 7ce8de2e6..12a7c95e0 100644 --- a/server/e2e/e2e_suite_test.go +++ b/server/e2e/e2e_suite_test.go @@ -133,7 +133,7 @@ func buildTestFS() storagetest.FakeFS { // Note: "musicbrainz_trackid" is an alias for the musicbrainz_recordingid tag (populates MbzRecordingID), // "musicbrainz_releasetrackid" is an alias for the musicbrainz_trackid tag (populates MbzReleaseTrackID). "Rock/The Beatles/Abbey Road/01 - Come Together.mp3": abbeyRoad(track(1, "Come Together", - _t{"musicbrainz_releasetrackid": mbidComeTogether, "musicbrainz_trackid": mbidComeTogetherRec})), + _t{"musicbrainz_releasetrackid": mbidComeTogether, "musicbrainz_trackid": mbidComeTogetherRec, "bpm": 120})), "Rock/The Beatles/Abbey Road/02 - Something.mp3": abbeyRoad(track(2, "Something", _t{"musicbrainz_releasetrackid": mbidSomething, "musicbrainz_trackid": mbidSomethingRec})), // Rock / The Beatles / Help! (no MBIDs) diff --git a/server/e2e/subsonic_playlists_test.go b/server/e2e/subsonic_playlists_test.go index 466e68cf0..467535df7 100644 --- a/server/e2e/subsonic_playlists_test.go +++ b/server/e2e/subsonic_playlists_test.go @@ -646,5 +646,43 @@ var _ = Describe("Playlist Endpoints", Ordered, func() { stringResp := doReq("getPlaylist", "id", stringPls.ID) Expect(stringResp.Playlist.SongCount).To(Equal(boolResp.Playlist.SongCount)) }) + + DescribeTable("isMissing/isPresent partition all songs for nullable column fields", + func(fieldName string) { + allPls := &model.Playlist{ + Name: "All Songs " + fieldName, + OwnerID: adminUser.ID, + Rules: &criteria.Criteria{Expression: criteria.Contains{"title": ""}}, + } + Expect(ds.Playlist(ctx).Put(allPls)).To(Succeed()) + missingPls := &model.Playlist{ + Name: "Missing " + fieldName, + OwnerID: adminUser.ID, + Rules: &criteria.Criteria{Expression: criteria.All{criteria.IsMissing{fieldName: true}}}, + } + Expect(ds.Playlist(ctx).Put(missingPls)).To(Succeed()) + presentPls := &model.Playlist{ + Name: "Present " + fieldName, + OwnerID: adminUser.ID, + Rules: &criteria.Criteria{Expression: criteria.All{criteria.IsPresent{fieldName: true}}}, + } + Expect(ds.Playlist(ctx).Put(presentPls)).To(Succeed()) + + allResp := doReq("getPlaylist", "id", allPls.ID) + missingResp := doReq("getPlaylist", "id", missingPls.ID) + presentResp := doReq("getPlaylist", "id", presentPls.ID) + + Expect(allResp.Status).To(Equal(responses.StatusOK)) + Expect(allResp.Playlist.SongCount).To(BeNumerically(">", int32(0))) + Expect(missingResp.Playlist.SongCount + presentResp.Playlist.SongCount). + To(Equal(allResp.Playlist.SongCount)) + }, + Entry("bpm", "bpm"), + Entry("bitdepth", "bitdepth"), + Entry("lyrics", "lyrics"), + Entry("mbz_recording_id", "mbz_recording_id"), + Entry("album", "album"), + Entry("comment", "comment"), + ) }) }) diff --git a/server/subsonic/helpers.go b/server/subsonic/helpers.go index e4c39e373..e6c6f9114 100644 --- a/server/subsonic/helpers.go +++ b/server/subsonic/helpers.go @@ -18,6 +18,7 @@ import ( "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/server/subsonic/responses" + "github.com/navidrome/navidrome/utils/gg" "github.com/navidrome/navidrome/utils/number" "github.com/navidrome/navidrome/utils/req" "github.com/navidrome/navidrome/utils/slice" @@ -250,7 +251,7 @@ func osChildFromMediaFile(ctx context.Context, mf model.MediaFile) *responses.Op } child.Comment = mf.Comment child.SortName = sortName(mf.SortTitle, mf.OrderTitle) - child.BPM = int32(mf.BPM) + child.BPM = int32(gg.V(mf.BPM)) child.MediaType = responses.MediaTypeSong child.MusicBrainzId = mf.MbzRecordingID child.Isrc = mf.Tags.Values(model.TagISRC) @@ -262,7 +263,7 @@ func osChildFromMediaFile(ctx context.Context, mf model.MediaFile) *responses.Op } child.ChannelCount = int32(mf.Channels) child.SamplingRate = int32(mf.SampleRate) - child.BitDepth = int32(mf.BitDepth) + child.BitDepth = int32(gg.V(mf.BitDepth)) child.Genres = toItemGenres(mf.Genres) child.Moods = mf.Tags.Values(model.TagMood) child.Groupings = mf.Tags.Values(model.TagGrouping) diff --git a/server/subsonic/transcode_test.go b/server/subsonic/transcode_test.go index adc7b7600..29a883ac1 100644 --- a/server/subsonic/transcode_test.go +++ b/server/subsonic/transcode_test.go @@ -205,7 +205,7 @@ var _ = Describe("Transcode endpoints", func() { It("includes transcode stream when transcoding", func() { mockMFRepo.SetData(model.MediaFiles{ - {ID: "song-2", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24}, + {ID: "song-2", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: new(24)}, }) mockTD.decision = &stream.TranscodeDecision{ MediaID: "song-2", From af78bdeb3a8b42089d7a35080cffe6ece125c894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Sat, 13 Jun 2026 13:29:29 -0400 Subject: [PATCH 12/18] fix(artwork): never serve artist folder images as album art (#5596) * test(artwork): add failing e2e tests for artist image leaking as album art Reproduces a v0.62.0 regression (#5451/#5457): the album cover-art parent-folder fallback can include the artist folder, serving the artist thumbnail (e.g. Artist/folder.jpg) as album art for any album without image files in its own folder(s). Covers three scenarios: a plain Artist/Album layout with no album images, a single-disc album spread across sibling folders under the artist folder, and a spread album whose own front.jpg is shadowed by the artist's cover.jpg via CoverArtPriority order. Also adds an albumByName test helper for multi-album layouts. The tests are expected to fail until the parent-folder inclusion is gated by a structural check (skip the common parent when audio from other albums lives under it). * fix(artwork): never serve artist folder images as album art The album cover-art parent-folder fallback (introduced in #5451/#5457) could include the artist folder as a source of album images, serving the artist thumbnail (e.g. Artist/folder.jpg) as cover art for any album without image files in its own folder(s). This affected both plain Artist/Album layouts and single-disc albums spread across sibling folders under the artist folder. Gate the common-parent inclusion with a structural check: the parent only qualifies as an album root when no audio belonging to other albums lives in it or anywhere beneath it. An artist folder contains other albums' tracks, while an album root above disc subfolders contains only this album's, so the check works for any disc folder naming scheme and never affects the multi-disc fixes from #5376/#5456. A single-album artist with no images anywhere remains structurally indistinguishable from an album root and is a known residual case. * refactor(artwork): move album-root audio check into folder repository Replace the raw subtree SQL (LIKE/ESCAPE expression and wildcard escaping) that lived in core/artwork with an explicit FolderRepository.HasAudioOutsideFolders method, implemented in the persistence layer next to the existing folder-subtree query pattern. This also removes the test mock's brittle dispatch that sniffed the generated SQL to recognize the query; the fake now overrides the new method directly. Extract the whole parent-folder resolution from loadAlbumFoldersPaths into an albumRootParent helper, flattening four levels of nesting back into a linear flow. Behavior is unchanged; the unit test for a parent containing audio moved to the persistence suite, with added coverage for subtree boundaries, missing folders, and LIKE-wildcard escaping in folder paths. * refactor(persistence): use exists helper in HasAudioOutsideFolders Replace the hand-rolled count(*) query with the repository's canonical exists helper, as suggested in PR review. --- core/artwork/e2e/album_test.go | 92 +++++++++++++++++++++++++++ core/artwork/e2e/suite_test.go | 14 ++++ core/artwork/reader_album.go | 70 ++++++++++++++------ core/artwork/reader_album_test.go | 55 ++++++++++++++++ core/artwork/reader_artist_test.go | 8 +++ model/folder.go | 4 ++ persistence/folder_repository.go | 28 ++++++++ persistence/folder_repository_test.go | 61 ++++++++++++++++++ 8 files changed, 311 insertions(+), 21 deletions(-) diff --git a/core/artwork/e2e/album_test.go b/core/artwork/e2e/album_test.go index e765e1b1b..5e61684cc 100644 --- a/core/artwork/e2e/album_test.go +++ b/core/artwork/e2e/album_test.go @@ -357,6 +357,98 @@ var _ = Describe("Album artwork resolution", func() { }) }) + // Regression introduced in v0.62.0 (#5451 + #5457): the parent-folder + // fallback can pick up images from the ARTIST folder, serving the artist + // thumbnail as album art for any album without its own image files. + When("an album has no images and the artist folder has folder.jpg", func() { + // Artist/ + // ├── folder.jpg ← artist thumbnail, must NOT become album art + // ├── Album A/ + // │ └── 01 - Track.mp3 (no images) + // └── Album B/ + // ├── 01 - Track.mp3 + // └── cover.jpg + It("does not use the artist image as album art", func() { + conf.Server.CoverArtPriority = defaultCoverPriority + setLayout(fstest.MapFS{ + "Artist/folder.jpg": imageFile("artist-thumbnail"), + "Artist/Album A/01 - Track.mp3": trackFile(1, "Track A", map[string]any{"album": "Album A", "albumartist": "Artist"}), + "Artist/Album B/01 - Track.mp3": trackFile(1, "Track B", map[string]any{"album": "Album B", "albumartist": "Artist"}), + "Artist/Album B/cover.jpg": imageFile("album-b"), + }) + scan() + + alA := albumByName("Album A") + _, err := readArtworkOrErr(alA.CoverArtID()) + Expect(err).To(HaveOccurred(), + "Album A has no images of its own, so it must fall through to the placeholder "+ + "instead of inheriting the artist folder's folder.jpg") + + alB := albumByName("Album B") + Expect(readArtwork(alB.CoverArtID())).To(Equal(imageBytes("album-b"))) + }) + }) + + When("a single-disc album is spread across sibling folders under the artist folder", func() { + // Artist/ + // ├── folder.jpg ← artist thumbnail, must NOT become album art + // ├── Album A/ + // │ └── 01 - Track.mp3 (album: "Album A") + // ├── Album A bonus/ + // │ └── 02 - Track.mp3 (album: "Album A" — same album, second folder) + // └── Album B/ + // ├── 01 - Track.mp3 + // └── cover.jpg + It("does not use the artist image as album art for the spread album", func() { + conf.Server.CoverArtPriority = defaultCoverPriority + setLayout(fstest.MapFS{ + "Artist/folder.jpg": imageFile("artist-thumbnail"), + "Artist/Album A/01 - Track.mp3": trackFile(1, "Track A1", map[string]any{"album": "Album A", "albumartist": "Artist"}), + "Artist/Album A bonus/02 - Track.mp3": trackFile(2, "Track A2", map[string]any{"album": "Album A", "albumartist": "Artist"}), + "Artist/Album B/01 - Track.mp3": trackFile(1, "Track B", map[string]any{"album": "Album B", "albumartist": "Artist"}), + "Artist/Album B/cover.jpg": imageFile("album-b"), + }) + scan() + + alA := albumByName("Album A") + Expect(alA.FolderIDs).To(HaveLen(2), + "sanity check: scanner should treat the two sibling folders as one spread album") + _, err := readArtworkOrErr(alA.CoverArtID()) + Expect(err).To(HaveOccurred(), + "the spread album has no images of its own, so it must fall through to the "+ + "placeholder instead of inheriting the artist folder's folder.jpg") + }) + }) + + When("a spread album has its own front.jpg but the artist folder has cover.jpg", func() { + // Artist/ + // ├── cover.jpg ← artist image; matches cover.* (first pattern), + // │ must NOT shadow the album's own front.jpg + // ├── Album A/ + // │ ├── 01 - Track.mp3 (album: "Album A") + // │ └── front.jpg ← should win + // ├── Album A bonus/ + // │ └── 02 - Track.mp3 (album: "Album A") + // └── Album B/ + // └── 01 - Track.mp3 + It("prefers the album's own art over the artist image", func() { + conf.Server.CoverArtPriority = defaultCoverPriority + setLayout(fstest.MapFS{ + "Artist/cover.jpg": imageFile("artist-image"), + "Artist/Album A/01 - Track.mp3": trackFile(1, "Track A1", map[string]any{"album": "Album A", "albumartist": "Artist"}), + "Artist/Album A/front.jpg": imageFile("album-a-front"), + "Artist/Album A bonus/02 - Track.mp3": trackFile(2, "Track A2", map[string]any{"album": "Album A", "albumartist": "Artist"}), + "Artist/Album B/01 - Track.mp3": trackFile(1, "Track B", map[string]any{"album": "Album B", "albumartist": "Artist"}), + }) + scan() + + alA := albumByName("Album A") + Expect(alA.FolderIDs).To(HaveLen(2), + "sanity check: scanner should treat the two sibling folders as one spread album") + Expect(readArtwork(alA.CoverArtID())).To(Equal(imageBytes("album-a-front"))) + }) + }) + When("embedded is first in CoverArtPriority but the track has no embedded art", func() { // Artist/ // └── Album/ diff --git a/core/artwork/e2e/suite_test.go b/core/artwork/e2e/suite_test.go index 733e2e98c..06cc05b6f 100644 --- a/core/artwork/e2e/suite_test.go +++ b/core/artwork/e2e/suite_test.go @@ -2,6 +2,7 @@ package artworke2e_test import ( "context" + "fmt" "path/filepath" "testing" @@ -104,3 +105,16 @@ func firstAlbum() model.Album { Expect(albums).To(HaveLen(1), "expected exactly one album, got %d", len(albums)) return albums[0] } + +func albumByName(name string) model.Album { + GinkgoHelper() + albums, err := ds.Album(ctx).GetAll(model.QueryOptions{}) + Expect(err).ToNot(HaveOccurred()) + for _, al := range albums { + if al.Name == name { + return al + } + } + Fail(fmt.Sprintf("album %q not found among %d albums", name, len(albums))) + return model.Album{} +} diff --git a/core/artwork/reader_album.go b/core/artwork/reader_album.go index 73ba9b5ee..8ad07773b 100644 --- a/core/artwork/reader_album.go +++ b/core/artwork/reader_album.go @@ -113,28 +113,12 @@ func loadAlbumFoldersPaths(ctx context.Context, ds model.DataStore, albums ...mo return nil, nil, nil, err } - folderIDSet := make(map[string]bool, len(folderIDs)) - for _, id := range folderIDs { - folderIDSet[id] = true + parent, err := albumRootParent(ctx, ds, folders, folderIDs) + if err != nil { + return nil, nil, nil, err } - - // Check if all folders share a common parent that is not already included. - // This finds cover art in the album root folder (e.g., "Artist/Album/cover.jpg" - // when tracks are in disc subfolders like "Artist/Album/CD1/" and "Artist/Album/CD2/"). - // For single-folder albums, the parent is only included when the folder has no - // images of its own (indicating a disc subfolder needing parent artwork). - if commonParentID := commonParentFolder(folders, folderIDSet); commonParentID != "" { - if len(folders) >= 2 || !anyFolderHasImages(folders) { - parentFolder, err := ds.Folder(ctx).Get(commonParentID) - if errors.Is(err, model.ErrNotFound) { - log.Warn(ctx, "Parent folder not found for album cover art lookup", "parentID", commonParentID) - } else if err != nil { - return nil, nil, nil, err - } - if parentFolder != nil && parentFolder.ParentID != "" { - folders = append(folders, *parentFolder) - } - } + if parent != nil { + folders = append(folders, *parent) } var paths []string @@ -159,6 +143,50 @@ func loadAlbumFoldersPaths(ctx context.Context, ds model.DataStore, albums ...mo return paths, imgFiles, &updatedAt, nil } +// albumRootParent returns the common parent of the album's folders when it +// qualifies as the album's root folder (e.g. "Artist/Album" above disc +// subfolders), or nil when there is no such parent. This finds cover art in +// the album root folder when tracks live in disc subfolders, like +// "Artist/Album/cover.jpg" with tracks in "Artist/Album/CD1/" and +// "Artist/Album/CD2/". The parent must look like an album root, not an +// artist-level folder — it qualifies only when it holds no audio belonging to +// other albums — so artist images are never served as album art. +func albumRootParent(ctx context.Context, ds model.DataStore, folders []model.Folder, folderIDs []string) (*model.Folder, error) { + folderIDSet := make(map[string]bool, len(folderIDs)) + for _, id := range folderIDs { + folderIDSet[id] = true + } + commonParentID := commonParentFolder(folders, folderIDSet) + if commonParentID == "" { + return nil, nil + } + // Single-folder albums only use the parent when the folder has no images + // of its own (indicating a disc subfolder needing parent artwork). + if len(folders) < 2 && anyFolderHasImages(folders) { + return nil, nil + } + parent, err := ds.Folder(ctx).Get(commonParentID) + if errors.Is(err, model.ErrNotFound) { + log.Warn(ctx, "Parent folder not found for album cover art lookup", "parentID", commonParentID) + return nil, nil + } + if err != nil { + return nil, err + } + if parent.ParentID == "" { + // The library root can never be an album root + return nil, nil + } + hasOtherAudio, err := ds.Folder(ctx).HasAudioOutsideFolders(*parent, folderIDs) + if err != nil { + return nil, err + } + if hasOtherAudio { + return nil, nil + } + return parent, nil +} + func anyFolderHasImages(folders []model.Folder) bool { for _, f := range folders { if len(f.ImageFiles) > 0 { diff --git a/core/artwork/reader_album_test.go b/core/artwork/reader_album_test.go index 1cf039bee..fe4a1a545 100644 --- a/core/artwork/reader_album_test.go +++ b/core/artwork/reader_album_test.go @@ -339,6 +339,61 @@ var _ = Describe("Album Artwork Reader", func() { Expect(repo.getCallCount).To(Equal(1)) }) + It("does not include parent images when other albums' audio lives under the parent", func() { + // Simulates: Artist/folder.jpg with Artist/Album (no images) and + // another album's tracks elsewhere under the artist folder + repo.result = []model.Folder{ + { + ID: "folder1", + Path: "Artist", + Name: "Album", + ParentID: "artistFolder", + ImagesUpdatedAt: now, + ImageFiles: []string{}, + }, + } + repo.parentResult = &model.Folder{ + ID: "artistFolder", + Path: ".", + Name: "Artist", + ParentID: "libraryRoot", + ImagesUpdatedAt: expectedAt, + ImageFiles: []string{"folder.jpg"}, + } + repo.hasOtherAudio = true + + _, imgFiles, _, err := loadAlbumFoldersPaths(ctx, ds, album) + + Expect(err).ToNot(HaveOccurred()) + Expect(imgFiles).To(BeEmpty()) + }) + + It("propagates errors from the album-root check", func() { + repo.result = []model.Folder{ + { + ID: "folder1", + Path: "Artist/Album", + Name: "disc1", + ParentID: "albumFolder", + ImagesUpdatedAt: now, + ImageFiles: []string{}, + }, + } + repo.parentResult = &model.Folder{ + ID: "albumFolder", + Path: "Artist", + Name: "Album", + ParentID: "artistFolder", + ImagesUpdatedAt: expectedAt, + ImageFiles: []string{"cover.jpg"}, + } + repo.otherAudioErr = errors.New("db connection failed") + + _, _, _, err := loadAlbumFoldersPaths(ctx, ds, album) + + Expect(err).To(MatchError("db connection failed")) + }) + It("propagates non-ErrNotFound errors from parent folder lookup", func() { repo.result = []model.Folder{ { diff --git a/core/artwork/reader_artist_test.go b/core/artwork/reader_artist_test.go index 50ca3a2ce..6d6d58fc5 100644 --- a/core/artwork/reader_artist_test.go +++ b/core/artwork/reader_artist_test.go @@ -702,12 +702,20 @@ type fakeFolderRepo struct { getErr error getCallCount int err error + // hasOtherAudio is returned by HasAudioOutsideFolders (the album-root + // check). False means the parent qualifies as an album root. + hasOtherAudio bool + otherAudioErr error } func (f *fakeFolderRepo) GetAll(...model.QueryOptions) ([]model.Folder, error) { return f.result, f.err } +func (f *fakeFolderRepo) HasAudioOutsideFolders(model.Folder, []string) (bool, error) { + return f.hasOtherAudio, f.otherAudioErr +} + func (f *fakeFolderRepo) Get(id string) (*model.Folder, error) { f.getCallCount++ if f.getErr != nil { diff --git a/model/folder.go b/model/folder.go index 7a769735e..39cb6db84 100644 --- a/model/folder.go +++ b/model/folder.go @@ -86,6 +86,10 @@ type FolderRepository interface { GetAll(...QueryOptions) ([]Folder, error) CountAll(...QueryOptions) (int64, error) GetFolderUpdateInfo(lib Library, targetPaths ...string) (map[string]FolderUpdateInfo, error) + // HasAudioOutsideFolders reports whether any folder in parent's subtree + // (including parent itself) contains audio files and is not one of the + // given folder IDs. + HasAudioOutsideFolders(parent Folder, excludeFolderIDs []string) (bool, error) Put(*Folder) error MarkMissing(missing bool, ids ...string) error GetTouchedWithPlaylists() (FolderCursor, error) diff --git a/persistence/folder_repository.go b/persistence/folder_repository.go index f7bb6a4fe..624ef21b8 100644 --- a/persistence/folder_repository.go +++ b/persistence/folder_repository.go @@ -7,6 +7,7 @@ import ( "iter" "maps" "os" + "path" "path/filepath" "slices" "strings" @@ -188,6 +189,33 @@ func (r folderRepository) queryFolderUpdateInfo(where And) (map[string]model.Fol return m, nil } +// HasAudioOutsideFolders reports whether any folder in parent's subtree +// (including parent itself) contains audio files and is not one of the given +// folder IDs. LIKE wildcards in the parent path are escaped, so it is always +// matched as a literal prefix. +func (r folderRepository) HasAudioOutsideFolders(parent model.Folder, excludeFolderIDs []string) (bool, error) { + if parent.NumAudioFiles > 0 { + return true, nil + } + parentPath := strings.TrimPrefix(path.Join(parent.Path, parent.Name), "/") + return r.exists(And{ + Eq{"library_id": parent.LibraryID, "missing": false}, + Gt{"num_audio_files": 0}, + NotEq{"id": excludeFolderIDs}, + Or{ + // Direct children have path = parentPath; deeper descendants match the prefix + Eq{"path": parentPath}, + Expr(`path LIKE ? ESCAPE '\'`, escapeLikePrefix(parentPath)+"/%"), + }, + }) +} + +// escapeLikePrefix escapes SQL LIKE wildcards so a string can be used as a +// literal prefix in a LIKE pattern (with ESCAPE '\'). +func escapeLikePrefix(s string) string { + return strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`).Replace(s) +} + func (r folderRepository) Put(f *model.Folder) error { dbf := dbFolder{Folder: f} _, err := r.put(dbf.ID, &dbf) diff --git a/persistence/folder_repository_test.go b/persistence/folder_repository_test.go index ebc08fd04..413a6b38f 100644 --- a/persistence/folder_repository_test.go +++ b/persistence/folder_repository_test.go @@ -217,6 +217,67 @@ var _ = Describe("FolderRepository", func() { }) }) + Describe("HasAudioOutsideFolders", func() { + var albumRoot, disc1, disc2 *model.Folder + + // TestHasAudio/Album/ + // ├── CD1/ (audio, belongs to the album) + // └── CD2/ (audio, belongs to the album) + BeforeEach(func() { + albumRoot = model.NewFolder(testLib, "TestHasAudio/Album") + disc1 = model.NewFolder(testLib, "TestHasAudio/Album/CD1") + disc1.NumAudioFiles = 5 + disc2 = model.NewFolder(testLib, "TestHasAudio/Album/CD2") + disc2.NumAudioFiles = 5 + for _, f := range []*model.Folder{albumRoot, disc1, disc2} { + Expect(repo.Put(f)).To(Succeed()) + } + }) + + It("returns false when all audio under the parent belongs to the given folders", func() { + Expect(repo.HasAudioOutsideFolders(*albumRoot, []string{disc1.ID, disc2.ID})).To(BeFalse()) + }) + + It("returns true when another folder under the parent has audio", func() { + bonus := model.NewFolder(testLib, "TestHasAudio/Album/Bonus") + bonus.NumAudioFiles = 1 + Expect(repo.Put(bonus)).To(Succeed()) + + Expect(repo.HasAudioOutsideFolders(*albumRoot, []string{disc1.ID, disc2.ID})).To(BeTrue()) + }) + + It("returns true when the parent itself contains audio files", func() { + albumRoot.NumAudioFiles = 2 + + Expect(repo.HasAudioOutsideFolders(*albumRoot, []string{disc1.ID, disc2.ID})).To(BeTrue()) + }) + + It("ignores audio outside the parent's subtree", func() { + other := model.NewFolder(testLib, "TestHasAudio/Other Album") + other.NumAudioFiles = 10 + Expect(repo.Put(other)).To(Succeed()) + + Expect(repo.HasAudioOutsideFolders(*albumRoot, []string{disc1.ID, disc2.ID})).To(BeFalse()) + }) + + It("ignores missing folders", func() { + gone := model.NewFolder(testLib, "TestHasAudio/Album/Gone") + gone.NumAudioFiles = 3 + gone.Missing = true + Expect(repo.Put(gone)).To(Succeed()) + + Expect(repo.HasAudioOutsideFolders(*albumRoot, []string{disc1.ID, disc2.ID})).To(BeFalse()) + }) + + It("does not treat LIKE wildcards in the parent path as patterns", func() { + // "TestHas_udio" would LIKE-match "TestHasAudio" if "_" were not escaped + wildcardRoot := model.NewFolder(testLib, "TestHas_udio/Album") + Expect(repo.Put(wildcardRoot)).To(Succeed()) + + Expect(repo.HasAudioOutsideFolders(*wildcardRoot, []string{"none"})).To(BeFalse()) + }) + }) + Describe("wrapFolderCursor", func() { It("does not panic when the cursor yields a dbFolder with nil Folder", func() { // Simulate what queryWithStableResults does on the rows.Err() path: From c466f6b612a89b7b80a27d24a48a2f8cd1e2a2d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Sat, 13 Jun 2026 13:58:26 -0400 Subject: [PATCH 13/18] fix(artwork): prevent WebP segfault on 32-bit and disable WebP-by-default in Docker (#5606) * fix(artwork): avoid WebP segfault on 32-bit ARM On 32-bit ARM, the gen2brain/webp native libwebp path uses ebitengine/purego reverse callbacks, which purego does not support on that architecture. Selecting it crashes the process with a SIGSEGV when encoding or decoding WebP cover art, taking down the whole server on the first web UI artwork request (issue #5597). Force the safe WASM path on armv7/v6 in two layers: build the Docker arm binary with the gen2brain/webp "nodynamic" tag so purego is never linked, and add a runtime GOARCH guard in the init hook so source builds on 32-bit ARM are also protected. arm64 keeps the native libwebp path. * fix(artwork): also disable native WebP on 32-bit x86 purego's callback implementation is built with the constraint !386 && !arm, so 32-bit x86 (386) crashes with the same SIGSEGV as 32-bit ARM when the native libwebp path is used. Navidrome ships linux/386 and windows/386 builds, so guard 386 alongside arm: extend the runtime GOARCH check and the Docker nodynamic build tag to cover both. 64-bit arches keep the native libwebp path. * fix(artwork): rely on nodynamic build tag, drop ineffective runtime guard The previous runtime GOARCH guard did not actually prevent the crash: gen2brain/webp selects the native (purego) vs WASM backend in its own package init() and registers the purego write callback at import time, before any Navidrome hook runs. webp.Dynamic() is only a status getter, and Decode/Encode branch on the library's unexported flag, so the guard merely skipped a log line while the native path stayed active. The effective fix is the nodynamic build tag (applied for 32-bit ARM and x86 in the Dockerfile), which compiles gen2brain/webp WASM-only so purego is never linked. Drop the misleading guard and document that source builds on 32-bit architectures must be built with -tags nodynamic. * fix(artwork): don't enable WebP encoding by default in Docker The Docker image set ND_ENABLEWEBPENCODING=true, which (a) forced cover-art thumbnails through WebP for every install and (b) overrode any EnableWebPEncoding=false set in the user's navidrome.toml, since env vars take precedence over the config file in Viper. On 32-bit platforms the only available WebP backend is the WASM encoder, which is slow on the underpowered hardware those builds typically run on, so enabling it by default is the wrong tradeoff there. Remove the env default and leave EnableWebPEncoding off unless the user opts in. Combined with the nodynamic build tag, 32-bit images neither crash nor pay the WASM cost out of the box. A smarter automatic policy (use WebP only when native libwebp is available) can be revisited separately. --- Dockerfile | 11 +++++++++-- core/artwork/reader_resized.go | 6 ++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index ad1e2a41c..e8a00f470 100644 --- a/Dockerfile +++ b/Dockerfile @@ -69,8 +69,16 @@ RUN --mount=type=bind,source=. \ set -e xx-go --wrap export CGO_ENABLED=1 + # Native libwebp (gen2brain/webp) uses ebitengine/purego reverse callbacks, + # which purego does not support on 32-bit ARM or x86 and crash with a SIGSEGV + # (issue #5597). Build those arches with the "nodynamic" tag so gen2brain/webp + # is WASM-only and never links the purego path. 64-bit arches keep native libwebp. + BUILD_TAGS=netgo,sqlite_fts5 + if [ "$(xx-info arch)" = "arm" ] || [ "$(xx-info arch)" = "386" ]; then + BUILD_TAGS=${BUILD_TAGS},nodynamic + fi # -latomic is required on 32-bit arm (arm/v6, arm/v7) so SQLite's 64-bit atomics resolve. - go build -tags=netgo,sqlite_fts5 -ldflags="-w -s \ + go build -tags=${BUILD_TAGS} -ldflags="-w -s \ -linkmode=external -extldflags '-latomic' \ -X github.com/navidrome/navidrome/consts.gitSha=${GIT_SHA} \ -X github.com/navidrome/navidrome/consts.gitTag=${GIT_TAG}" \ @@ -159,7 +167,6 @@ ENV ND_MUSICFOLDER=/music ENV ND_DATAFOLDER=/data ENV ND_CONFIGFILE=/data/navidrome.toml ENV ND_PORT=4533 -ENV ND_ENABLEWEBPENCODING=true RUN touch /.nddockerenv EXPOSE ${ND_PORT} diff --git a/core/artwork/reader_resized.go b/core/artwork/reader_resized.go index 85a19a4c3..08f42f130 100644 --- a/core/artwork/reader_resized.go +++ b/core/artwork/reader_resized.go @@ -21,6 +21,12 @@ import ( func init() { conf.AddHook(func() { + // gen2brain/webp selects native (purego/libwebp) vs WASM in its own + // package init() and exposes the result only via webp.Dynamic(); there is + // no runtime way to switch back. On 32-bit ARM/x86 the purego callback path + // crashes (issue #5597), so those builds must be compiled with the + // "nodynamic" tag (see Dockerfile), which makes webp.Dynamic() report an + // error here and forces the safe WASM path. if err := webp.Dynamic(); err != nil { log.Debug("Using WASM WebP encoder/decoder", "reason", err) } else { From f3887df334b5c0afb1e53c9106e3134890d0b57b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Sun, 14 Jun 2026 10:47:11 -0400 Subject: [PATCH 14/18] perf(smartplaylists): merge negated artist/tag rules into one NOT EXISTS * fix(smartplaylists): merge negated artist/tag rules in AND groups Smart playlists with many negated role/tag conditions ANDed together (e.g. 100+ "isNot artist" rules, issue #5511) generated one correlated NOT EXISTS subquery per rule, scanning media_file_artists for every candidate row. On large libraries this took minutes and triggered API timeouts and SQLite lock contention. By De Morgan, "NOT EXISTS(role=X) AND NOT EXISTS(role=Y)" is equivalent to "NOT EXISTS(role=X OR role=Y)", so multiple negated conditions for the same field can be collapsed into a single batched NOT EXISTS. This mirrors the existing OR-group merge that #5515 added for positive conditions. The shared grouping/batching logic is extracted into mergeSameFieldConds, parameterized by polarity, so the OR/positive and AND/negated paths reuse one algorithm instead of duplicating it. roleCondGroup/tagCondGroup gain a 'not' flag to emit the negated subquery. Benchmark (323k tracks, 120 isNot artist rules, reporter's exact shape): merged ~54ms vs unmerged ~8.7s steady-state (~160x faster). * docs: trim redundant comments on merge helpers The De Morgan explanation was repeated across three doc comments. Keep it in one place (mergeNegatedJsonConds, where negation is introduced) and reduce the shared core and group-type comments to concise one-liners. --- persistence/criteria_sql.go | 72 +++++++++++------ persistence/criteria_sql_benchmark_test.go | 55 +++++++++++++ persistence/criteria_sql_test.go | 91 ++++++++++++++++++++++ 3 files changed, 193 insertions(+), 25 deletions(-) diff --git a/persistence/criteria_sql.go b/persistence/criteria_sql.go index fa769ef40..b74f498d0 100644 --- a/persistence/criteria_sql.go +++ b/persistence/criteria_sql.go @@ -140,7 +140,7 @@ func (c smartPlaylistCriteria) exprSQL(expr criteria.Expression) (squirrel.Sqliz } and = append(and, cond) } - return and, nil + return mergeNegatedJsonConds(and), nil case criteria.Any: or := squirrel.Or{} for _, child := range e { @@ -454,6 +454,25 @@ const jsonCondBatchSize = 350 // This turns N separate correlated subqueries into ceil(N/batchSize), dramatically // improving performance for smart playlists with many patterns. func mergeJsonConds(or squirrel.Or) squirrel.Sqlizer { + if merged, ok := mergeSameFieldConds(or, false); ok { + return squirrel.Or(merged) + } + return or +} + +// mergeNegatedJsonConds is the AND-group counterpart to mergeJsonConds, merging negated +// conditions. By De Morgan, "NOT EXISTS(X) AND NOT EXISTS(Y)" == "NOT EXISTS(X OR Y)". +func mergeNegatedJsonConds(and squirrel.And) squirrel.Sqlizer { + if merged, ok := mergeSameFieldConds(and, true); ok { + return squirrel.And(merged) + } + return and +} + +// mergeSameFieldConds groups roleCond/tagCond entries that share a field and the requested +// polarity, replacing each group of 2+ with batched roleCondGroup/tagCondGroup subqueries. +// Returns the rewritten conditions and whether any merge happened. +func mergeSameFieldConds(conds []squirrel.Sqlizer, negated bool) ([]squirrel.Sqlizer, bool) { type condEntry struct { index int cond squirrel.Sqlizer @@ -465,10 +484,10 @@ func mergeJsonConds(or squirrel.Or) squirrel.Sqlizer { tag string } groups := make(map[string]*group) - for i, s := range or { + for i, s := range conds { switch c := s.(type) { case roleCond: - if c.not || c.cond == nil { + if c.not != negated || c.cond == nil { continue } g, exists := groups["role:"+c.role] @@ -478,7 +497,7 @@ func mergeJsonConds(or squirrel.Or) squirrel.Sqlizer { } g.entries = append(g.entries, condEntry{index: i, cond: c.cond}) case tagCond: - if c.not || c.cond == nil { + if c.not != negated || c.cond == nil { continue } g, exists := groups["tag:"+c.tag] @@ -490,7 +509,6 @@ func mergeJsonConds(or squirrel.Or) squirrel.Sqlizer { } } - merged := false remove := make(map[int]bool) var additions []squirrel.Sqlizer for _, key := range slices.Sorted(maps.Keys(groups)) { @@ -498,45 +516,42 @@ func mergeJsonConds(or squirrel.Or) squirrel.Sqlizer { if len(g.entries) < 2 { continue } - merged = true - for _, e := range g.entries { - remove[e.index] = true - } - conds := make([]squirrel.Sqlizer, len(g.entries)) + batchConds := make([]squirrel.Sqlizer, len(g.entries)) for i, e := range g.entries { - conds[i] = e.cond + remove[e.index] = true + batchConds[i] = e.cond } if g.isRole { role := key[len("role:"):] - for batch := range slices.Chunk(conds, jsonCondBatchSize) { - additions = append(additions, roleCondGroup{role: role, conds: batch}) + for batch := range slices.Chunk(batchConds, jsonCondBatchSize) { + additions = append(additions, roleCondGroup{role: role, conds: batch, not: negated}) } } else { - for batch := range slices.Chunk(conds, jsonCondBatchSize) { - additions = append(additions, tagCondGroup{tag: g.tag, numeric: g.numeric, conds: batch}) + for batch := range slices.Chunk(batchConds, jsonCondBatchSize) { + additions = append(additions, tagCondGroup{tag: g.tag, numeric: g.numeric, conds: batch, not: negated}) } } } - if !merged { - return or + if len(remove) == 0 { + return conds, false } - result := make(squirrel.Or, 0, len(or)-len(remove)+len(additions)) - for i, s := range or { + result := make([]squirrel.Sqlizer, 0, len(conds)-len(remove)+len(additions)) + for i, s := range conds { if !remove[i] { result = append(result, s) } } - result = append(result, additions...) - return result + return append(result, additions...), true } -// roleCondGroup represents multiple role conditions for the same role, merged into -// a single EXISTS subquery for performance. +// roleCondGroup represents multiple role conditions for the same role, merged into a single +// (optionally negated) EXISTS subquery for performance. type roleCondGroup struct { role string conds []squirrel.Sqlizer + not bool } func (g roleCondGroup) ToSql() (string, []any, error) { @@ -551,15 +566,19 @@ func (g roleCondGroup) ToSql() (string, []any, error) { allArgs = append(allArgs, args...) } cond := roleExistsSQL("(" + strings.Join(innerParts, " OR ") + ")") + if g.not { + cond = "not " + cond + } return cond, allArgs, nil } -// tagCondGroup represents multiple tag conditions for the same tag, merged into -// a single EXISTS subquery for performance. +// tagCondGroup represents multiple tag conditions for the same tag, merged into a single +// (optionally negated) EXISTS subquery for performance. type tagCondGroup struct { tag string numeric bool conds []squirrel.Sqlizer + not bool } func (g tagCondGroup) ToSql() (string, []any, error) { @@ -578,6 +597,9 @@ func (g tagCondGroup) ToSql() (string, []any, error) { } cond := fmt.Sprintf("exists (select 1 from json_tree(media_file.tags, '$.%s') where key='value' and (%s))", g.tag, strings.Join(innerParts, " OR ")) + if g.not { + cond = "not " + cond + } return cond, allArgs, nil } diff --git a/persistence/criteria_sql_benchmark_test.go b/persistence/criteria_sql_benchmark_test.go index d901e9eda..1dcf97871 100644 --- a/persistence/criteria_sql_benchmark_test.go +++ b/persistence/criteria_sql_benchmark_test.go @@ -60,6 +60,61 @@ func BenchmarkSmartPlaylistRole(b *testing.B) { }) } +// BenchmarkSmartPlaylistNegatedRole compares performance for smart playlists with many +// negated role conditions ANDed together (e.g. 500 "isNot artist" rules, issue #5511) +// between the current implementation (merged NOT EXISTS via criteria pipeline) and the +// old baseline (one separate NOT EXISTS subquery per pattern). +func BenchmarkSmartPlaylistNegatedRole(b *testing.B) { + configtest.SetupConfig() + tmpDir := b.TempDir() + conf.Server.DbPath = filepath.Join(tmpDir, "bench-smartpl-neg.db") + cleanup := db.Init(context.Background()) + defer cleanup() + log.SetLevel(log.LevelFatal) + + conn := dbx.NewFromDB(db.Db(), db.Dialect) + ctx := log.NewContext(context.Background()) + user := model.User{ID: "bench-user", UserName: "bench", Name: "Bench User", IsAdmin: true} + ctx = request.WithUser(ctx, user) + + setupBenchData(b, ctx, conn, user) + criteria.AddRoles([]string{"artist"}) + + // Build the criteria expression: 500 "isNot artist" patterns in an AND group + allExprs := make(criteria.All, benchNumPatterns) + for i := range benchNumPatterns { + allExprs[i] = criteria.IsNot{"artist": fmt.Sprintf("Artist %04d", i)} + } + expr := criteria.Criteria{Expression: allExprs, Sort: "title", Limit: 500} + + b.Run("Current", func(b *testing.B) { + benchmarkCriteriaPipeline(b, ctx, expr) + }) + b.Run("Baseline_UnmergedNotExists", func(b *testing.B) { + benchmarkUnmergedNegatedJSONTree(b, ctx) + }) +} + +// benchmarkUnmergedNegatedJSONTree builds the old-style query with N separate negated +// json_tree EXISTS subqueries ANDed together (the pre-optimization baseline). +func benchmarkUnmergedNegatedJSONTree(b *testing.B, ctx context.Context) { + b.Helper() + + var sb strings.Builder + sb.WriteString("SELECT media_file.id FROM media_file WHERE (") + args := make([]any, 0, benchNumPatterns) + for i := range benchNumPatterns { + if i > 0 { + sb.WriteString(" AND ") + } + sb.WriteString("not exists (select 1 from json_tree(media_file.participants, '$.artist') where key='name' and value = ?)") + args = append(args, fmt.Sprintf("Artist %04d", i)) + } + sb.WriteString(") ORDER BY media_file.title LIMIT 500") + + runBenchQuery(b, ctx, sb.String(), args) +} + // benchmarkCriteriaPipeline runs the criteria through the actual production code path: // newSmartPlaylistCriteria → Where() → ToSql(), then executes the resulting query. func benchmarkCriteriaPipeline(b *testing.B, ctx context.Context, expr criteria.Criteria) { diff --git a/persistence/criteria_sql_test.go b/persistence/criteria_sql_test.go index 0257fa0cf..9ff7f1f07 100644 --- a/persistence/criteria_sql_test.go +++ b/persistence/criteria_sql_test.go @@ -408,6 +408,97 @@ var _ = Describe("Smart playlist criteria SQL", func() { Expect(sql).To(ContainSubstring("value LIKE ? OR value LIKE ?")) Expect(args).To(HaveLen(2 + 2 + 1)) // 2 tag patterns + 2 role patterns + 1 role name }) + + It("merges negated role conditions in an AND group into a single NOT EXISTS", func() { + expr := criteria.All{ + criteria.IsNot{"artist": "Beatles"}, + criteria.IsNot{"artist": "Kraftwerk"}, + } + sqlizer, err := newSmartPlaylistCriteria(criteria.Criteria{Expression: expr}).Where() + Expect(err).ToNot(HaveOccurred()) + + sql, args, err := sqlizer.ToSql() + Expect(err).ToNot(HaveOccurred()) + // A single NOT EXISTS with both names ORed inside (De Morgan) + Expect(strings.Count(sql, "not exists")).To(Equal(1)) + Expect(sql).To(ContainSubstring("artist.name = ? OR artist.name = ?")) + Expect(args).To(HaveExactElements("artist", "Beatles", "Kraftwerk")) + }) + + It("merges negated notContains role conditions in an AND group", func() { + expr := criteria.All{ + criteria.NotContains{"artist": "Beatles"}, + criteria.NotContains{"artist": "Kraftwerk"}, + } + sqlizer, err := newSmartPlaylistCriteria(criteria.Criteria{Expression: expr}).Where() + Expect(err).ToNot(HaveOccurred()) + + sql, args, err := sqlizer.ToSql() + Expect(err).ToNot(HaveOccurred()) + Expect(strings.Count(sql, "not exists")).To(Equal(1)) + Expect(sql).To(ContainSubstring("artist.name LIKE ? OR artist.name LIKE ?")) + Expect(args).To(HaveExactElements("artist", "%Beatles%", "%Kraftwerk%")) + }) + + It("merges negated tag conditions in an AND group into a single NOT EXISTS", func() { + expr := criteria.All{ + criteria.NotContains{"genre": "Rock"}, + criteria.NotContains{"genre": "Metal"}, + } + sqlizer, err := newSmartPlaylistCriteria(criteria.Criteria{Expression: expr}).Where() + Expect(err).ToNot(HaveOccurred()) + + sql, args, err := sqlizer.ToSql() + Expect(err).ToNot(HaveOccurred()) + Expect(strings.Count(sql, "not exists")).To(Equal(1)) + Expect(sql).To(ContainSubstring("value LIKE ? OR value LIKE ?")) + Expect(args).To(HaveExactElements("%Rock%", "%Metal%")) + }) + + It("does not merge a single negated condition with a positive one of the same role in AND", func() { + // AND of mixed polarity must not be collapsed: NOT EXISTS(a) AND EXISTS(b) + // is not equivalent to any single merged subquery. + expr := criteria.All{ + criteria.Contains{"artist": "Beatles"}, + criteria.IsNot{"artist": "Kraftwerk"}, + } + sqlizer, err := newSmartPlaylistCriteria(criteria.Criteria{Expression: expr}).Where() + Expect(err).ToNot(HaveOccurred()) + + sql, _, err := sqlizer.ToSql() + Expect(err).ToNot(HaveOccurred()) + // One positive EXISTS and one negated NOT EXISTS, kept separate + Expect(strings.Count(sql, "not exists")).To(Equal(1)) + Expect(strings.Count(sql, "exists")).To(Equal(2)) // "not exists" contains "exists" + }) + + It("does not merge negated conditions of different roles in AND", func() { + expr := criteria.All{ + criteria.IsNot{"artist": "Beatles"}, + criteria.IsNot{"composer": "Lennon"}, + } + sqlizer, err := newSmartPlaylistCriteria(criteria.Criteria{Expression: expr}).Where() + Expect(err).ToNot(HaveOccurred()) + + sql, _, err := sqlizer.ToSql() + Expect(err).ToNot(HaveOccurred()) + Expect(strings.Count(sql, "not exists")).To(Equal(2)) + }) + + It("batches large negated AND groups to avoid SQLite expression tree depth limit", func() { + allExprs := make(criteria.All, jsonCondBatchSize+1) + for i := range allExprs { + allExprs[i] = criteria.IsNot{"artist": fmt.Sprintf("Artist%d", i)} + } + sqlizer, err := newSmartPlaylistCriteria(criteria.Criteria{Expression: allExprs}).Where() + Expect(err).ToNot(HaveOccurred()) + + sql, args, err := sqlizer.ToSql() + Expect(err).ToNot(HaveOccurred()) + // Two NOT EXISTS subqueries (one batch of jsonCondBatchSize, one of 1) + Expect(strings.Count(sql, "not exists")).To(Equal(2)) + Expect(args).To(HaveLen(2 + jsonCondBatchSize + 1)) + }) }) Describe("joins", func() { From 2c90685bc29ae5c1623f25ab5d15cd039725569a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Sun, 14 Jun 2026 13:39:16 -0400 Subject: [PATCH 15/18] fix(scanner): import playlists skipped when no admin existed yet (#5609) * fix(scanner): import playlists skipped when no admin existed yet (#5499) On a fresh install the first scan runs before any admin user exists, so the scanner's playlist phase skips all playlists (playlists are owned by the first admin). Nothing re-imported them afterwards because folder selection is gated on updated_at > last_scan_at, which nothing bumps. The playlist phase now: - resolves the admin at phase time (FindFirstAdmin) instead of trusting the context snapshot taken at scan start, so a long admin-less scan still imports playlists in its own phase if an admin was created meanwhile; - records a persisted PlaylistsImportPending flag when no admin exists yet; - when that flag is set, imports ALL playlist folders via a new GetAllWithPlaylists (bypassing the timestamp gate) and clears the flag. Playlists are recovered by the next scan that runs with an admin, with no dependency on scan duration and no changes to the auth/server layers. * fix(scanner): surface datastore errors in playlist import deferral (#5499) Address review feedback: - distinguish model.ErrNotFound (no admin yet -> defer) from real datastore errors when resolving the admin, so DB failures are propagated, not swallowed; - propagate the error if the pending-import flag can't be persisted, so a scan doesn't complete as successful without recording the recovery; - surface read errors when checking the pending flag. Also name the no-admin condition for readability. * fix(scanner): simplify admin existence check in playlist import Signed-off-by: Deluan * fix(scanner): streamline folder access in playlist import logic Signed-off-by: Deluan --------- Signed-off-by: Deluan --- consts/consts.go | 3 + model/folder.go | 3 + persistence/folder_repository.go | 12 +++ persistence/folder_repository_test.go | 32 ++++++++ scanner/phase_4_playlists.go | 78 ++++++++++++++---- scanner/phase_4_playlists_test.go | 110 ++++++++++++++++++++++++-- tests/mock_user_repo.go | 12 +++ 7 files changed, 226 insertions(+), 24 deletions(-) diff --git a/consts/consts.go b/consts/consts.go index edd8f2b54..4baf4610d 100644 --- a/consts/consts.go +++ b/consts/consts.go @@ -14,6 +14,9 @@ const ( DefaultDbPath = "navidrome.db?cache=shared&_busy_timeout=15000&_journal_mode=WAL&_foreign_keys=on&synchronous=normal" InitialSetupFlagKey = "InitialSetup" FullScanAfterMigrationFlagKey = "FullScanAfterMigration" + // PlaylistsImportPendingFlagKey marks that playlist import was deferred because + // no admin user existed yet; the next scan with an admin imports them. + PlaylistsImportPendingFlagKey = "PlaylistsImportPending" LastScanErrorKey = "LastScanError" LastScanTypeKey = "LastScanType" LastScanStartTimeKey = "LastScanStartTime" diff --git a/model/folder.go b/model/folder.go index 39cb6db84..81800c072 100644 --- a/model/folder.go +++ b/model/folder.go @@ -93,4 +93,7 @@ type FolderRepository interface { Put(*Folder) error MarkMissing(missing bool, ids ...string) error GetTouchedWithPlaylists() (FolderCursor, error) + // GetAllWithPlaylists returns all non-missing folders with playlists, ignoring + // the scan-timestamp gate used by GetTouchedWithPlaylists. + GetAllWithPlaylists() (FolderCursor, error) } diff --git a/persistence/folder_repository.go b/persistence/folder_repository.go index 624ef21b8..8fb7f0296 100644 --- a/persistence/folder_repository.go +++ b/persistence/folder_repository.go @@ -250,6 +250,18 @@ func (r folderRepository) GetTouchedWithPlaylists() (model.FolderCursor, error) return wrapFolderCursor(cursor), nil } +func (r folderRepository) GetAllWithPlaylists() (model.FolderCursor, error) { + query := r.selectFolder().Where(And{ + Eq{"missing": false}, + Gt{"num_playlists": 0}, + }) + cursor, err := queryWithStableResults[dbFolder](r.sqlRepository, query) + if err != nil { + return nil, err + } + return wrapFolderCursor(cursor), nil +} + func wrapFolderCursor(cursor iter.Seq2[dbFolder, error]) model.FolderCursor { return func(yield func(model.Folder, error) bool) { for f, err := range cursor { diff --git a/persistence/folder_repository_test.go b/persistence/folder_repository_test.go index 413a6b38f..a8945dfee 100644 --- a/persistence/folder_repository_test.go +++ b/persistence/folder_repository_test.go @@ -317,4 +317,36 @@ var _ = Describe("FolderRepository", func() { Expect(folders[0].ID).To(Equal("f1")) }) }) + + Describe("GetAllWithPlaylists", func() { + It("returns all non-missing folders with playlists, ignoring the scan-timestamp gate", func() { + withPls := model.NewFolder(testLib, "TestAllPls/WithPls") + withPls.NumPlaylists = 2 + noPls := model.NewFolder(testLib, "TestAllPls/NoPls") + noPls.NumPlaylists = 0 + missingWithPls := model.NewFolder(testLib, "TestAllPls/Missing") + missingWithPls.NumPlaylists = 1 + missingWithPls.Missing = true + + Expect(repo.Put(withPls)).To(Succeed()) + Expect(repo.Put(noPls)).To(Succeed()) + Expect(repo.Put(missingWithPls)).To(Succeed()) + + // Force the folder's updated_at to the past so GetTouchedWithPlaylists + // (which gates on updated_at > last_scan_at) would NOT return it. + _, err := conn.NewQuery("UPDATE folder SET updated_at = {:t} WHERE id = {:id}"). + Bind(dbx.Params{"t": "2000-01-01 00:00:00", "id": withPls.ID}).Execute() + Expect(err).ToNot(HaveOccurred()) + + var ids []string + cursor, err := repo.GetAllWithPlaylists() + Expect(err).ToNot(HaveOccurred()) + for f, err := range cursor { + Expect(err).ToNot(HaveOccurred()) + ids = append(ids, f.ID) + } + + Expect(ids).To(ConsistOf(withPls.ID)) // only the non-missing folder with playlists + }) + }) }) diff --git a/scanner/phase_4_playlists.go b/scanner/phase_4_playlists.go index d52743966..8ba014235 100644 --- a/scanner/phase_4_playlists.go +++ b/scanner/phase_4_playlists.go @@ -2,6 +2,7 @@ package scanner import ( "context" + "errors" "fmt" "os" "strings" @@ -10,6 +11,7 @@ import ( ppl "github.com/google/go-pipeline/pkg/pipeline" "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/core/artwork" "github.com/navidrome/navidrome/core/playlists" "github.com/navidrome/navidrome/log" @@ -18,12 +20,13 @@ import ( ) type phasePlaylists struct { - ctx context.Context - scanState *scanState - ds model.DataStore - pls playlists.Playlists - cw artwork.CacheWarmer - refreshed atomic.Uint32 + ctx context.Context + scanState *scanState + ds model.DataStore + pls playlists.Playlists + cw artwork.CacheWarmer + refreshed atomic.Uint32 + pendingImport bool } func createPhasePlaylists(ctx context.Context, scanState *scanState, ds model.DataStore, pls playlists.Playlists, cw artwork.CacheWarmer) *phasePlaylists { @@ -49,22 +52,41 @@ func (p *phasePlaylists) produce(put func(entry *model.Folder)) error { log.Info(p.ctx, "Playlists will not be imported, AutoImportPlaylists is set to false") return nil } - u, _ := request.UserFrom(p.ctx) - if !u.IsAdmin || u.ID == "" { - log.Warn(p.ctx, "Playlists will not be imported, as there are no admin users yet, "+ - "Please create an admin user first, and then update the playlists for them to be imported") - return nil + + // Resolve the admin at phase time (the producer runs late in the scan), so an + // admin created while the scan was in progress is picked up. Assigned once, + // before any put() below, so the channel send synchronizes it with the stages. + admin, err := p.ds.User(p.ctx).FindFirstAdmin() + if err != nil && !errors.Is(err, model.ErrNotFound) { + return fmt.Errorf("finding admin user: %w", err) + } + noAdmin := admin == nil || admin.ID == "" + if noAdmin { + return p.deferImport() + } + p.ctx = request.WithUser(p.ctx, *admin) + + // When recovering a deferred import, scan all playlist folders, not just touched ones. + pending, err := p.importPending() + if err != nil { + return fmt.Errorf("checking pending playlist import: %w", err) + } + p.pendingImport = pending + var cursor model.FolderCursor + if p.pendingImport { + cursor, err = p.ds.Folder(p.ctx).GetAllWithPlaylists() + } else { + cursor, err = p.ds.Folder(p.ctx).GetTouchedWithPlaylists() + } + if err != nil { + return fmt.Errorf("loading folders with playlists: %w", err) } count := 0 - cursor, err := p.ds.Folder(p.ctx).GetTouchedWithPlaylists() - if err != nil { - return fmt.Errorf("loading touched folders: %w", err) - } - log.Debug(p.ctx, "Scanner: Checking playlists that may need refresh") + log.Debug(p.ctx, "Scanner: Checking playlists that may need refresh", "pendingImport", p.pendingImport) for folder, err := range cursor { if err != nil { - return fmt.Errorf("loading touched folder: %w", err) + return fmt.Errorf("loading folder with playlists: %w", err) } count++ put(&folder) @@ -78,6 +100,23 @@ func (p *phasePlaylists) produce(put func(entry *model.Folder)) error { return nil } +// deferImport records the pending-import flag so a later scan with an admin can +// import the playlists, and returns an error if the flag can't be persisted (so +// the scan does not complete as successful without recording the recovery). +func (p *phasePlaylists) deferImport() error { + if err := p.ds.Property(p.ctx).Put(consts.PlaylistsImportPendingFlagKey, "1"); err != nil { + return fmt.Errorf("recording pending playlist import: %w", err) + } + log.Warn(p.ctx, "Playlists will not be imported, as there are no admin users yet. "+ + "They will be imported automatically once an admin user is created.") + return nil +} + +func (p *phasePlaylists) importPending() (bool, error) { + v, err := p.ds.Property(p.ctx).DefaultGet(consts.PlaylistsImportPendingFlagKey, "0") + return v == "1", err +} + func (p *phasePlaylists) stages() []ppl.Stage[*model.Folder] { return []ppl.Stage[*model.Folder]{ ppl.NewStage(p.processPlaylistsInFolder, ppl.Name("process playlists in folder"), ppl.Concurrency(3)), @@ -123,6 +162,11 @@ func (p *phasePlaylists) finalize(err error) error { } else { p.scanState.changesDetected.Store(true) } + if p.pendingImport && err == nil { + if derr := p.ds.Property(p.ctx).Delete(consts.PlaylistsImportPendingFlagKey); derr != nil { + log.Warn(p.ctx, "Scanner: Could not clear pending playlist-import flag", derr) + } + } logF(p.ctx, "Scanner: Finished refreshing playlists", "refreshed", refreshed, err) return err } diff --git a/scanner/phase_4_playlists_test.go b/scanner/phase_4_playlists_test.go index 0e01a7549..49ffc7fb7 100644 --- a/scanner/phase_4_playlists_test.go +++ b/scanner/phase_4_playlists_test.go @@ -9,10 +9,10 @@ import ( "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf/configtest" + "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/core/artwork" "github.com/navidrome/navidrome/core/playlists" "github.com/navidrome/navidrome/model" - "github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/tests" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -30,14 +30,22 @@ var _ = Describe("phasePlaylists", func() { cw artwork.CacheWarmer ) + var userRepo *tests.MockedUserRepo + var propRepo *tests.MockedPropertyRepo + BeforeEach(func() { DeferCleanup(configtest.SetupConfig()) conf.Server.AutoImportPlaylists = true ctx = context.Background() - ctx = request.WithUser(ctx, model.User{ID: "123", IsAdmin: true}) folderRepo = &mockFolderRepository{} + userRepo = tests.CreateMockUserRepo() + // An admin user exists by default, so playlist import proceeds. + Expect(userRepo.Put(&model.User{ID: "123", UserName: "admin", IsAdmin: true})).To(Succeed()) + propRepo = &tests.MockedPropertyRepo{} ds = &tests.MockDataStore{ - MockedFolder: folderRepo, + MockedFolder: folderRepo, + MockedUser: userRepo, + MockedProperty: propRepo, } pls = &mockPlaylists{} cw = artwork.NoopCacheWarmer() @@ -84,6 +92,81 @@ var _ = Describe("phasePlaylists", func() { Expect(called).To(BeFalse()) Expect(err).To(MatchError(ContainSubstring("error loading folders"))) }) + + It("sets the pending flag and imports nothing when no admin user exists", func() { + // Remove the admin user; produce resolves the admin at phase time. + userRepo.Data = map[string]*model.User{} + folderRepo.SetData(map[*model.Folder]error{ + {Path: "/path/to/folder1"}: nil, + }) + + called := false + err := phase.produce(func(folder *model.Folder) { called = true }) + + Expect(err).ToNot(HaveOccurred()) + Expect(called).To(BeFalse()) + v, _ := propRepo.Get(consts.PlaylistsImportPendingFlagKey) + Expect(v).To(Equal("1")) + }) + + It("returns an error (not a silent defer) on a datastore failure resolving the admin", func() { + userRepo.Error = errors.New("db is locked") + + err := phase.produce(func(folder *model.Folder) {}) + + Expect(err).To(MatchError(ContainSubstring("finding admin user"))) + // Must NOT have set the pending flag on a real error. + _, getErr := propRepo.Get(consts.PlaylistsImportPendingFlagKey) + Expect(getErr).To(HaveOccurred()) + }) + + It("returns an error when the pending flag cannot be persisted", func() { + userRepo.Data = map[string]*model.User{} // no admin -> defer path + propRepo.Error = errors.New("property table unavailable") + + err := phase.produce(func(folder *model.Folder) {}) + + Expect(err).To(MatchError(ContainSubstring("recording pending playlist import"))) + }) + + It("imports all playlist folders when the pending flag is set", func() { + Expect(propRepo.Put(consts.PlaylistsImportPendingFlagKey, "1")).To(Succeed()) + folderRepo.SetAllData(map[*model.Folder]error{ + {Path: "/path/to/folder1"}: nil, + {Path: "/path/to/folder2"}: nil, + }) + // Touched set is empty: proves selection used GetAllWithPlaylists. + folderRepo.SetData(map[*model.Folder]error{}) + + var produced []*model.Folder + err := phase.produce(func(folder *model.Folder) { produced = append(produced, folder) }) + + Expect(err).ToNot(HaveOccurred()) + Expect(produced).To(HaveLen(2)) + Expect(phase.pendingImport).To(BeTrue()) + }) + }) + + Describe("finalize", func() { + It("clears the pending flag after a successful pending import", func() { + Expect(propRepo.Put(consts.PlaylistsImportPendingFlagKey, "1")).To(Succeed()) + phase.pendingImport = true + + Expect(phase.finalize(nil)).To(Succeed()) + + _, err := propRepo.Get(consts.PlaylistsImportPendingFlagKey) + Expect(err).To(HaveOccurred()) // deleted + }) + + It("keeps the pending flag when the import failed", func() { + Expect(propRepo.Put(consts.PlaylistsImportPendingFlagKey, "1")).To(Succeed()) + phase.pendingImport = true + + Expect(phase.finalize(errors.New("boom"))).To(HaveOccurred()) + + v, _ := propRepo.Get(consts.PlaylistsImportPendingFlagKey) + Expect(v).To(Equal("1")) + }) }) Describe("processPlaylistsInFolder", func() { @@ -141,12 +224,13 @@ func (p *mockPlaylists) ImportFromFolder(ctx context.Context, folder *model.Fold type mockFolderRepository struct { model.FolderRepository - data map[*model.Folder]error + data map[*model.Folder]error + allData map[*model.Folder]error } -func (f *mockFolderRepository) GetTouchedWithPlaylists() (model.FolderCursor, error) { +func cursorFromData(data map[*model.Folder]error) model.FolderCursor { return func(yield func(model.Folder, error) bool) { - for folder, err := range f.data { + for folder, err := range data { if err != nil { if !yield(model.Folder{}, err) { return @@ -157,9 +241,21 @@ func (f *mockFolderRepository) GetTouchedWithPlaylists() (model.FolderCursor, er return } } - }, nil + } +} + +func (f *mockFolderRepository) GetTouchedWithPlaylists() (model.FolderCursor, error) { + return cursorFromData(f.data), nil +} + +func (f *mockFolderRepository) GetAllWithPlaylists() (model.FolderCursor, error) { + return cursorFromData(f.allData), nil } func (f *mockFolderRepository) SetData(m map[*model.Folder]error) { f.data = m } + +func (f *mockFolderRepository) SetAllData(m map[*model.Folder]error) { + f.allData = m +} diff --git a/tests/mock_user_repo.go b/tests/mock_user_repo.go index 7c7dadbc4..2d6ff3c02 100644 --- a/tests/mock_user_repo.go +++ b/tests/mock_user_repo.go @@ -57,6 +57,18 @@ func (u *MockedUserRepo) FindByUsernameWithPassword(username string) (*model.Use return u.FindByUsername(username) } +func (u *MockedUserRepo) FindFirstAdmin() (*model.User, error) { + if u.Error != nil { + return nil, u.Error + } + for _, usr := range u.Data { + if usr.IsAdmin { + return usr, nil + } + } + return nil, model.ErrNotFound +} + func (u *MockedUserRepo) Get(id string) (*model.User, error) { if u.Error != nil { return nil, u.Error From c4c70519b5f3a2a6eaf01f5786754d899cda9bfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Sun, 14 Jun 2026 16:52:01 -0400 Subject: [PATCH 16/18] fix(transcoding): enforce server-side player MaxBitRate on /rest/stream (#5611) * fix(transcoding): enforce player MaxBitRate on getTranscodeDecision The Web UI streams via getTranscodeDecision, which (since #5473) ignored the server-side player config. Apply the player's MaxBitRate as a bitrate ceiling on the client's declared limits before MakeDecision, restoring per-player bitrate enforcement without reintroducing the forced-format override. Fixes #5583. * test(e2e): assert player MaxBitRate is enforced on getTranscodeDecision Invert the assertions added in #5473 that expected the player cap to be ignored; getTranscodeDecision now enforces it (issue #5583). * feat(ui): clarify web player ignores forced transcoding format Add helper text to the Transcoding field on the player edit form when the player is the NavidromeUI web client, since it enforces only the Max. Bit Rate, not the forced format. Part of issue #5583. * refactor(stream): extract ClientInfo.CapBitrate, share across transcode paths Move the player MaxBitRate ceiling logic into a canonical ClientInfo.CapBitrate method in core/stream, used by both getTranscodeDecision and the legacy ResolveRequest path. Removes handler-layer duplication and corrects a misleading comment that wrongly implied the legacy single-field cap was buggy. * fix(transcoding): downsample on legacy /stream when only player MaxBitRate is set A bare /stream or /download request from a player configured with a server-side MaxBitRate (but no forced format) was served raw, ignoring the cap. buildLegacyClientInfo now triggers DefaultDownsamplingFormat when the player MaxBitRate alone is below the source bitrate, matching the already-correct forced-format and request-bitrate paths. Part of #5583. * fix(ui): add Brazilian Portuguese translation for player transcoding helper text Translates the new resources.player.helperTexts.transcodingId key added for the web player transcoding-format clarification. Part of #5583. * fix(ui): restore Transcoding field styling and render helper text The TranscodingInput wrapper swallowed the variant SimpleForm injects into its direct children (field lost its outlined box) and put helperText on the ReferenceInput, which does not forward it to the input. Spread the form props onto ReferenceInput and move helperText to the SelectInput child so both the outlined styling and the helper text render. Part of #5583. * fix(i18n): update Brazilian Portuguese translation for album artist field Signed-off-by: Deluan * fix(ui): clean up comments in PlayerEdit component Signed-off-by: Deluan * test(ui): mock useTranslate in PlayerEdit test for determinism Avoid depending on ra-core's out-of-provider translation behavior, which can vary by version. Part of #5583. --------- Signed-off-by: Deluan --- core/stream/legacy_client.go | 20 +++++-- core/stream/legacy_client_test.go | 86 ++++++++++++++++++++++++--- core/stream/types.go | 19 ++++++ core/stream/types_test.go | 59 ++++++++++++++++++ resources/i18n/pt-br.json | 7 ++- server/e2e/subsonic_transcode_test.go | 46 +++++++------- server/subsonic/transcode.go | 10 ++++ server/subsonic/transcode_test.go | 71 ++++++++++++++++++++++ ui/src/i18n/en.json | 3 + ui/src/player/PlayerEdit.jsx | 33 +++++++--- ui/src/player/PlayerEdit.test.jsx | 54 +++++++++++++++++ 11 files changed, 364 insertions(+), 44 deletions(-) create mode 100644 core/stream/types_test.go create mode 100644 ui/src/player/PlayerEdit.test.jsx diff --git a/core/stream/legacy_client.go b/core/stream/legacy_client.go index 9dd6179a0..652e42eba 100644 --- a/core/stream/legacy_client.go +++ b/core/stream/legacy_client.go @@ -12,7 +12,7 @@ import ( // buildLegacyClientInfo translates legacy Subsonic stream/download parameters // into a ClientInfo for use with MakeDecision. -func buildLegacyClientInfo(mf *model.MediaFile, reqFormat string, reqBitRate int) *ClientInfo { +func buildLegacyClientInfo(mf *model.MediaFile, reqFormat string, reqBitRate int, playerMaxBitRate int) *ClientInfo { ci := &ClientInfo{Name: "legacy"} // Determine target format for transcoding @@ -22,6 +22,10 @@ func buildLegacyClientInfo(mf *model.MediaFile, reqFormat string, reqBitRate int targetFormat = reqFormat case reqBitRate > 0 && reqBitRate < mf.BitRate && conf.Server.DefaultDownsamplingFormat != "": targetFormat = conf.Server.DefaultDownsamplingFormat + case playerMaxBitRate > 0 && playerMaxBitRate < mf.BitRate && conf.Server.DefaultDownsamplingFormat != "": + // Server-side player MaxBitRate alone forces downsampling, even when the + // client sent no format/bitrate params (issue #5583, legacy /stream path). + targetFormat = conf.Server.DefaultDownsamplingFormat } if targetFormat != "" { @@ -63,15 +67,19 @@ func (s *deciderService) ResolveRequest(ctx context.Context, mf *model.MediaFile return req } - clientInfo := buildLegacyClientInfo(mf, reqFormat, reqBitRate) + playerMaxBitRate := 0 + if player, ok := request.PlayerFrom(ctx); ok { + playerMaxBitRate = player.MaxBitRate + } + + clientInfo := buildLegacyClientInfo(mf, reqFormat, reqBitRate, playerMaxBitRate) // Apply server-side player transcoding override before making the decision if trc, ok := request.TranscodingFrom(ctx); ok && trc.TargetFormat != "" { clientInfo = applyServerOverride(ctx, clientInfo, &trc) - } else if player, ok := request.PlayerFrom(ctx); ok && player.MaxBitRate > 0 { - if clientInfo.MaxAudioBitrate == 0 || player.MaxBitRate < clientInfo.MaxAudioBitrate { - modified := *clientInfo - modified.MaxAudioBitrate = player.MaxBitRate + } else if player, ok := request.PlayerFrom(ctx); ok { + modified := *clientInfo + if modified.CapBitrate(player.MaxBitRate) { clientInfo = &modified log.Debug(ctx, "Applied player MaxBitRate cap", "playerMaxBitRate", player.MaxBitRate, "client", clientInfo.Name) } diff --git a/core/stream/legacy_client_test.go b/core/stream/legacy_client_test.go index 2ddd74ce0..bc8405976 100644 --- a/core/stream/legacy_client_test.go +++ b/core/stream/legacy_client_test.go @@ -21,7 +21,7 @@ var _ = Describe("buildLegacyClientInfo", func() { }) It("sets transcoding profile for explicit format without bitrate", func() { - ci := buildLegacyClientInfo(mf, "mp3", 0) + ci := buildLegacyClientInfo(mf, "mp3", 0, 0) Expect(ci.Name).To(Equal("legacy")) Expect(ci.TranscodingProfiles).To(HaveLen(1)) @@ -34,7 +34,7 @@ var _ = Describe("buildLegacyClientInfo", func() { }) It("does not add direct play profile when explicit format differs from source (no bitrate)", func() { - ci := buildLegacyClientInfo(mf, "opus", 0) + ci := buildLegacyClientInfo(mf, "opus", 0, 0) Expect(ci.TranscodingProfiles).To(HaveLen(1)) Expect(ci.TranscodingProfiles[0].Container).To(Equal("opus")) @@ -42,7 +42,7 @@ var _ = Describe("buildLegacyClientInfo", func() { }) It("adds direct play profile when explicit format matches source format", func() { - ci := buildLegacyClientInfo(mf, "flac", 0) + ci := buildLegacyClientInfo(mf, "flac", 0, 0) Expect(ci.TranscodingProfiles).To(HaveLen(1)) Expect(ci.TranscodingProfiles[0].Container).To(Equal("flac")) @@ -52,7 +52,7 @@ var _ = Describe("buildLegacyClientInfo", func() { }) It("sets transcoding profile and bitrate for explicit format with bitrate", func() { - ci := buildLegacyClientInfo(mf, "mp3", 192) + ci := buildLegacyClientInfo(mf, "mp3", 192, 0) Expect(ci.TranscodingProfiles).To(HaveLen(1)) Expect(ci.TranscodingProfiles[0].Container).To(Equal("mp3")) @@ -63,7 +63,7 @@ var _ = Describe("buildLegacyClientInfo", func() { }) It("returns direct play profile when no format and no bitrate", func() { - ci := buildLegacyClientInfo(mf, "", 0) + ci := buildLegacyClientInfo(mf, "", 0, 0) Expect(ci.DirectPlayProfiles).To(HaveLen(1)) Expect(ci.DirectPlayProfiles[0].Containers).To(BeEmpty()) @@ -77,7 +77,7 @@ var _ = Describe("buildLegacyClientInfo", func() { DeferCleanup(configtest.SetupConfig()) conf.Server.DefaultDownsamplingFormat = "opus" - ci := buildLegacyClientInfo(mf, "", 128) + ci := buildLegacyClientInfo(mf, "", 128, 0) Expect(ci.TranscodingProfiles).To(HaveLen(1)) Expect(ci.TranscodingProfiles[0].Container).To(Equal("opus")) @@ -91,7 +91,7 @@ var _ = Describe("buildLegacyClientInfo", func() { }) It("returns direct play when bitrate >= source bitrate", func() { - ci := buildLegacyClientInfo(mf, "", 960) + ci := buildLegacyClientInfo(mf, "", 960, 0) Expect(ci.DirectPlayProfiles).To(HaveLen(1)) Expect(ci.DirectPlayProfiles[0].Containers).To(BeEmpty()) @@ -100,6 +100,51 @@ var _ = Describe("buildLegacyClientInfo", func() { Expect(ci.TranscodingProfiles).To(BeEmpty()) Expect(ci.MaxAudioBitrate).To(BeZero()) }) + + It("uses default downsampling format when player MaxBitRate is below source and no format/bitrate", func() { + DeferCleanup(configtest.SetupConfig()) + conf.Server.DefaultDownsamplingFormat = "opus" + + ci := buildLegacyClientInfo(mf, "", 0, 256) + + Expect(ci.TranscodingProfiles).To(HaveLen(1)) + Expect(ci.TranscodingProfiles[0].Container).To(Equal("opus")) + Expect(ci.TranscodingProfiles[0].AudioCodec).To(Equal("opus")) + Expect(ci.DirectPlayProfiles).To(HaveLen(1)) + Expect(ci.DirectPlayProfiles[0].Containers).To(Equal([]string{"flac"})) + }) + + It("does not downsample when player MaxBitRate is >= source bitrate", func() { + DeferCleanup(configtest.SetupConfig()) + conf.Server.DefaultDownsamplingFormat = "opus" + + ci := buildLegacyClientInfo(mf, "", 0, 960) + + Expect(ci.TranscodingProfiles).To(BeEmpty()) + Expect(ci.DirectPlayProfiles).To(HaveLen(1)) + Expect(ci.DirectPlayProfiles[0].Containers).To(BeEmpty()) + }) + + It("does not downsample when DefaultDownsamplingFormat is empty even with player cap", func() { + DeferCleanup(configtest.SetupConfig()) + conf.Server.DefaultDownsamplingFormat = "" + + ci := buildLegacyClientInfo(mf, "", 0, 256) + + Expect(ci.TranscodingProfiles).To(BeEmpty()) + Expect(ci.DirectPlayProfiles).To(HaveLen(1)) + Expect(ci.DirectPlayProfiles[0].Containers).To(BeEmpty()) + }) + + It("prefers explicit request format over player cap", func() { + DeferCleanup(configtest.SetupConfig()) + conf.Server.DefaultDownsamplingFormat = "opus" + + ci := buildLegacyClientInfo(mf, "mp3", 0, 256) + + Expect(ci.TranscodingProfiles).To(HaveLen(1)) + Expect(ci.TranscodingProfiles[0].Container).To(Equal("mp3")) + }) }) var _ = Describe("ResolveRequest", func() { @@ -289,6 +334,33 @@ var _ = Describe("ResolveRequest", func() { Expect(req.Format).To(Equal("raw")) }) + + It("downsamples using DefaultDownsamplingFormat when only player MaxBitRate is set (no format/bitrate)", func() { + DeferCleanup(configtest.SetupConfig()) + conf.Server.DefaultDownsamplingFormat = "opus" + + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)}) + playerCtx := request.WithPlayer(ctx, model.Player{MaxBitRate: 256}) + + decider := svc.(*deciderService) + req := decider.ResolveRequest(playerCtx, mf, "", 0, 0) + + Expect(req.Format).To(Equal("opus")) + Expect(req.BitRate).To(Equal(256)) + }) + + It("serves raw when only player MaxBitRate is set but no DefaultDownsamplingFormat", func() { + DeferCleanup(configtest.SetupConfig()) + conf.Server.DefaultDownsamplingFormat = "" + + mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: new(16)}) + playerCtx := request.WithPlayer(ctx, model.Player{MaxBitRate: 256}) + + decider := svc.(*deciderService) + req := decider.ResolveRequest(playerCtx, mf, "", 0, 0) + + Expect(req.Format).To(Equal("raw")) + }) }) Context("fallback for unknown format", func() { diff --git a/core/stream/types.go b/core/stream/types.go index bd8ce292c..11642c11c 100644 --- a/core/stream/types.go +++ b/core/stream/types.go @@ -40,6 +40,25 @@ type ClientInfo struct { CodecProfiles []CodecProfile } +// CapBitrate lowers the client's declared audio bitrate limits to maxKbps, +// never raising them. A zero limit means "unlimited" and is set to maxKbps. +// Returns true if anything changed. No-op when maxKbps <= 0. +func (ci *ClientInfo) CapBitrate(maxKbps int) bool { + if maxKbps <= 0 { + return false + } + changed := false + if ci.MaxAudioBitrate == 0 || maxKbps < ci.MaxAudioBitrate { + ci.MaxAudioBitrate = maxKbps + changed = true + } + if ci.MaxTranscodingAudioBitrate == 0 || maxKbps < ci.MaxTranscodingAudioBitrate { + ci.MaxTranscodingAudioBitrate = maxKbps + changed = true + } + return changed +} + // DirectPlayProfile describes a format the client can play directly type DirectPlayProfile struct { Containers []string diff --git a/core/stream/types_test.go b/core/stream/types_test.go new file mode 100644 index 000000000..2d2a83d06 --- /dev/null +++ b/core/stream/types_test.go @@ -0,0 +1,59 @@ +package stream + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ClientInfo", func() { + Describe("CapBitrate", func() { + It("is a no-op when maxKbps is zero", func() { + ci := &ClientInfo{MaxAudioBitrate: 320, MaxTranscodingAudioBitrate: 320} + Expect(ci.CapBitrate(0)).To(BeFalse()) + Expect(ci.MaxAudioBitrate).To(Equal(320)) + Expect(ci.MaxTranscodingAudioBitrate).To(Equal(320)) + }) + + It("is a no-op when maxKbps is negative", func() { + ci := &ClientInfo{MaxAudioBitrate: 320, MaxTranscodingAudioBitrate: 320} + Expect(ci.CapBitrate(-1)).To(BeFalse()) + Expect(ci.MaxAudioBitrate).To(Equal(320)) + Expect(ci.MaxTranscodingAudioBitrate).To(Equal(320)) + }) + + It("sets both limits when both are zero (unlimited)", func() { + ci := &ClientInfo{} + Expect(ci.CapBitrate(256)).To(BeTrue()) + Expect(ci.MaxAudioBitrate).To(Equal(256)) + Expect(ci.MaxTranscodingAudioBitrate).To(Equal(256)) + }) + + It("lowers limits higher than maxKbps", func() { + ci := &ClientInfo{MaxAudioBitrate: 320, MaxTranscodingAudioBitrate: 500} + Expect(ci.CapBitrate(192)).To(BeTrue()) + Expect(ci.MaxAudioBitrate).To(Equal(192)) + Expect(ci.MaxTranscodingAudioBitrate).To(Equal(192)) + }) + + It("does not raise limits lower than maxKbps", func() { + ci := &ClientInfo{MaxAudioBitrate: 128, MaxTranscodingAudioBitrate: 96} + Expect(ci.CapBitrate(320)).To(BeFalse()) + Expect(ci.MaxAudioBitrate).To(Equal(128)) + Expect(ci.MaxTranscodingAudioBitrate).To(Equal(96)) + }) + + It("reports changed when only one limit is lowered", func() { + ci := &ClientInfo{MaxAudioBitrate: 320, MaxTranscodingAudioBitrate: 128} + Expect(ci.CapBitrate(192)).To(BeTrue()) + Expect(ci.MaxAudioBitrate).To(Equal(192)) + Expect(ci.MaxTranscodingAudioBitrate).To(Equal(128)) + }) + + It("caps only the zero (unlimited) limit", func() { + ci := &ClientInfo{MaxAudioBitrate: 128, MaxTranscodingAudioBitrate: 0} + Expect(ci.CapBitrate(192)).To(BeTrue()) + Expect(ci.MaxAudioBitrate).To(Equal(128)) + Expect(ci.MaxTranscodingAudioBitrate).To(Equal(192)) + }) + }) +}) diff --git a/resources/i18n/pt-br.json b/resources/i18n/pt-br.json index d9f29f5d4..bc4d149a1 100644 --- a/resources/i18n/pt-br.json +++ b/resources/i18n/pt-br.json @@ -4,7 +4,7 @@ "song": { "name": "Música |||| Músicas", "fields": { - "albumArtist": "Artista", + "albumArtist": "Artista do Álbum", "duration": "Duração", "trackNumber": "#", "playCount": "Execuções", @@ -57,7 +57,7 @@ "album": { "name": "Álbum |||| Álbuns", "fields": { - "albumArtist": "Artista", + "albumArtist": "Artista do Álbum", "artist": "Artista", "duration": "Duração", "songCount": "Músicas", @@ -187,6 +187,9 @@ "lastSeen": "Últ. acesso", "reportRealPath": "Use paths reais", "scrobbleEnabled": "Enviar scrobbles para serviços externos" + }, + "helperTexts": { + "transcodingId": "O player web ignora o formato de conversão e aplica apenas o limite de Bitrate máx." } }, "transcoding": { diff --git a/server/e2e/subsonic_transcode_test.go b/server/e2e/subsonic_transcode_test.go index ae3d6208c..c769fd2d4 100644 --- a/server/e2e/subsonic_transcode_test.go +++ b/server/e2e/subsonic_transcode_test.go @@ -396,30 +396,34 @@ var _ = Describe("Transcode Endpoints", Ordered, func() { }) }) - Describe("player MaxBitRate cap is ignored", func() { - It("allows direct play even when source bitrate exceeds player MaxBitRate", func() { + Describe("player MaxBitRate cap is enforced", func() { + It("forces transcode when source bitrate exceeds player MaxBitRate", func() { setPlayerMaxBitRate(320) // 320 kbps cap - // FLAC is 900kbps, player cap is 320, but getTranscodeDecision - // ignores server-side overrides — client profiles are used as-is + // FLAC is 900kbps. Player cap (320) < source → direct play is + // rejected and the file is transcoded down. resp := doPostReq("getTranscodeDecision", flacAndMp3Client, "mediaId", flacTrackID, "mediaType", "song") Expect(resp.Status).To(Equal(responses.StatusOK)) Expect(resp.TranscodeDecision).ToNot(BeNil()) - Expect(resp.TranscodeDecision.CanDirectPlay).To(BeTrue()) + Expect(resp.TranscodeDecision.CanDirectPlay).To(BeFalse()) + Expect(resp.TranscodeDecision.CanTranscode).To(BeTrue()) + Expect(resp.TranscodeDecision.TranscodeStream).ToNot(BeNil()) + // Target bitrate is capped at the player MaxBitRate (320kbps). + Expect(resp.TranscodeDecision.TranscodeStream.AudioBitrate).To(Equal(int32(320000))) }) - It("uses only client limit, not player MaxBitRate", func() { + It("uses the player cap when it is more restrictive than the client limit", func() { setPlayerMaxBitRate(192) // 192 kbps player cap - // Client caps at 320kbps (bitrateCapClient), player is more restrictive at 192 - // but getTranscodeDecision ignores player cap → client limit (320kbps) applies + // Client caps at 320kbps (bitrateCapClient); player is more + // restrictive at 192 → player cap wins. resp := doPostReq("getTranscodeDecision", bitrateCapClient, "mediaId", flacTrackID, "mediaType", "song") Expect(resp.Status).To(Equal(responses.StatusOK)) Expect(resp.TranscodeDecision).ToNot(BeNil()) Expect(resp.TranscodeDecision.CanTranscode).To(BeTrue()) Expect(resp.TranscodeDecision.TranscodeStream).ToNot(BeNil()) - // Only client limit (320kbps) applies → 320000 bps - Expect(resp.TranscodeDecision.TranscodeStream.AudioBitrate).To(Equal(int32(320000))) + // Player cap (192kbps) applies → 192000 bps. + Expect(resp.TranscodeDecision.TranscodeStream.AudioBitrate).To(Equal(int32(192000))) }) }) @@ -475,35 +479,33 @@ var _ = Describe("Transcode Endpoints", Ordered, func() { }) }) - Describe("player MaxBitRate is ignored by getTranscodeDecision", func() { - It("does not inject maxAudioBitrate from player cap", func() { + Describe("player MaxBitRate injected by getTranscodeDecision", func() { + It("injects the player cap as the transcode target when the client declares none", func() { setPlayerMaxBitRate(320) - // opusTranscodeClient has no client bitrate limits - // Player cap is 320, but getTranscodeDecision ignores it - // FLAC (900kbps) → can't direct play → transcode to opus using format default + // opusTranscodeClient has no client bitrate limits. The player + // cap (320) is injected, so FLAC (900kbps) → opus is capped at 320. resp := doPostReq("getTranscodeDecision", opusTranscodeClient, "mediaId", flacTrackID, "mediaType", "song") Expect(resp.Status).To(Equal(responses.StatusOK)) Expect(resp.TranscodeDecision).ToNot(BeNil()) Expect(resp.TranscodeDecision.CanTranscode).To(BeTrue()) Expect(resp.TranscodeDecision.TranscodeStream).ToNot(BeNil()) Expect(resp.TranscodeDecision.TranscodeStream.Codec).To(Equal("opus")) - // Bitrate should be opus format default (128kbps), not player cap (320kbps) - Expect(resp.TranscodeDecision.TranscodeStream.AudioBitrate).To(Equal(int32(128000))) + // Bitrate is the player cap (320kbps), not the opus format default. + Expect(resp.TranscodeDecision.TranscodeStream.AudioBitrate).To(Equal(int32(320000))) }) - It("uses only client maxTranscodingAudioBitrate, ignoring player cap", func() { + It("keeps the lower client maxTranscodingAudioBitrate over a higher player cap", func() { setPlayerMaxBitRate(320) - // maxTranscodeBitrateClient: maxTranscodingAudioBitrate=192000 (192kbps) - // Player cap is 320, but getTranscodeDecision ignores it - // Only client maxTranscodingAudioBitrate=192 applies + // maxTranscodeBitrateClient: maxTranscodingAudioBitrate=192000 (192kbps). + // Player cap (320) is higher → the lower client limit wins. resp := doPostReq("getTranscodeDecision", maxTranscodeBitrateClient, "mediaId", flacTrackID, "mediaType", "song") Expect(resp.Status).To(Equal(responses.StatusOK)) Expect(resp.TranscodeDecision).ToNot(BeNil()) Expect(resp.TranscodeDecision.CanTranscode).To(BeTrue()) Expect(resp.TranscodeDecision.TranscodeStream).ToNot(BeNil()) - // maxTranscodingAudioBitrate=192 → 192000 bps + // Client limit (192kbps) wins → 192000 bps. Expect(resp.TranscodeDecision.TranscodeStream.AudioBitrate).To(Equal(int32(192000))) }) }) diff --git a/server/subsonic/transcode.go b/server/subsonic/transcode.go index 578ad44fc..c95c25cb0 100644 --- a/server/subsonic/transcode.go +++ b/server/subsonic/transcode.go @@ -11,6 +11,7 @@ import ( "github.com/navidrome/navidrome/core/stream" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/utils/req" ) @@ -278,6 +279,15 @@ func (api *Router) GetTranscodeDecision(w http.ResponseWriter, r *http.Request) return stream.IsAACCodec(p.Container) }) + // Apply the player's MaxBitRate as a ceiling on the client's declared + // limits (issue #5583). Both fields are capped because the client sends + // them independently here; capping only MaxAudioBitrate would let an + // independent MaxTranscodingAudioBitrate slip through computeBitrate. + if player, ok := request.PlayerFrom(ctx); ok && clientInfo.CapBitrate(player.MaxBitRate) { + log.Debug(ctx, "Applied player MaxBitRate cap to transcode decision", + "playerMaxBitRate", player.MaxBitRate, "client", clientInfo.Name) + } + // Get media file mf, err := api.ds.MediaFile(ctx).Get(mediaID) if err != nil { diff --git a/server/subsonic/transcode_test.go b/server/subsonic/transcode_test.go index 29a883ac1..4a1017752 100644 --- a/server/subsonic/transcode_test.go +++ b/server/subsonic/transcode_test.go @@ -9,6 +9,7 @@ import ( "github.com/navidrome/navidrome/core/stream" "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/tests" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -234,6 +235,76 @@ var _ = Describe("Transcode endpoints", func() { Expect(resp.TranscodeDecision.TranscodeStream).ToNot(BeNil()) Expect(resp.TranscodeDecision.TranscodeStream.Container).To(Equal("mp3")) }) + + Describe("player MaxBitRate cap", func() { + withPlayer := func(r *http.Request, maxBitRate int) *http.Request { + ctx := request.WithPlayer(r.Context(), model.Player{Client: "NavidromeUI", MaxBitRate: maxBitRate}) + return r.WithContext(ctx) + } + + BeforeEach(func() { + mockMFRepo.SetData(model.MediaFiles{ + {ID: "song-1", Suffix: "flac", Codec: "FLAC", BitRate: 900, Channels: 2, SampleRate: 44100}, + }) + mockTD.decision = &stream.TranscodeDecision{MediaID: "song-1", CanDirectPlay: true} + mockTD.token = "token" + }) + + It("caps client MaxAudioBitrate at the player MaxBitRate when client declares none", func() { + body := `{"directPlayProfiles":[{"containers":["flac"],"protocols":["http"]}]}` + r := withPlayer(newJSONPostRequest("mediaId=song-1&mediaType=song", body), 320) + + _, err := router.GetTranscodeDecision(w, r) + + Expect(err).ToNot(HaveOccurred()) + Expect(mockTD.capturedClient).ToNot(BeNil()) + Expect(mockTD.capturedClient.MaxAudioBitrate).To(Equal(320)) + Expect(mockTD.capturedClient.MaxTranscodingAudioBitrate).To(Equal(320)) + }) + + It("does not raise a lower client-declared limit", func() { + // Client declares 192 kbps (192000 bps); player cap is 320 — client wins. + body := `{"maxAudioBitrate":192000,"directPlayProfiles":[{"containers":["flac"],"protocols":["http"]}]}` + r := withPlayer(newJSONPostRequest("mediaId=song-1&mediaType=song", body), 320) + + _, err := router.GetTranscodeDecision(w, r) + + Expect(err).ToNot(HaveOccurred()) + Expect(mockTD.capturedClient.MaxAudioBitrate).To(Equal(192)) + }) + + It("lowers a higher client-declared limit to the player cap", func() { + // Client declares 320 kbps (320000 bps); player cap is 192 — player wins. + body := `{"maxAudioBitrate":320000,"directPlayProfiles":[{"containers":["flac"],"protocols":["http"]}]}` + r := withPlayer(newJSONPostRequest("mediaId=song-1&mediaType=song", body), 192) + + _, err := router.GetTranscodeDecision(w, r) + + Expect(err).ToNot(HaveOccurred()) + Expect(mockTD.capturedClient.MaxAudioBitrate).To(Equal(192)) + Expect(mockTD.capturedClient.MaxTranscodingAudioBitrate).To(Equal(192)) + }) + + It("does nothing when no player is in context", func() { + body := `{"maxAudioBitrate":320000,"directPlayProfiles":[{"containers":["flac"],"protocols":["http"]}]}` + r := newJSONPostRequest("mediaId=song-1&mediaType=song", body) + + _, err := router.GetTranscodeDecision(w, r) + + Expect(err).ToNot(HaveOccurred()) + Expect(mockTD.capturedClient.MaxAudioBitrate).To(Equal(320)) + }) + + It("does nothing when player MaxBitRate is 0", func() { + body := `{"maxAudioBitrate":320000,"directPlayProfiles":[{"containers":["flac"],"protocols":["http"]}]}` + r := withPlayer(newJSONPostRequest("mediaId=song-1&mediaType=song", body), 0) + + _, err := router.GetTranscodeDecision(w, r) + + Expect(err).ToNot(HaveOccurred()) + Expect(mockTD.capturedClient.MaxAudioBitrate).To(Equal(320)) + }) + }) }) Describe("GetTranscodeStream", func() { diff --git a/ui/src/i18n/en.json b/ui/src/i18n/en.json index 74fb23ab9..595b20a0d 100644 --- a/ui/src/i18n/en.json +++ b/ui/src/i18n/en.json @@ -187,6 +187,9 @@ "lastSeen": "Last Seen At", "reportRealPath": "Report Real Path", "scrobbleEnabled": "Send Scrobbles to external services" + }, + "helperTexts": { + "transcodingId": "The web player ignores the transcoding format and only enforces the Max. Bit Rate limit." } }, "transcoding": { diff --git a/ui/src/player/PlayerEdit.jsx b/ui/src/player/PlayerEdit.jsx index 1826500bd..d09ed855f 100644 --- a/ui/src/player/PlayerEdit.jsx +++ b/ui/src/player/PlayerEdit.jsx @@ -8,6 +8,7 @@ import { SelectInput, ReferenceInput, useTranslate, + useRecordContext, } from 'react-admin' import { Title } from '../common' import config from '../config' @@ -19,17 +20,35 @@ const PlayerTitle = ({ record }) => { return } +export const TranscodingInput = (props) => { + const translate = useTranslate() + const record = useRecordContext(props) + const isWebPlayer = record?.client === 'NavidromeUI' + return ( + <ReferenceInput + {...props} + source="transcodingId" + reference="transcoding" + sort={{ field: 'name', order: 'ASC' }} + > + <SelectInput + source="name" + resettable + helperText={ + isWebPlayer + ? translate('resources.player.helperTexts.transcodingId') + : undefined + } + /> + </ReferenceInput> + ) +} + const PlayerEdit = (props) => ( <Edit title={<PlayerTitle />} {...props}> <SimpleForm variant={'outlined'}> <TextInput source="name" validate={[required()]} /> - <ReferenceInput - source="transcodingId" - reference="transcoding" - sort={{ field: 'name', order: 'ASC' }} - > - <SelectInput source="name" resettable /> - </ReferenceInput> + <TranscodingInput /> <SelectInput source="maxBitRate" resettable choices={BITRATE_CHOICES} /> <BooleanInput source="reportRealPath" fullWidth /> {(config.lastFMEnabled || config.listenBrainzEnabled) && ( diff --git a/ui/src/player/PlayerEdit.test.jsx b/ui/src/player/PlayerEdit.test.jsx new file mode 100644 index 000000000..2b8c862e2 --- /dev/null +++ b/ui/src/player/PlayerEdit.test.jsx @@ -0,0 +1,54 @@ +import * as React from 'react' +import { render, screen, cleanup } from '@testing-library/react' +import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' +import { useRecordContext } from 'react-admin' +import { TranscodingInput } from './PlayerEdit' + +vi.mock('react-admin', async () => { + const actual = await vi.importActual('react-admin') + return { + ...actual, + useRecordContext: vi.fn(), + // Mock useTranslate to return the key verbatim so assertions don't depend + // on ra-core's out-of-provider translation behavior. + useTranslate: () => (key) => key, + // Render the inputs as simple stand-ins so we can read their props. + ReferenceInput: ({ children, variant }) => ( + <div data-testid="reference-input" data-variant={variant || ''}> + {children} + </div> + ), + SelectInput: ({ helperText }) => ( + <div data-testid="select-input" data-helpertext={helperText || ''} /> + ), + } +}) + +describe('<TranscodingInput />', () => { + beforeEach(() => { + useRecordContext.mockReset() + }) + afterEach(cleanup) + + it('shows helper text for the NavidromeUI player', () => { + useRecordContext.mockReturnValue({ client: 'NavidromeUI' }) + render(<TranscodingInput />) + expect(screen.getByTestId('select-input').dataset.helpertext).toBe( + 'resources.player.helperTexts.transcodingId', + ) + }) + + it('shows no helper text for other clients', () => { + useRecordContext.mockReturnValue({ client: 'DSub' }) + render(<TranscodingInput />) + expect(screen.getByTestId('select-input').dataset.helpertext).toBe('') + }) + + it('forwards the form variant injected by SimpleForm to the input', () => { + useRecordContext.mockReturnValue({ client: 'DSub' }) + render(<TranscodingInput variant="outlined" />) + expect(screen.getByTestId('reference-input').dataset.variant).toBe( + 'outlined', + ) + }) +}) From 08a027dbcc1ab9d3b6f22982c1954c4353a4521d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= <deluan@navidrome.org> Date: Sun, 14 Jun 2026 20:52:19 -0400 Subject: [PATCH 17/18] fix(transcoding): honor player forced format on the WebUI transcode flow (#5613) * feat(stream): add ClientInfo.ForceFormat for browser-aware forced format Restricts the client to a forced transcoding format and suppresses direct play, but only when the client declares it supports that format. Part of #5583. * fix(transcoding): honor player forced format on getTranscodeDecision When the WebUI player has a forced transcoding format configured and the browser declares it can play that format, transcode to it (suppressing direct play). Fall back to normal negotiation with a warning when the format is unsupported. The MaxBitRate cap still applies on top. Fixes #5583. * test(e2e): cover player forced format on getTranscodeDecision Forced format honored when the client supports it, falls back to negotiation otherwise, and the MaxBitRate cap still applies on top. Part of #5583. * feat(ui): remove obsolete 'format ignored' helper text on player form The web player now honors the forced transcoding format, so the caveat added in #5611 no longer applies. Reverts the Transcoding field to a plain selector. Part of #5583. --- core/stream/types.go | 25 +++++++++ core/stream/types_test.go | 74 +++++++++++++++++++++++++++ resources/i18n/pt-br.json | 3 -- server/e2e/subsonic_transcode_test.go | 48 +++++++++++++++++ server/subsonic/transcode.go | 13 +++++ server/subsonic/transcode_test.go | 67 ++++++++++++++++++++++++ ui/src/i18n/en.json | 3 -- ui/src/player/PlayerEdit.jsx | 33 +++--------- ui/src/player/PlayerEdit.test.jsx | 54 ------------------- 9 files changed, 234 insertions(+), 86 deletions(-) delete mode 100644 ui/src/player/PlayerEdit.test.jsx diff --git a/core/stream/types.go b/core/stream/types.go index 11642c11c..19474dd91 100644 --- a/core/stream/types.go +++ b/core/stream/types.go @@ -59,6 +59,31 @@ func (ci *ClientInfo) CapBitrate(maxKbps int) bool { return changed } +// ForceFormat narrows the client to transcoding to targetFormat and suppresses +// direct play, but only if the client already declares a profile for that +// format. All matching profiles are kept so negotiation can still pick among +// them (e.g. by protocol). Returns false (no-op) when targetFormat is empty or +// unsupported. +func (ci *ClientInfo) ForceFormat(targetFormat string) bool { + if targetFormat == "" { + return false + } + var matched []Profile + for i := range ci.TranscodingProfiles { + // matchesContainer is alias-aware, so a forced "oga" (legacy Opus + // target_format) still matches a resolved "opus" profile. + if _, format := resolveTargetFormat(&ci.TranscodingProfiles[i]); matchesContainer(format, []string{targetFormat}) { + matched = append(matched, ci.TranscodingProfiles[i]) + } + } + if len(matched) == 0 { + return false + } + ci.TranscodingProfiles = matched + ci.DirectPlayProfiles = nil + return true +} + // DirectPlayProfile describes a format the client can play directly type DirectPlayProfile struct { Containers []string diff --git a/core/stream/types_test.go b/core/stream/types_test.go index 2d2a83d06..eff408362 100644 --- a/core/stream/types_test.go +++ b/core/stream/types_test.go @@ -56,4 +56,78 @@ var _ = Describe("ClientInfo", func() { Expect(ci.MaxTranscodingAudioBitrate).To(Equal(192)) }) }) + + Describe("ForceFormat", func() { + It("restricts to the forced format and clears direct play when supported", func() { + ci := &ClientInfo{ + DirectPlayProfiles: []DirectPlayProfile{{Containers: []string{"flac"}, AudioCodecs: []string{"flac"}}}, + TranscodingProfiles: []Profile{ + {Container: "flac", AudioCodec: "flac", Protocol: ProtocolHTTP}, + {Container: "ogg", AudioCodec: "opus", Protocol: ProtocolHTTP}, + {Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP}, + }, + } + ok := ci.ForceFormat("opus") + Expect(ok).To(BeTrue()) + Expect(ci.TranscodingProfiles).To(HaveLen(1)) + Expect(ci.TranscodingProfiles[0].AudioCodec).To(Equal("opus")) + Expect(ci.DirectPlayProfiles).To(BeEmpty()) + }) + + It("matches a container-only forced format (mp3)", func() { + ci := &ClientInfo{ + TranscodingProfiles: []Profile{ + {Container: "ogg", AudioCodec: "opus", Protocol: ProtocolHTTP}, + {Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP}, + }, + } + ok := ci.ForceFormat("mp3") + Expect(ok).To(BeTrue()) + Expect(ci.TranscodingProfiles).To(HaveLen(1)) + Expect(ci.TranscodingProfiles[0].Container).To(Equal("mp3")) + }) + + It("matches the forced format against codec aliases (oga/opus)", func() { + ci := &ClientInfo{ + TranscodingProfiles: []Profile{ + {Container: "ogg", AudioCodec: "opus", Protocol: ProtocolHTTP}, + {Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP}, + }, + } + // Legacy DBs may store the Opus transcoding as target_format "oga". + ok := ci.ForceFormat("oga") + Expect(ok).To(BeTrue()) + Expect(ci.TranscodingProfiles).To(HaveLen(1)) + Expect(ci.TranscodingProfiles[0].AudioCodec).To(Equal("opus")) + }) + + It("is a no-op when the forced format is not supported by the client", func() { + original := []Profile{{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP}} + ci := &ClientInfo{ + DirectPlayProfiles: []DirectPlayProfile{{Containers: []string{"flac"}}}, + TranscodingProfiles: original, + } + ok := ci.ForceFormat("opus") + Expect(ok).To(BeFalse()) + Expect(ci.TranscodingProfiles).To(Equal(original)) + Expect(ci.DirectPlayProfiles).To(HaveLen(1)) + }) + + It("is a no-op for an empty target format", func() { + ci := &ClientInfo{TranscodingProfiles: []Profile{{Container: "mp3", AudioCodec: "mp3"}}} + Expect(ci.ForceFormat("")).To(BeFalse()) + }) + + It("keeps all matching profiles when multiple resolve to the forced format", func() { + first := Profile{Container: "ogg", AudioCodec: "opus", Protocol: ProtocolHTTP} + second := Profile{Container: "ogg", AudioCodec: "opus", Protocol: ProtocolHTTP, MaxAudioChannels: 2} + other := Profile{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP} + ci := &ClientInfo{TranscodingProfiles: []Profile{first, other, second}} + + ok := ci.ForceFormat("opus") + + Expect(ok).To(BeTrue()) + Expect(ci.TranscodingProfiles).To(ConsistOf(first, second)) + }) + }) }) diff --git a/resources/i18n/pt-br.json b/resources/i18n/pt-br.json index bc4d149a1..b3b3bab2f 100644 --- a/resources/i18n/pt-br.json +++ b/resources/i18n/pt-br.json @@ -187,9 +187,6 @@ "lastSeen": "Últ. acesso", "reportRealPath": "Use paths reais", "scrobbleEnabled": "Enviar scrobbles para serviços externos" - }, - "helperTexts": { - "transcodingId": "O player web ignora o formato de conversão e aplica apenas o limite de Bitrate máx." } }, "transcoding": { diff --git a/server/e2e/subsonic_transcode_test.go b/server/e2e/subsonic_transcode_test.go index c769fd2d4..afe7d52ca 100644 --- a/server/e2e/subsonic_transcode_test.go +++ b/server/e2e/subsonic_transcode_test.go @@ -159,11 +159,22 @@ var _ = Describe("Transcode Endpoints", Ordered, func() { Expect(ds.Player(ctx).Put(player)).To(Succeed()) } + setPlayerForcedFormat := func(format string) { + doReq("ping") + player, err := ds.Player(ctx).FindMatch(adminUser.ID, "test-client", "") + Expect(err).ToNot(HaveOccurred()) + trc, err := ds.Transcoding(ctx).FindByFormat(format) + Expect(err).ToNot(HaveOccurred()) + player.TranscodingId = trc.ID + Expect(ds.Player(ctx).Put(player)).To(Succeed()) + } + AfterEach(func() { // Reset player MaxBitRate to 0 after each test player, err := ds.Player(ctx).FindMatch(adminUser.ID, "test-client", "") if err == nil { player.MaxBitRate = 0 + player.TranscodingId = "" _ = ds.Player(ctx).Put(player) } }) @@ -509,6 +520,43 @@ var _ = Describe("Transcode Endpoints", Ordered, func() { Expect(resp.TranscodeDecision.TranscodeStream.AudioBitrate).To(Equal(int32(192000))) }) }) + + Describe("player forced format", func() { + It("transcodes a FLAC to the forced opus format when the client supports it", func() { + setPlayerForcedFormat("opus") + + resp := doPostReq("getTranscodeDecision", opusTranscodeClient, "mediaId", flacTrackID, "mediaType", "song") + Expect(resp.Status).To(Equal(responses.StatusOK)) + Expect(resp.TranscodeDecision).ToNot(BeNil()) + Expect(resp.TranscodeDecision.CanTranscode).To(BeTrue()) + Expect(resp.TranscodeDecision.TranscodeStream).ToNot(BeNil()) + Expect(resp.TranscodeDecision.TranscodeStream.Codec).To(Equal("opus")) + }) + + It("falls back to negotiation when the client does not support the forced format", func() { + setPlayerForcedFormat("opus") + + resp := doPostReq("getTranscodeDecision", mp3OnlyClient, "mediaId", flacTrackID, "mediaType", "song") + Expect(resp.Status).To(Equal(responses.StatusOK)) + Expect(resp.TranscodeDecision).ToNot(BeNil()) + Expect(resp.TranscodeDecision.CanTranscode).To(BeTrue()) + Expect(resp.TranscodeDecision.TranscodeStream).ToNot(BeNil()) + Expect(resp.TranscodeDecision.TranscodeStream.Container).To(Equal("mp3")) + }) + + It("applies maxBitRate on top of the forced format", func() { + setPlayerForcedFormat("opus") + setPlayerMaxBitRate(96) + + resp := doPostReq("getTranscodeDecision", opusTranscodeClient, "mediaId", flacTrackID, "mediaType", "song") + Expect(resp.Status).To(Equal(responses.StatusOK)) + Expect(resp.TranscodeDecision).ToNot(BeNil()) + Expect(resp.TranscodeDecision.CanTranscode).To(BeTrue()) + Expect(resp.TranscodeDecision.TranscodeStream).ToNot(BeNil()) + Expect(resp.TranscodeDecision.TranscodeStream.Codec).To(Equal("opus")) + Expect(resp.TranscodeDecision.TranscodeStream.AudioBitrate).To(Equal(int32(96000))) + }) + }) }) Describe("getTranscodeStream", func() { diff --git a/server/subsonic/transcode.go b/server/subsonic/transcode.go index c95c25cb0..511db2b85 100644 --- a/server/subsonic/transcode.go +++ b/server/subsonic/transcode.go @@ -279,6 +279,19 @@ func (api *Router) GetTranscodeDecision(w http.ResponseWriter, r *http.Request) return stream.IsAACCodec(p.Container) }) + // Honor the player's forced transcoding format, falling back to normal + // negotiation when the client can't play it (issue #5583). + if trc, ok := request.TranscodingFrom(ctx); ok && trc.TargetFormat != "" { + if !clientInfo.ForceFormat(trc.TargetFormat) { + clientName := clientInfo.Name + if player, ok := request.PlayerFrom(ctx); ok && player.Client != "" { + clientName = player.Client + } + log.Debug(ctx, "Player forced format not supported by client; falling back to negotiation", + "forcedFormat", trc.TargetFormat, "client", clientName) + } + } + // Apply the player's MaxBitRate as a ceiling on the client's declared // limits (issue #5583). Both fields are capped because the client sends // them independently here; capping only MaxAudioBitrate would let an diff --git a/server/subsonic/transcode_test.go b/server/subsonic/transcode_test.go index 4a1017752..7e36ab243 100644 --- a/server/subsonic/transcode_test.go +++ b/server/subsonic/transcode_test.go @@ -305,6 +305,73 @@ var _ = Describe("Transcode endpoints", func() { Expect(mockTD.capturedClient.MaxAudioBitrate).To(Equal(320)) }) }) + + Describe("player forced format", func() { + withForcedFormat := func(r *http.Request, format string, maxBitRate int) *http.Request { + ctx := r.Context() + ctx = request.WithTranscoding(ctx, model.Transcoding{TargetFormat: format}) + if maxBitRate > 0 { + ctx = request.WithPlayer(ctx, model.Player{Client: "NavidromeUI", MaxBitRate: maxBitRate}) + } + return r.WithContext(ctx) + } + + BeforeEach(func() { + mockMFRepo.SetData(model.MediaFiles{ + {ID: "song-1", Suffix: "flac", Codec: "FLAC", BitRate: 900, Channels: 2, SampleRate: 44100}, + }) + mockTD.decision = &stream.TranscodeDecision{MediaID: "song-1", CanTranscode: true} + mockTD.token = "token" + }) + + It("forces a supported format and clears direct play", func() { + body := `{"directPlayProfiles":[{"containers":["flac"],"audioCodecs":["flac"],"protocols":["http"]}], + "transcodingProfiles":[{"container":"ogg","audioCodec":"opus","protocol":"http"}, + {"container":"mp3","audioCodec":"mp3","protocol":"http"}]}` + r := withForcedFormat(newJSONPostRequest("mediaId=song-1&mediaType=song", body), "opus", 0) + + _, err := router.GetTranscodeDecision(w, r) + + Expect(err).ToNot(HaveOccurred()) + Expect(mockTD.capturedClient.TranscodingProfiles).To(HaveLen(1)) + Expect(mockTD.capturedClient.TranscodingProfiles[0].AudioCodec).To(Equal("opus")) + Expect(mockTD.capturedClient.DirectPlayProfiles).To(BeEmpty()) + }) + + It("falls back to negotiation when the forced format is unsupported", func() { + // Forced format is opus, but the client only declares mp3 and flac. + // Should fall back to negotiating among the client's own profiles. + body := `{"directPlayProfiles":[{"containers":["flac"],"audioCodecs":["flac"],"protocols":["http"]}], + "transcodingProfiles":[ + {"container":"flac","audioCodec":"flac","protocol":"http"}, + {"container":"mp3","audioCodec":"mp3","protocol":"http"}]}` + r := withForcedFormat(newJSONPostRequest("mediaId=song-1&mediaType=song", body), "opus", 0) + + _, err := router.GetTranscodeDecision(w, r) + + Expect(err).ToNot(HaveOccurred()) + // Profiles left intact for normal negotiation (forced format not applied). + Expect(mockTD.capturedClient.TranscodingProfiles).To(HaveLen(2)) + Expect(mockTD.capturedClient.DirectPlayProfiles).ToNot(BeEmpty()) + }) + + It("applies the maxBitRate cap on top of the forced format", func() { + // Client supports opus + mp3; forced format opus must be selected, + // and the maxBitRate cap applied on top. + body := `{"transcodingProfiles":[ + {"container":"ogg","audioCodec":"opus","protocol":"http"}, + {"container":"mp3","audioCodec":"mp3","protocol":"http"}]}` + r := withForcedFormat(newJSONPostRequest("mediaId=song-1&mediaType=song", body), "opus", 128) + + _, err := router.GetTranscodeDecision(w, r) + + Expect(err).ToNot(HaveOccurred()) + Expect(mockTD.capturedClient.TranscodingProfiles).To(HaveLen(1)) + Expect(mockTD.capturedClient.TranscodingProfiles[0].AudioCodec).To(Equal("opus")) + Expect(mockTD.capturedClient.MaxAudioBitrate).To(Equal(128)) + Expect(mockTD.capturedClient.MaxTranscodingAudioBitrate).To(Equal(128)) + }) + }) }) Describe("GetTranscodeStream", func() { diff --git a/ui/src/i18n/en.json b/ui/src/i18n/en.json index 595b20a0d..74fb23ab9 100644 --- a/ui/src/i18n/en.json +++ b/ui/src/i18n/en.json @@ -187,9 +187,6 @@ "lastSeen": "Last Seen At", "reportRealPath": "Report Real Path", "scrobbleEnabled": "Send Scrobbles to external services" - }, - "helperTexts": { - "transcodingId": "The web player ignores the transcoding format and only enforces the Max. Bit Rate limit." } }, "transcoding": { diff --git a/ui/src/player/PlayerEdit.jsx b/ui/src/player/PlayerEdit.jsx index d09ed855f..1826500bd 100644 --- a/ui/src/player/PlayerEdit.jsx +++ b/ui/src/player/PlayerEdit.jsx @@ -8,7 +8,6 @@ import { SelectInput, ReferenceInput, useTranslate, - useRecordContext, } from 'react-admin' import { Title } from '../common' import config from '../config' @@ -20,35 +19,17 @@ const PlayerTitle = ({ record }) => { return <Title subTitle={`${resourceName} ${record ? record.name : ''}`} /> } -export const TranscodingInput = (props) => { - const translate = useTranslate() - const record = useRecordContext(props) - const isWebPlayer = record?.client === 'NavidromeUI' - return ( - <ReferenceInput - {...props} - source="transcodingId" - reference="transcoding" - sort={{ field: 'name', order: 'ASC' }} - > - <SelectInput - source="name" - resettable - helperText={ - isWebPlayer - ? translate('resources.player.helperTexts.transcodingId') - : undefined - } - /> - </ReferenceInput> - ) -} - const PlayerEdit = (props) => ( <Edit title={<PlayerTitle />} {...props}> <SimpleForm variant={'outlined'}> <TextInput source="name" validate={[required()]} /> - <TranscodingInput /> + <ReferenceInput + source="transcodingId" + reference="transcoding" + sort={{ field: 'name', order: 'ASC' }} + > + <SelectInput source="name" resettable /> + </ReferenceInput> <SelectInput source="maxBitRate" resettable choices={BITRATE_CHOICES} /> <BooleanInput source="reportRealPath" fullWidth /> {(config.lastFMEnabled || config.listenBrainzEnabled) && ( diff --git a/ui/src/player/PlayerEdit.test.jsx b/ui/src/player/PlayerEdit.test.jsx deleted file mode 100644 index 2b8c862e2..000000000 --- a/ui/src/player/PlayerEdit.test.jsx +++ /dev/null @@ -1,54 +0,0 @@ -import * as React from 'react' -import { render, screen, cleanup } from '@testing-library/react' -import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' -import { useRecordContext } from 'react-admin' -import { TranscodingInput } from './PlayerEdit' - -vi.mock('react-admin', async () => { - const actual = await vi.importActual('react-admin') - return { - ...actual, - useRecordContext: vi.fn(), - // Mock useTranslate to return the key verbatim so assertions don't depend - // on ra-core's out-of-provider translation behavior. - useTranslate: () => (key) => key, - // Render the inputs as simple stand-ins so we can read their props. - ReferenceInput: ({ children, variant }) => ( - <div data-testid="reference-input" data-variant={variant || ''}> - {children} - </div> - ), - SelectInput: ({ helperText }) => ( - <div data-testid="select-input" data-helpertext={helperText || ''} /> - ), - } -}) - -describe('<TranscodingInput />', () => { - beforeEach(() => { - useRecordContext.mockReset() - }) - afterEach(cleanup) - - it('shows helper text for the NavidromeUI player', () => { - useRecordContext.mockReturnValue({ client: 'NavidromeUI' }) - render(<TranscodingInput />) - expect(screen.getByTestId('select-input').dataset.helpertext).toBe( - 'resources.player.helperTexts.transcodingId', - ) - }) - - it('shows no helper text for other clients', () => { - useRecordContext.mockReturnValue({ client: 'DSub' }) - render(<TranscodingInput />) - expect(screen.getByTestId('select-input').dataset.helpertext).toBe('') - }) - - it('forwards the form variant injected by SimpleForm to the input', () => { - useRecordContext.mockReturnValue({ client: 'DSub' }) - render(<TranscodingInput variant="outlined" />) - expect(screen.getByTestId('reference-input').dataset.variant).toBe( - 'outlined', - ) - }) -}) From f0625ff709f790fab4230444ac32eeb49055c728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= <deluan@navidrome.org> Date: Mon, 15 Jun 2026 16:23:39 -0400 Subject: [PATCH 18/18] perf(subsonic): speed up getRandomSongs with two-phase random-rowid selection (#5618) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getRandomSongs used a single ORDER BY random() over the media_file table. At large library sizes that forces SQLite to scan the wide table and sort every matching row before applying the limit — roughly 4 seconds for a 1M-track library, regardless of how many songs are requested. Add MediaFileRepository.GetRandom, which does this in two passes: first select N random rowids over a narrow index (filters + library scope only, no wide columns or joins), so the random sort runs over compact, index-friendly data; then hydrate just those rows with the full select. The wide media_file row is never part of the sort. End-to-end this drops getRandomSongs from ~4s to ~0.3s on a 1M-track library, and the cost no longer grows with the requested size. The handler now calls GetRandom directly. The filter helper that builds the genre/year filters is renamed SongsByRandom -> SongsByGenreAndYearRange to reflect what it does, since the random ordering is now owned by GetRandom rather than a Sort option. Filters (genre, year, library) compose into the first pass unchanged. The album random list path is left as-is (far fewer rows, already fast). --- model/mediafile.go | 3 + persistence/mediafile_repository.go | 34 +++++++++ persistence/mediafile_repository_test.go | 97 ++++++++++++++++++++++++ server/subsonic/album_lists.go | 5 +- server/subsonic/filter/filters.go | 6 +- tests/mock_mediafile_repo.go | 11 +++ 6 files changed, 150 insertions(+), 6 deletions(-) diff --git a/model/mediafile.go b/model/mediafile.go index 718f0443d..6a489bcd5 100644 --- a/model/mediafile.go +++ b/model/mediafile.go @@ -439,6 +439,9 @@ type MediaFileRepository interface { Get(id string) (*MediaFile, error) GetWithParticipants(id string) (*MediaFile, error) GetAll(options ...QueryOptions) (MediaFiles, error) + // GetRandom returns up to options.Max media files in random order, applying the same + // filters as GetAll. Sort/Order are ignored. + GetRandom(options ...QueryOptions) (MediaFiles, error) GetAllByTags(tag TagName, values []string, options ...QueryOptions) (MediaFiles, error) GetCursor(options ...QueryOptions) (MediaFileCursor, error) Delete(id string) error diff --git a/persistence/mediafile_repository.go b/persistence/mediafile_repository.go index 559378262..dd8145eae 100644 --- a/persistence/mediafile_repository.go +++ b/persistence/mediafile_repository.go @@ -207,6 +207,40 @@ func (r *mediaFileRepository) GetAll(options ...model.QueryOptions) (model.Media return res.toModels(), nil } +// GetRandom uses two passes so the random sort runs over a narrow rowid index instead of the +// wide media_file row: pick random rowids first, then hydrate only those. +func (r *mediaFileRepository) GetRandom(options ...model.QueryOptions) (model.MediaFiles, error) { + var opt model.QueryOptions + if len(options) > 0 { + opt = options[0] + } + + rowidQuery := Select("media_file.rowid").From(r.tableName) + rowidQuery = r.applyFilters(rowidQuery, model.QueryOptions{Filters: opt.Filters}) + rowidQuery = r.applyLibraryFilter(rowidQuery) + rowidQuery = rowidQuery.OrderBy("random()") + if opt.Max > 0 { + rowidQuery = rowidQuery.Limit(uint64(opt.Max)) + } + + var rowids []int64 + if err := r.queryAllSlice(rowidQuery, &rowids); err != nil { + return nil, err + } + if len(rowids) == 0 { + return model.MediaFiles{}, nil + } + + // Re-shuffle in Phase 2: `WHERE rowid IN (...)` returns rows in ascending rowid order, not + // the random order from Phase 1. Sorting only the (<=Max) hydrated rows is negligible. + sq := r.selectMediaFile().Where(Eq{"media_file.rowid": rowids}).OrderBy("random()") + var res dbMediaFiles + if err := r.queryAll(sq, &res); err != nil { + return nil, err + } + return res.toModels(), nil +} + func (r *mediaFileRepository) GetAllByTags(tag model.TagName, values []string, options ...model.QueryOptions) (model.MediaFiles, error) { placeholders := make([]string, len(values)) args := make([]any, len(values)) diff --git a/persistence/mediafile_repository_test.go b/persistence/mediafile_repository_test.go index 4c2363e43..7989dc25a 100644 --- a/persistence/mediafile_repository_test.go +++ b/persistence/mediafile_repository_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "reflect" "time" "github.com/Masterminds/squirrel" @@ -106,6 +107,102 @@ var _ = Describe("MediaRepository", func() { } }) + Describe("GetRandom", func() { + It("returns the requested number of distinct, fully-hydrated media files", func() { + results, err := mr.GetRandom(model.QueryOptions{Max: 5}) + Expect(err).ToNot(HaveOccurred()) + Expect(results).To(HaveLen(5)) + + // Each returned row must match its GetAll counterpart exactly — proves Phase 2 + // hydrates full rows (not bare rowids) — and ids must be distinct. + byID := map[string]model.MediaFile{} + all, err := mr.GetAll() + Expect(err).ToNot(HaveOccurred()) + for _, mf := range all { + byID[mf.ID] = mf + } + seen := map[string]bool{} + for _, mf := range results { + expected, ok := byID[mf.ID] + Expect(ok).To(BeTrue(), "returned id must be a real media file") + Expect(mf.Title).To(Equal(expected.Title), "row must be fully hydrated") + Expect(seen[mf.ID]).To(BeFalse(), "no duplicate rows") + seen[mf.ID] = true + } + }) + + It("returns all matching files when Max exceeds the total", func() { + results, err := mr.GetRandom(model.QueryOptions{Max: 1000}) + Expect(err).ToNot(HaveOccurred()) + Expect(results).To(HaveLen(13)) + }) + + It("honors filters", func() { + results, err := mr.GetRandom(model.QueryOptions{ + Max: 10, + Filters: squirrel.Eq{"media_file.title": "Antenna"}, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(results).ToNot(BeEmpty()) + for _, mf := range results { + Expect(mf.Title).To(Equal("Antenna")) + } + }) + + It("returns varying results across calls", func() { + // Retry a few times: two random draws of 5 from 13 rows differ with near-certainty. + first, err := mr.GetRandom(model.QueryOptions{Max: 5}) + Expect(err).ToNot(HaveOccurred()) + firstIDs := func() []string { + ids := make([]string, len(first)) + for i, mf := range first { + ids[i] = mf.ID + } + return ids + }() + differed := false + for range 10 { + next, err := mr.GetRandom(model.QueryOptions{Max: 5}) + Expect(err).ToNot(HaveOccurred()) + nextIDs := make([]string, len(next)) + for i, mf := range next { + nextIDs[i] = mf.ID + } + if !reflect.DeepEqual(firstIDs, nextIDs) { + differed = true + break + } + } + Expect(differed).To(BeTrue(), "GetRandom should not return an identical set every call") + }) + + It("randomizes order even when Max exceeds the total", func() { + // Same set of rows every time (all 13), but the order must still be shuffled — + // guards against Phase 2's `rowid IN (...)` returning rows in rowid order. + first, err := mr.GetRandom(model.QueryOptions{Max: 100}) + Expect(err).ToNot(HaveOccurred()) + Expect(first).To(HaveLen(13)) + firstIDs := make([]string, len(first)) + for i, mf := range first { + firstIDs[i] = mf.ID + } + differed := false + for range 10 { + next, err := mr.GetRandom(model.QueryOptions{Max: 100}) + Expect(err).ToNot(HaveOccurred()) + nextIDs := make([]string, len(next)) + for i, mf := range next { + nextIDs[i] = mf.ID + } + if !reflect.DeepEqual(firstIDs, nextIDs) { + differed = true + break + } + } + Expect(differed).To(BeTrue(), "order must vary even when returning all rows") + }) + }) + Describe("Put CreatedAt behavior (#5050)", func() { It("sets CreatedAt to now when inserting a new file with zero CreatedAt", func() { before := time.Now().Add(-time.Second) diff --git a/server/subsonic/album_lists.go b/server/subsonic/album_lists.go index 0d82c8be9..24bbca960 100644 --- a/server/subsonic/album_lists.go +++ b/server/subsonic/album_lists.go @@ -240,10 +240,11 @@ func (api *Router) GetRandomSongs(r *http.Request) (*responses.Subsonic, error) if err != nil { return nil, err } - opts := filter.SongsByRandom(genre, fromYear, toYear) + opts := filter.SongsByGenreAndYearRange(genre, fromYear, toYear) opts = filter.ApplyLibraryFilter(opts, musicFolderIds) + opts.Max = size - songs, err := api.getSongs(r.Context(), 0, size, opts) + songs, err := api.ds.MediaFile(r.Context()).GetRandom(opts) if err != nil { log.Error(r, "Error retrieving random songs", err) return nil, err diff --git a/server/subsonic/filter/filters.go b/server/subsonic/filter/filters.go index 856870a6c..c3710394f 100644 --- a/server/subsonic/filter/filters.go +++ b/server/subsonic/filter/filters.go @@ -90,10 +90,8 @@ func SongsByAlbum(albumId string) Options { }) } -func SongsByRandom(genre string, fromYear, toYear int) Options { - options := Options{ - Sort: "random()", - } +func SongsByGenreAndYearRange(genre string, fromYear, toYear int) Options { + options := Options{} ff := And{} if genre != "" { ff = append(ff, filterByGenre(genre)) diff --git a/tests/mock_mediafile_repo.go b/tests/mock_mediafile_repo.go index 01eacae30..f15ba1bc6 100644 --- a/tests/mock_mediafile_repo.go +++ b/tests/mock_mediafile_repo.go @@ -98,6 +98,17 @@ func (m *MockMediaFileRepo) GetAll(qo ...model.QueryOptions) (model.MediaFiles, return result, nil } +func (m *MockMediaFileRepo) GetRandom(qo ...model.QueryOptions) (model.MediaFiles, error) { + res, err := m.GetAll(qo...) + if err != nil { + return nil, err + } + if len(qo) > 0 && qo[0].Max > 0 && len(res) > qo[0].Max { + res = res[:qo[0].Max] + } + return res, nil +} + func (m *MockMediaFileRepo) Put(mf *model.MediaFile) error { if m.Err { return errors.New("error")