Commit Graph

68 Commits

Author SHA1 Message Date
Deluan Quintão
3158451b8d refactor(server): drop redundant error return from req.Strings parsing (#5812)
* refactor(req): drop redundant error return from Strings

The error from Strings carried no information beyond emptiness — it fired
exactly when the param was absent — and nearly every caller discarded it with
a blank identifier. Strings now just returns the values (empty when absent),
making the common optional-list reads one clean expression.

The few required-param callers (scrobble, createShare) check for emptiness and
return the same Subsonic error code 10 as before; their e2e tests now pin that
code. Ints and Times keep their contracts by synthesizing ErrMissingParam
themselves, so selectedMusicFolderIds is untouched. The jellyfin parseFields
helper is inlined away, since ParseFields(p.Strings("fields")...) now
compiles directly.

* docs(req): clarify Strings returns nil when param is absent
2026-07-18 19:30:04 -04:00
Deluan Quintão
fe6ac2e577 feat(jellyfin): experimental Jellyfin Music API support (#5730)
* feat(sharing): enable sharing by default

Flip the EnableSharing default from false to true so new installations have the sharing feature available out of the box. Users can still disable it via the EnableSharing config option.

The native API only registers the /share route when sharing is enabled, so the nativeapi tests that build the router without wiring a share service now explicitly disable sharing in their setup to avoid registering a route backed by a nil service.

* feat(jellyfin): add config flag and URL path constant

Adds the disabled-by-default Server.Jellyfin config option (Enabled,
ServerName) and consts.URLPathJellyfinAPI, following the existing
LastFM/ListenBrainz patterns. Later tasks will use these to mount the
Jellyfin-compatible API router.

* fix(jellyfin): default Jellyfin ServerName to "Navidrome"

* feat(jellyfin): package skeleton with System handshake endpoints

Adds server/jellyfin: the Router (mirrors server/subsonic and
server/public), its Wire-friendly New(...) constructor, a chi routes()
table, and the ok() JSON response helper. Implements the unauthenticated
handshake surface Jellyfin clients probe first: GET /System/Info/Public,
GET+POST /System/Ping, and GET /QuickConnect/Enabled (quick connect is
unsupported, so it always reports disabled).

The public info payload's Id must be stable across restarts (Jellyfin
clients cache ServerId), so it reuses the get-or-create Property pattern
already used for InsightsID in core/metrics/insights.go: fetch
consts.JellyfinServerIDKey from the Property repository, generating and
persisting a new UUID on first read. A dedicated key (rather than the
existing InsightsID) keeps the anonymous telemetry identifier from being
exposed on this unauthenticated endpoint.

* test(jellyfin): cover ping and quickConnectEnabled handlers

Both were added alongside the System/Info/Public handshake endpoint but
had no direct test coverage.

* fix(jellyfin): memoize stable server Id and cover get-or-create path

* feat(jellyfin): wire and mount the router behind Jellyfin.Enabled

Add jellyfin.New to the shared Wire provider set and a
CreateJellyfinAPIRouter injector, then mount the router at /jellyfin
in startServer(), gated by conf.Server.Jellyfin.Enabled, mirroring the
existing LastFM/ListenBrainz router mounts.

* feat(jellyfin): add Jellyfin DTOs and model mappers

Adds BaseItemDto, QueryResult, UserItemDataDto, UserDto,
AuthenticationResult, MediaSourceInfo, PlaybackInfoResponse,
NameGuidPair and SessionInfo DTOs to server/jellyfin/dto, plus
model-to-DTO mappers for songs, albums, artists and genres that
later browse/stream/write endpoints will consume.

* test(jellyfin): cover GenreToBaseItem mapper

* feat(jellyfin): advertise Jellyfin-compatible version, brand ServerName with Navidrome version

* fix(jellyfin): map LastPlayedDate and omit zero track/disc numbers

* feat(jellyfin): authentication middleware and AuthenticateByName login

* fix(jellyfin): reject empty login password, wire ServerName, add negative auth tests

* refactor(jellyfin): use new(x) builtin instead of intPtr helper

* feat(jellyfin): user views and current-user endpoints

* feat(jellyfin): Items query engine, item detail, and latest

Adds the /Items universal query endpoint, dispatching by IncludeItemTypes
over albums/artists/songs/genres with ParentId, SearchTerm, Filters=IsFavorite,
SortBy/SortOrder and StartIndex/Limit support, plus GET /Items/{itemId} and
/Users/{userId}/Items/Latest. Reuses server/subsonic/filter builders (by
artist/album/starred) instead of hand-rolled squirrel filters, and resolves
sort keys per item type since each repository maps sort names to different
real/aliased columns.

Also adds the missing CountAll to tests.MockArtistRepo, exposed by this task
(the mock embedded a nil ArtistRepository for it and would panic on use).

* refactor(server): extract shared query filter builders to server/filter

Moves server/subsonic/filter to server/filter so server/jellyfin can use
the shared query-option builders without importing server/subsonic,
enforcing the rule that no API package imports another API's package.

* feat(jellyfin): full multi-library support with per-user access scoping

Replace the single hardcoded "music" UserView with one CollectionFolder
view per library the user can access, and scope every /Items browse
query (albums, songs, artists, /Latest) to those libraries via
server/filter's ApplyLibraryFilter/ApplyArtistLibraryFilter.

ParentId is now disambiguated: a numeric value the user has access to is
treated as a library scope (browsing a UserView), otherwise it falls
through as an entity id (artist/album), which safely matches nothing
rather than leaking another library's content. getItem now 404s when
fetching an album or song outside the user's accessible libraries;
artists are skipped (they can span multiple libraries) with a TODO.

Genres remain unscoped since they're global tags, not per-library
entities.

* test(jellyfin): cover admin library-access path and drop nil test contexts

* feat(jellyfin): Artists and Genres endpoints

Add /Artists, /Artists/AlbumArtists, /Genres and /MusicGenres. Artists
listing is library-scoped (defaulting to the user's accessible
libraries, narrowed by an accessible ParentId), delegating access
control to listArtists/ApplyArtistLibraryFilter. Genres are global and
unscoped, matching the ML decision already made for listGenres.

* refactor(jellyfin): share resolveLibraryScope between items and artists

* feat(jellyfin): item image endpoint via artwork service

* fix(jellyfin): let net/http sniff image Content-Type instead of forcing jpeg

* feat(jellyfin): audio streaming and PlaybackInfo with library access control

* feat(jellyfin): favorites and rating write-back with library access control

Adds POST/DELETE handlers for /Users/{userId}/FavoriteItems/{itemId} and
/Users/{userId}/Items/{itemId}/Rating. A shared resolveAnnotated helper
probes album/artist/media file (mirroring getItem's order) and 404s before
writing if the user lacks access to the album/song's library; artists are
exempt since they span multiple libraries. Ratings are halved coming in
(Jellyfin 0-10 -> Navidrome 0-5) to match the doubling in dto.UserData.

Also teaches MockMediaFileRepo and MockArtistRepo's SetStar/SetRating (and
MockAlbumRepo's, previously a no-op) to actually mutate the backing data so
write-back can be asserted in tests.

* feat(jellyfin): playback reporting and scrobbling

Add /Sessions/Playing[/Progress|/Stopped] and /Sessions/Capabilities[/Full]
handlers, backed by core/scrobbler.PlayTracker. A new withPlayer middleware
resolves/registers a model.Player from the Emby DeviceId header (used
directly as the stable player id, unlike Subsonic's cookie fallback) and
injects it into the request context for scrobbling.

The Stopped report calls ReportPlayback with IgnoreScrobble to end the
now-playing session without double-counting, then calls Submit as the
single source of the play-count increment and external scrobble.

* feat(jellyfin): playlist read and write-back

Adds POST /Playlists, GET/POST/DELETE /Playlists/{id}/Items. Tags each
playlist item with PlaylistItemId (the entry's position within the
playlist) so DELETE .../Items?EntryIds=... can remove a specific
occurrence by the id core/playlists.RemoveTracks actually expects,
rather than the song id used everywhere else.

* test(jellyfin): cover empty Ids/EntryIds in playlist add/remove

* feat(jellyfin): unknown-route logging, generic 500s, rating clamp, docs

Hardening pass ahead of real client testing: unmatched routes and
unsupported methods now return a logged, JSON 404 instead of chi's
default plain-text response, so a missing endpoint a client needs is
easy to spot in the logs. Internal errors (ffmpeg output, file paths,
etc.) no longer leak into 500 response bodies -- a shared
internalError helper logs the real error server-side and always
returns a generic message. Inbound Jellyfin ratings are clamped to
0-10 before being halved into Navidrome's 0-5 scale, and /System/Ping
now replies with a bare plain-text body as real Jellyfin servers do.
Adds a README with an enable/curl walkthrough and known limitations.

* docs(jellyfin): clarify public image endpoint and accessibleLibraryIDs comments

* fix(jellyfin): case-insensitive path routing for Jellyfin client compatibility

* refactor(server): extract case-insensitive path routing to a shared helper

* fix(jellyfin): return User Policy and Configuration so Finamp completes login

* fix(jellyfin): support Playlist type, multi-type Items queries, and PlayCount/DatePlayed sort

Real Finamp requests break against three /Items query engine bugs: Playlist
requests fell through to albums, multi-type IncludeItemTypes (e.g. Finamp's
favorites screen) only returned the first requested type, and comma-separated
SortBy lists (e.g. "DateCreated,SortName") were matched as one opaque string
so they always fell through to the repo default.

Also extends tests/mock_playlist_repo.go with SetData/GetAll so playlist
listing is testable like the other mock repos.

* feat(jellyfin): implement /socket WebSocket for real-time client sessions

* fix(jellyfin): resolve library-view ids in getItem so clients can load the library

* fix(jellyfin): serve direct file at /Items/{id}/File and accept ApiKey query param

* fix(jellyfin): resolve playlist ids in getItem

* fix(jellyfin): read lowercase ids/entryIds playlist params and add playlist-users endpoints

* fix(jellyfin): include MediaSources with Size/Bitrate on tracks so clients show download size

* fix(jellyfin): populate all required MediaSourceInfo bool/array fields to match Jellyfin

* fix(jellyfin): hex-encode item ids at the API boundary for Jellyfin client compatibility

Finamp (and presumably other clients) parses ids as radix-16, but Navidrome's
base62 nanoids aren't valid hex and crash its queue packing. Hex-encode every
id emitted by the Jellyfin API and decode every id received, keeping the
transform reversible and stateless at the boundary rather than touching any
model id.

* fix(jellyfin): populate MediaStreams with the audio stream so clients can size/transcode

* fix(jellyfin): support Ids batch-fetch in /Items so clients can fetch items by id

* fix(jellyfin): do not disable Jellyfin server when disabling external services

* fix(jellyfin): emit per-image ImageBlurHashes so clients de-dupe images and stop warning

* fix(jellyfin): support playlist cover upload/delete and display via item image endpoints

* feat(jellyfin): implement GET /Playlists/{id} for playlist visibility

Finamp's playlist edit screen calls GET /Playlists/{id} to read the
OpenAccess (public visibility) flag; we only had the sub-routes, so it
404'd and the edit screen failed to load. Return the Jellyfin PlaylistDto
shape (OpenAccess from Public, empty Shares, media item ids).

* fix(jellyfin): display playlist cover art

Uploaded playlist covers never showed in clients for two reasons: the
playlist BaseItemDto advertised no Primary ImageTag (so clients didn't
know to fetch a cover), and the public image endpoint resolved artwork
under the request's anonymous context, so a private playlist failed its
visibility filter and fell back to the placeholder. Advertise the Primary
image tag/blurhash on playlists, and resolve artwork under an elevated
context (as core/artwork's cache warmer does).

* fix(jellyfin): expand album/artist/playlist ids when building playlists

Jellyfin clients (Finamp) send container ids — an album, artist or
playlist — in a playlist's Ids list and expect the server to expand each
into its child tracks. core/playlists only understands media file ids, so
creating or adding with an album id silently produced an empty playlist.
Expand container ids to their tracks (in order) before create/add; bare
song ids still pass through. Adds filter.SongsByArtistID.

* fix(jellyfin): support playlist deletion via DELETE /Items/{id}

Finamp deletes a playlist with DELETE /Items/{id}, which we didn't route,
so deletion silently failed. Implement it via core/playlists.Delete (which
enforces ownership and removes the cover file). Only playlists are
deletable through this API; non-playlist ids return 404, non-owners 403.

* test(jellyfin): add e2e suite harness + smoke tests

* test(jellyfin): e2e for system, auth, routing, browsing, annotations

* test(jellyfin): e2e for playlists (CRUD, expansion, cover) and item images

* test(jellyfin): e2e for streaming, sessions, and multi-user access control

* fix(jellyfin): artist search 500 (library filter leaked into FTS query)

getArtists/listArtists reused the browse-path filters (notMissing +
ApplyArtistLibraryFilter) for the search path, but artist Search expects a
sole Eq{library_id} filter it can consume as a scope — artists have no
library_id column, so the compound/join filter leaked into the FTS query
and 500'd. Every Finamp artist search failed (the filter is applied even
unscoped, since admins resolve to all library ids). Build search filters
separately. Adds e2e search coverage.

* feat(jellyfin): implement POST /Playlists/{id} to update name, visibility, tracks

Finamp edits a playlist (make public, rename, reorder) via POST
/Playlists/{id}, which we didn't route, so every edit 404'd. Implement it:
Ids present -> replace track list (Create with existing id, preserving
name); otherwise update Name/IsPublic via core/playlists.Update. Adds a
shared playlistError helper (403/404/500) reused by deleteItem, plus e2e
coverage.

* docs(jellyfin): update README for playlists, images, id encoding, and e2e

* fix(jellyfin): default album track listing to track order

Browsing an album's tracks (Items?ParentId=<albumId>&IncludeItemTypes=Audio)
took only SongsByAlbum's filters and dropped its Sort, so tracks came back
in arbitrary order. Default opts.Sort to the album (disc+track) order when
browsing an album without an explicit SortBy — matching Subsonic's GetAlbum
and real Jellyfin. An explicit SortBy still wins.

* fix(jellyfin): filter items by AlbumArtistIds/ArtistIds

An artist's page in Finamp sends ParentId=<libraryId> (scoping) plus
AlbumArtistIds/ArtistIds/contributingArtistIds for the artist itself, but
queryItems only honored ParentId, so an artist's albums and tracks came
back unfiltered (every artist's content). Parse the artist-id params and
apply AlbumsByArtistID (albums) / SongsByArtistID (tracks). Adds e2e
coverage.

* fix(jellyfin): only count a play past the scrobble threshold

reportPlaybackStopped set IgnoreScrobble and force-submitted a play on
every Stopped report, so a briefly-played track (e.g. an immediate skip)
was marked played. Finamp sends Stopped on every track switch, so the
threshold must be applied server-side. Let ReportPlayback's StateStopped
logic decide (play + scrobble only past 50% of the track / 4-minute cap)
instead. Subsonic differs because there the client gates submission.

* fix(jellyfin): sort album tracks by track number when client sends IndexNumber SortBy

Finamp's album view requests SortBy=ParentIndexNumber,IndexNumber,SortName
(disc, track, name), but applySort didn't recognize ParentIndexNumber or
IndexNumber and fell through to SortName, sorting tracks alphabetically by
title. Map both keys to the album (disc+track) sort. The reversed-title
fixture lets the e2e tell track order from title order.

* fix(jellyfin): honor the isFavorite query param for favorites filtering

Finamp's artist 'Favourite tracks' widget requests favorites via the
standalone isFavorite=true query param, not Filters=IsFavorite, so the
filter was ignored and non-favorited tracks were returned. Detect both
forms. (The widget's reshuffling is Finamp's explicit SortBy=Random.)

* feat(jellyfin): emit DateCreated (Date Added) on items

BaseItemDto had no DateCreated, so clients showed 'No Date Added' and had
nothing to sort 'Recently Added' by. Emit it as ISO 8601 from each entity's
CreatedAt (the same field the recently_added sort uses) for songs, albums
and artists.

* fix(jellyfin): set ArtistItems/AlbumArtists on songs (Now Playing artist)

SongToBaseItem only set Artists (names) and AlbumArtist, not the structured
ArtistItems/AlbumArtists. Finamp's Now Playing screen reads ArtistItems and
shows 'Unknown Artist' when it's absent. Populate both (track artist and
album artist) as name+id pairs, mirroring AlbumToBaseItem.

* docs(jellyfin): note synthetic blurhash as a follow-up

Document that ImageBlurHashes are derived from the item id (a solid-color
placeholder), not computed from the cover art like real Jellyfin, and
outline what a proper implementation would take.

* docs(jellyfin): note WebSocket events and favourited playlists as follow-ups

* fix(jellyfin): filter the Artists page by role (album artist vs performer)

/Artists and /Artists/AlbumArtists both called the same role-agnostic
handler, so Navidrome's per-role artist entries (composers, arrangers,
performers) all showed as Album Artists, and the two tabs were identical.
Filter /Artists/AlbumArtists to RoleAlbumArtist and /Artists to RoleArtist
(and the MusicArtist browse to album artists), via filter.ArtistsByRole.
Verified live: Beatles Singles' album-artists dropped 138->5, composers
excluded, performers (Billy Preston) show only under /Artists.

* fix(jellyfin): sort playlists by name (missing Playlist sort mapping)

applySort had no sortColumnsByType entry for the Playlist type, so
SortBy=SortName was ignored and the Playlists screen showed them in the
repo's default order. Map SortName/Name -> name (as Subsonic does) and
DateCreated -> created_at. Verified live: case-insensitive alphabetical.

* fix(jellyfin): read query params case-insensitively (Jellify support)

Jellify (and the official Jellyfin TypeScript SDK) send query params in
camelCase (parentId, albumArtistIds, artistIds, includeItemTypes), where
Finamp sends PascalCase. Our handlers read fixed-case keys, so every
Jellify filter/sort/paging param was silently dropped:

- an artist's page listed albums and tracks from all artists
- opening an album listed every album instead of its tracks

Real Jellyfin binds query params case-insensitively (ASP.NET model
binding), so add a normalizeQueryKeys middleware that folds every query
key to lowercase once, and read params by their lowercase name. This
also removes the scattered dual-case hacks (Ids/ids, IsFavorite,
ContributingArtistIds, api_key/ApiKey, queryParam) that let this bug
class through.

Also infer the child type from an album parent: Jellify browses an album
with only parentId (no IncludeItemTypes), and Jellyfin infers Audio from
the parent; without it we fell back to listing all albums.

Verified live against Finamp+Jellify and with 4 new e2e specs replaying
Jellify's exact request shapes.

* fix(jellyfin): advertise LocalAddress in the public system info handshake

Jellify (and other @jellyfin/sdk clients) that connect by raw address fall
back to HTTP when TLS isn't available, then adopt the handshake's
LocalAddress as their server base URL. We never populated it, so Jellify's
SDK `api` object was undefined and sign-in crashed with "Cannot read
property 'configuration' of undefined" (getUserApi(api!) with an undefined
api). Over an HTTPS connection Jellify uses connectionType=hostname and
never reads LocalAddress, which is why it worked while an HTTPS proxy was
in front.

Populate LocalAddress from the request — scheme + host (honoring
X-Forwarded-*), plus the /jellyfin mount path — matching real Jellyfin,
which always sends it. Export server.ServerAddress for the resolution.

* fix(jellyfin): separate "Featured On" from an artist's own discography

Jellify's artist page fetches the discography via albumArtistIds and the
"Featured On" section via contributingArtistIds, relying on the server to
return disjoint sets. We collapsed albumArtistIds/artistIds/
contributingArtistIds into one AlbumsByArtistID filter, so an artist's own
albums appeared in both sections.

Add AlbumsByContributingArtistID — albums where the artist is a track
artist but NOT the album artist — matching Jellyfin's ContributingArtistIds
(in Artists, not in AlbumArtists), and route contributingArtistIds to it.

* fix(jellyfin): accept a bare Authorization token for audio streaming

Jellify's native player (react-native-nitro-player) authenticates the audio
stream by setting a bare Authorization header carrying the raw access token
({ AUTHORIZATION: api.accessToken }), not the "MediaBrowser ... Token=" scheme
parseEmbyAuth understands. tokenFromRequest didn't recognize it, so every
/Audio/{id}/stream request from the native player 401'd and no audio played
(the JS client still optimistically posted playback progress, masking it).

Accept a bare (or "Bearer <token>") Authorization header, as real Jellyfin
does. Verified live: the native player's exact request now streams 200.

* fix(jellyfin): embed a self-authenticating stream URL in PlaybackInfo

Jellify's native audio player (react-native-nitro-player / ExoPlayer) fetches
the stream without forwarding any auth — captured request headers were only
Connection/Icy-Metadata/Accept-Encoding/User-Agent, no Authorization and no
api_key — so every /Audio/{id}/stream request 401'd and nothing played (the
JS client still optimistically posted progress, masking it).

Real Jellyfin returns stream URLs with the token embedded; we returned none,
so Jellify fell back to a token-less DirectPlay URL. Populate
MediaSources[0].TranscodingUrl with /Audio/{id}/universal?api_key=<caller
token>, which Jellify's player uses verbatim. Direct-play clients (Finamp
builds its own /Items/{id}/File?ApiKey URL) ignore the field, so they're
unaffected.

* feat(jellyfin): serve per-item UserData (GET /UserItems/{id}/UserData)

Jellify fetches this per item to render played/favourite indicators; we 404'd
it. Resolve the item via the existing getItem resolver (which loads the
caller's annotations and enforces the same library-access gate) and return its
UserData, falling back to an empty-but-valid object for items without
annotations (e.g. playlists). Also registers the legacy
/Users/{userId}/Items/{itemId}/UserData spelling.

* refactor(jellyfin): drop unused bare-Authorization token support

Added mid-troubleshooting on the theory that Jellify's native player sends a
bare Authorization header, but the debug capture showed it sends no auth header
at all — the real fix was embedding api_key in PlaybackInfo's TranscodingUrl.
No client reaches this path (Jellify JS uses the MediaBrowser scheme, Finamp
uses X-Emby-Token, the native player uses the api_key URL), so remove it and
its tests per YAGNI. Effectively reverts 4f8ac1e0.

* feat(jellyfin): implement Similar endpoints via the external provider

Add GET /Artists/{id}/Similar (related artists) and GET /Items/{id}/Similar
(similar songs for a track, similar albums for an album, related artists for an
artist), sourced from the same external.Provider (Last.fm etc.) that powers
Subsonic's getArtistInfo2/getSimilarSongs. Only library-present artists are
returned so each is navigable; provider errors and unknown ids degrade to an
empty 200 result, so clients (Jellify) stop hammering these with 404 retries.

Injects external.Provider into the Router (regenerated via make wire).

* perf(jellyfin): make Similar endpoints non-blocking (bounded quick wait)

Each /Similar request fetched from Last.fm synchronously (500ms-1.4s), and
Jellify requests Similar for many items at once on the home/artist screens, so
the whole screen stalled — a home pull-to-refresh dragged for seconds.

Run the external lookup on a background context and return within a 500ms quick
wait: a cached artist resolves instantly, a cold one returns empty now while the
lookup finishes caching in the background, so a later load is fast and
populated. Verified live: cold calls bounded at ~500ms (was up to 1.4s), warm
calls 3-8ms.

* fix(jellyfin): answer ManualPlaylistsFolder so the home stops stalling

Jellify resolves its "playlists library" via IncludeItemTypes=
ManualPlaylistsFolder, then lists playlists with ParentId set to that folder's
id. We didn't recognize the type, so parseTypes fell back to MusicAlbum and
returned the album list. Jellify's query then found no item with
CollectionType=playlists and resolved undefined — which React Query rejects,
retrying it in a backoff loop that stalled the home pull-to-refresh for ~5s
(every response was fast server-side; the delay was the client's retries).

Return a synthetic "playlists" folder (CollectionType=playlists) for the
ManualPlaylistsFolder query, resolve ParentId=<that folder> to the user's
playlists, and give playlists a Path under "data" (Jellify drops playlists
whose Path lacks it). Adds a Path field to BaseItemDto.

* docs(jellyfin): note lyrics and InstantMix/sonic-similarity as follow-ups

* fix(jellyfin): genre paging params and same-key casing collisions

getGenres read StartIndex/Limit in PascalCase, which normalizeQueryKeys had
already folded to lowercase, so genre paging was silently ignored; totals now
come from the full (small) genre list instead of the page length.
normalizeQueryKeys now merges values when two casings of a key collide,
instead of nondeterministically keeping one.

* fix(jellyfin): round ratings to the nearest star instead of truncating

Rating is a nullable double 0-10 in Jellyfin's contract. Truncating integer
division stored 9 as 4 stars, and both Rating=1 and fractional values (which
failed integer parsing) became 0 — silently deleting the rating. Parse as
float, round, and floor nonzero input at one star.

* fix(jellyfin): apply Name/IsPublic sent together with a track replacement

Jellyfin's UpdatePlaylist applies every provided field, but the Ids branch
returned early, silently discarding a rename or visibility change sent in the
same body (core/playlists.Create with an existing id ignores the name).

* fix(jellyfin): don't rotate the stored server id on transient DB errors

serverID treated any Property.Get error as "no id yet" and persisted a fresh
UUID over JellyfinServerID, with sync.Once pinning it for the process lifetime
— a busy DB or canceled request context on the first request would break every
client's cached ServerId. Only ErrNotFound mints a new id now, and failures
yield an uncached temporary value so the next request retries.

* fix(jellyfin): report real search totals instead of page length or unfiltered counts

Artist search returned TotalRecordCount = len(page), so clients stopped after
the first page; album/song search counted via CountAll, which can't see the
search term, so clients paged through phantom results. The repos' Search API
has no match count, so fetch one row beyond the page: offset+len is exact on
the last page and a strictly growing lower bound before it — paging clients
terminate exactly at the last match.

* fix(jellyfin): browse playlists via the generic /Items path and resolve the playlists folder by id

A typeless /Items?ParentId=<playlistId> (legal in real Jellyfin, used by
generic clients) fell through to the MusicAlbum default and returned an empty
list; it now returns the playlist's tracks, paginated, with visibility
enforced by GetWithTracks. resolveItemByID also answers the synthetic
playlists-folder id the server itself advertises instead of 404ing it.

* perf(jellyfin): cap per-type queries in multi-type /Items requests

The multi-type merge path queried each type with a zero-value QueryOptions —
no LIMIT in SQL — materializing every matching row (each with embedded
MediaSources) just to slice out one page in memory. Each type now fetches at
most StartIndex+Limit rows, the worst case one type can contribute to the
merged window; totals still come from CountAll.

* fix(jellyfin): dedupe and bound the background Similar fetches

awaitSimilar spawned a detached, deadline-free goroutine per request; clients
re-polling after the empty quick-wait response piled up duplicate provider
chains (no singleflight anywhere below) racing writes on the same artist row.
Identical in-flight requests now share one fetch — keyed per user, since the
mapped items embed the user's annotations — and the background context gets a
one-minute deadline so a hung provider can't hold goroutines forever.

* fix(jellyfin): gate private playlist covers on the public image route

The unauthenticated image endpoint elevated every request to an admin context,
so anyone who knew or guessed a playlist id could fetch another user's private
uploaded cover. Library artwork still resolves elevated (Jellyfin clients fetch
images without auth headers), but playlist covers are now served only when the
playlist is public or the request's optional token identifies its owner or an
admin; everyone else gets the placeholder.

* fix(log): redact full api_key values, including JWTs

The api_key pattern matched only word characters, stopping at a JWT's first
'.' — the Jellyfin API embeds the session JWT as api_key in TranscodingUrl, so
request logs kept its payload and signature, enough to reconstruct a replayable
token by prepending the constant header. Match to the next query separator
instead, like the sibling s=/p=/jwt= patterns.

* fix(jellyfin): record LastLoginAt on Jellyfin logins

authenticateByName re-implements credential validation and skipped the
UpdateLastLoginAt call the web UI's validateLogin makes, so users who only log
in via Jellyfin clients showed a never/stale Last Login in the admin UI.

* perf(jellyfin): batch song resolution in /Items?ids= and playlist expansion

Both paths probed up to four repositories per client-supplied id. Songs — the
common case — now resolve via chunked media_file.id IN queries (same pattern
as playqueue's loadTracks); only the residue pays the container probes.

* fix(jellyfin): wait for the real Similar result instead of answering a cacheable empty list

The 500ms quick wait returned an empty 200 for any cold lookup —
indistinguishable from "no similar items exist", so clients cached the wrong
answer until an app restart. With fetches deduplicated, wait up to the agents'
HTTP timeout for the actual result; only a hung provider now yields the empty
fallback, and its fetch still warms the cache in the background. Also makes
the dedup test deterministic (the old one raced its release channel).

* refactor(tests): extract the shared e2e harness into tests/harness

The Subsonic and Jellyfin e2e suites each carried their own copy of the
golden-DB lifecycle (boot, seed users/library, scan, WAL snapshot), the
ATTACH-DATABASE restore, fixture-FS registration, and the SpyStreamer /
NoopFFmpeg doubles. Those now live in one importable package (same pattern as
core/storage/storagetest); fixture libraries and request helpers stay
per-suite since they encode each API's test expectations.

* docs(jellyfin): make comments concise

Compress the narrative comments accumulated during live client testing into
short why-only notes; keep the client quirks, security rationales and gotchas,
drop the exposition. Comments-only change (net -141 lines).

* fix(tests): silence gosec taint false-positive in harness snapshot write

The write moved from a _test.go file (which gosec skips) into the importable
harness package; the path derives from GinkgoT().TempDir().

* fix(jellyfin): route the current /UserFavoriteItems favorite endpoint

@jellyfin/sdk 0.13.0 (used by Jellify) posts favorites to
POST/DELETE /UserFavoriteItems/{itemId}, while we only routed the legacy
/Users/{userId}/FavoriteItems/{itemId} (Finamp). Jellify favorites 404'd
("Failed to add favourite"). Route both spellings to the same handlers.

* feat(jellyfin): honor Fields on /Items and add missing conformance fields

Match real Jellyfin's response shape: gate MediaSources (and MediaStreams)
behind Fields=MediaSources instead of always embedding them — clients that
need Size request it, as Finamp does — which cuts a 46-track artist response
from ~87KB to a fraction. Also emit the always-present fields Jellyfin sets:
ServerId (stamped centrally in ok), LocationType, HasLyrics, and SortName
(when Fields=SortName). PlaybackInfo still carries MediaSources (its purpose).

* fix(jellyfin): address code review security and correctness findings

Applies the reviewed findings from PR #5730:

- Gate similar songs/albums on the caller's library access, so the
  external provider can't surface metadata from libraries the user
  cannot see. Also clamp the client-supplied limit before it sizes any
  allocation or provider fetch (CodeQL user-controlled allocation).
- Prepend the /jellyfin mount prefix to the PlaybackInfo TranscodingUrl,
  so a client resolving it as an absolute host path still reaches the
  mounted router.
- Recognize WebP and GIF magic numbers on raw cover uploads, matching
  the formats Navidrome already supports.
- Bound the playlist cover upload: honor EnableArtworkUpload for
  non-admins and cap the body with MaxBytesReader/MaxImageUploadSize,
  mirroring the native image endpoint.
- Propagate non-not-found repository errors from resolveAnnotated as 500
  instead of silently returning 404.
- Normalize the literal prefix of mixed literal.param path segments
  (e.g. STREAM.mp3) so case-insensitive routing reaches stream.{container}.
- Rate-limit POST /Users/AuthenticateByName with the same per-IP limiter
  as /auth/login when AuthRequestLimit is set.
- Guard against a nil user in authenticateByName.

* fix(jellyfin): correct playlist track browsing and multi-id edits

Fixes three playlist issues, two found testing against Jellify and one
from the PR review:

- Resolve a playlist ParentId to its tracks even when the client sends
  IncludeItemTypes=Audio. Jellify opens a playlist with
  ParentId=<playlist>&IncludeItemTypes=Audio; the id was routed through
  listSongs as an album id, returning an empty list.
- Read repeated id query params (ids=X&ids=Y), not just the first value.
  Jellify's @jellyfin/sdk serializes id arrays as repeated params, so
  adding an album (which it expands client-side into many ids) only
  added the first track. Applies to both add and remove; the
  comma-separated form other clients use still works.
- Let an explicit empty Ids array clear a playlist. Ids is now a pointer
  so an omitted field still means 'leave unchanged', while an empty list
  clears the tracks (via RemoveTracks, since the repository skips track
  writes for an empty list).

* fix(jellyfin): register clients as players on any authenticated request

Jellyfin clients did not appear in the players list. Unlike Subsonic,
whose getPlayer middleware runs on every authenticated endpoint, the
Jellyfin router only registered a player on the /Sessions/Playing
reports, so browsing or streaming never created one.

Apply withPlayer to the whole authenticated group, mirroring Subsonic,
so the calling device registers (and scrobbling has a player) as soon as
it makes any authenticated request. Two follow-ups found while testing:

- Skip registration when the request carries no client/device info (no
  X-Emby-Authorization, e.g. the /socket handshake that auths via
  ?api_key= only), which otherwise created a junk player named ' []'.
- URL-decode the X-Emby-Authorization field values. Jellify's
  @jellyfin/sdk percent-encodes them (Device='Pixel%208%20Pro') while
  Finamp sends them raw, so the player name showed as
  'Jellify [Pixel%208%20Pro]'.

* refactor(jellyfin): move case-insensitive routing into the jellyfin package

The case-insensitive path normalization lived in the server package but
was only ever used by the Jellyfin router (its whole purpose is that
Jellyfin clients route case-insensitively while chi does not). Move it
into server/jellyfin and unexport it, so it sits with its only caller
and no longer needs to be exported across a package boundary.

* docs(jellyfin): document player registration, playlist and image behavior

Updates the package README for the behavior added/fixed this round:

- New 'Players and sessions' section: any authenticated request now
  registers the device as a player (like Subsonic), with the
  Client [Device] naming, URL-decoding of the Emby auth fields, and the
  /socket skip that avoids a nameless player.
- Authentication: note AuthenticateByName is rate-limited per IP.
- Playlists: repeated vs comma-separated id params, and that an explicit
  empty Ids clears the playlist while an omitted Ids leaves it untouched.
- Cover art: WebP/GIF magic-number detection plus the MaxImageUploadSize
  and EnableArtworkUpload gates.
- Endpoints table: add the /Similar and /UserFavoriteItems /
  /UserItems/.../UserData routes that were already served but unlisted.

* feat(jellyfin): expose configured users on the login user-picker

Adds Jellyfin.ExposedPublicUsers, a comma-separated allowlist of
usernames that GET /Users/Public advertises so Jellyfin clients (Finamp,
Jellify) can show a login user-picker instead of a blank username field.

The endpoint is unauthenticated, so it defaults to exposing no users and
never lists the full user table: only the admin-configured names are
returned, resolved live per request (a name that doesn't exist is skipped
and logged). Each entry is a minimal DTO (Name, Id) with no
Policy/Configuration, so admin status isn't leaked pre-login, and no
avatar since Navidrome has no per-user profile images.

* style(jellyfin): trim redundant comments

Remove comments that restated the code or duplicated an explanation
already given nearby, keeping the ones that capture non-obvious rationale
(client-specific quirks, gotchas). Comment-only change; no behavior
difference.

* perf(db): keep query planner statistics trustworthy with full ANALYZE

PRAGMA optimize's internal ANALYZE runs with a limited analysis budget
(~2000 rows) that writes wrong sqlite_stat1 entries for low-cardinality
indexes: on a 96K-track library it claimed (missing, library_id) narrows
to ~2000 rows when it matches the whole table. The planner then prefers
that index over the sort index and falls back to a full-table temp
B-tree sort per request, turning paginated song listings into
multi-second queries (reproduced at 5.5s on real hardware; ~90x slower
than with correct stats). Every index-creating migration re-triggered
the poisoning via the post-migration optimize, and the daily optimizer
could re-trigger it on large library changes. Setting analysis_limit on
the connection does not help: optimize ignores it.

Run a plain full ANALYZE instead: after migrations with schema changes,
and in db.Optimize (daily schedule and scan-end). Stats are stored in
the database file, so one connection suffices and the per-connection
pool loop is gone. The Optimize call at shutdown is removed: stats are
maintained at migration/scan/daily points, and an ANALYZE during
shutdown only delays it and races container stop timeouts.

* perf(db): add covering index for title-sorted song listings

Deep pagination over songs sorted by title (WHERE missing/library_id,
ORDER BY order_title LIMIT/OFFSET - the shape Jellyfin clients use to
enumerate the library, and non-admin native/Subsonic song lists share)
walked media_file_order_title and fetched the table row for every
skipped entry just to evaluate the filter and the annotation/bookmark
join keys: offset+limit random reads, seconds per page on cold spinning
disks.

The new (missing, library_id, order_title, id) index makes the offset
skip fully index-resident: filter columns and the join key come from the
index, and only the emitted page touches table rows. Measured on a
96K-track library: offset 50000 drops from ~6.5s (poisoned stats) /
~100ms (good stats, warm) to 24ms, and cold deep pages on NAS hardware
from ~7s to ~0.5s.

* feat(jellyfin): support transcoding via HLS playlist and server-forced player format

- withPlayer now propagates the player's configured transcoding into the
  request context (like Subsonic's getPlayer), so a format forced in
  Settings > Players applies to the Jellyfin stream endpoints
- new GET /Audio/{itemId}/main.m3u8, the endpoint Finamp plays through
  when its transcoding setting is enabled: a single-segment HLS VOD
  playlist pointing at the existing progressive transcode endpoint
- streamAudio: treat audioBitRate as bits/sec (Jellyfin convention) and
  fall back to audioCodec as target format when no container is given

* fix(jellyfin): honor GenreIds when browsing genre albums and tracks

Finamp's genre screen sends ParentId=<libraryId> plus GenreIds=<genreId>,
but /Items ignored the param, so every genre returned the whole library.

- filter.ByGenreID delegates to persistence.TagIDFilter (exported, was
  tagIDFilter), the same mechanism behind the native API's genre_id filter
- id lists are read via queryIDs, covering both spellings clients use
  (comma-separated and repeated params); /Items?ids= gains the repeated
  form too

* feat(jellyfin): filter album artists by GenreIds

Finamp's artist tab sends GenreIds to /Artists/AlbumArtists when a genre
filter is active; the param was ignored, returning all artists.

filter.ArtistsByGenreID matches artists credited as album artist on an
album with the genre, via a non-correlated semi-join over album
participants (86ms on a 29K-artist library; the correlated EXISTS form
takes 11 minutes). Applied on the browse path of /Artists* and
/Items?IncludeItemTypes=MusicArtist.

The performers variant (/Artists?GenreIds=) still ignores the filter: it
needs the same semi-join against media_file.participants, unmeasured on
large libraries.

* fix(jellyfin): version playlist image tag so clients refresh uploaded covers

Uploads were stored and served correctly, but Finamp kept showing the old
cover: it caches covers keyed by blurHash (its imageId is the item id,
which never changes), and both our image tag and synthetic blurhash were
derived from the playlist id alone.

The tag is now <id>-<UpdatedAt millis hex> (SetImage/RemoveImage go
through a full Put, which bumps UpdatedAt), and the blurhash derives from
the tag, so every cover change rotates both. UpdatedAt over-invalidates
(any playlist edit busts the cover cache), which only costs a refetch;
hashing the actual image remains the proper long-term tag.

An e2e test guards the whole chain, since a partial Put(pls, cols...)
would silently stop bumping UpdatedAt.

* fix(jellyfin): align cover upload limit and validation with the native endpoint

Cover uploads of large photos failed with a silent 400: the
MaxImageUploadSize cap was applied to the wire body, which Jellyfin
clients base64-encode (4/3 inflation), so the effective raw-image limit
was only ~7.5MB — and Finamp uploads picked photos uncompressed.

Align with the native endpoint:
- the limit caps the decoded image; the read cap allows for base64
  inflation
- validate by decoding (image.DecodeConfig) and take the storage
  extension from the real format instead of the Content-Type header,
  which clients get wrong (Finamp falls back to image/jpeg); a HEIC or
  corrupt file is now rejected instead of stored as a broken cover
- rejected uploads log the reason (size/limit/decode error); diagnosing
  this from production logs previously required guesswork

* fix(jellyfin): optimize SongsByArtistID filter to improve performance at library scale

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

* fix(jellyfin): sort tracks by release year for SortBy=PremiereDate

Finamp's "Latest Releases" artist section sends
SortBy=PremiereDate,Album,... descending; PremiereDate wasn't in the
Audio sort map, so applySort fell through to Album and the view came
back in reverse album-name order (a 2002 remix album first, the 2013
release last).

Map premieredate/productionyear to the year sort key, matching the
ProductionYear the DTO exposes for songs and the existing MusicAlbum
mapping (premieredate -> max_year).

* feat(jellyfin): expose PremiereDate on tracks and albums

Finamp's "Latest Releases" artist/genre sections re-sort the merged
server responses client-side by PremiereDate and keep the top 5. Without
the field every comparison returns equal and Dart's unstable sort leaves
the picks in arbitrary order — a 2007 remix could lead the list even
with the server sorting by year correctly.

Serialize PremiereDate as ISO 8601 from the date tag (padding partial
"2007"/"2007-02" values so DateTime.tryParse accepts them), falling
back to the year; omitted when neither exists.

* feat(jellyfin): resolve Finamp-truncated item ids (saved queue restore)

Finamp persists its play queue by packing every item id into exactly 16
bytes (packIds assumes Jellyfin's 32-hex GUID ids), so our longer ids
come back truncated after an app restart and "Failed to restore queue"
loops forever: the /Items?ids= batch resolves nothing.

Navidrome ids can't be made GUID-shaped (nanoid ids can exceed 128 bits),
so compensate server-side: a 16-char id — a length no Navidrome id family
uses — is resolved by unique-prefix range scan, with ambiguity failing
safe. The ids= batch echoes the id as requested (Finamp matches restored
items by its stored ids), and stream, image, item, user-data, favorite,
rating and playback-report endpoints accept truncated ids transparently.

The proper fix belongs upstream in Finamp's packIds; documented in the
README so this layer can be removed once that ships.

* feat(jellyfin): implement Items/{id}/InstantMix

Finamp requests an instant mix on every track tap when its "start
instant mix for individual tracks" setting is on (plus the long-press
menus); the 404 made those taps fail with an error and play nothing.

A track seed returns itself first — Finamp plays exactly what comes
back — followed by the external provider's similar songs, capped at the
requested limit and filtered to the caller's libraries. Container seeds
(artist/album) return the provider's similar-songs blend. Provider
errors and unknown seeds degrade to seed-only/empty results instead of
404s, reusing the Similar endpoints' bounded-wait singleflight (with a
distinct cache key, since mixes and similar lists answer different
shapes). Sonic-similarity backing stays a follow-up (see README).

* style(jellyfin): trim wordy comments

* refactor(jellyfin): apply cleanup review findings

- batch truncated-id resolution in /Items?ids=: one chunked range query
  for all media-file prefixes instead of a query per id (a restored
  queue sends hundreds of truncated ids)
- resolve truncated ids on the /Similar endpoints too, matching the
  neighboring InstantMix; document which entry points don't resolve
- hoist maxImageUploadSize to core, deleting the byte-identical copies
  in nativeapi and jellyfin (tests moved to core)
- drop the unused type parameter on premiereDate

* fix(jellyfin): never drop the instant mix seed on a slow provider

The seed track was built inside the awaited provider fetch, so when the
external agent was slow or unreachable the request hit the 10s wait and
answered a fully empty mix — Finamp then played nothing on tap, even
though the seed needs no provider at all (seen live: Last.fm unreachable
from the server, responseSize=49).

Build the seed outside the await: only the similar-songs tail is fetched
and bounded, and a timeout now degrades to a seed-only mix. Also folds
instantMixForSong into getInstantMix, since the tail is exactly
similarSongs.

* fix(jellyfin): prefer the recommended Authorization scheme when picking a token

Jellyfin's authorization guidance deprecates X-Emby-Token,
X-MediaBrowser-Token, X-Emby-Authorization and api_key; the Authorization
MediaBrowser scheme is the recommended form. All spellings stay accepted,
but when a client sends several, the recommended one now wins. Adds
coverage for the canonical Authorization header, which no test exercised
directly.

* refactor(jellyfin): rename parseEmbyAuth to parseMediaBrowserAuth

The scheme is named MediaBrowser; the old name evoked the deprecated
X-Emby-* spellings even though the function also parses the recommended
Authorization header.

* fix(jellyfin): prefer the Authorization header over X-Emby-Authorization

The recommended header now wins when both carry MediaBrowser data — but
only when it actually parses as MediaBrowser: a reverse proxy may inject
Basic/Digest credentials into Authorization while the client sends the
deprecated header, and those must not swallow the client's auth.

* fix(jellyfin): require the MediaBrowser scheme when parsing auth headers

The parser extracted key="value" pairs from any Authorization value; a
foreign scheme whose parameters happened to use our field names would
have been misread as client auth. Validate the scheme word instead
(case-insensitively, per HTTP), accepting the legacy "Emby" spelling
like real Jellyfin. Replaces the any-recognized-field heuristic for
detecting proxy-injected Basic/Digest credentials.

* Revert "perf(db): keep query planner statistics trustworthy with full ANALYZE"

This reverts commit 118563e1053196f0e2e42dd4bee54e39a04ab145.
The planner-statistics work now lives in its own PR (#5740); this branch
keeps only the covering-index migration.

* fix(db): renumber jellyfin covering-index migration after master's latest

* fix(db): renumber Jellyfin covering-index migration

* docs(jellyfin): document playlist annotations

* refactor(log): clarify comments on external services query params

* refactor(e2e): move Subsonic e2e suite to server/subsonic/e2e

All files in server/e2e were Subsonic tests, so relocate the package
under server/subsonic/e2e to sit alongside the API it exercises. The
package name stays 'e2e'; only doc/comment references are updated.

* refactor(filter): consolidate genre filters and unexport tagIDFilter

Route filterByGenre, ByGenreID, and ArtistsByGenreID through a single
genreTagFilter helper so the EXISTS json_tree(tags,"$.genre") predicate
lives in one place. With server/filter no longer using it, persistence's
TagIDFilter is only referenced within the package, so unexport it.

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-07-14 11:46:37 -04:00
Kendall Garner
4998ac2c59 feat(server): add scrobble history Native API (#5761)
* initial scrobble api

* feat: add scrobble retrieval api

* address feedback (1)

* fix spelling

* be explicit about get

* add primary key field, update index, remove rowid references

* use unix timestamp for input and output

---------

Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2026-07-13 11:32:03 -04:00
Deluan Quintão
37e75c4354 feat(sharing): enable sharing by default (#5714)
Flip the EnableSharing default from false to true so new installations have the sharing feature available out of the box. Users can still disable it via the EnableSharing config option.

The native API only registers the /share route when sharing is enabled, so the nativeapi tests that build the router without wiring a share service now explicitly disable sharing in their setup to avoid registering a route backed by a nil service.
2026-07-04 19:55:34 -04:00
Deluan
efe9291db0 refactor: multiple syntax updates for Go 1.26
Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-19 18:02:36 -03:00
Deluan Quintão
8f0b4930ff refactor(conf): replace eager dir creation with lazy Dir type (#5495)
* feat(conf): add Dir type with lazy directory creation

Introduces the Dir type that wraps a directory path string and defers
os.MkdirAll until the first call to Path() or MustPath(), using sync.Once
to ensure the creation happens exactly once. Implements fmt.Stringer,
encoding.TextMarshaler, and encoding.TextUnmarshaler for config integration.
Includes Ginkgo/Gomega tests covering all methods and error paths.

* refactor(conf): replace eager dir creation with lazy Dir type

Change DataFolder, CacheFolder, Plugins.Folder, and Backup.Path from
string to Dir. Remove all os.MkdirAll calls from Load() so directories
are created lazily on first Path()/MustPath() call. Artwork folder
creation was already handled at point-of-use in image_upload.go.

Add SnapshotConfig() to conf package for safe test config save/restore
that avoids copying sync.Once inside Dir fields. Fix copy-lock vet
warning in nativeapi/config.go by marshalling pointer instead of value.

* refactor(conf): migrate tests and db init to lazy Dir type

Update all test files to use conf.NewDir() for Dir field assignments.
Ensure DataFolder is created lazily when the database is first opened
in db.Db(). Remove eager directory creation from conf.Load() tests.

* fix(conf): address review findings for Dir type

- Use os.ModePerm for DataFolder/CacheFolder (was 0700, should match
  original behavior). Add NewDirWithPerm for PluginsFolder (0700).
- Use Path() instead of MustPath() in db.Prune() to avoid logFatal
  from background cron job.
- Panic on marshal/unmarshal errors in SnapshotConfig (test helper).
- Clean up redundant String()/MustPath() calls in plugin manager.
- Remove dead code in dir_test.go.

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

* fix(conf): add GoString to Dir for clean config dump output

Implement fmt.GoStringer on Dir so pretty.Sprintf shows the path
string instead of internal struct fields (sync.Once, perm, err).
Also add TODO comment to configtest about removing the indirection.

* fix(dir): improve error logging in MustPath method

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

* refactor(tests): remove redundant tests for unwritable DataFolder and CacheFolder

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

* fix(conf): address PR review feedback

- Ensure Plugins.Folder always uses 0700, even when user-configured
  (previously only the derived default got restrictive permissions).
- Create LogFile parent directory before opening, so LogFile paths
  inside a not-yet-created DataFolder work correctly.

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-13 17:44:22 -03:00
Deluan Quintão
64c8d3f4c5 ci: run Go tests on Windows (#5380)
* ci(windows): add skeleton go-windows job (compile-only smoke test)

* ci(windows): fix comment to reference Task 7 not Task 6

* ci(windows): harden PATH visibility and set explicit bash shell

* ci(windows): enable full go test suite and ndpgen check

* test(gotaglib): skip Unix-only permission tests on Windows

* test(lyrics): skip Windows-incompatible tests

* test(utils): skip Windows-incompatible tests

* test(mpv): skip Windows-incompatible playback tests

Skip 3 subprocess-execution tests that rely on Unix-style mpv
invocation; .bat output includes \r-terminated lines that break
argument parsing (#TBD-mpv-windows).

* test(storage): skip Windows-incompatible tests

Skip relative-path test where filepath.Join uses backslash but the
storage implementation returns a forward-slash URL path
(#TBD-path-sep-storage).

* test(storage/local): skip Windows-incompatible tests

Skip 13 tests that fail because url.Parse("file://" + windowsPath)
treats the drive letter colon as an invalid port; also skip the
Windows drive-letter path test that exposes a backslash vs
forward-slash normalisation bug (#TBD-path-sep-storage-local).

* test(playlists): skip Windows-incompatible tests

* test(model): skip Windows-incompatible tests

* test(model/metadata): skip Windows-incompatible tests

* test(core): skip Windows-incompatible tests

AbsolutePath uses filepath.Join which produces OS-native path separators;
skip the assertion test on Windows until the production code is fixed
(#TBD-path-sep-core).

* test(artwork): skip Windows-incompatible tests

Artwork readers produce OS-native path separators on Windows while tests
assert forward-slash paths; skip 11 affected tests pending a fix in
production code (#TBD-path-sep-artwork).

* test(persistence): skip Windows-incompatible tests

Skip flaky timestamp comparison (#TBD-flake-persistence) and path-separator
real-bugs (#TBD-path-sep-persistence) in FolderRepository.GetFolderUpdateInfo
which uses filepath.Clean/os.PathSeparator converting stored forward-slash paths
to backslashes on Windows.

* test(scanner): skip Windows-incompatible tests

Skip symlink tests (Unix-assumption), ndignore path-separator bugs
(#TBD-path-sep-scanner) in processLibraryEvents/resolveFolderPath where
filepath.Rel/filepath.Split return backslash paths incompatible with fs.FS
forward-slash expectations, error message mismatch on Windows, and file
format upgrade detection (#TBD-path-sep-scanner).

* test(plugins): skip Windows-incompatible tests

Add //go:build !windows tags to test files that reference the suite
bootstrap (testManager, testdataDir, createTestManager) which is only
compiled on non-Windows. Add a Windows-only suite stub that skips all
specs via BeforeEach to prevent [build failed] on Windows CI.

* test(server): skip Windows-incompatible tests

Skip createUnixSocketFile tests that rely on Unix file permission bits
(chmod/fchmod) which are not supported on Windows.

* test(nativeapi): skip Windows-incompatible tests

Skip the i18n JSON validation test that uses filepath.Join to build
embedded-FS paths; filepath.Join produces backslashes on Windows which
breaks fs.Open (embedded FS always uses forward slashes).

* test(e2e): skip Windows-incompatible tests

On Windows, SQLite holds file locks that prevent the Ginkgo TempDir
DeferCleanup from deleting the DB file. Register an explicit db.Close
DeferCleanup (LIFO before TempDir cleanup) on Windows so the file lock
is released before the temp directory is removed.

* test(windows): fix e2e AfterSuite and skip remaining scanner path test

* test(scanner): skip another Windows path-sep test (#TBD-path-sep-scanner)

* test(subsonic): skip timing-flaky test on Windows (#TBD-flake-time-resolution-subsonic)

* test(scanner): skip 'detects file moved to different folder' on Windows

* test(scanner): consolidate 'Library changes' Windows skips into BeforeEach

* test(scanner): close DB before TempDir cleanup to fix Windows file lock

* test(scanner): skip ScanFolders suite on Windows instead of closing shared DB

* ci: retrigger for Windows soak run 2/3

* ci: retrigger for Windows soak run 3/3

* ci: retrigger for Windows soak run 3/3 (take 2)

* test(scanner): skip Multi-Library suite on Windows (SQLite file lock)

* ci(windows): promote go-windows to blocking status check

* test(plugins): run platform-neutral specs on Windows, drop blanket Skip

* test(windows): make tests cross-platform instead of skipping

- subsonic: back-date submissionTime baseline by 1s so
  BeTemporally(">") passes under millisecond clock resolution
- persistence: sleep briefly between Put calls so UpdatedAt is
  strictly after CreatedAt on low-resolution clocks
- utils/files: close tempFile before os.Remove so the test works on
  Windows (where an open handle holds a file lock)
- tests.TempFile: close the handle before returning; metadata tests
  no longer leak the open file into Ginkgo's TempDir cleanup

Resolves Copilot review comments on #5380.

* test(tests): add SkipOnWindows helper to reduce boilerplate

Introduces tests.SkipOnWindows(reason) that wraps the 3-line
runtime.GOOS guard pattern used in every Windows-skipped spec.

* test(adapters): use tests.SkipOnWindows helper

* test(core): use tests.SkipOnWindows helper

* test(model): use tests.SkipOnWindows helper

* test(persistence): use tests.SkipOnWindows helper

* test(scanner): use tests.SkipOnWindows helper

* test(server): use tests.SkipOnWindows helper

* test(plugins): run pure-Go unit tests on Windows

config_validation_test, manager_loader_test, and migrate_test have no
WASM/exec dependencies and don't rely on the make-built test plugins
from plugins_suite_test.go. Let them run on Windows too.
2026-04-19 13:16:47 -04:00
m8tec
c49e5855b9 feat(artwork): make max image upload size configurable (#5335)
* feat(config): make max image upload size configurable

Let max image upload size be set from config or environment instead of a fixed 10 MB cap. The upload handler still falls back to 10 MB when MaxImageUploadSize is not set.

Signed-off-by: M8te <38794725+m8tec@users.noreply.github.com>

* feat(config): support human-readable MaxImageUploadSize values

Max image upload size can now be configured as a readable string like 10MB or 1GB instead of raw bytes. The config load validates it at startup, and the upload handler parses it before applying request limits (10MB fallback if it fails).

+ MaxImageUploadSize as human-readable string
+ removed redundant max(1, ...) to address code review
+ cap memory usage of ParseMultipartForm to 10MB (address code review)

Signed-off-by: M8te <38794725+m8tec@users.noreply.github.com>

* refactor(config): consolidate MaxImageUploadSize default and add tests

Move the "10MB" default constant to consts.DefaultMaxImageUploadSize so
both the viper default and the runtime fallback share a single source of
truth. Improve the validator error message with fmt.Errorf wrapping to
match the project convention (e.g. validatePurgeMissingOption). Add unit
tests for validateMaxImageUploadSize (valid/invalid inputs) and
maxImageUploadSize (configured, empty, invalid, raw bytes). Compute
maxImageSize once at handler creation rather than per request.

---------

Signed-off-by: M8te <38794725+m8tec@users.noreply.github.com>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2026-04-12 11:16:00 -04:00
Deluan
f33ca75378 refactor: rename EnableCoverArtUpload to EnableArtworkUpload
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.
2026-03-27 19:33:46 -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
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
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
3cd5d16b0a chore: upgrade golangci-lint to 2.11 and fix lint issues
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-06 19:23:47 -05:00
Deluan
435fb0b076 feat(server): add EnableCoverArtUpload config option
Allow administrators to disable playlist cover art upload/removal for
non-admin users via the new EnableCoverArtUpload config option (default: true).

- Guard uploadPlaylistImage and deletePlaylistImage endpoints (403 for non-admin when disabled)
- Set CoverArtRole in Subsonic GetUser/GetUsers responses based on config and admin status
- Pass config to frontend and conditionally hide upload/remove UI controls
- Admins always retain upload capability regardless of setting
2026-03-02 16:59:05 -05:00
adrbn
d004f99f8f feat(playlist): add custom playlist cover art upload (#5110)
* feat(playlist): add custom playlist cover art upload - #406

Allow users to upload, view, and remove custom cover images for playlists.
Custom images take priority over the auto-generated tiled artwork.

Backend:
- Add `image_path` column to playlist table (migration with proper rollback)
- Add `SetImage`/`RemoveImage` methods to playlist service
- Add `POST/DELETE /api/playlist/{id}/image` endpoints
- Prioritize custom image in artwork reader pipeline
- Clean up image files on playlist deletion
- Use glob-based cleanup to prevent orphaned files across format changes
- Reject uploads with undetermined image type (400)

Frontend:
- Hover overlay on playlist cover with upload (camera) and remove (trash) buttons
- Lightbox for full-size cover art viewing
- Cover art thumbnails in the playlist list view
- Loading/error states and i18n strings

Closes #406

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: adrbn <128328324+adrbn@users.noreply.github.com>

* refactor: rename playlist image path migration file

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

* fix(playlist): address review feedback for cover art upload - #406

- Use httpClient instead of raw fetch for image upload/remove
- Revert glob cleanup to simple imagePath check
- Add log.Error before all error HTTP responses
- Add backend tests for SetImage and RemoveImage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: adrbn <128328324+adrbn@users.noreply.github.com>

* refactor(playlist): use Playlist.ArtworkPath() for image storage

Migrate all playlist image path handling to use the new
Playlist.ArtworkPath() method as the single source of truth. The DB now
stores only the filename (e.g. "pls-1.jpg") instead of a relative path,
and images are stored under {DataFolder}/artwork/playlist/ instead of
{DataFolder}/playlist_images/. The artwork root directory is created at
startup alongside DataFolder and CacheFolder. This also removes the
conf dependency from reader_playlist.go since path resolution is now
fully encapsulated in the model.

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

* refactor(playlist): streamline artwork image selection logic

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

* refactor: move translation keys, add pt-BR translations

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

* refactor(playlist): rename image_path to image_file

Rename the playlist cover art column and field from image_path/ImagePath
to image_file/ImageFile across the migration, model, service, tests, and
UI. The new name more accurately describes what the field stores (a
filename, not a path) and aligns with the existing ImageFiles/IsImageFile
naming conventions in the codebase.

---------

Signed-off-by: adrbn <128328324+adrbn@users.noreply.github.com>
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2026-03-01 14:07:18 -05:00
Deluan Quintão
d9a215e1e3 feat(plugins): allow mounting library directories as read-write (#5122)
* feat(plugins): mount library directories as read-only by default

Add an AllowWriteAccess boolean to the plugin model, defaulting to
false. When off, library directories are mounted with the extism "ro:"
prefix (read-only). Admins can explicitly grant write access via a new
toggle in the Library Permission card.

* test: add tests to buildAllowedPaths

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

* chore: improve allowed paths logging for library access

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-28 10:59:13 -05:00
Deluan Quintão
7ad2907719 refactor: move playlist business logic from repositories to service layer (#5027)
* refactor: move playlist business logic from repositories to core.Playlists service

Move authorization, permission checks, and orchestration logic from
playlist repositories to the core.Playlists service, following the
existing pattern used by core.Share and core.Library.

Changes:
- Expand core.Playlists interface with read, mutation, track management,
  and REST adapter methods
- Add playlistRepositoryWrapper for REST Save/Update/Delete with
  permission checks (follows Share/Library pattern)
- Simplify persistence/playlist_repository.go: remove isWritable(),
  auth checks from Delete()/Put()/updatePlaylist()
- Simplify persistence/playlist_track_repository.go: remove
  isTracksEditable() and permission checks from Add/Delete/Reorder
- Update Subsonic API handlers to route through service
- Update Native API handlers to accept core.Playlists instead of
  model.DataStore

* test: add coverage for playlist service methods and REST wrapper

Add 30 new tests covering the service methods added during the playlist
refactoring:

- Delete: owner, admin, denied, not found
- Create: new playlist, replace tracks, admin bypass, denied, not found
- AddTracks: owner, admin, denied, smart playlist, not found
- RemoveTracks: owner, smart playlist denied, non-owner denied
- ReorderTrack: owner, smart playlist denied
- NewRepository wrapper: Save (owner assignment, ID clearing),
  Update (owner, admin, denied, ownership change, not found),
  Delete (delegation with permission checks)

Expand mockedPlaylistRepo with Get, Delete, Tracks, GetWithTracks, and
rest.Persistable methods. Add mockedPlaylistTrackRepo for track
operation verification.

* fix: add authorization check to playlist Update method

Added ownership verification to the Subsonic Update endpoint in the
playlist service layer. The authorization check was present in the old
repository code but was not carried over during the refactoring to the
service layer, allowing any authenticated user to modify playlists they
don't own via the Subsonic API. Also added corresponding tests for the
Update method's permission logic.

* refactor: improve playlist permission checks and error handling, add e2e tests

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

* refactor: rename core.Playlists to playlists package and update references

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

* refactor: rename playlists_internal_test.go to parse_m3u_test.go and update tests; add new parse_nsp.go and rest_adapter.go files

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

* fix: block track mutations on smart playlists in Create and Update

Create now rejects replacing tracks on smart playlists (pre-existing
gap). Update now uses checkTracksEditable instead of checkWritable
when track changes are requested, restoring the protection that was
removed from the repository layer during the refactoring. Metadata-only
updates on smart playlists remain allowed.

* test: add smart playlist protection tests to ensure readonly behavior and mutation restrictions

* refactor: optimize track removal and renumbering in playlists

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

* refactor: implement track reordering in playlists with SQL updates

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

* refactor: wrap track deletion and reordering in transactions for consistency

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

* refactor: remove unused getTracks method from playlistTrackRepository

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

* refactor: optimize playlist track renumbering with CTE-based UPDATE

Replace the DELETE + re-INSERT renumbering strategy with a two-step
UPDATE approach using a materialized CTE and ROW_NUMBER() window
function. The previous approach (SELECT all IDs, DELETE all tracks,
re-INSERT in chunks of 200) required 13 SQL operations for a 2000-track
playlist. The new approach uses just 2 UPDATEs: first negating all IDs
to clear the positive space, then assigning sequential positions via
UPDATE...FROM with a CTE. This avoids the UNIQUE constraint violations
that affected the original correlated subquery while reducing per-delete
request time from ~110ms to ~12ms on a 2000-track playlist.

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

* refactor: rename New function to NewPlaylists for clarity

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

* refactor: update mock playlist repository and tests for consistency

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-21 19:57:13 -05:00
Deluan
5fa8356b31 chore(deps): bump golangci-lint to v2.10.0 and suppress new gosec false positives
Bump golangci-lint from v2.9.0 to v2.10.0, which includes a newer gosec
with additional taint-analysis rules (G117, G703, G704, G705) and a
stricter G101 check. Added inline //nolint:gosec comments to suppress
21 false positives across 19 files: struct fields flagged as secrets
(G117), w.Write calls flagged as XSS (G705), HTTP client calls flagged
as SSRF (G704), os.Stat/os.ReadFile/os.Remove flagged as path traversal
(G703), and a sort mapping flagged as hardcoded credentials (G101).

Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-17 09:28:42 -05:00
Deluan
b64d8ad334 fix(server): return 404 instead of 500 for non-existent playlists
The native API endpoints GET /playlist/{id}/tracks and
GET /playlist/{id}/tracks/{id} were panicking with a nil pointer
dereference (resulting in a 500) when the playlist did not exist.
This happened because Tracks() returns nil for missing playlists,
and the nil repository was passed directly to the rest handler.
Extracted a shared playlistTracksHandler that checks for nil and
returns 404 early. Added tests covering both the error and happy paths.
2026-02-15 22:39:27 -05:00
Maximilian
a704e86ac1 refactor: run Go modernize (#5002) 2026-02-08 09:57:30 -05:00
Deluan Quintão
f1e75c40dc feat(plugins): add JSONForms-based plugin configuration UI (#4911)
* feat(plugins): add JSONForms schema for plugin configuration

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

* feat: enhance error handling by formatting validation errors with field names

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

* feat: enforce required fields in config validation and improve error handling

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

* format JS code

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

* feat: add config schema validation and enhance manifest structure

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

* feat: refactor plugin config parsing and add unit tests

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

* feat: add config validation error message in Portuguese

* feat: enhance AlwaysExpandedArrayLayout with description support and improve array control testing

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

* feat: update Discord Rust plugin configuration to use JSONForm for user tokens and enhance schema validation

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

* fix: resolve React Hooks linting issues in plugin UI components

* Apply suggestions from code review

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* format code

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

* feat: migrate schema validation to use santhosh-tekuri/jsonschema and improve error formatting

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

* address PR comments

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

* fix flaky test

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

* feat: enhance array layout and configuration handling with AJV defaults

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

* feat: implement custom tester to exclude enum arrays from AlwaysExpandedArrayLayout

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

* feat: add error boundary for schema rendering and improve error messages

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

* feat: refine non-enum array control logic by utilizing JSONForms schema resolution

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

* feat: add error styling to ToggleEnabledSwitch for disabled state

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

* feat: adjust label positioning and styling in SchemaConfigEditor for improved layout

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

* feat: implement outlined input controls renderers to replace custom fragile CSS

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

* feat: remove margin from last form control inside array items for better spacing

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

* feat: enhance AJV error handling to transform required errors for field-level validation

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

* feat: set default value for User Tokens in manifest.json to improve user experience

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

* format

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

* feat: add margin to outlined input controls for improved spacing

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

* feat: remove redundant margin rule for last form control in array items

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

* feat: adjust font size of label elements in SchemaConfigEditor for improved readability

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-19 20:51:00 -05:00
Deluan Quintão
03a45753e9 feat(plugins): New Plugin System with multi-language PDK support (#4833)
* chore(plugins): remove the old plugins system implementation

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

* feat(plugins): implement new plugin system with using Extism

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

* feat(plugins): add capability detection for plugins based on exported functions

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

* feat(plugins): add auto-reload functionality for plugins with file watcher support

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

* feat(plugins): add auto-reload functionality for plugins with file watcher support

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

* refactor(plugins): standardize variable names and remove superfluous wrapper functions

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

* fix(plugins): improve error handling and logging in plugin manager

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

* refactor(plugins): implement plugin function call helper and refactor MetadataAgent methods

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

* fix(plugins): race condition in plugin manager

* tests(plugins): change BeforeEach to BeforeAll in MetadataAgent tests

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

* tests(plugins): optimize tests

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

* tests(plugins): more optimizations

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

* test(plugins): ignore goroutine leaks from notify library in tests

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

* feat(plugins): add Wikimedia plugin for Navidrome to fetch artist metadata

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

* feat(plugins): enhance plugin logging and set User-Agent header

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

* feat(plugins): implement scrobbler plugin with authorization and scrobbling capabilities

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

* feat(plugins): integrate logs

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

* refactor(plugins): clean up manifest struct and improve plugin loading logic

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

* feat(plugins): add metadata agent and scrobbler schemas for bootstrapping plugins

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

* feat(hostgen): add hostgen tool for generating Extism host function wrappers

- Implemented hostgen tool to generate wrappers from annotated Go interfaces.
- Added command-line flags for input/output directories and package name.
- Introduced parsing and code generation logic for host services.
- Created test data for various service interfaces and expected generated code.
- Added documentation for host services and annotations for code generation.
- Implemented SubsonicAPI service with corresponding generated code.

* feat(subsonicapi): update Call method to return JSON string response

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

* feat(plugins): implement SubsonicAPI host function integration with permissions

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

* fix(generator): error-only methods in response handling

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

* feat(plugins): generate client wrappers for host functions

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

* refactor(generator): remove error handling for response.Error in client templates

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

* feat(scheduler): add Scheduler service interface with host function wrappers for scheduling tasks

* feat(plugins): add WASI build constraints to client wrapper templates, to avoid lint errors

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

* feat(scheduler): implement Scheduler service with one-time and recurring scheduling capabilities

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

* refactor(manifest): remove unused ConfigPermission from permissions schema

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

* feat(scheduler): add scheduler callback schema and implementation for plugins

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

* refactor(scheduler): streamline scheduling logic and remove unused callback tracking

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

* refactor(scheduler): add Close method for resource cleanup on plugin unload

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

* docs(scheduler): clarify SchedulerCallback requirement for scheduling functions

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

* fix: update wasm build rule to include all Go files in the directory

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

* feat: rewrite the wikimedia plugin using the XTP CLI

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

* refactor(scheduler): replace uuid with id.NewRandom for schedule ID generation

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

* refactor: capabilities registration

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

* test: add scheduler service isolation test for plugin instances

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

* refactor: update plugin manager initialization and encapsulate logic

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

* feat: add WebSocket service definitions for plugin communication

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

* feat: implement WebSocket service for plugin integration and connection management

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

* feat: add Crypto Ticker example plugin for real-time cryptocurrency price updates via Coinbase WebSocket API

Also add the lifecycle capability

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

* fix: use context.Background() in invokeCallback for scheduled tasks

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

* refactor: rename plugin.create() to plugin.instance()

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

* refactor: rename pluginInstance to plugin for consistency across the codebase

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

* refactor: simplify schedule cloning in Close method and enhance plugin cleanup error handling

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

* feat: implement Artwork service for generating artwork URLs in Navidrome plugins - WIP

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

* refactor: moved public URL builders to avoid import cycles

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

* feat: add Cache service for in-memory TTL-based caching in plugins

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

* feat: add Discord Rich Presence example plugin for Navidrome integration

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

* refactor: host function wrappers to use structured request and response types

- Updated the host function signatures in `nd_host_artwork.go`, `nd_host_scheduler.go`, `nd_host_subsonicapi.go`, and `nd_host_websocket.go` to accept a single parameter for JSON requests.
- Introduced structured request and response types for various cache operations in `nd_host_cache.go`.
- Modified cache functions to marshal requests to JSON and unmarshal responses, improving error handling and code clarity.
- Removed redundant memory allocation for string parameters in favor of JSON marshaling.
- Enhanced error handling in WebSocket and cache operations to return structured error responses.

* refactor: error handling in various plugins to convert response.Error to Go errors

- Updated error handling in `nd_host_scheduler.go`, `nd_host_websocket.go`, `nd_host_artwork.go`, `nd_host_cache.go`, and `nd_host_subsonicapi.go` to convert string errors from responses into Go errors.
- Removed redundant error checks in test data plugins for cleaner code.
- Ensured consistent error handling across all plugins to improve reliability and maintainability.

* refactor: rename fake plugins to test plugins for clarity in integration tests

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

* feat: add help target to Makefile for plugin usage instructions

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

* feat: add Cover Art Archive plugin as an example of Python plugin

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

* feat: update Makefile and README to clarify Go plugin usage

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

* feat: include plugin capabilities in loading log message

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

* feat: add trace logging for plugin availability and error handling in agents

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

* feat: add Now Playing Logger plugin to showcase calling host functions from Python plugins

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

* feat: generate Python client wrappers for various host services

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

* feat: add generated host function wrappers for Scheduler and SubsonicAPI services

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

* feat: update Python plugin documentation and usage instructions for host function wrappers

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

* feat: add Webhook Scrobbler plugin in Rust to send HTTP notifications on scrobble events

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

* feat: enable parallel loading of plugins during startup

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

* docs: update README to include WebSocket callback schema in plugin documentation

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

* feat: extend plugin watcher with improved logging and debounce duration adjustment

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

* add trace message for plugin recompiles

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

* feat: implement plugin cache purging functionality

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

* test: move purgeCacheBySize unit tests

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

* feat(plugins UI): add plugin repository and database support

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

* feat(plugins UI): add plugin management routes and middleware

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

* feat(plugins UI): implement plugin synchronization with database for add, update, and remove actions

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

* feat(plugins UI): add PluginList and PluginShow components with plugin management functionality

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

* feat(plugins): optimize plugin change detection

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

* refactor(plugins UI): improve PluginList structure

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

* feat(plugins UI): enhance PluginShow with author, website, and permissions display

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

* feat(plugins UI): refactor to use MUI and RA components

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

* feat(plugins UI): add error handling for plugin enable/disable actions

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

* refactor(plugins): inject PluginManager into native API

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

* refactor(plugins): update GetManager to accept DataStore parameter

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

* feat(plugins): add subsonicRouter to Manager and refactor host service registration

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

* refactor(plugins): enhance debug logging for plugin actions and recompile logic

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

* refactor(plugins): break manager.go into smaller, focused files

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

* refactor(plugins): streamline error handling and improve plugin retrieval logic

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

* refactor(plugins): update newWebSocketService to use WebSocketPermission for allowed hosts

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

* refactor(plugins): introduce ToggleEnabledSwitch for managing plugin enable/disable state

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

* docs: update READMEs

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

* feat(library): add Library service for metadata access and filesystem integration

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

* feat(plugins): add Library Inspector plugin for periodic library inspection and file size logging

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

* docs: update README to reflect JSON configuration format for plugins

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

* fix(build): update target to wasm32-wasip1 for improved WASI support

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

* feat(plugins): implement configuration management UI with key-value pairs support

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

* feat(ui): adjust grid layout in InfoRow component for improved responsiveness

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

* feat(plugins): rename ErrorIndicator to EnabledOrErrorField and enhance error handling logic

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

* feat(i18n): add Portuguese translations for plugin management and notifications

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

* feat(plugins): add support for .ndp plugin packages and update build process

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

* docs: update README for .ndp plugin packaging and installation instructions

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

* feat(plugins): implement KVStore service for persistent key-value storage

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

* docs: enhance README with Extism plugin development resources and recommendations

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

* feat(plugins): integrate event broker into plugin manager

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

* feat(plugins): update config handling in PluginShow to track last record state

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

* feat(plugins): add Rust host function library and example implementation of Discord Rich Presence plugin in Rust

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

* feat(plugins): generate Rust lib.rs file to expose host function wrappers

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

* refactor(plugins): update JSON field names to camelCase for consistency

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

* refactor: reduce cyclomatic complexity by refactoring main function

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

* feat(plugins): enhance Rust code generation with typed struct support and improved type handling

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

* feat(plugins): add Go client library with host function wrappers and documentation

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

* feat(plugins): generate Go client stubs for non-WASM platforms

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

* feat(plugins): update client template file names for consistency

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

* feat(plugins): add initial implementation of the Navidrome Plugin Development Kit code generator - Pahse 1

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

* feat(plugins): implementation of the Navidrome Plugin Development Kit with generated client wrappers and service interfaces - Phase 2

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

* feat(plugins): implementation of the Navidrome Plugin Development Kit with generated client wrappers and service interfaces - Phase 2 (2)

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

* feat(plugins): implementation of the Navidrome Plugin Development Kit with generated client wrappers and service interfaces - Phase 3

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

* feat(plugins): implementation of the Navidrome Plugin Development Kit with generated client wrappers and service interfaces - Phase 4

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

* feat(plugins): implementation of the Navidrome Plugin Development Kit with generated client wrappers and service interfaces - Phase 5

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

* refactor(plugins): consistent naming/types across PDK

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

* refactor(plugins): streamline plugin function signatures and error handling

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

* refactor(plugins): update scrobbler interface to return errors directly instead of response structs

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

* test: make all test plugins use the PDK

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

* refactor(plugins): reorganize and sort type definitions for consistency

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

* refactor(plugins): update error handling for methods to return errors directly

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

* refactor(plugins): update function signatures to return values directly instead of response structs

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

* refactor(plugins): update request/response types to use private naming conventions

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

* build: mark .wasm files as intermediate for cleanup after building .ndp

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

* refactor(plugins): consolidate PDK module path and update Go version to 1.25

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

* feat: implement Rust PDK

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

* refactor(plugins): reorganize Rust output structure to follow standard conventions

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

* refactor(plugins): update Discord Rich Presence and Library Inspector plugins to use nd-pdk for service calls and implement lifecycle management

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

* refactor(plugins): update macro names for websocket and metadata registration to improve clarity and consistency

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

* refactor(plugins): rename scheduler callback methods for consistency and clarity

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

* refactor(plugins): update export wrappers to use `//go:wasmexport` for WebAssembly compatibility

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

* docs: update plugin registration docs

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

* fix(plugins): generate host wrappers

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

* test(plugins): conditionally run goleak checks based on CI environment

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

* docs: update README to reflect changes in plugin import paths

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

* refactor(plugins): update plugin instance creation to accept context for cancellation support

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

* fix(plugins): update return types in metadata interfaces to use pointers

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

* fix(plugins): enhance type handling for Rust and XTP output in capability generation

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

* fix(plugins): update IsAuthorized method to return boolean instead of response object

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

* test(plugins): add unit tests for rustOutputType and isPrimitiveRustType functions

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

* feat(plugins): implement XTP JSONSchema validation for generated schemas

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

* fix(plugins): update response types in testMetadataAgent methods to use pointers

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

* docs: update Go and Rust plugin developer sections for clarity

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

* docs: correct example link for library inspector in README

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

* docs: clarify artwork URL generation capabilities in service descriptions

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

* docs: update README to include Rust PDK crate information for plugin developers

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

* fix: handle URL parsing errors and use atomic upsert in plugin repository

Added proper error handling for url.Parse calls in PublicURL and AbsoluteURL
functions. When parsing fails, PublicURL now falls back to AbsoluteURL, and
AbsoluteURL logs the error and returns an empty string, preventing malformed
URLs from being generated.

Replaced the non-atomic UPDATE-then-INSERT pattern in plugin repository Put
method with a single atomic INSERT ... ON CONFLICT statement. This eliminates
potential race conditions and improves consistency with the upsert pattern
already used in host_kvstore.go.

* feat: implement mock service instances for non-WASM builds using testify/mock

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

* refactor: Discord RPC struct to encapsulate WebSocket logic

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

* feat: add support for experimental WebAssembly threads

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

* feat: add PDK abstraction layer with mock support for non-WASM builds

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

* feat: add unit tests for Discord plugin and RPC functionality

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

* fix: update return types in minimalPlugin and wikimediaPlugin methods to use pointers

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

* fix: context cancellation and implement WebSocket callback timeout for improved error handling

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

* feat: conditionally include error handling in generated client code templates

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

* feat: implement ConfigService for plugin configuration management

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

* feat: enhance plugin manager to support metrics recording

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

* refactor: make MockPDK private

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

* refactor: update interface types to use 'any' in plugin repository methods

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

* refactor: rename List method to Keys for clarity in configuration management

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

* test: add ndpgen plugin tests in the pipeline and update Makefile

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

* feat: add users permission management to plugin system

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

* refactor: streamline users integration tests and enhance plugin user management

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

* refactor: remove UserID from scrobbler request structure

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

* test: add integration tests for UsersService enable gate behavior

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

* feat: implement user permissions for SubsonicAPI and scrobbler plugins

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

* fix: show proper error in the UI when enabling a plugin fails

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

* feat: add library permission management to plugin system

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

* feat: add user permission for processing scrobbles in Discord Rich Presence plugin

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

* fix: implement dynamic loading for buffered scrobbler plugins

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

* feat: add GetAdmins method to retrieve admin users from the plugin

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

* feat: update Portuguese translations for user and library permissions

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

* reorder migrations

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

* fix: remove unnecessary bulkActionButtons prop from PluginList component

* feat: add manual plugin rescan functionality and corresponding UI action

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

* feat: implement user/library and plugin management integration with cleanup on deletion

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

* feat: replace core mock services with test-specific implementations to avoid import cycles

* feat: add ID fields to Artist and Song structs and enhance track loading logic by prioritizing ID matches

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

* feat: update plugin permissions from allowedHosts to requiredHosts for better clarity and consistency

* feat: refactor plugin host permissions to use RequiredHosts directly for improved clarity

* fix: don't record metrics for plugin calls that aren't implemented at all

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

* fix: enhance connection management with improved error handling and cleanup logic

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

* feat: introduce ArtistRef struct for better artist representation and update track metadata handling

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

* feat: update user configuration handling to use user key prefix for improved clarity

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

* feat: enhance ConfigCard input fields with multiline support and vertical resizing

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

* fix: rust plugin compilation error

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

* feat: implement IsOptionPattern method for better return type handling in Rust PDK generation

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-01-14 19:22:48 -05:00
Deluan Quintão
5ce6e16d96 fix: album statistics not updating after deleting missing files (#4668)
* feat: add album refresh functionality after deleting missing files

Implemented RefreshAlbums method in AlbumRepository to recalculate album attributes (size, duration, song count) from their constituent media files. This method processes albums in batches to maintain efficiency with large datasets.

Added integration in deleteMissingFiles to automatically refresh affected albums in the background after deleting missing media files, ensuring album statistics remain accurate. Includes comprehensive test coverage for various scenarios including single/multiple albums, empty batches, and large batch processing.

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

* refactor: extract missing files deletion into reusable service layer

Extracted inline deletion logic from server/nativeapi/missing.go into a new core.MissingFiles service interface and implementation. This provides better separation of concerns and testability.

The MissingFiles service handles:
- Deletion of specific or all missing files via transaction
- Garbage collection after deletion
- Extraction of affected album IDs from missing files
- Background refresh of artist and album statistics

The deleteMissingFiles HTTP handler now simply delegates to the service, removing 70+ lines of inline logic. All deletion, transaction, and stat refresh logic is now centralized in core/missing_files.go.

Updated dependency injection to provide MissingFiles service to the native API router. Renamed receiver variable from 'n' to 'api' throughout native_api.go for consistency.

* refactor: consolidate maintenance operations into unified service

Consolidate MissingFiles and RefreshAlbums functionality into a new Maintenance service. This refactoring:
- Creates core.Maintenance interface combining DeleteMissingFiles, DeleteAllMissingFiles, and RefreshAlbums methods
- Moves RefreshAlbums logic from AlbumRepository persistence layer to core Maintenance service
- Removes MissingFiles interface and moves its implementation to maintenanceService
- Updates all references in wire providers, native API router, and handlers
- Removes RefreshAlbums interface method from AlbumRepository model
- Improves separation of concerns by centralizing maintenance operations in the core domain

This change provides a cleaner API and better organization of maintenance-related database operations.

* refactor: remove MissingFiles interface and update references

Remove obsolete MissingFiles interface and its references:
- Delete core/missing_files.go and core/missing_files_test.go
- Remove RefreshAlbums method from AlbumRepository interface and implementation
- Remove RefreshAlbums tests from AlbumRepository test suite
- Update wire providers to use NewMaintenance instead of NewMissingFiles
- Update native API router to use Maintenance service
- Update missing.go handler to use Maintenance interface

All functionality is now consolidated in the core.Maintenance service.

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

* refactor: rename RefreshAlbums to refreshAlbums and update related calls

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

* refactor: optimize album refresh logic and improve test coverage

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

* refactor: simplify logging setup in tests with reusable LogHook function

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

* refactor: add synchronization to logger and maintenance service for thread safety

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2025-11-08 20:11:00 -05:00
Deluan Quintão
00c83af170 feat: Multi-library support (#4181)
* feat(database): add user_library table and library access methods

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

# Conflicts:
#	tests/mock_library_repo.go

* feat(database): enhance user retrieval with library associations

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

* feat(api): implement library management and user-library association endpoints

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

* feat(api): restrict access to library and config endpoints to admin users

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

* refactor(library): implement library management service and update API routes

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

* feat(database): add library filtering to album, folder, and media file queries

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

* refactor library service to use REST repository pattern and remove CRUD operations

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

* add total_duration column to library and update user_library table

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

* fix migration file name

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

* feat(library): add library management features including create, edit, delete, and list functionalities - WIP

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

* feat(library): enhance library validation and management with path checks and normalization - WIP

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

* feat(library): improve library path validation and error handling - WIP

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

* use utils/formatBytes

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

* simplify DeleteLibraryButton.jsx

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

* feat(library): enhance validation messages and error handling for library paths

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

* lint

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

* test(scanner): add tests for multi-library scanning and validation

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

* test(scanner): improve handling of filesystem errors and ensure warnings are returned

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

* feat(controller): add function to retrieve the most recent scan time across all libraries

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

* feat(library): add additional fields and restructure LibraryEdit component for enhanced statistics display

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

* feat(library): enhance LibraryCreate and LibraryEdit components with additional props and styling

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

* feat(mediafile): add LibraryName field and update queries to include library name

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

* feat(missingfiles): add library filter and display in MissingFilesList component

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

* feat(library): implement scanner interface for triggering library scans on create/update

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

# Conflicts:
#	cmd/wire_gen.go
#	cmd/wire_injectors.go

# Conflicts:
#	cmd/wire_gen.go

# Conflicts:
#	cmd/wire_gen.go
#	cmd/wire_injectors.go

* feat(library): trigger scan after successful library deletion to clean up orphaned data

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

* rename migration file for user library table to maintain versioning order

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

* refactor: move scan triggering logic into a helper method for clarity

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

* feat(library): add library path and name fields to album and mediafile models

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

* feat(library): add/remove watchers on demand, not only when server starts

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

* refactor(scanner): streamline library handling by using state-libraries for consistency

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

* fix: track processed libraries by updating state with scan timestamps

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

* prepend libraryID for track and album PIDs

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

* feat(repository): apply library filtering in CountAll methods for albums, folders, and media files

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

* feat(user): add library selection for user creation and editing

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

* feat(library): implement library selection functionality with reducer and UI component

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

# Conflicts:
#	.github/copilot-instructions.md

# Conflicts:
#	.gitignore

* feat(library): add tests for LibrarySelector and library selection hooks

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

* test: add unit tests for file utility functions

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

* feat(library): add library ID filtering for album resources

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

* feat(library): streamline library ID filtering in repositories and update resource filtering logic

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

* fix(repository): add table name handling in filter functions for SQL queries

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

* feat(library): add refresh functionality on LibrarySelector close

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

* feat(artist): add library ID filtering for artists in repository and update resource filtering logic

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

# Conflicts:
#	persistence/artist_repository.go

* Add library_id field support for smart playlists

- Add library_id field to smart playlist criteria system
- Supports Is and IsNot operators for filtering by library ID
- Includes comprehensive test coverage for single values and lists
- Enables creation of library-specific smart playlists

* feat(subsonic): implement user-specific library access in GetMusicFolders

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

* feat(library): enhance LibrarySelectionField to extract library IDs from record

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

* feat(subsonic): update GetIndexes and GetArtists method to support library ID filtering

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

* fix: ensure LibrarySelector dropdown refreshes on button close

Added refresh() call when closing the dropdown via button click to maintain
consistency with the ClickAwayListener behavior. This ensures the UI
updates properly regardless of how the dropdown is closed, fixing an
inconsistent refresh behavior between different closing methods.

The fix tracks the previous open state and calls refresh() only when
the dropdown was open and is being closed by the button click.

* refactor: simplify getUserAccessibleLibraries function and update related tests

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

* feat: enhance selectedMusicFolderIds function to handle valid music folder IDs and improve fallback logic

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

* refactor: change ArtistRepository.GetIndex to accept multiple library IDs

Updated the GetIndex method signature to accept a slice of library IDs instead of a single ID, enabling support for filtering artists across multiple libraries simultaneously.

Changes include:
- Modified ArtistRepository interface in model/artist.go
- Updated implementation in persistence/artist_repository.go with improved library filtering logic
- Refactored Subsonic API browsing.go to use new selectedMusicFolderIds helper
- Added comprehensive test coverage for multiple library scenarios
- Updated mock repository implementation for testing

This change improves flexibility for multi-library operations while maintaining backward compatibility through the selectedMusicFolderIds helper function.

* feat: add library access validation to selectedMusicFolderIds

Enhanced the selectedMusicFolderIds function to validate musicFolderId parameters
against the user's accessible libraries. Invalid library IDs (those the user
doesn't have access to) are now silently filtered out, improving security by
preventing users from accessing libraries they don't have permission for.

Changes include:
- Added validation logic to check musicFolderId parameters against user's accessible libraries
- Added slices package import for efficient validation
- Enhanced function documentation to clarify validation behavior
- Added comprehensive test cases covering validation scenarios
- Maintains backward compatibility with existing behavior

* feat: implement multi-library support for GetAlbumList and GetAlbumList2 endpoints

- Enhanced selectedMusicFolderIds helper to validate and filter library IDs
- Added ApplyLibraryFilter function in filter/filters.go for library filtering
- Updated getAlbumList to support musicFolderId parameter filtering
- Added comprehensive tests for multi-library functionality
- Supports single and multiple musicFolderId values
- Falls back to all accessible libraries when no musicFolderId provided
- Validates library access permissions for user security

* feat: implement multi-library support for GetRandomSongs, GetSongsByGenre, GetStarred, and GetStarred2

- Added multi-library filtering to GetRandomSongs endpoint using musicFolderId parameter
- Added multi-library filtering to GetSongsByGenre endpoint using musicFolderId parameter
- Enhanced GetStarred and GetStarred2 to filter artists, albums, and songs by library
- Added Options field to MockMediaFileRepo and MockArtistRepo for test compatibility
- Added comprehensive Ginkgo/Gomega tests for all new multi-library functionality
- All tests verify proper SQL filter generation and library access validation
- Supports single/multiple musicFolderId values with fallback to all accessible libraries

* refactor: optimize starred items queries with parallel execution and fix test isolation

Refactored starred items functionality by extracting common logic into getStarredItems()
method that executes artist, album, and media file queries in parallel for better performance.
This eliminates code duplication between GetStarred and GetStarred2 methods while improving
response times through concurrent database queries using run.Parallel().

Also fixed test isolation issues by adding missing auth.Init(ds) call in album lists test setup.
This resolves nil pointer dereference errors in GetStarred and GetStarred2 tests when run independently.

* fix: add ApplyArtistLibraryFilter to filter artists by associated music folders

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

* feat: add library access methods to User model

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

* feat: implement library access filtering for artist queries based on user permissions

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

* feat: enhance artist library filtering based on user permissions and optimize library ID retrieval

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

* fix: return error when any musicFolderId is invalid or inaccessible

Changed behavior from silently filtering invalid library IDs to returning
ErrorDataNotFound (code 70) when any provided musicFolderId parameter
is invalid or the user doesn't have access to it.

The error message includes the specific library number for better debugging.
This affects album/song list endpoints (getAlbumList, getRandomSongs,
getSongsByGenre, getStarred) to provide consistent error handling
across all Subsonic API endpoints.

Updated corresponding tests to expect errors instead of silent filtering.

* feat: add musicFolderId parameter support to Search2 and Search3 endpoints

Implemented musicFolderId parameter support for Subsonic API Search2 and Search3 endpoints, completing multi-library functionality across all Subsonic endpoints.

Key changes:
- Added musicFolderId parameter handling to Search2 and Search3 endpoints
- Updated search logic to filter results by specified library or all accessible libraries when parameter not provided
- Added proper error handling for invalid/inaccessible musicFolderId values
- Refactored SearchableRepository interface to support library filtering with variadic QueryOptions
- Updated repository implementations (Album, Artist, MediaFile) to handle library filtering in search operations
- Added comprehensive test coverage with robust assertions verifying library filtering works correctly
- Enhanced mock repositories to capture QueryOptions for test validation

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

* feat: refresh LibraryList on scan end

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

* fix: allow editing name of main library

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

* refactor: implement SendBroadcastMessage method for event broadcasting

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

* feat: add event broadcasting for library creation, update, and deletion

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

* feat: add useRefreshOnEvents hook for custom refresh logic on event changes

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

* feat: enhance library management with refresh event broadcasting

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

* feat: replace AddUserLibrary and RemoveUserLibrary with SetUserLibraries for better library management

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

* chore: remove commented-out genre repository code from persistence tests

* feat: enhance library selection with master checkbox functionality

Added a master checkbox to the SelectLibraryInput component, allowing users to select or deselect all libraries at once. This improves user experience by simplifying the selection process when multiple libraries are available. Additionally, updated translations in the en.json file to include a new message for selecting all libraries, ensuring consistency in user interface messaging.

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

* feat: add default library assignment for new users

Introduced a new column `default_new_users` in the library table to
facilitate automatic assignment of default libraries to new regular users.
When a new user is created, they will now be assigned to libraries marked
as default, enhancing user experience by ensuring they have immediate access
to essential resources. Additionally, updated the user repository logic
to handle this new functionality and modified the user creation validation
to reflect that library selection is optional for non-admin users.

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

* fix: correct updated_at assignment in library repository

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

* fix: improve cache buffering logic

Refactored the cache buffering logic to ensure thread safety when checking
the buffer length

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

* fix formating

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

* feat: implement per-library artist statistics with automatic aggregation

Implemented comprehensive multi-library support for artist statistics that
automatically aggregates stats from user-accessible libraries. This fundamental
change moves artist statistics from global scope to per-library granularity
while maintaining backward compatibility and transparent operation.

Key changes include:
- Migrated artist statistics from global artist.stats to per-library library_artist.stats
- Added automatic library filtering and aggregation in existing Get/GetAll methods
- Updated role-based filtering to work with per-library statistics storage
- Enhanced statistics calculation to process and store stats per library
- Implemented user permission-aware aggregation that respects library access control
- Added comprehensive test coverage for library filtering and restricted user access
- Created helper functions to ensure proper library associations in tests

This enables users to see statistics that accurately reflect only the content
from libraries they have access to, providing proper multi-tenant behavior
while maintaining the existing API surface and UI functionality.

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

* feat: add multi-library support with per-library tag statistics - WIP

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

* refactor: genre and tag repositories. add comprehensive tests

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

* feat: add multi-library support to tag repository system

Implemented comprehensive library filtering for tag repositories to support the multi-library feature. This change ensures that users only see tags from libraries they have access to, while admin users can see all tags.

Key changes:
- Enhanced TagRepository.Add() method to accept libraryID parameter for proper library association
- Updated baseTagRepository to implement library-aware queries with proper joins
- Added library_tag table integration for per-library tag statistics
- Implemented user permission-based filtering through user_library associations
- Added comprehensive test coverage for library filtering scenarios
- Updated UI data provider to include tag filtering by selected libraries
- Modified scanner to pass library ID when adding tags during folder processing

The implementation maintains backward compatibility while providing proper isolation between libraries for tag-based operations like genres and other metadata tags.

* refactor: simplify artist repository library filtering

Removed conditional admin logic from applyLibraryFilterToArtistQuery method
and unified the library filtering approach to match the tag repository pattern.
The method now always uses the same SQL join structure regardless of user role,
with admin access handled automatically through user_library associations.

Added artistLibraryIdFilter function to properly qualify library_id column
references and prevent SQL ambiguity errors when multiple tables contain
library_id columns. This ensures the filter targets library_artist.library_id
specifically rather than causing ambiguous column name conflicts.

* fix: resolve LibrarySelectionField validation error for non-admin users

Fixed validation error 'At least one library must be selected for non-admin users' that appeared even when libraries were selected. The issue was caused by a data format mismatch between backend and frontend.

The backend sends user data with libraries as an array of objects, but the LibrarySelectionField component expects libraryIds as an array of IDs. Added data transformation in the data provider's getOne method to automatically convert libraries array to libraryIds format when fetching user records.

Also extracted validation logic into a separate userValidation module for better code organization and added comprehensive test coverage to prevent similar issues.

* refactor: remove unused library access functions and related tests

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

* refactor: rename search_test.go to searching_test.go for consistency

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

* fix: add user context to scrobble buffer getParticipants call

Added user context handling to scrobbleBufferRepository.Next method to resolve
SQL error 'no such column: library_artist.library_id' when processing scrobble
entries in multi-library environments. The artist repository now requires user
context for proper library filtering, so we fetch the user and temporarily
inject it into the context before calling getParticipants. This ensures
background scrobbling operations work correctly with multi-library support.

* feat: add cross-library move detection for scanner

Implemented cross-library move detection for the scanner phase 2 to properly handle files moved between libraries. This prevents users from losing play counts, ratings, and other metadata when moving files across library boundaries.

Changes include:
- Added MediaFileRepository methods for two-tier matching: FindRecentFilesByMBZTrackID (primary) and FindRecentFilesByProperties (fallback)
- Extended scanner phase 2 pipeline with processCrossLibraryMoves stage that processes files unmatched within their library
- Implemented findCrossLibraryMatch with MusicBrainz Release Track ID priority and intrinsic properties fallback
- Updated producer logic to handle missing tracks without matches, ensuring cross-library processing
- Updated tests to reflect new producer behavior and cross-library functionality

The implementation uses existing moveMatched function for unified move operations, automatically preserving all user data through database foreign key relationships. Cross-library moves are detected using the same Equals() and IsEquivalent() matching logic as within-library moves for consistency.

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

* feat: add album annotation reassignment for cross-library moves

Implemented album annotation reassignment functionality for the scanner's missing tracks phase. When tracks move between libraries and change album IDs, the system now properly reassigns album annotations (starred status, ratings) from the old album to the new album. This prevents loss of user annotations when tracks are moved across library boundaries.

The implementation includes:
- Thread-safe annotation reassignment using mutex protection
- Duplicate reassignment prevention through processed album tracking
- Graceful error handling that doesn't fail the entire move operation
- Comprehensive test coverage for various scenarios including error conditions

This enhancement ensures data integrity and user experience continuity during cross-library media file movements.

* fix: address PR review comments for multi-library support

Fixed several issues identified in PR review:

- Removed unnecessary artist stats initialization check since the map is already initialized in PostScan()
- Improved code clarity in user repository by extracting isNewUser variable to avoid checking count == 0 twice
- Fixed library selection logic to properly handle initial library state and prevent overriding user selections

These changes address code quality and logic issues identified during the multi-library support PR review.

* feat: add automatic playlist statistics refreshing

Implemented automatic playlist statistics (duration, size, song count) refreshing
when tracks are modified. Added new refreshStats() method to recalculate
statistics from playlist tracks, and SetTracks() method to update tracks
and refresh statistics atomically. Modified all track manipulation methods
(RemoveTracks, AddTracks, AddMediaFiles) to automatically refresh statistics.
Updated playlist repository to use the new SetTracks method for consistent
statistics handling.

* refactor: rename AddTracks to AddMediaFilesByID for clarity

Renamed the AddTracks method to AddMediaFilesByID throughout the codebase
to better reflect its purpose of adding media files to a playlist by their IDs.
This change improves code readability and makes the method name more descriptive
of its actual functionality. Updated all references in playlist model, tests,
core playlist logic, and Subsonic API handlers to use the new method name.

* refactor: consolidate user context access in persistence layer

Removed duplicate helper functions userId() and isAdmin() from sql_base_repository.go and consolidated all user context access to use loggedUser(r.ctx).ID and loggedUser(r.ctx).IsAdmin consistently across the persistence layer.

This change eliminates code duplication and provides a single, consistent pattern for accessing user context information in repository methods. All functionality remains unchanged - this is purely a code cleanup refactoring.

* refactor: eliminate MockLibraryService duplication using embedded struct

- Replace 235-line MockLibraryService with 40-line embedded struct pattern
- Enhance MockLibraryRepo with service-layer methods (192→310 lines)
- Maintain full compatibility with existing tests
- All 72 nativeapi specs pass with proper error handling

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

* refactor: cleanup

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2025-07-18 18:41:12 -04:00
Deluan Quintão
b63630fa6e fix(scanner) artist stats not refreshing during quick scan and after missing file deletion (#4269)
* Fix artist not being marked as touched during quick scans

When a new album is added during quick scans, artists were not being
marked as 'touched' due to media files having older modification times
than the scan completion time.

Changes:
- Add 'updated_at' to artist Put() columns in scanner to ensure
  timestamp is set when artists are processed
- Simplify RefreshStats query to check artist.updated_at directly
  instead of complex media file joins
- Artists from new albums now properly get refreshed in later phases

This fixes the issue where newly added albums would have incomplete
artist information after quick scans.

* fix(missing): refresh artist stats in background after deleting missing files

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

* fix(request): add InternalAuth to user context

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

* Add comprehensive test for artist stats update during quick scans

- Add test that verifies artist statistics are correctly updated when new files are added during incremental scans
- Test ensures both overall stats (AlbumCount, SongCount) and role-specific stats are properly refreshed
- Validates fix for artist stats not being refreshed during quick scans when new albums are added
- Uses real artist repository instead of mock to verify actual stats calculation behavior

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2025-06-26 15:50:56 -04:00
Deluan Quintão
410e457e5a feat(server): add update and clear play queue endpoints to native API (#4215)
* Refactor queue payload handling

* Refine queue update validation

* refactor(queue): avoid loading tracks for validation

* refactor/rename repository methods

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

* more tests

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

* refactor

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2025-06-11 12:02:31 -04:00
Deluan Quintão
8fcd8ba61a feat(server): add index-based play queue endpoints to native API (#4210)
* Add migration converting playqueue current to index

* refactor

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

* fix(queue): ensure valid current index and improve test coverage

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2025-06-10 23:00:44 -04:00
Deluan Quintão
4172d2332a feat(ui): add song Love and Rating functionality to playlist view (#4134)
* feat(ui): add playlist track love button

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

* feat(ui): add star rating feature for playlist tracks

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

* fix(ui): handle loading state and error logging in toggle love and rating components

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2025-06-04 20:38:28 -04:00
Deluan
9c4af3c6d0 fix(server): don't override /song routes
Signed-off-by: Deluan <deluan@navidrome.org>
2025-06-01 14:41:50 -04:00
Deluan Quintão
36ed2f2f58 refactor: simplify configuration endpoint with JSON serialization (#4159)
* refactor(config): reorganize configuration handling

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

* refactor(aboutUtils): improve array formatting and handling in TOML conversion

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

* refactor(aboutUtils): add escapeTomlKey function to handle special characters in TOML keys

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

* fix(test): remove unused getNestedValue function

* fix(ui): apply prettier formatting

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2025-05-31 19:37:23 -04:00
Deluan Quintão
ded8cf236e feat(ui): add 'Show in Playlist' context menu (#4139)
* Update song playlist menu and endpoint

* feat(ui): show submenu on click, not on hover

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

* feat(ui): integrate dataProvider for fetching playlists in song context menu

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

* feat(ui): update song context menu to use dataProvider for fetching playlists and inspecting songs

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

* feat(ui): stop event propagation when closing playlist submenu

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

* feat(ui): add 'show in playlist' option to options object

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2025-05-30 21:26:35 -04:00
Deluan Quintão
6dd98e0bed feat(ui): add configuration tab in About dialog (#4142)
* Flatten config endpoint and improve About dialog

* add config resource

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

* fix(ui): replace `==` with `===`

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

* feat(ui): add environment variables

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

* feat(ui): add sensitive value redaction

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

* feat(ui): more translations

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

* address PR comments

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

* feat(ui): add configuration export feature in About dialog

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

* feat(ui): translate development flags section header

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

* refactor

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

* feat(api): refactor routes for keepalive and insights endpoints

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

* lint

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

* fix(ui): enhance string escaping in formatTomlValue function

Updated the formatTomlValue function to properly escape backslashes in addition to quotes. Added new test cases to ensure correct handling of strings containing both backslashes and quotes.

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

* feat(ui): adjust dialog size

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2025-05-30 21:07:08 -04:00
Deluan Quintão
3ccc02f375 feat(ui): add remove all missing files functionality (#4108)
* Add remove all missing files feature

* test: update mediafile_repository tests for missing files deletion

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2025-05-22 22:28:10 -04:00
Deluan Quintão
1468a56808 fix(server): reduce SQLite "database busy" errors (#3760)
* fix(scanner): remove transactions where they are not strictly needed

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

* fix(server): force setStar transaction to start as IMMEDIATE

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

* fix(server): encapsulated way to upgrade tx to write mode

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

* fix(server): use tx immediate for some playlist endpoints

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

* make more transactions immediate (#3759)

---------

Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Kendall Garner <17521368+kgarner7@users.noreply.github.com>
2025-02-26 22:01:49 -05:00
Deluan Quintão
c795bcfcf7 feat(bfr): Big Refactor: new scanner, lots of new fields and tags, improvements and DB schema changes (#2709)
* fix(server): more race conditions when updating artist/album from external sources

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

* feat(scanner): add .gitignore syntax to .ndignore. Resolves #1394

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

* fix(ui): null

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

* fix(scanner): pass configfile option to child process

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

* fix(scanner): resume interrupted fullScans

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

* fix(scanner): remove old scanner code

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

* fix(scanner): rename old metadata package

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

* fix(scanner): move old metadata package

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

* fix: tests

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

* chore(deps): update Go to 1.23.4

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

* fix: logs

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

* fix(test):

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

* fix: log level

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

* fix: remove log message

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

* feat: add config for scanner watcher

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

* refactor: children playlists

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

* refactor: replace `interface{}` with `any`

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

* fix: smart playlists with genres

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

* fix: allow any tags in smart playlists

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

* fix: artist names in playlists

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

* fix: smart playlist's sort by tags

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

* feat(subsonic): add moods to child

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

* feat(subsonic): add moods to AlbumID3

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

* refactor(subsonic): use generic JSONArray for OS arrays

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

* refactor(subsonic): use https in test

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

* feat(subsonic): add releaseTypes to AlbumID3

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

* feat(subsonic): add recordLabels to AlbumID3

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

* refactor(subsonic): rename JSONArray to Array

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

* feat(subsonic): add artists to AlbumID3

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

* feat(subsonic): add artists to Child

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

* fix(scanner): do not pre-populate smart playlists

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

* feat(subsonic): implement a simplified version of ArtistID3.

See https://github.com/opensubsonic/open-subsonic-api/discussions/120

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

* feat(subsonic): add artists to album child

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

* feat(subsonic): add contributors to mediafile Child

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

* feat(subsonic): add albumArtists to mediafile Child

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

* feat(subsonic): add displayArtist and displayAlbumArtist

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

* feat(subsonic): add displayComposer to Child

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

* feat(subsonic): add roles to ArtistID3

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

* fix(subsonic): use " • " separator for displayComposer

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

* refactor:

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

* fix(subsonic):

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

* fix(subsonic): respect `PreferSortTags` config option

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

* refactor(subsonic):

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

* refactor: optimize purging non-unused tags

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

* refactor: don't run 'refresh artist stats' concurrently with other transactions

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

* refactor:

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

* fix: log message

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

* feat: add Scanner.ScanOnStartup config option, default true

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

* feat: better json parsing error msg when importing NSPs

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

* fix: don't update album's imported_time when updating external_metadata

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

* fix: handle interrupted scans and full scans after migrations

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

* feat: run `analyze` when migration requires a full rescan

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

* feat: run `PRAGMA optimize` at the end of the scan

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

* fix: don't update artist's updated_at when updating external_metadata

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

* feat: handle multiple artists and roles in smart playlists

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

* feat(ui): dim missing tracks

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

* fix: album missing logic

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

* fix: error encoding in gob

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

* feat: separate warnings from errors

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

* fix: mark albums as missing if they were contained in a deleted folder

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

* refactor: add participant names to media_file and album tables

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

* refactor: use participations in criteria, instead of m2m relationship

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

* refactor: rename participations to participants

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

* feat(subsonic): add moods to album child

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

* fix: albumartist role case

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

* feat(scanner): run scanner as an external process by default

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

* fix(ui): show albumArtist names

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

* fix(ui): dim out missing albums

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

* fix: flaky test

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

* fix(server): scrobble buffer mapping. fix #3583

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

* refactor: more participations renaming

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

* fix: listenbrainz scrobbling

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

* feat: send release_group_mbid to listenbrainz

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

* feat(subsonic): implement OpenSubsonic explicitStatus field (#3597)

* feat: implement OpenSubsonic explicitStatus field

* fix(subsonic): fix failing snapshot tests

* refactor: create helper for setting explicitStatus

* fix: store smaller values for explicit-status on database

* test: ToAlbum explicitStatus

* refactor: rename explicitStatus helper function

---------

Co-authored-by: Deluan Quintão <deluan@navidrome.org>

* fix: handle album and track tags in the DB based on the mappings.yaml file

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

* save similar artists as JSONB

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

* fix: getAlbumList byGenre

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

* detect changes in PID configuration

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

* set default album PID to legacy_pid

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

* fix tests

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

* fix SIGSEGV

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

* fix: don't lose album stars/ratings when migrating

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

* store full PID conf in properties

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

* fix: keep album annotations when changing PID.Album config

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

* fix: reassign album annotations

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

* feat: use (display) albumArtist and add links to each artist

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

* fix: not showing albums by albumartist

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

* fix: error msgs

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

* fix: hide PID from Native API

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

* fix: album cover art resolution

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

* fix: trim participant names

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

* fix: reduce watcher log spam

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

* fix: panic when initializing the watcher

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

* fix: various artists

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

* fix: don't store empty lyrics in the DB

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

* remove unused methods

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

* drop full_text indexes, as they are not being used by SQLite

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

* keep album created_at when upgrading

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

* fix(ui): null pointer

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

* fix: album artwork cache

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

* fix: don't expose missing files in Subsonic API

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

* refactor: searchable interface

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

* fix: filter out missing items from subsonic search

* fix: filter out missing items from playlists

* fix: filter out missing items from shares

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

* feat(ui): add filter by artist role

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

* feat(subsonic): only return albumartists in getIndexes and getArtists endpoints

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

* sort roles alphabetically

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

* fix: artist playcounts

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

* change default Album PID conf

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

* fix albumartist link when it does not match any albumartists values

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

* fix `Ignoring filter not whitelisted` (role) message

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

* fix: trim any names/titles being imported

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

* remove unused genre code

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

* serialize calls to Last.fm's getArtist

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

xxx

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

* add counters to genres

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

* nit: fix migration `notice` message

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

* optimize similar artists query

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

* fix: last.fm.getInfo when mbid does not exist

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

* ui only show missing items for admins

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

* don't allow interaction with missing items

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

* Add Missing Files view (WIP)

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

* refactor: merged tag_counts into tag table

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

* add option to completely disable automatic scanner

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

* add delete missing files functionality

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

* fix: playlists not showing for regular users

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

* reduce updateLastAccess frequency to once every minute

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

* reduce update player frequency to once every minute

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

* add timeout when updating player

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

* remove dead code

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

* fix duplicated roles in stats

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

* add `; ` to artist splitters

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

* fix stats query

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

* more logs

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

* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP

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

* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP

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

* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP

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

* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP

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

* add record label filter

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

* add release type filter

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

* fix purgeUnused tags

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

* add grouping filter to albums

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

* allow any album tags to be used in as filters in the API

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

* remove empty tags from album info

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

* comments in the migration

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

* fix: Cannot read properties of undefined

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

* fix: listenbrainz scrobbling (#3640)

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

* fix: remove duplicated tag values

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

* fix: don't ignore the taglib folder!

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

* feat: show track subtitle tag

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

* fix: show artists stats based on selected role

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

* fix: inspect

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

* add media type to album info/filters

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

* fix: change format of subtitle in the UI

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

* fix: subtitle in Subsonic API and search

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

* fix: subtitle in UI's player

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

* fix: split strings should be case-insensitive

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

* disable ScanSchedule

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

* increase default sessiontimeout

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

* add sqlite command line tool to docker image

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

* fix: resources override

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

* fix: album PID conf

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

* change migration to mark current artists as albumArtists

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

* feat(ui): Allow filtering on multiple genres (#3679)

* feat(ui): Allow filtering on multiple genres

Signed-off-by: Henrik Nordvik <henrikno@gmail.com>
Signed-off-by: Deluan <deluan@navidrome.org>

* add multi-genre filter in Album list

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

---------

Signed-off-by: Henrik Nordvik <henrikno@gmail.com>
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Henrik Nordvik <henrikno@gmail.com>

* add more multi-valued tag filters to Album and Song views

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

* fix(ui): unselect missing files after removing

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

* fix(ui): song filter

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

* fix sharing tracks. fix #3687

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

* use rowids when using search for sync (ex: Symfonium)

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

* fix "Report Real Paths" option for subsonic clients

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

* fix "Report Real Paths" option for subsonic clients for search

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

* add libraryPath to Native API /songs endpoint

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

* feat(subsonic): add album version

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

* made all tags lowercase as they are case-insensitive anyways.

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

* feat(ui): Show full paths, extended properties for album/song (#3691)

* feat(ui): Show full paths, extended properties for album/song

- uses library path + os separator + path
- show participants (album/song) and tags (song)
- make album/participant clickable in show info

* add source to path

* fix pathSeparator in UI

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

* fix local artist artwork (#3695)

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

* fix: parse vorbis performers

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

* refactor: clean function into smaller functions

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

* fix translations for en and pt

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

* add trace log to show annotations reassignment

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

* add trace log to show annotations reassignment

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

* fix: allow performers without instrument/subrole

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

* refactor: metadata clean function again

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

* refactor: optimize split function

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

* refactor: split function is now a method of TagConf

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

* fix: humanize Artist total size

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

* add album version to album details

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

* don't display album-level tags in SongInfo

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

* fix genre clicking in Album Page

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

* don't use mbids in Last.fm api calls.

From https://discord.com/channels/671335427726114836/704303730660737113/1337574018143879248:

With MBID:
```
GET https://ws.audioscrobbler.com/2.0/?api_key=XXXX&artist=Van+Morrison&format=json&lang=en&mbid=a41ac10f-0a56-4672-9161-b83f9b223559&method=artist.getInfo

{
artist: {
name: "Bee Gees",
mbid: "bf0f7e29-dfe1-416c-b5c6-f9ebc19ea810",
url: "https://www.last.fm/music/Bee+Gees",
}
```

Without MBID:
```
GET https://ws.audioscrobbler.com/2.0/?api_key=XXXX&artist=Van+Morrison&format=json&lang=en&method=artist.getInfo

{
artist: {
name: "Van Morrison",
mbid: "a41ac10f-0a56-4672-9161-b83f9b223559",
url: "https://www.last.fm/music/Van+Morrison",
}
```

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

* better logging for when the artist folder is not found

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

* fix various issues with artist image resolution

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

* hide "Additional Tags" header if there are none.

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

* simplify tag rendering

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

* enhance logging for artist folder detection

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

* make folderID consistent for relative and absolute folderPaths

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

* handle more folder paths scenarios

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

* filter out other roles when SubsonicArtistParticipations = true

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

* fix "Cannot read properties of undefined"

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

* fix lyrics and comments being truncated (#3701)

* fix lyrics and comments being truncated

* specifically test for lyrics and comment length

* reorder assertions

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

---------

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

* fix(server): Expose library_path for playlist (#3705)

Allows showing absolute path for UI, and makes "report real path" work for playlists (Subsonic)

* fix BFR on Windows (#3704)

* fix potential reflected cross-site scripting vulnerability

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

* hack to make it work on Windows

* ignore windows executables

* try fixing the pipeline

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

* allow MusicFolder in other drives

* move windows local drive logic to local storage implementation

---------

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

* increase pagination sizes for missing files

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

* reduce level of "already scanning" watcher log message

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

* only count folders with audio files in it

See https://github.com/navidrome/navidrome/discussions/3676#discussioncomment-11990930

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

* add album version and catalog number to search

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

* add `organization` alias for `recordlabel`

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

* remove mbid from Last.fm agent

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

* feat: support inspect in ui (#3726)

* inspect in ui

* address round 1

* add catalogNum to AlbumInfo

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

* remove dependency on metadata_old (deprecated) package

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

* add `RawTags` to model

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

* support parsing MBIDs for roles (from the https://github.com/kgarner7/picard-all-mbids plugin) (#3698)


* parse standard roles, vorbis/m4a work for now

* fix djmixer

* working roles, use DJ-mix

* add performers to file

* map mbids

* add a few more tests

* add test

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

* try to simplify the performers logic

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

* stylistic changes

---------

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

* remove param mutation

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

* run automated SQLite optimizations

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

* fix playlists import/export on Windows

* fix import playlists

* fix export playlists

* better handling of Windows volumes

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

* handle more album ID reassignments

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

* allow adding/overriding tags in the config file

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

* fix(ui): Fix playlist track id, handle missing tracks better (#3734)

- Use `mediaFileId` instead of `id` for playlist tracks
- Only fetch if the file is not missing
- If extractor fails to get the file, also error (rather than panic)

* optimize DB after each scan.

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

* remove sortable from AlbumSongs columns

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

* simplify query to get missing tracks

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

* mark Scanner.Extractor as deprecated

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
Signed-off-by: Henrik Nordvik <henrikno@gmail.com>
Co-authored-by: Caio Cotts <caio@cotts.com.br>
Co-authored-by: Henrik Nordvik <henrikno@gmail.com>
Co-authored-by: Kendall Garner <17521368+kgarner7@users.noreply.github.com>
2025-02-19 20:35:17 -05:00
Deluan Quintão
8e2052ff95 feat(Insights): add anonymous usage data collection (#3543)
* feat(insights): initial code (WIP)

* feat(insights): add more info

* feat(insights): add fs info

* feat(insights): export insights.Data

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

* feat(insights): more config info

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

* refactor(insights): move data struct to its own package

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

* refactor(insights): omit some attrs if empty

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

* feat(insights): send insights to server, add option to disable

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

* fix(insights): remove info about anonymous login

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

* chore(insights): fix lint

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

* fix(insights): disable collector if EnableExternalServices is false

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

* fix(insights): fix type casting for 32bit platforms

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

* fix(insights): remove EnableExternalServices from the collection (as it will always be false)

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

* chore(insights): fix lint

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

* refactor(insights): rename function for consistency

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

* feat(insights): log the data sent to the collector server

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

* feat(insights): add last collection timestamp to the "about" dialog.

Also add opt-out info to the SignUp form

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

* feat(insights): only sends the initial data collection after an admin user is created

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

* feat(insights): remove dangling comment

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

* feat(insights): Translate insights messages

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

* fix(insights): reporting empty library

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

* refactor: move URL to consts.js

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2024-12-17 17:10:55 -05:00
Deluan
c1adf407a1 refactor: load translations with sync.OnceValues
Signed-off-by: Deluan <deluan@navidrome.org>
2024-11-18 14:07:31 +02:00
Deluan Quintão
a9334b7787 feat(ui): show user's lastAccess (#3342)
* feat(server): update user's lastAccess

* feat(ui): show user's lastAccess
2024-09-30 20:46:10 -04:00
Kendall Garner
9f1794b97e Only refresh smart playlist when fetching first track (#3244)
* Only refresh smart playlist when fetching first track

* res -> w
2024-09-10 20:18:37 -04:00
Deluan
dfcc189cff Replace all utils.Param* with req.Params 2023-12-21 17:41:09 -05:00
Sam Watson
26472f46fe POST endpoint for importing m3u playlists - #2078 (#2273)
* wip: API endpoint for creating playlists from m3u files

* wip: get user id from context

* temporarily disable failing test

* custom logic for playlist route to accomodate m3u content type

* incorporate playlist parsing into existing logic in core

* re-enable test

* fix locally failing test

* Address requested changes.

* Improve ImportFile tests.

* Remove ownerID as a parameter of ImportM3U.

* Write tests for ImportM3U.

* Separate ImportM3U test into two.

* Test OwnerID and playlist Name.

---------

Co-authored-by: Sam Watson <SwatsonCodes@users.noreply.github.com>
Co-authored-by: caiocotts <caio@cotts.com.br>
2023-11-01 14:59:47 -04:00
Deluan
1c7fb74a1d Fix writeEvents race condition.
This required removing the compress middleware from the /events route.
2023-04-01 20:54:15 -04:00
Deluan
68e6115789 Rename DevEnableShare to EnableSharing 2023-01-29 20:33:10 -05:00
Deluan
e2ea5eba8c Disable creation of shares when feature is disabled.
Fix https://github.com/navidrome/navidrome/pull/2106#issuecomment-1404731388
2023-01-26 10:12:52 -05:00
Kendall Garner
8877b1695a Add Internet Radio support (#2063)
* add internet radio support

* Add dynamic sidebar icon to Radios

* Fix typos

* Make URL suffix consistent

* Fix typo

* address feedback

* Don't need to preload when playing Internet Radios

* Reorder migration, or else it won't be applied

* Make Radio list view responsive

Also added filter by name, removed RadioActions and RadioContextMenu, and added a default radio icon, in case of favicon is not available.

* Simplify StreamField usage

* fix button, hide progress on mobile

* use js styles over index.css

Co-authored-by: Deluan <deluan@navidrome.org>
2023-01-15 15:11:37 -05:00
Deluan
16c869ec86 Optimize playlist cover generation 2023-01-13 22:18:34 -05:00
Deluan
387acc5f63 Add public endpoint to expose images 2023-01-13 22:18:34 -05:00
Deluan
df0f140f9f Don't refresh smart playlists when generating covers 2023-01-01 20:28:03 -05:00
Deluan
5943e8f953 Rename log.LevelCritical to log.LevelFatal 2022-12-21 14:53:36 -05:00
Deluan
28389fb05e Add command line M3U exporter. Closes #1914 2022-12-21 14:39:40 -05:00