Commit Graph

4709 Commits

Author SHA1 Message Date
dependabot[bot] 4c91936848 chore(deps): bump picomatch in /ui (#5248)
Bumps  and [picomatch](https://github.com/micromatch/picomatch). These dependencies needed to be updated together.

Updates `picomatch` from 2.3.1 to 2.3.2
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

Updates `picomatch` from 4.0.3 to 4.0.4
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
- dependency-name: picomatch
  dependency-version: 4.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-25 18:24:46 -04:00
cafecitopuro 0a0f1779cb feat(ui): add Nutball theme (#4544)
* add nutball theme

* fix album grid outline

* cleanup

* Pagination fix + accent color update

* Fix animation on Activity icon

* style: fix prettier formatting in nutball theme

---------

Co-authored-by: Deluan <deluan@navidrome.org>
2026-03-24 19:39:02 -04:00
Tom Boucher 356b0716b6 fix(scanner): exclude Vorbis VERSION from albumversion tag mapping (#5194)
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>
2026-03-23 18:32:05 -04:00
Kendall Garner 8a19fa9991 fix(server): require additional variable to enable systemd logging (#5222)
* fix(logging): require additional variable to enable systemd logging

* use a better name
2026-03-23 18:09:59 -04:00
dependabot[bot] 221d301c42 chore(deps): bump nick-fields/retry from 3 to 4 in /.github/workflows (#5241)
Bumps [nick-fields/retry](https://github.com/nick-fields/retry) from 3 to 4.
- [Release notes](https://github.com/nick-fields/retry/releases)
- [Commits](https://github.com/nick-fields/retry/compare/v3...v4)

---
updated-dependencies:
- dependency-name: nick-fields/retry
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 14:19:16 -04:00
Deluan 4cca7bce4e test: increase FlakeAttempts for library directory tests and remove flaky job test 2026-03-23 11:59:11 -04:00
Deluan d91b5e8f4d refactor: simplify playlist name extraction using strings.CutPrefix 2026-03-23 11:40:16 -04:00
Deluan 03608d3eef feat(subsonic): add coverArt to internetRadioStation response
Add OpenSubsonic coverArt extension to GetInternetRadios, showing
uploaded radio images for non-legacy clients.

Ref: https://github.com/opensubsonic/open-subsonic-api/pull/224
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-22 15:22:02 -04:00
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