Commit Graph

4701 Commits

Author SHA1 Message Date
Deluan cb396f3dba feat(ui): increase cover art size to 600px and use CatmullRom scaling
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>
2026-03-22 14:55:14 -04:00
Deluan 400a079fcd fix(ui): fix hover overlay not covering full album cover
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.
2026-03-21 19:19:03 -04:00
Deluan 03844a9a36 feat(plugins): add NoFollowRedirects option to HTTPRequest
Allow plugins to opt out of automatic redirect following on a per-request
basis. When set to true, the response returns the redirect status code and
Location header directly instead of following to the final destination.
2026-03-20 18:16:07 -04:00
Deluan Quintão 5cd1fcb492 feat(scheduler): add crontab(5) random ~ syntax support (#5233)
* feat(scheduler): add CrontabSchedule with crontab(5) random ~ syntax

Implement ParseCrontab() that extends robfig/cron with support for
the crontab(5) random ~ operator (e.g., 0~30 * * * *). Random values
are resolved fresh on each Next() call for load spreading.

Supports A~B, ~B, A~, and bare ~ forms in all 6 fields (including
seconds). Expressions without ~ delegate to robfig's standard parser
with zero overhead.

Integrates into scheduler.Add() and conf.validateSchedule() so that
scanner.schedule and backup.schedule config values accept ~ syntax.

* refactor(scheduler): resolve random ~ values once at parse time

Change from per-Next() randomization to per-parse randomization,
matching crontab(5) semantics. This prevents double-firing within
the same period when random values land after the current time.

ParseCrontab now resolves ~ fields to concrete values, substitutes
them into the spec string, and delegates to robfig's parser. This
eliminates CrontabSchedule, randomField, and resolveField entirely.

* test(scheduler): replace WaitGroup with channel for job execution synchronization

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-20 08:57:13 -04:00
JRoshthen1 a4c289b28c feat(ui): add Slovak language translation (#5231)
* feat(i18n): Add Slovak language translation

Signed-off-by: jrosh <martin@jrosh.eu>

* fix(i18n): Fix typos and add missing translations

Signed-off-by: jrosh <martin@jrosh.eu>

---------

Signed-off-by: jrosh <martin@jrosh.eu>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2026-03-19 13:33:09 -04:00
Deluan f7b60c7952 fix(tests): fix race condition in CacheWarmer pre-cache size test
The test was checking that the buffer was drained before asserting on
cached sizes, but the buffer is cleared before processBatch completes.
Use Eventually on getCachedSizes() directly to properly wait for the
artwork caching to finish.
2026-03-19 13:14:24 -04:00
Deluan Quintão ba8d427890 feat(ui): add cover art support for internet radio stations (#5229)
* feat(artwork): add KindRadioArtwork and EntityRadio constant

* feat(model): add UploadedImage field and artwork methods to Radio

* feat(model): add Radio to GetEntityByID lookup chain

* feat(db): add uploaded_image column to radio table

* feat(artwork): add radio artwork reader with uploaded image fallback

* feat(api): add radio image upload/delete endpoints

* feat(ui): add radio artwork ID prefix to getCoverArtUrl

* feat(ui): add cover art display and upload to RadioEdit

* feat(ui): add cover art thumbnails to radio list

* feat(ui): prefer artwork URL in radio player helper

* refactor: remove redundant code in radio artwork

- Remove duplicate Avatar rendering in RadioList by reusing CoverArtField
- Remove redundant UpdatedAt assignment in radio image handlers (already set by repository Put)

* refactor(ui): extract shared useImageLoadingState hook

Move image loading/error/lightbox state management into a shared
useImageLoadingState hook in common/. Consolidates duplicated logic
from AlbumDetails, PlaylistDetails, RadioEdit, and artist detail views.

* feat(ui): use radio placeholder icon when no uploaded image

Remove album placeholder fallback from radio artwork reader so radios
without an uploaded image return ErrUnavailable. On the frontend, show
the internet-radio-icon.svg placeholder instead of requesting server
artwork when no image is uploaded, allowing favicon fallback in the
player.

* refactor(ui): update defaultOff fields in useSelectedFields for RadioList

Signed-off-by: Deluan <deluan@navidrome.org>

* fix: address code review feedback

- Add missing alt attribute to CardMedia in RadioEdit for accessibility
- Fix UpdateInternetRadio to preserve UploadedImage field by fetching
  existing radio before updating (prevents Subsonic API from clearing
  custom artwork)
- Add Reader() level tests to verify ErrUnavailable is returned when
  radio has no uploaded image

* refactor: add colsToUpdate to RadioRepository.Put

Use the base sqlRepository.put with column filtering instead of
hand-rolled SQL. UpdateInternetRadio now specifies only the Subsonic API
fields, preventing UploadedImage from being cleared. Image upload/delete
handlers specify only UploadedImage.

* fix: ensure UpdatedAt is included in colsToUpdate for radio Put

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-18 18:57:33 -04:00
Deluan Quintão 3f7226d253 fix(server): improve transcoding failure diagnostics and error responses (#5227)
* fix(server): capture ffmpeg stderr and warn on empty transcoded output

When ffmpeg fails during transcoding (e.g., missing codec like libopus),
the error was silently discarded because stderr was sent to io.Discard
and the HTTP response returned 200 OK with a 0-byte body.

- Capture ffmpeg stderr in a bounded buffer (4KB) and include it in the
  error message when the process exits with a non-zero status code
- Log a warning when transcoded output is 0 bytes, guiding users to
  check codec support and enable Trace logging for details
- Remove log level guard so transcoding errors are always logged, not
  just at Debug level

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(server): return proper error responses for empty transcoded output

Instead of returning HTTP 200 with 0-byte body when transcoding fails,
return a Subsonic error response (for stream/download/getTranscodeStream)
or HTTP 500 (for public shared streams). This gives clients a clear
signal that the request failed rather than a misleading empty success.

Signed-off-by: Deluan <deluan@navidrome.org>

* test(e2e): add tests for empty transcoded stream error responses

Add E2E tests verifying that stream and download endpoints return
Subsonic error responses when transcoding produces empty output.
Extend spyStreamer with SimulateEmptyStream and SimulateError fields
to support failure injection in tests.

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor(server): extract stream serving logic into Stream.Serve method

Extract the duplicated non-seekable stream serving logic (header setup,
estimateContentLength, HEAD draining, io.Copy with error/empty detection)
from server/subsonic/stream.go and server/public/handle_streams.go into a
single Stream.Serve method on core/stream. Both callers now delegate to it,
eliminating ~30 lines of near-identical code.

* fix(server): return 200 with empty body for stream/download on empty transcoded output

Don't return a Subsonic error response when transcoding produces empty
output on stream/download endpoints — just log the error and return 200
with an empty body. The getTranscodeStream and public share endpoints
still return HTTP 500 for empty output. Stream.Serve now returns
(int64, error) so callers can check the byte count.

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-18 12:39:03 -04:00
Deluan 00b8fbd789 feat(artwork): add UIThumbnailSize constant and update cache warmer to pre-cache thumbnails
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-18 08:52:52 -04:00
Deluan Quintão 31d94acfe7 fix(scanner): widen WASM panic recovery to cover tag/property reading (#5223)
* fix(scanner): widen WASM panic recovery to cover tag/property reading

The panic recovery in gotaglib's extractMetadata was only inside
openFile(), which covers taglib.OpenStream(). Panics from f.AllTags()
and f.Properties() (e.g. readString crashes on malformed files) were
uncaught, crashing the scanner subprocess with exit status 2.

Move the recover() up to extractMetadata() so it covers the entire
tag reading lifecycle, matching the CGO taglib wrapper's approach.

Fixes #5220

* fix(scanner): use consistent log key "filePath" in panic recovery

* fix(scanner): include stack trace in WASM panic recovery log

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-18 08:03:46 -04:00
Deluan b5164c61ab build(worktree): add script for setting up git worktrees
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-17 21:34:00 -04:00
Deluan a83ebd1c98 fix(ui): hide pagination during album list loading
Added a custom AlbumListPagination component that returns null while the
list is loading, preventing stale pagination controls from appearing
alongside the Loading spinner when navigating to the Random album view.
2026-03-17 20:49:35 -04:00
Deluan d2a54243a8 fix(ui): prevent layout flash on album grid during cover loading
Added aspect-ratio: 1 to the cover container so it reserves the correct
square dimensions immediately on first render, before react-measure
reports the container width. Previously, contentRect.bounds.width started
as undefined/0, causing images to render with zero height and producing a
brief flash of compressed tiles before the measurement callback fired.
2026-03-17 20:24:21 -04:00
Deluan b013b71ba9 fix(server): clean up uploaded artist images during GC
When artists are purged during garbage collection, any custom uploaded
cover images were left orphaned on disk. Modified purgeEmpty() to query
for uploaded_image filenames before the bulk DELETE, then remove the
corresponding files from disk afterwards. Image cleanup is best-effort
to avoid failing the GC if a file is already missing or inaccessible.

Also populated album_artists entries in the persistence test suite setup
to reflect the actual album-artist relationships from test data, ensuring
purgeEmpty() doesn't inadvertently delete shared test artists.
2026-03-17 19:47:09 -04:00
Deluan ad92b752be chore(deps): update dependencies for go-sqlite3, golang.org/x packages
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-17 18:34:13 -04:00
Kendall Garner f39d75e7d2 fix(subsonic): never omit duration for AlbumID3 (#5217) 2026-03-17 13:20:10 -04:00
Deluan 693abe2f6b fix(build): regenerate package-lock.json for navidrome-music-player 4.25.2
The lockfile still referenced the local file path from testing,
causing CI to fail resolving the navidrome-music-player import.
Regenerated to point to the npm registry.
2026-03-17 12:28:20 -04:00
Deluan a0fe728098 fix(player): fix play next after transcoding changes
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-17 12:15:03 -04:00
Simon Teixidor 8f05f7815e fix(server): use http.TimeFormat for Last-Modified header (#5219)
Navidrome returns Last-Modified values like `Fri, 12 Dec 2025 03:32:26
UTC`. This is invalid according to RFC 7231 which requires HTTP dates to
use GMT instead of UTC. Switch to http.TimeFormat instead of
time.RFC1123 to resolve the issue.
2026-03-17 08:04:47 -04:00
Deluan Quintão 2f5b2b5135 fix(artwork): fallback mediafile cover art to disc artwork before album (#5216)
* fix(artwork): fallback mediafile cover art to disc artwork before album

Changed the mediafile cover art fallback chain to go through disc artwork
before album artwork (mediafile → disc → album). Previously, mediafiles
without embedded art fell back directly to album cover, bypassing any
disc-specific artwork. Renamed AlbumCoverArtID() to DiscCoverArtID() to
encapsulate the disc-vs-album decision in a single method, used by both
CoverArtID() and the mediafile artwork reader.

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(artwork): fix cache invalidation for mediafile and album cover art

Include imagesUpdatedAt from album folders in the mediafile artwork
reader's cache key, so that when a cover image file changes on disk
(without audio metadata changes) the mediafile cache properly
invalidates. Also include CoverArtPriority unconditionally in the album
artwork reader's cache key hash, so that changing the priority order
with external services disabled correctly invalidates the album cache.

* fix(artwork): skip disc artwork resolution for single-disc albums

Single-disc albums with DiscNumber=1 were unnecessarily routed through
discArtworkReader, which does extra DB queries only to fall through to
album art anyway. Now only multi-disc albums use the disc fallback path.

* refactor(artwork): restore AlbumCoverArtID as a separate method

Extract AlbumCoverArtID back out of DiscCoverArtID so the single-disc
fallback path in reader_mediafile can reference it by name instead of
inlining the artwork ID construction.

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-16 18:08:39 -04:00
Deluan Quintão e7c6e78dd0 fix(db): normalize timestamps and fix recently added album sorting (#5176)
* fix(db): normalize timestamps and fix recently added album sorting

SQLite stores timestamps as TEXT and uses string comparison for ORDER BY.
Timestamps in RFC3339 T-format ('2024-01-01T10:00:00Z') sort incorrectly
against space-format ('2024-01-01 10:00:00+00:00') because 'T' (ASCII 84)
> ' ' (ASCII 32), causing albums with T-format timestamps to appear as
newer than they are in the "Recently Added" list.

This adds a migration to normalize all T-format timestamps across all
tables to the space-format expected by go-sqlite3, wraps the
recently_added sort with datetime() to make it format-agnostic, and
replaces the plain album timestamp indexes with expression indexes to
maintain query performance.

* fix(test): improve recently_added sort test robustness

Use same-date timestamps (2024-01-15T08:00:00Z vs 2024-01-15 20:00:00)
so the T-vs-space character difference at position 10 actually triggers
the sorting bug. Initialize index variables to -1 and assert both test
albums are found before comparing positions.

* chore(db): update migration timestamp to 2026-03-16
2026-03-16 07:55:22 -04:00
Deluan 9ae9134a91 feat(ui): integrate CoverArtAvatar component into AlbumTableView
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-16 06:48:03 -04:00
Deluan cefa6e9619 feat(ui): add CoverArtAvatar component and integrate it into artist and playlist lists
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-16 06:48:03 -04:00
Deluan Quintão ab8a58157a feat: add artist image uploads and image-folder artwork source (#5198)
* feat: add shared ImageUploadService for entity image management

* feat: add UploadedImage field and methods to Artist model

* feat: add uploaded_image column to artist table

* feat: add ArtistImageFolder config option

* refactor: wire ImageUploadService and delegate playlist file ops to it

Wire ImageUploadService into the DI container and refactor the playlist
service to delegate image file operations (SetImage/RemoveImage) to the
shared ImageUploadService, removing duplicated file I/O logic. A local
ImageUploadService interface is defined in core/playlists to avoid an
import cycle between core and core/playlists.

* feat: artist artwork reader checks uploaded image first

* feat: add image-folder priority source for artist artwork

* feat: cache key invalidation for image-folder and uploaded images

* refactor: extract shared image upload HTTP helpers

* feat: add artist image upload/delete API endpoints

* refactor: playlist handlers use shared image upload helpers

* feat: add shared ImageUploadOverlay component

* feat: add i18n keys for artist image upload

* feat: add image upload overlay to artist detail pages

* refactor: playlist details uses shared ImageUploadOverlay component

* fix: add gosec nolint directive for ParseMultipartForm

* refactor: deduplicate image upload code and optimize dir scanning

- Remove dead ImageFilename methods from Artist and Playlist models
  (production code uses core.imageFilename exclusively)
- Extract shared uploadedImagePath helper in model/image.go
- Extract findImageInArtistFolder to deduplicate dir-scanning logic
  between fromArtistImageFolder and getArtistImageFolderModTime
- Fix fileInputRef in useCallback dependency array

* fix: include artist UpdatedAt in artwork cache key

Without this, uploading or deleting an artist image would not
invalidate the cached artwork because the cache key was only based
on album folder timestamps, not the artist's own UpdatedAt field.

* feat: add Portuguese translations for artist image upload

* refactor: use shared i18n keys for cover art upload messages

Move cover art upload/remove translations from per-entity sections
(artist, playlist) to a shared top-level "message" section, avoiding
duplication across entity types and translation files.

* refactor: move cover art i18n keys to shared message section for all languages

* refactor: simplify image upload code and eliminate redundancies

Extracted duplicate image loading/lightbox state logic from
DesktopArtistDetails and MobileArtistDetails into a shared
useArtistImageState hook. Moved entity type constants to the consts
package and replaced raw string literals throughout model, core, and
nativeapi packages. Exported model.UploadedImagePath and reused it in
core/image_upload.go to consolidate path construction. Cached the
ArtistImageFolder lookup result in artistReader to eliminate a redundant
os.ReadDir call on every artwork request.

Signed-off-by: Deluan <deluan@navidrome.org>

* style: fix prettier formatting in ImageUploadOverlay

* fix: address code review feedback on image upload error handling

- RemoveImage now returns errors instead of swallowing them
- Artist handlers distinguish not-found from other DB errors
- Defer multipart temp file cleanup after parsing

* fix: enforce hard request size limit with MaxBytesReader for image uploads

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-15 22:19:55 -04:00
Deluan Quintão be06196168 fix(ui): update Bulgarian, Catalan, Danish, German, Greek, Spanish, Finnish, French, Galician, Russian, Slovenian, Swedish, Thai, Chinese (traditional) translations from POEditor (#5044)
Co-authored-by: navidrome-bot <navidrome-bot@navidrome.org>
2026-03-15 20:44:59 -04:00
Thiago Sfredo 36aea8a11f feat(ui): add tooltips for long playlist and album names - 5068 (#5070)
* style(ui): add tooltips for long playlist and album names - 5068

Signed-off-by: Thiago Sfreddo <sfredo@gmail.com>

* fix dnd and improve performance

Signed-off-by: Thiago Sfreddo <sfredo@gmail.com>

* lint fixes

Signed-off-by: Thiago Sfreddo <sfredo@gmail.com>

* fix(ui): update tooltip styles for improved visibility and consistency

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(ui): add overflow tooltip to playlist name for better visibility

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor(ui): simplify OverflowTooltip and improve render performance

- Inline styles from useMenuTooltipStyles into OverflowTooltip (single consumer)
- Use MUI named colors (grey[700]/grey[300] with alpha) instead of raw rgba
- Stabilize ref callback with useCallback to avoid unnecessary ref churn
- Memoize Tooltip classes and hoist TransitionProps to module level
- Fix useLayoutEffect dependency: observe DOM size, not title string

---------

Signed-off-by: Thiago Sfreddo <sfredo@gmail.com>
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2026-03-15 14:55:55 -04:00
Tom Boucher aa93911991 feat(server): add syslog priority prefixes for systemd-journald (#5192)
* fix: add syslog priority prefixes for systemd-journald

When running under systemd, all log messages were assigned priority 3
(error) by journald because navidrome wrote plain text to stderr without
syslog priority prefixes.

Add a journalFormatter that wraps the existing logrus formatter and
prepends <N> syslog priority prefixes (RFC 5424) to each log line.
The formatter is automatically enabled when the JOURNAL_STREAM
environment variable is set (indicating the process is managed by
systemd).

Priority mapping:
- Fatal/Panic → <2>/<0> (crit/emerg)
- Error → <3> (err)
- Warn → <4> (warning)
- Info → <6> (info)
- Debug/Trace → <7> (debug)

Fixes #5142

* test: refactor journalFormatter tests to use Ginkgo and DescribeTable

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan <deluan@navidrome.org>
2026-03-15 14:14:05 -04:00
Tom Boucher c42570446b fix(ui): allow DefaultTheme "Auto" from config (#5190)
* fix(ui): allow DefaultTheme "Auto" from config

When DefaultTheme is set to "Auto" in the server config, the
defaultTheme() function in themeReducer now returns AUTO_THEME_ID
instead of falling through to the DarkTheme fallback.

This allows useCurrentTheme to correctly read prefers-color-scheme
and select Light or Dark theme automatically for new/incognito users.

Adds themeReducer unit tests covering Auto, named-theme, and
unrecognized-value fallback paths.

* chore: format

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan <deluan@navidrome.org>
2026-03-15 14:00:21 -04:00
Deluan a887521d7a fix(subsonic): always include mandatory title field in Child responses
Removed `omitempty` from the `Title` struct tag in the `Child` response
type. The Subsonic/OpenSubsonic API spec requires `title` to be a
mandatory field, but songs with empty titles caused the field to be
omitted entirely, crashing clients like Symfonium during sync.

Ref: https://support.symfonium.app/t/app-gets-stuck-on-syncing-large-database/13004/8
2026-03-15 13:36:26 -04:00
Deluan Quintão 69e7d163fc remove built-in Spotify integration (#5197)
* refactor: remove built-in Spotify integration

Remove the Spotify adapter and all related configuration, replacing
the built-in integration with the plugin system. This deletes the
adapters/spotify package, removes Spotify config options (ID/Secret),
updates the default agents list from "deezer,lastfm,spotify" to
"deezer,lastfm", and cleans up all references across configuration,
metrics, logging, artwork caching, and documentation. Users with
Spotify config options will now see a warning that the options are
no longer available.

* feat: add ListenBrainz to list of default agents

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-15 13:18:54 -04:00
Deluan Quintão 6b8fcc37c6 fix(share): add ownership checks to Delete and Update (#5189)
* test(share): add failing tests for Delete ownership checks

* fix(share): add ownership check to Delete

* test(share): add failing tests for Update ownership checks

* fix(share): add ownership check to Update

* refactor(share): extract checkOwnership helper with lightweight query

- Deduplicate ownership check from Delete and Update into a single helper
- Use a minimal single-column SELECT instead of Get (avoids loadMedia overhead)
- Use positive bypass form (IsAdmin || invalidUserId) matching codebase convention

* fix(share): convert model.ErrNotFound to rest.ErrNotFound in checkOwnership

Ensure consistent 404 responses when a nonexistent share ID is passed
to Delete or Update, by handling the conversion in checkOwnership
rather than relying on the subsequent write operation.
2026-03-15 00:12:58 -04:00
Deluan 197d357f02 fix(ui): prevent mobile touch events from triggering playback after lightbox close
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-14 21:47:26 -04:00
Deluan 549b812633 fix(ui): prevent duplicate getCoverArt requests on artist page
useMediaQuery defaults to false on the first render (SSR compat),
causing MobileArtistDetails to briefly render on desktop. Its CSS
background-image triggered a full-size image fetch before the
component switched to DesktopArtistDetails, which fetched again.

Pass noSsr: true so the media query evaluates synchronously, and
cap the mobile background image to 800px.
2026-03-14 20:36:57 -04:00
Deluan c63346de04 chore: run go mod tidy after dependency replacements 2026-03-14 10:23:45 -04:00
Deluan ba3974ee59 refactor(shellquote): replace go-shellquote with custom shell quoting implementation 2026-03-14 10:23:45 -04:00
Deluan 8939f31d55 refactor(jsoncommentstrip): replace go-jsoncommentstrip with custom JSON comment stripping 2026-03-14 10:18:56 -04:00
Deluan d79b812467 refactor(natural): replace maruel/natural with custom natural sort implementation 2026-03-14 10:18:56 -04:00
Deluan Quintão 55331b5fd9 fix(scanner): prevent duplicate tracks when multiple missing files match same target (#5183)
In processMissingTracks, matched tracks were not removed from the candidate
pool after being consumed by moveMatched. This allowed the same target track
to be paired with multiple missing tracks, creating duplicate non-missing
records with the same path. Track consumed matches in a usedMatched map so
each target is used at most once.

Fixes #5169
2026-03-14 00:07:21 -04:00
Deluan d042fc138c refactor(nanoid): replace gonanoid with custom nanoid implementation for ID generation
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-13 21:06:26 -04:00
Deluan 55e10b9c77 fix(playlist): update smart playlist rules during metadata update
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-13 19:20:07 -04:00
Deluan Quintão 49a14d4583 feat(artwork): add per-disc cover art support (#5182)
* feat(artwork): add KindDiscArtwork and ParseDiscArtworkID

Add new disc artwork kind with 'dc' prefix for per-disc cover art
support. The composite ID format is albumID:discNumber, parsed by
the new ParseDiscArtworkID helper.

* feat(conf): add DiscArtPriority configuration option

Default: 'disc*.*, cd*.*, embedded'. Controls how per-disc cover
art is resolved, following the same pattern as CoverArtPriority
and ArtistArtPriority.

* feat(artwork): implement extractDiscNumber helper

Extracts disc number from filenames based on glob patterns by
parsing leading digits from the wildcard-matched portion.
Used for matching disc-specific artwork files like disc1.jpg.

* feat(artwork): implement fromDiscExternalFile source function

Disc-aware variant of fromExternalFile that filters image files
by disc number (extracted from filename) or folder association
(for multi-folder albums).

* feat(artwork): implement discArtworkReader

Resolves disc artwork using DiscArtPriority config patterns.
Supports glob patterns with disc number extraction, embedded
images from first track, and falls back to album cover art.
Handles both multi-folder and single-folder multi-disc albums.

* feat(artwork): register disc artwork reader in dispatcher

Add KindDiscArtwork case to getArtworkReader switch, routing
disc artwork requests to the new discArtworkReader.

* feat(subsonic): add CoverArt field to DiscTitle response

Implements OpenSubsonic PR #220: optional cover art ID in
DiscTitle responses for per-disc artwork support.

* feat(subsonic): populate CoverArt in DiscTitle responses

Each DiscTitle now includes a disc artwork ID (dc-albumID:discNum)
that clients can use with getCoverArt to retrieve per-disc artwork.

* style: fix file permission in test to satisfy gosec

* feat(ui): add disc cover art display and lightbox functionality

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor: simplify disc artwork code

- Add DiscArtworkID constructor to encapsulate the "albumID:discNumber"
  format in one place
- Convert fromDiscExternalFile to a method on discArtworkReader,
  reducing parameter count from 6 to 2
- Remove unused rootFolder field from discArtworkReader

* style: fix prettier formatting in subsonic index

* style(ui): move cursor style to makeStyles in SongDatagrid

* feat(artwork): add discsubtitle option to DiscArtPriority

Allow matching disc cover art by the disc's subtitle/name.
When the "discsubtitle" keyword is in the priority list, image files
whose stem matches the disc subtitle (case-insensitive) are used.
This is useful for box sets with named discs (e.g., "The Blue Disc.jpg").

* feat(configuration): update discartpriority to include cover art options

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-13 18:33:18 -04:00
Deluan Quintão a50b2a1e72 feat(artwork): preserve animated image artwork during resize (#5184)
* feat(artwork): preserve animated image artwork during resize

Detect animated GIFs, WebPs, and APNGs via lightweight byte scanning
and preserve their animation when serving resized artwork. Animated GIFs
are converted to animated WebP via ffmpeg with optional downscaling;
animated WebP/APNG are returned as-is since ffmpeg cannot re-encode them.

Adds ConvertAnimatedImage to the FFmpeg interface for piping stdin data
through ffmpeg with animated WebP output.

* fix(artwork): address code review feedback for animated artwork

Fix ReadCloser leak where ffmpeg pipe's Close was discarded by
io.NopCloser wrapping — now preserves ReadCloser semantics when the
resized reader already supports Close. Use uint64 for PNG chunk position
to prevent potential overflow on 32-bit platforms. Add integration tests
for the animation branching logic in resizeImage.
2026-03-13 18:11:12 -04:00
Deluan Quintão 4ddb0774ec perf(artwork): improve image serving performance with WebP encoding and optimized pipeline (#5181)
* test(artwork): add benchmark helpers for generating test images

* test(artwork): add image decode benchmarks for JPEG/PNG at various sizes

* test(artwork): add image resize benchmarks for Lanczos at various sizes

* test(artwork): add image encode benchmarks for JPEG quality levels and PNG

* test(artwork): add full resize pipeline benchmark (decode+resize+encode)

* test(artwork): add tag extraction benchmark for embedded art

* test(cache): add file cache benchmarks for read, write, and concurrent access

* test(artwork): add E2E benchmarks for artwork.Get with cache on/off and concurrency

* fix(test): use absolute path for tag extraction benchmark fixture

* test(artwork): add resize alternatives benchmark comparing resamplers

* perf(artwork): switch to CatmullRom resampler and JPEG for square images

Replace imaging.Lanczos with imaging.CatmullRom for image resizing
(30% faster, indistinguishable quality at thumbnail sizes). Stop forcing
PNG encoding for square images when the source is JPEG — JPEG is smaller
and faster to encode. Square images from JPEG sources went from 52ms to
10ms (80% improvement). Add sync.Pool for encode buffers to reduce GC
pressure under concurrent load.

* perf(artwork): increase cache warmer concurrency from 2 to 4 workers

Resize is CPU-bound, so more workers improve throughput on multi-core
systems. Doubled worker count to better utilize available cores during
background cache warming.

* perf(artwork): switch to xdraw.ApproxBiLinear and always encode as JPEG

Replace disintegration/imaging with golang.org/x/image/draw for image
resizing. This eliminates ~92K allocations per resize (from imaging's
internal goroutine parallelism) down to ~20, reducing GC pressure under
concurrent load.

Always encode resized artwork as JPEG regardless of source format, since
cover art doesn't need transparency. This is ~5x faster than PNG encode
and produces much smaller output (e.g. 18KB JPEG vs 124KB PNG).

* perf(artwork): skip external API call when artist image URL is cached

ArtistImage() was always calling the external agent (Spotify/Last.fm)
to get the image URL, even when the artist already had URLs stored in
the database. This caused every artist image request to block on an
external API call, creating severe serialization when loading artist
grids (5-20 seconds for the first page).

Now use the stored URL directly when available. Artists with no stored
URL still fetch synchronously. Background refresh via UpdateArtistInfo
handles TTL-based URL updates.

* perf(artwork): increase getCoverArt throttle from NumCPU/3 to NumCPU

The previous default of max(2, NumCPU/3) was too aggressive for artist
images which are I/O-bound (downloading from external CDNs), not
CPU-bound. On an 8-core machine this meant only 2 concurrent requests,
causing a staircase pattern where 12 images took ~2.4s wall-clock.

Bumping to max(4, NumCPU) cuts wall-clock time by ~50% for artist image
grids while still preventing unbounded concurrency for CPU-bound resizes.

* perf(artwork): encode resized images as WebP instead of JPEG

Switch from JPEG to WebP encoding for resized artwork using gen2brain/webp
(libwebp via WASM, no CGo). WebP produces ~74% smaller output at the same
quality with only ~25% slower full-pipeline encode time (cached, so only
paid once per artwork+size).

Use NRGBA image type to preserve alpha channel in WebP output, and
transparent padding for square canvas instead of black.

Also removes the disintegration/imaging dependency entirely by replacing
imaging.Fill in playlist tile generation with a custom fillCenter function
using xdraw.ApproxBiLinear.

* perf(artwork): switch from ApproxBiLinear to BiLinear scaling for improved image processing

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor(configuration): rename CoverJpegQuality to CoverArtQuality and update references

Signed-off-by: Deluan <deluan@navidrome.org>

* feat(artwork): add DevJpegCoverArt option to control JPEG encoding for cover art

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(artwork): remove redundant transparent fill and handle encode errors in resizeImage

Removed a no-op draw.Draw call that filled the NRGBA canvas with
transparent pixels — NewNRGBA already zero-initializes to fully
transparent. Also added an early return on encode failure to avoid
allocating and copying potentially corrupt buffer data before returning
the error.

* fix(configuration): reorder default agents (deezer is faster)

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(test): resolve dogsled lint warning in tag extraction benchmark

Use all return values from runtime.Caller instead of discarding three
with blank identifiers, which triggered the dogsled linter.

* fix(artwork): revert cache key format

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(configuration): remove deprecated CoverJpegQuality field and update references to CoverArtQuality

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-13 09:35:59 -04:00
Deluan 0790f66627 fix(scanner): increase watcher channel buffers to prevent dropped filesystem events
When files were moved between libraries, the small channel buffers (size 1)
throughout the watcher pipeline caused backpressure that led to dropped
filesystem events. This meant only some of the affected folders were scanned,
preventing cross-library move detection from working correctly.

Increase all watcher channel buffers to 500 and switch to blocking sends
to ensure no filesystem events are silently dropped.
2026-03-12 17:07:34 -04:00
Deluan Quintão d0fbba14ff fix(db): check both name and target_format in default transcodings migration (#5175)
The ensure_default_transcodings migration only checked target_format
before inserting, but the transcoding table has UNIQUE constraints on
both name and target_format. Older installations may have entries where
the name matches a default (e.g., 'opus audio') but the target_format
differs (e.g., 'oga' instead of 'opus'), causing a UNIQUE constraint
violation on name during the INSERT.

Fixes #5174
2026-03-12 11:39:31 -04:00
Kendall Garner 903e3f070f fix(subsonic): always return required playqueue fields (#5172) 2026-03-12 08:29:37 -04:00
Deluan Quintão 0312eb33f1 fix(ui): improve browser codec detection and limit Safari transcoding to mp3 (#5171)
* fix: update codec MIME types to support multiple variants for better compatibility

Signed-off-by: Deluan <deluan@navidrome.org>

* fix: limit Safari transcoding to mp3

Signed-off-by: Deluan <deluan@navidrome.org>

* style: format browserProfile test file with prettier

* fix: comment

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-12 08:21:49 -04:00
Deluan 5ecbe31a06 fix: implement fallback to DefaultDownsamplingFormat for unknown formats
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-11 09:46:13 -04:00
Deluan Quintão d8bc41fbb1 fix: use ADTS for AAC transcoding, temporarily exclude AAC from transcode decisions (#5167)
* fix: use ADTS format for AAC transcoding to avoid silent output on ffmpeg 8.0+

The fragmented MP4 muxer (`-f ipod -movflags frag_keyframe+empty_moov`)
produces corrupt/silent audio when ffmpeg pipes to stdout, confirmed on
ffmpeg 8.0+. The moof atom offset values are zeroed out in pipe mode,
causing AAC decoder errors. Switch to `-f adts` (raw AAC framing) which
works reliably via pipe and is widely supported by clients including
UPnP/Sonos devices.

* fix: exclude AAC from transcode decision, as it is not working for Sonos.

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-11 09:26:32 -04:00
Deluan 51c48bcacd fix(ui): enforce consistent delete button contrast for delete in AMusic theme
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-10 18:12:57 -04:00