* fix(msi): include ffprobe executable in MSI build
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ffmpeg): add IsProbeAvailable() to FFmpeg interface
Add runtime check for ffprobe binary availability with cached result
and startup logging. When ffprobe is missing, logs a warning at startup.
* feat(stream): guard MakeDecision behind ffprobe availability
When ffprobe is not available, MakeDecision returns a decision with
ErrorReason set and both CanDirectPlay and CanTranscode false, instead
of failing with an opaque exec error.
* feat(subsonic): only advertise transcoding extension when ffprobe is available
The OpenSubsonic transcoding extension is now conditionally included
based on ffprobe availability, so clients know not to call
getTranscodeDecision when ffprobe is missing.
* refactor(ffmpeg): move ffprobe startup warning to initial_setup
Move the ffprobe availability warning from the lazy IsProbeAvailable()
check to checkFFmpegInstallation() in server/initial_setup.go, alongside
the existing ffmpeg warning. This ensures the warning appears at startup
rather than on first endpoint call.
* fix(e2e): set noopFFmpeg.IsProbeAvailable to true
The e2e tests use pre-populated probe data and don't need a real ffprobe
binary. Setting IsProbeAvailable to true allows the transcode decision
logic to proceed normally in e2e tests.
* fix(stream): only guard on ffprobe when probing is needed
Move the IsProbeAvailable() guard inside the SkipProbe check so that
legacy stream requests (which pass SkipProbe: true) are not blocked
when ffprobe is missing. The guard only applies when probing is
actually required (i.e., getTranscodeDecision endpoint).
* refactor(stream): fall back to tag metadata when ffprobe is unavailable
Instead of blocking getTranscodeDecision when ffprobe is missing,
fall back to tag-based metadata (same behavior as /rest/stream).
The transcoding extension is always advertised. A startup warning
still alerts admins when ffprobe is not found.
* fix(stream): downgrade ffprobe-unavailable log to Debug
Avoids log spam when clients call getTranscodeDecision repeatedly
without ffprobe installed. The startup warning in initial_setup.go
already alerts admins at Warn level.
---------
Signed-off-by: Deluan <deluan@navidrome.org>
The cleanupLoop goroutine could execute cleanupExpired against a closed
database because Close() did not wait for the goroutine to exit before
calling db.Close(). This caused 'sql: database is closed' errors during
plugin unload or shutdown.
Close() now cancels the cleanup goroutine's context and waits for it to
finish via a sync.WaitGroup before running the final cleanup and closing
the database.
Signed-off-by: Deluan <deluan@navidrome.org>
The Squiddies Glass theme applies a CSS color filter to all images inside
table cells (MuiTableCell '& img'), which was intended for small playback
indicator icons. This inadvertently also applied to disc cover art
thumbnails in multi-disc album views, turning them into solid color
blocks. Adding 'filter: none !important' to the discCoverArt style
ensures cover art images are always displayed correctly regardless of
the active theme.
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: allow WAV direct play by aliasing pcm and wav codecs
WAV files were being transcoded to FLAC even when the browser declared
native WAV support. The backend normalizes ffprobe's pcm_s16le (and
similar PCM variants) to the internal codec name "pcm", while browsers
advertise WAV support as audioCodecs:["wav"] in their client profile.
The direct-play codec check compared these literally and rejected the
match with "audio codec not supported", forcing a needless FLAC
transcode.
Added {"pcm", "wav"} to codecAliasGroups so the matcher treats them
as equivalent. The container check runs first, so AIFF files (which also
normalize to codec "pcm" but use container "aiff") cannot
accidentally match a WAV direct-play profile.
* feat: include profile details in direct-play rejection reasons
The transcodeReason array returned by getTranscodeDecision previously
contained one generic string per failed DirectPlayProfile (e.g., five
copies of "container not supported"), making it hard to correlate a
reason with the profile that rejected the stream.
Each rejection reason now embeds the offending source value (in single
quotes) along with a compact representation of the full profile that
rejected it, rendered as [container/codec]. For example, clients with
two distinct ogg-container profiles (opus and vorbis) produced two
identical rejection strings; they now read "container 'wav' not
supported by profile [ogg/opus]" and "container 'wav' not supported
by profile [ogg/vorbis]", making each entry in the transcodeReason
array unique and self-describing.
A small describeProfile helper renders profiles as [container/codec]
(or [container] when no codec is constrained).
* refactor(stream): address code review — narrow pcm/wav match, tighten tests
Responds to reviewer feedback on the initial PR:
- Replace the symmetric pcm↔wav codec alias with a contextual
isPCMInWAVMatch check in checkDirectPlayProfile. The alias
unconditionally equated the two names in matchesCodec, which would
let AIFF sources (also normalized to codec "pcm") falsely satisfy
a codec-only ["wav"] direct-play profile that omitted containers.
The new check additionally requires src.Container == "wav" before
bridging the names, closing the false-positive path.
- Tighten the rejection-reason test assertions to verify the new
formatted output (source value + profile descriptor) instead of
just matching loose substrings like "container", preventing
unrelated rejections from satisfying the expectations.
- Add coverage for the WAV→wav-codec acceptance path and for the
AIFF-in-wav-codec-profile rejection path to pin down the contract
of isPCMInWAVMatch.
* refactor(codec): rename isPCMInWAVMatch to matchesPCMWAVBridge for clarity
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(db): resolve schema inconsistencies in library_artist and scrobble_buffer tables
* fix(db): address PR comments around speed of the migration
* fix(db): simplify schema inconsistencies migration
Remove ineffective PRAGMA foreign_keys and cache_size statements, which are
no-ops inside goose's wrapping transaction. Drop the down migration body
(Navidrome does not run down migrations) and document the intent. Rename
the file to refresh the timestamp after rebase.
---------
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
PublicURL() copied only the scheme and host from conf.Server.ShareURL,
silently dropping any path component. This broke OpenGraph image URLs
(and other share links) when ShareURL was configured with a path prefix
like https://example.com/navi — generated URLs pointed to /share/img/...
at the root instead of /navi/share/img/...
Now the ShareURL path is prepended to the resource path, with trailing
slashes trimmed. When ShareURL has no path, behavior is unchanged.
* fix(ui): regenerate package-lock.json to have integrity fields
* chore(deps): update esbuild and related packages to version 0.27.7
Signed-off-by: Deluan <deluan@navidrome.org>
* chore(lint): exclude node_modules from golangci-lint
Prevents lint errors from Go files inside npm packages under
ui/node_modules from being picked up by golangci-lint.
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan Quintão <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 3a213ef03e401930977138afe0e84c83290df683.
* 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>
buildOSPlaylist was the only OpenSubsonic builder function missing the
LegacyClients guard, causing attributes like `validUntil` and `readonly`
to appear in playlist XML responses for legacy clients like DSub2000.
This caused a crash when DSub2000 tried to parse evaluated smart
playlists containing the `validUntil` attribute.
The coverArt field was returning the raw uploaded image filename instead
of the standard ra-{id} artwork ID format. This caused getCoverArt to
fail when clients passed the coverArt value directly. Now uses
CoverArtID().String() consistent with how albums, artists, and playlists
return their coverArt values. Fixes#5293.
Fix#5284
Several configOptions struct fields were missing corresponding
viper.SetDefault entries, making them invisible to environment variable
overrides and config file parsing. Added defaults for mpvpath (consistent
with ffmpegpath), artistimagefolder, and plugins.loglevel.
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(external): refresh stale artist image URLs on expiry
ArtistImage() was serving cached image URLs from the database
indefinitely, ignoring ExternalInfoUpdatedAt. When users changed agent
configuration (e.g. disabling Deezer), old URLs persisted because only
the UpdateArtistInfo code path checked the TTL.
Now ArtistImage() checks the expiry and enqueues a background refresh
when the cached info is stale, matching the pattern used by
refreshArtistInfo(). The stale URL is still returned immediately to
avoid blocking clients.
Fixes#5266
* test: add expired artist image info test with log assertion
Verify that ArtistImage() enqueues a background refresh when cached
info is expired, by capturing log output and checking for the expected
debug message. Also asserts the stale URL is returned immediately
without calling the agent.
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: only enqueue refresh when returning a stale cached URL
Move the expiry check to the else branch so we only enqueue a
background refresh when a cached image URL exists and is being
returned. This avoids doubling external API calls when the URL is
empty (synchronous fetch) but ExternalInfoUpdatedAt is old.
---------
Signed-off-by: Deluan <deluan@navidrome.org>
ffmpeg.ExtractImage returns a pipe-based reader immediately, before ffmpeg
finishes processing. When the audio file has no embedded image stream (e.g.
a plain MP3), ffmpeg exits with an error that closes the pipe asynchronously.
The selectImageReader function saw the non-nil reader as a success and
returned it instead of falling through to the next source in the chain
(album art). This caused getCoverArt to return an error response for tracks
on albums where the disc artwork reader was invoked but no embedded art
existed.
Fixed by reading one byte from the pipe to validate the stream delivers
data before returning it. If the read fails, the reader is closed and nil
is returned, allowing the fallback chain to continue to album artwork.
Closes#5265
Replace 14 repeated fmt.Fprintln(os.Stderr, "FATAL:", ...)/os.Exit(1)
patterns with a single logFatal function. This reduces duplication
and makes all fatal config paths testable via SetLogFatal.
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: add toPascalCase helper for config key display
Adds a toPascalCase helper that converts dotted lowercase Viper config keys
(e.g. 'scanner.schedule') to PascalCase (e.g. 'Scanner.Schedule') for use
in user-facing warning messages. Includes export_test.go binding and a
full Ginkgo DescribeTable test suite covering simple, dotted, multi-segment,
already-capitalized, and empty-string cases.
* feat: remap ND_-prefixed env var names found in config files
Detect when users mistakenly use environment variable names (like
ND_ADDRESS) in config files, remap them to canonical keys, and warn.
Fatal error if both ND_ and canonical versions of the same key exist.
Closes#5242
Replaced single Read assertion with Eventually loop to drain buffered
pipe data after context cancellation. The previous test assumed the first
Read after cancel() would fail, but ffmpeg may have already written data
into the pipe buffer before being killed, causing the Read to succeed
from buffered content.
The config flag gates all image uploads (artists, radios, playlists),
not just cover art. Rename it to accurately reflect its scope across
the backend config, native API permission check, Subsonic CoverArtRole,
serve_index JSON key, and frontend config.
FTS5's implicit AND (space-separated tokens) silently fails when combined
with parenthesized OR groups produced by processPunctuatedWords. For example,
searching "you've got" generated the query `("you ve" OR youve*) got*` which
returned no results. Using explicit AND (`("you ve" OR youve*) AND got*`)
resolves this FTS5 quirk. Since implicit and explicit AND are semantically
identical in FTS5, this change is safe for all queries unconditionally.
* feat(ui): cancel in-flight image requests on pagination and cache across remounts
When paginating quickly through list/grid views, image requests for previous
pages were never canceled, queuing on the server and blocking new images.
This adds a useImageUrl hook that loads images via fetch() with AbortController,
so requests are canceled when components unmount. A module-level cache (URL →
blob URL) with reference counting ensures React Admin refreshes display images
instantly without re-fetching.
* feat(ui): update AlbumListPagination to conditionally render based on albumListType
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): abort all in-flight image fetches on pagination change
Pagination component now watches page/perPage via useListContext and
calls abortAllInFlight() when either changes, freeing the browser
connection pool immediately for the next page's data request.
Also adds empty placeholder style to CoverArtAvatar so it renders as a
clean transparent area while loading instead of the default person icon.
* Revert "feat(ui): abort all in-flight image fetches on pagination change"
This reverts commit 3bc09f9d0374aa63572a381e38a30e2f2cec4da8.
* fix(ui): limit concurrent image fetches to prevent connection pool saturation
With <img src>, the browser prioritizes API requests over image loads.
With fetch(), all requests compete equally for the HTTP/1.1 connection
pool (6 per origin), causing API requests to queue behind images and
making pagination feel unresponsive. Caps concurrent image fetches at
4 with a pending queue, leaving connections free for API requests.
Queued fetches for unmounted components are removed without ever
hitting the network.
* fix(ui): fix queued fetch not aborted on unmount
Set queued=false when doFetch executes from the pending queue, so
cleanup correctly calls controller.abort() instead of searching an
already-drained queue.
---------
Signed-off-by: Deluan <deluan@navidrome.org>
The Vorbis/FLAC VERSION field is for track-level disambiguation (e.g.
remix, live, 30s edit), not album versioning. Including it in the
albumversion aliases caused albums to split incorrectly when tracks
had different VERSION values and no MusicBrainz Album ID was set.
Remove 'version' from the albumversion aliases in mappings.yaml.
Users who want the old behavior can re-add it via Tags config.
Update the PID test to use 'albumversion' directly instead of
'version' as the raw PID attribute, with a realistic value.
Fixes#5082
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
Increased the UI cover art request size from 300px to 600px for sharper
images on high-DPI displays. Replaced BiLinear with CatmullRom (bicubic)
interpolation for higher quality image resizing. Extracted the hardcoded
size into a COVER_ART_SIZE constant in the frontend and consolidated
backend sizes into a CacheWarmerImageSizes slice. Removed the unused
UIThumbnailSize constant.
Signed-off-by: Deluan <deluan@navidrome.org>
Removed marginBottom: '3px' from tileBar and tileBarMobile styles that
was causing the hover overlay to not fully cover the album cover art.
The margin pushed the absolutely-positioned GridListTileBar up, leaving
a visible gap at the bottom. This became apparent after d2a54243a added
aspectRatio: 1 to the cover container.