* feat(model): add per-library PID config columns
* refactor(metadata): pass PID config to ToMediaFile and add spec validation
* feat(scanner): rescan only libraries whose PID config changed
* feat(server): validate library PID config and rescan on change
* feat(ui): edit per-library PID config
* fix(ui): label the PID mode selects
* fix: tighten per-library PID rescan edge cases
An interrupted PID rescan no longer upgrades every library to a full scan, a save that loses the race for the scanner logs at debug, the confirm dialog only shows when the effective PID spec changes, and it now gets translation keys.
* refactor(metadata): pass the library to ToMediaFile
ToMediaFile and core.Inspect took the library ID and its PID config as
separate arguments, so a caller could mix values from two libraries. They
now take the model.Library and resolve the effective PID config from it.
* chore: tidy per-library PID comments, PropTypes and migration
Trim comments that restated the code, add PropTypes to the new UI
components, and recreate the migration with make migration-sql.
* fix(ui): show the PID spec help under its input
* feat(cmd): make inspect use the file's library PID config
inspect always used the global PID config, so it showed different IDs than
the scanner for files in a library with an override. It now finds the
file's library in the DB and uses its effective config, falling back to
the global config when there is no DB or the file is outside every
library. It never creates a DB. The library path matcher moves from
core/playlists to model so both can use it.
* refactor: simplify per-library PID code
Share the DB-file check between CLI commands, move ErrAlreadyScanning to
model so core no longer imports scanner, read the libraries once for
insights, and let ValidatePIDSpec accept an empty spec and look tags up
directly. In the scanner, use FullScanInProgress instead of a second
flag, and skip recomputing album IDs when the album spec did not change.
In the UI, share the PID inputs between Create and Edit, and use docsUrl.
* feat(ui): add section titles to Library Create and pre-fill Custom PID specs
Custom now starts from the global spec, so admins edit a working spec
instead of typing one from scratch.
* fix(inspect): map files with the library-relative path the scanner uses
Inspect gave metadata the file's directory as typed, so folder-based PIDs
never matched the DB. It now uses the path relative to the library root,
through the scanner's helper, which moves to model.
* fix(scanner): say when a PID rescan only covers target folders
* fix: reject tag aliases in album PID specs and match root libraries
Tags are stored under canonical names, so an alias in a spec always reads
as empty. In an album spec that gives every album the same ID, so album
specs now require the tag name. Track specs keep accepting aliases, since
the default one uses them. LibraryMatcher now matches paths under a
library at the filesystem root.
* refactor(model): move the tag alias lookup to tag_mappings.go
* test: run the library matcher and inspect tests on Windows
Build test paths with filepath instead of Unix literals, so they use the
OS separator like filepath.Abs output, and drop the Windows skips.
* feat(ui): add pt-BR translations for per-library PID settings
* refactor(persistence): adopt generic deluan/rest repository API
Pin deluan/rest to the refactor branch. REST-facing repository methods
take a context and return typed values. Drop DataStore.Resource and
ResourceRepository; the native API names typed repositories directly
through a per-request adapter that later commits remove.
* refactor(persistence): base repository helpers take a context
* refactor(persistence): LibraryRepository takes a context per call
* refactor(persistence): PropertyRepository takes a context per call
* refactor(persistence): UserPropsRepository takes a context per call
* refactor(persistence): TranscodingRepository takes a context per call
* refactor(persistence): ShareRepository takes a context per call
* refactor(persistence): PlayerRepository takes a context per call
* refactor(persistence): RadioRepository takes a context per call
* refactor(persistence): PlayQueueRepository takes a context per call
* refactor(persistence): Tag and Genre repositories take a context per call
* refactor(persistence): PluginRepository takes a context per call
* refactor(persistence): Scrobble repositories take a context per call
* refactor(persistence): FolderRepository takes a context per call
* refactor(persistence): Artwork repositories take a context per call
* refactor(persistence): UserRepository takes a context per call
* refactor(persistence): ArtistRepository takes a context per call
ReadAll no longer rewrites the shared sort mappings for the role filter;
it works on a per-call copy.
* test(persistence): assert artist role sort sanitization in ReadAll
* refactor(persistence): AlbumRepository takes a context per call
* test(persistence): pass the test context to album repository helpers
* refactor(persistence): MediaFileRepository takes a context per call
* refactor(persistence): Playlist repositories take a context per call
* refactor(persistence): build all repositories once per store
* refactor(core): REST repository wrappers are built once
* refactor(persistence): repositories are stateless
Remove the context field from the base repository and the per-request
REST adapter. Enable the containedctx linter so no repository can hold a
request context again.
* chore(lint): skip containedctx in test files
* refactor: share simplifications from the stateless repositories sweep
Add deleteOwnedAll on sqlRepository and use it in player/share Delete
to remove the duplicated bulk-delete loop; have Share.Repository()
return model.ShareRepository so subsonic sharing.go drops its repeated
type assertions.
* chore(core): assert REST wrappers implement Persistable
* chore: reformat imports
* perf(persistence): build repositories on first use
Each transaction store used to construct all 21 repositories up front,
paying for filter and sort mapping setup the block never touched. Fields
are now sync.OnceValue thunks, so a store only builds what it uses.
* fix(persistence): clean plugin references per deleted user
A bulk user delete that fails on a later id had already removed the
earlier rows but skipped their plugin cleanup. Cleanup now runs right
after each successful delete.
* fix(core): unload disabled plugins even when a user delete fails
A bulk delete can fail on a later id after earlier users were removed
and their plugins auto-disabled. The wrapper returned before unloading,
leaving those plugins running until the next successful delete or a
restart.
* chore(deps): pin deluan/rest to v1.0.1
Replaces the pseudo-version of the refactor branch with the tagged
release. REST error messages now name the bare type (Artist, not
model.Artist).
* test: use the spec context instead of context.Background()
Replace the context.Background()/context.TODO() calls this branch added
to tests with the spec's ctx, GinkgoT().Context(), or t/b.Context(), so
repository calls are bound to the running spec's lifetime.
* test: declare the spec context once per Describe
Set ctx from GinkgoT().Context() first in each top-level BeforeEach and reuse it, building user contexts on top of it instead of repeating inline calls.
* feat(jellyfin): add Quick Connect sign-in
Jellyfin clients can now sign in without a password: the client shows a
6-digit code, a signed-in user approves it, and the client redeems a secret
for its access token.
- core/quickconnect: in-memory store shared by both routers through wire.
Codes expire after 10 minutes; a secret redeems only once (Jellyfin
allows repeats for 10 minutes); at most 1000 pending requests.
- Jellyfin API: Initiate, Connect, Authorize and AuthenticateWithQuickConnect.
Admins may approve for another user via UserId, like Swiftfin's admin page.
Initiate and redeem share the login rate limiter; Connect does not, since
Finamp and Streamyfin poll it every second.
- Web UI: a Quick Connect item in the user menu looks up the code and shows
the app and device before approving, so a user can't be tricked into
approving an unknown device blindly.
- Jellyfin.QuickConnect option, on by default like Jellyfin. It only matters
when the Jellyfin API is enabled.
* refactor(jellyfin): tidy Quick Connect naming and route guards
Group the Quick Connect routes under one requireQuickConnect guard, make the
request's device a named field so req.Device.ID can't be mistaken for a
request id, and rename the web API response type to quickConnectDevice.
* refactor(jellyfin): remove duplicated Jellyfin date formatting function
* test(jellyfin): set play count and starred in the song fixture literal
* refactor(jellyfin): inline the Quick Connect redeem body and use the shared date helper
* fix(jellyfin): bound the client fields Quick Connect keeps in memory
Initiate is unauthenticated and keeps the Client, Device, DeviceId and Version
header fields for up to ten minutes. With no header size limit, each pending
request could hold about 1 MB, and even a short field kept the whole header
alive because the parsed values are substrings of it. Reject fields over 512
bytes and copy the stored values.
Also answer 500 instead of 401 when the redeem user lookup fails for a reason
other than the user being gone.
* fix(jellyfin): rate-limit Quick Connect code approval
Any signed-in user could try codes without limit on the Jellyfin Authorize
endpoint and the web UI lookup/authorize endpoints, and so could approve
another person's pending device for their own account. Apply the same per-IP
limiter as the login (AuthRequestLimit/AuthWindowLength) to both surfaces.
* feat(config): add UIPlaybackReportInterval setting
* feat(server): expose playbackReportIntervalMs to UI config
* feat(ui): add playbackReportIntervalMs config default
* feat(ui): replace scrobble/nowPlaying with reportPlayback in subsonic API layer
* feat(ui): replace scrobble logic with reportPlayback state machine in Player
* refactor(ui): simplify Player heartbeat using useInterval hook
- Replace manual setInterval/clearInterval with existing useInterval hook
- Extract shared reportPlaybackUrl helper to deduplicate URL construction
- Use ref for currentTrackId in beforeunload to stabilize effect deps
- Have heartbeat read lastPositionMsRef instead of audioInstance.currentTime
* feat(ui): redesign NowPlaying panel with Discord-style layout
Show album art with play/pause overlay icon, track title, artist,
album name, progress bar with position/duration, and username.
* fix(ui): adjust NowPlaying panel height to fit 3 entries
* fix(ui): send stopped on player close and tab close while paused
- onBeforeDestroy now sends reportPlayback stopped before clearing queue
- beforeunload sends stopped beacon regardless of pause state
* feat(ui): animate NowPlaying progress bar with 1s client-side tick
* fix(ui): account for playbackRate in NowPlaying progress interpolation
* fix(ui): use timestamp-based interpolation for smooth NowPlaying progress
Replace tick counter with fetchedAt timestamp so the progress bar
advances smoothly without resetting on each server poll.
* fix(ui): fix NowPlaying progress bar not animating
Pass `now` (Date.now()) as a prop that changes every tick, so
React.memo'd components actually re-render each second.
* fix(ui): prevent progress bar reset on NowPlaying poll
Set fetchedAt and now atomically on fetch so the elapsed offset
starts at zero and the server's already-estimated positionMs
is used as the base without a visible jump.
* fix(ui): stamp entries with fetch time to prevent progress bar reset
Embed _fetchedAt timestamp directly into each entry object so the
position and its reference timestamp are always in the same state
update, eliminating the React 17 multi-setState batching race.
* fix(server): estimate position for starting state in GetNowPlaying
GetNowPlaying was only estimating elapsed position for the "playing"
state, returning raw positionMs=0 for "starting". Since the UI
player sends "starting" once and then doesn't update until the
60s heartbeat, NowPlaying polls returned 0 for up to a minute,
causing the progress bar to reset on every poll.
* fix(ui): send playing immediately after starting to enable position estimation
The server only estimates elapsed position for "playing" state in
GetNowPlaying. The Player was sending "starting" once and then not
updating until the 60s heartbeat, leaving the server state as
"starting" with positionMs=0 for up to a minute.
Now the Player follows up "starting" with an immediate "playing"
call, transitioning the server state so position estimation works
from the first poll.
* fix(subsonic): fix getNowPlaying returning same playerId for all entries
PlayerId was never incremented in the map callback, so every entry
got playerId=1. This caused the UI to use duplicate React keys,
mixing up rendered entries between players. Also use a stable
composite key in the UI instead of the sequential playerId.
* fix(ui): only send stopped beacon when tab is actually closing
Move the reportPlaybackBeacon call from beforeunload to pagehide.
beforeunload fires before the confirmation dialog, so cancelling
the close would still send stopped. pagehide only fires when the
page is actually being unloaded.
* fix(ui): revert to beforeunload for stopped beacon
pagehide does not fire reliably in Chrome when closing tabs.
Use beforeunload instead — if the user cancels the close dialog,
the heartbeat will re-register the NowPlaying entry on its next tick.
* fix(ui): use synchronous XHR for stopped report on tab close
Replace sendBeacon with synchronous XMLHttpRequest in beforeunload.
This blocks the page from closing until the server acknowledges
the stopped state, ensuring the NowPlaying entry is always removed.
* fix(ui): fix confirmation dialog and use fetch keepalive for tab close
- Move e.preventDefault() before the stopped report so the dialog
always shows regardless of XHR errors
- Use fetch with keepalive:true instead of sync XHR (more reliable,
non-blocking, survives page teardown)
- Fall back to sendBeacon if fetch throws
* fix(ui): prevent heartbeat from re-adding entry after stopped on tab close
Set a stoppedRef flag in beforeunload so the heartbeat interval
skips sending playing reports after stopped has been sent.
Without this, the heartbeat could re-register the NowPlaying
entry after the stopped event removed it.
* fix(ui): include client unique ID header in stopped report on tab close
Root cause: reportPlaybackSync (fetch keepalive) did not include the
X-ND-Client-Unique-Id header. Regular reportPlayback calls via
httpClient include this header, and the server uses it as the playMap
key. Without the header, the stopped call fell back to player.ID
as the key, which didn't match the entry added with the UUID key.
The playMap.Remove targeted the wrong key, so the entry persisted.
Fix: export clientUniqueId from httpClient and include it as a header
in the fetch keepalive request.
* fix(ui): use pagehide for stopped report to avoid premature send
beforeunload fires before the confirmation dialog, so the stopped
event was sent even when the user cancelled closing. Move the
stopped report to pagehide, which only fires when the page is
actually being unloaded (after confirmation).
* feat(server): broadcast NowPlaying SSE on every state change
Previously, the SSE broadcast only fired when the NowPlaying count
changed. Now it fires on every reportPlayback call (starting,
playing, paused, stopped), so the NowPlaying panel gets instant
updates for state transitions and position changes.
The UI reducer stores a nowPlayingLastUpdate timestamp alongside
the count, ensuring every SSE event triggers a re-fetch even when
the count is unchanged (e.g., pause/resume).
* fix(ui): clamp NowPlaying position to prevent negative time display
* fix(ui): debounce NowPlaying fetches to prevent progress bar trembling
During track changes, rapid SSE events (stopped, starting, playing)
triggered multiple refetches within milliseconds, each resetting the
interpolation base and causing the progress bar to oscillate. Skip
fetches within 1 second of the previous fetch.
* feat(ui): report playback position on seek
Send a reportPlayback(playing) call when the user seeks/scrubs in
the player, so the NowPlaying panel and server position stay in
sync immediately instead of waiting for the next 60s heartbeat.
* refactor: code review cleanup
- Export clientUniqueIdHeader from httpClient, use in subsonic layer
- Fix variable shadowing (now → fetchNow) in NowPlayingPanel fetchList
- Fix onBeforeDestroy nested dep (read isRadio from ref instead)
- Only broadcast SSE on state transitions, not heartbeat position updates
- Only enqueue NowPlaying to external scrobblers on state transitions
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): use trailing-edge debounce for NowPlaying fetch
Replace the leading-edge throttle (which fetched on the first event
and blocked subsequent ones) with a trailing-edge debounce (300ms).
During track transitions, the burst of events (stopped → starting →
playing) now collapses into a single fetch after the burst settles,
showing the new track immediately instead of briefly showing empty.
* fix(ui): only show overlay on NowPlaying artwork when paused
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(ui): remove unnecessary sendBeacon fallback from reportPlaybackSync
* refactor(ui): rename reportPlaybackSync to reportPlaybackKeepalive
The function was never synchronous — it uses fetch with keepalive:true,
which is fire-and-forget. The name now reflects the actual behavior.
* style: format code with prettier
* test: add tests for reportPlayback SSE broadcast and UI changes
- play_tracker: verify SSE broadcast on every state transition and
that broadcasts are skipped when EnableNowPlaying is false
- activityReducer: verify nowPlayingLastUpdate timestamp is set
- subsonic/index: verify reportPlayback URL construction
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): prevent NowPlaying from fetching every second when panel is open
fetchList had unstable identity because it depended on doFetch
(which depended on notify/dispatch). Each 1s setNow re-render
recreated the callback chain, re-triggering the useEffect that
calls fetchList. Use a ref for the fetch logic so fetchList has
a stable identity with empty deps.
* fix(ui): break fetch→dispatch→effect→fetch loop in NowPlaying panel
The fetch dispatched nowPlayingCountUpdate on every result, which
updated nowPlayingLastUpdate in Redux, which triggered the SSE
effect to call fetchList again — creating a fetch loop.
Fix: remove dispatch from fetch results. The badge count uses
entries.length (from local state) when entries are loaded, falling
back to Redux count (from SSE) when they aren't. SSE events remain
the only trigger for nowPlayingLastUpdate, breaking the loop.
* fix(ui): clear NowPlaying entries on panel close so badge uses SSE count
* style: format code with prettier
* fix: address code review feedback
- Fix currentTime truthiness check to handle position 0 correctly
- Report actual player state (playing/paused) on seek instead of
always sending 'playing'
- Remove idx from React key to avoid reorder issues
- Add debounce timer cleanup on unmount
- Keep entries on panel close so badge stays accurate from polling
- Fix test description to match actual assertion
* fix(ui): keep NowPlaying badge count accurate from polling
Add a separate nowPlayingCountSync action that updates the Redux
count without setting nowPlayingLastUpdate (which would trigger
the SSE effect and cause a fetch loop). Polling results now sync
the badge count via this action, so the badge stays accurate even
when SSE is unavailable.
* style: format code with prettier
---------
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: split html sanitization from plaintext handling
Add a dedicated SanitizeHTML helper for HTML-rendered values so entity-encoded markup is decoded before bluemonday sanitization. Use the new helper for the login welcome message and artist biographies while preserving SanitizeText semantics for lyrics and other plaintext callers. Add regression coverage for both helpers and the serveIndex welcomeMessage path.
* docs: add SanitizeText and SanitizeHTML godoc
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: preserve plain text in artist biographies
Revert artist biography storage to SanitizeText so entity-encoded plain text remains decoded for Subsonic consumers. This avoids double-escaping values like R&B in XML responses while keeping the new welcomeMessage HTML sanitization in place, and adds a regression test covering the biography storage behavior.
---------
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(artwork): rename DevJpegCoverArt to EnableWebPEncoding
Replaced the internal DevJpegCoverArt flag with a user-facing
EnableWebPEncoding config option (defaults to true). When disabled, the
fallback encoding now preserves the original image format — PNG sources
stay PNG for non-square resizes, matching v0.60.3 behavior. The previous
implementation incorrectly re-encoded PNG sources as JPEG in non-square
mode. Also added EnableWebPEncoding to the insights data.
* feat: add configurable UICoverArtSize option
Converted the hardcoded UICoverArtSize constant (600px) into a
configurable option, allowing users to reduce the cover art size
requested by the UI to mitigate slow image encoding. The value is
served to the frontend via the app config and used by all components
that request cover art. Also simplified the cache warmer by removing
a single-iteration loop in favor of direct code.
* style: fix prettier formatting in subsonic test
* feat: log WebP encoder/decoder selection
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(artwork): address PR review feedback
- Add DevJpegCoverArt to logRemovedOptions so users with the old config
key get a clear warning instead of a silent ignore.
- Include EnableWebPEncoding in the resized artwork cache key to prevent
stale WebP responses after toggling the setting.
- Skip animated GIF to WebP conversion via ffmpeg when EnableWebPEncoding
is false, so the setting is consistent across all image types.
- Fix data race in cache warmer by reading UICoverArtSize at construction
time instead of per-image, avoiding concurrent access with config
cleanup in tests.
- Clarify cache warmer docstring to accurately describe caching behavior.
* Revert "fix(artwork): address PR review feedback"
This reverts commit 3a213ef03e.
* fix(artwork): avoid data race in cache warmer config access
Capture UICoverArtSize at construction time instead of reading from
conf.Server on each doCacheImage call. The background goroutine could
race with test config cleanup, causing intermittent race detector
failures in CI.
* fix(configuration): clamp UICoverArtSize to be within 200 and 1200
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(artwork): preserve album cache key compatibility with v0.60.3
Restored the v0.60.3 hash input order for album artwork cache keys
(Agents + CoverArtPriority) so that existing caches remain valid on
upgrade when EnableExternalServices is true. Also ensures
CoverArtPriority is always part of the hash even when external services
are disabled, fixing a v0.60.3 bug where changing CoverArtPriority had
no effect on cache invalidation.
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: default EnableWebPEncoding to false and reduce artwork parallelism
Changed EnableWebPEncoding default to false so that upgrading users get
the same JPEG/PNG encoding behavior as v0.60.3 out of the box, avoiding
the WebP WASM overhead until native libwebp is available. Users can
opt in to WebP by setting EnableWebPEncoding=true. Also reduced the
default DevArtworkMaxRequests to half the CPU count (min 2) to lower
resource pressure during artwork processing.
* fix(configuration): update DefaultUICoverArtSize to 300
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(Makefile): append EXTRA_BUILD_TAGS to GO_BUILD_TAGS
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(server): add ExtAuth logout URL configuration (#4467)
When external authentication (reverse proxy auth) is active, the Logout
button is hidden because authentication is managed externally. Many
external auth services (Authelia, Authentik, Keycloak) provide a logout
URL that can terminate the session.
Add `ExtAuth.LogoutURL` config option that, when set, shows the Logout
button in the UI and redirects the user to the external auth provider's
logout endpoint instead of the Navidrome login page.
* feat(server): add validation for ExtAuth logout URL configuration
* feat(server): refactor ExtAuth logout URL validation to a reusable function
* fix(configuration): rename URL validation functions for consistency
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(configuration): rename URL validation functions for consistency
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
* build: add sqlite_fts5 build tag to enable FTS5 support
* feat: add SearchBackend config option (default: fts)
* feat: add buildFTS5Query for safe FTS5 query preprocessing
* feat: add FTS5 search backend with config toggle, refactor legacy search
- Add searchExprFunc type and getSearchExpr() for backend selection
- Rename fullTextExpr to legacySearchExpr
- Add ftsSearchExpr using FTS5 MATCH subquery
- Update fullTextFilter in sql_restful.go to use configured backend
* feat: add FTS5 migration with virtual tables, triggers, and search_participants
Creates FTS5 virtual tables for media_file, album, and artist with
unicode61 tokenizer and diacritic folding. Adds search_participants
column, populates from JSON, and sets up INSERT/UPDATE/DELETE triggers.
* feat: populate search_participants in PostMapArgs for FTS5 indexing
* test: add FTS5 search integration tests
* fix: exclude FTS5 virtual tables from e2e DB restore
The restoreDB function iterates all tables in sqlite_master and
runs DELETE + INSERT to reset state. FTS5 contentless virtual tables
cannot be directly deleted from. Since triggers handle FTS5 sync
automatically, simply skip tables matching *_fts and *_fts_* patterns.
* build: add compile-time guard for sqlite_fts5 build tag
Same pattern as netgo: compilation fails with a clear error if
the sqlite_fts5 build tag is missing.
* build: add sqlite_fts5 tag to reflex dev server config
* build: extract GO_BUILD_TAGS variable in Makefile to avoid duplication
* fix: strip leading * from FTS5 queries to prevent "unknown special query" error
* feat: auto-append prefix wildcard to FTS5 search tokens for broader matching
Every plain search token now gets a trailing * appended (e.g., "love" becomes
"love*"), so searching for "love" also matches "lovelace", "lovely", etc.
Quoted phrases are preserved as exact matches without wildcards. Results are
ordered alphabetically by name/title, so shorter exact matches naturally
appear first.
* fix: clarify comments about FTS5 operator neutralization
The comments said "strip" but the code lowercases operators to
neutralize them (FTS5 operators are case-sensitive). Updated comments
to accurately describe the behavior.
* fix: use fmt.Sprintf for FTS5 phrase placeholders
The previous encoding used rune('0'+index) which silently breaks with
10+ quoted phrases. Use fmt.Sprintf for arbitrary index support.
* fix: validate and normalize SearchBackend config option
Normalize the value to lowercase and fall back to "fts" with a log
warning for unrecognized values. This prevents silent misconfiguration
from typos like "FTS", "Legacy", or "fts5".
* refactor: improve documentation for build tags and FTS5 requirements
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: convert FTS5 query and search backend normalization tests to DescribeTable format
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: add sqlite_fts5 build tag to golangci configuration
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: add UISearchDebounceMs configuration option and update related components
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: fall back to legacy search when SearchFullString is enabled
FTS5 is token-based and cannot match substrings within words, so
getSearchExpr now returns legacySearchExpr when SearchFullString
is true, regardless of SearchBackend setting.
* fix: add sqlite_fts5 build tag to CI pipeline and Dockerfile
* fix: add WHEN clauses to FTS5 AFTER UPDATE triggers
Added WHEN clauses to the media_file_fts_au, album_fts_au, and
artist_fts_au triggers so they only fire when FTS-indexed columns
actually change. Previously, every row update (e.g., play count, rating,
starred status) triggered an unnecessary delete+insert cycle in the FTS
shadow tables. The WHEN clauses use IS NOT for NULL-safe comparison of
each indexed column, avoiding FTS index churn for non-indexed updates.
* feat: add SearchBackend configuration option to data and insights components
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: enhance input sanitization for FTS5 by stripping additional punctuation and special characters
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: add search_normalized column for punctuated name search (R.E.M., AC/DC)
Add index-time normalization and query-time single-letter collapsing to
fix FTS5 search for punctuated names. A new search_normalized column
stores concatenated forms of punctuated words (e.g., "R.E.M." → "REM",
"AC/DC" → "ACDC") and is indexed in FTS5 tables. At query time, runs of
consecutive single letters (from dot-stripping) are collapsed into OR
expressions like ("R E M" OR REM*) to match both the original tokens and
the normalized form. This enables searching by "R.E.M.", "REM", "AC/DC",
"ACDC", "A-ha", or "Aha" and finding the correct results.
* refactor: simplify isSingleUnicodeLetter to avoid []rune allocation
Use utf8.DecodeRuneInString to check for a single Unicode letter
instead of converting the entire string to a []rune slice.
* feat: define ftsSearchColumns for flexible FTS5 search column inclusion
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: update collapseSingleLetterRuns to return quoted phrases for abbreviations
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: implement extractPunctuatedWords to handle artist/album names with embedded punctuation
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: implement extractPunctuatedWords to handle artist/album names with embedded punctuation
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: punctuated word handling to improve processing of artist/album names
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: add CJK support for search queries with LIKE filters
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: enhance FTS5 search by adding album version support and CJK handling
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: search configuration to use structured options
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: enhance search functionality to support punctuation-only queries and update related tests
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Added a new event stream connection method to enhance the handling of
server events. This includes a reconnect mechanism for improved reliability
in case of connection errors. The configuration now allows toggling the
new event stream feature via `devNewEventStream`. Additionally, tests
were added to ensure the new functionality works as expected, including
reconnection behavior after an error.
Signed-off-by: Deluan <deluan@navidrome.org>
* ReplayGain support
- extract ReplayGain tags from files, expose via native api
- use metadata to normalize audio in web player
* make pre-push happy
* remove unnecessary prints
* remove another unnecessary print
* add tooltips, see metadata
* address comments, use settings instead
* remove console.log
* use better language for gain modes
* Refactor session_keys to its own package
* Adjust play_tracker
- Don't send external NowPlaying/Scrobble for tracks with unknown artist
- Continue to the next agent on error
* Implement ListenBrainz Agent and Auth Router
* Implement frontend for ListenBrainz linking
* Update listenBrainzRequest
- Don't marshal Player to json
- Rename Track to Title
* Return ErrRetryLater on ListenBrainz server errors
* Add tests for listenBrainzAgent
* Add tests for ListenBrainz Client
* Adjust ListenBrainzTokenDialog to handle errors better
* Refactor listenbrainz.formatListen and listenBrainzRequest structs
* Refactor agent auth_routers
* Refactor session_keys to agents package
* Add test for listenBrainzResponse
* Add tests for ListenBrainz auth_router
* Update ListenBrainzTokenDialog and auth_router
* Adjust player scrobble toggle
* Configure fetching from API and route
* pretty
* Remove errors
* Remove errors
* Remove errors
* Complete page for Desktop view
* Fix error
* Add xs Artist page
* Remove unused import
* Add styles for theme
* Change route path
* Remove artId useEffect array
* Remove array
* Fix cover load err
* Add redirect on err
* Remove route
* What's in a name? consistency :)
* Fix err
* Fix UI changes
* Fetch album from resource
* Renaming done
* Review changes
* Some touch-up
* Small refactor, to make naming and structure more consistent with AlbumShow
* Make artist's album list similar to original implementation
* Reuse AlbumGridView, to avoid duplication
* Add feature flag to enable new Artist Page, default false
* Better biography styling. Small refactorings,
* Don't encode quotes and other symbols
* Moved AlbumShow to correct folder
Co-authored-by: Deluan <deluan@navidrome.org>
* Show playlists in sidebar menu
* Fix menu
* Refresh playlist submenu when adding new playlist
* Group shared playlists below user's playlists
* Fix text overflow in menu options
* Add button in playlist menu to go to Playlists list
* Add config option `DevSidebarPlaylists` to enable this feature (default false)
* Make authentication part of the server, so it can be reused outside the Native API
This commit has broken tests after a rebase
* Serve frontend assets from `server`, and not from Native API
* Change Native API URL
* Fix auth tests
* Refactor server authentication
* Simplify authProvider, now subsonic token+salt comes from the server
* Don't send JWT token to UI when authenticated via Request Header
* Enable ReverseProxyWhitelist to be read from environment
2021-06-13 12:46:36 -04:00
Renamed from server/app/serve_index_test.go (Browse further)