Commit graph

4,959 commits

Author SHA1 Message Date
Deluan
ead911a9e1 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).
2026-07-05 23:22:37 -04:00
Deluan
b3247591cc fix(jellyfin): support playlist cover upload/delete and display via item image endpoints 2026-07-05 22:44:45 -04:00
Deluan
f80ee0e41b fix(jellyfin): emit per-image ImageBlurHashes so clients de-dupe images and stop warning 2026-07-05 22:35:01 -04:00
Deluan
cb7b938fe6 fix(jellyfin): do not disable Jellyfin server when disabling external services 2026-07-05 22:32:09 -04:00
Deluan
7953613d61 fix(jellyfin): support Ids batch-fetch in /Items so clients can fetch items by id 2026-07-05 22:24:28 -04:00
Deluan
c70d3c17e6 fix(jellyfin): populate MediaStreams with the audio stream so clients can size/transcode 2026-07-05 22:14:56 -04:00
Deluan
1703b59d24 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.
2026-07-05 22:07:32 -04:00
Deluan
b5158f8799 fix(jellyfin): populate all required MediaSourceInfo bool/array fields to match Jellyfin 2026-07-05 21:36:35 -04:00
Deluan
200ae482c3 fix(jellyfin): include MediaSources with Size/Bitrate on tracks so clients show download size 2026-07-05 21:31:06 -04:00
Deluan
da451cf927 fix(jellyfin): read lowercase ids/entryIds playlist params and add playlist-users endpoints 2026-07-05 21:23:55 -04:00
Deluan
17c1b44e60 fix(jellyfin): resolve playlist ids in getItem 2026-07-05 21:15:14 -04:00
Deluan
0c4e6b2c70 fix(jellyfin): serve direct file at /Items/{id}/File and accept ApiKey query param 2026-07-05 21:06:28 -04:00
Deluan
4e277b1929 fix(jellyfin): resolve library-view ids in getItem so clients can load the library 2026-07-05 20:55:09 -04:00
Deluan
da51d3f1c6 feat(jellyfin): implement /socket WebSocket for real-time client sessions 2026-07-05 20:43:03 -04:00
Deluan
3a155e23e8 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.
2026-07-05 20:28:20 -04:00
Deluan
ae78d779ca fix(jellyfin): return User Policy and Configuration so Finamp completes login 2026-07-05 19:56:26 -04:00
Deluan
50e0f91032 refactor(server): extract case-insensitive path routing to a shared helper 2026-07-05 18:19:41 -04:00
Deluan
8e55e64337 fix(jellyfin): case-insensitive path routing for Jellyfin client compatibility 2026-07-05 18:10:53 -04:00
Deluan
46f3b4dbc8 docs(jellyfin): clarify public image endpoint and accessibleLibraryIDs comments 2026-07-05 17:57:53 -04:00
Deluan
106a1e2501 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.
2026-07-05 17:33:41 -04:00
Deluan
15f5ba16e8 test(jellyfin): cover empty Ids/EntryIds in playlist add/remove 2026-07-05 17:27:17 -04:00
Deluan
34c1ebc4be 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.
2026-07-05 17:21:42 -04:00
Deluan
ca38e50c4f 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.
2026-07-05 17:05:43 -04:00
Deluan
d28adb2676 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.
2026-07-05 16:51:02 -04:00
Deluan
7e26f1fa8c feat(jellyfin): audio streaming and PlaybackInfo with library access control 2026-07-05 16:33:48 -04:00
Deluan
926a36acfa fix(jellyfin): let net/http sniff image Content-Type instead of forcing jpeg 2026-07-05 16:27:25 -04:00
Deluan
6e4a180feb feat(jellyfin): item image endpoint via artwork service 2026-07-05 16:20:39 -04:00
Deluan
30d3efebb9 refactor(jellyfin): share resolveLibraryScope between items and artists 2026-07-05 16:16:26 -04:00
Deluan
65b92168f9 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.
2026-07-05 16:10:07 -04:00
Deluan
c7d1f69c1b test(jellyfin): cover admin library-access path and drop nil test contexts 2026-07-05 16:04:10 -04:00
Deluan
fbce1d3500 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.
2026-07-05 15:53:02 -04:00
Deluan
d921bb6a95 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.
2026-07-05 15:42:20 -04:00
Deluan
0b9fbcd048 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).
2026-07-05 15:27:53 -04:00
Deluan
aacc46f7f5 feat(jellyfin): user views and current-user endpoints 2026-07-05 15:07:43 -04:00
Deluan
d5c6e9e451 refactor(jellyfin): use new(x) builtin instead of intPtr helper 2026-07-05 15:03:23 -04:00
Deluan
92c66178de fix(jellyfin): reject empty login password, wire ServerName, add negative auth tests 2026-07-05 15:01:02 -04:00
Deluan
535d252d17 feat(jellyfin): authentication middleware and AuthenticateByName login 2026-07-05 14:51:46 -04:00
Deluan
ce7e7513fb fix(jellyfin): map LastPlayedDate and omit zero track/disc numbers 2026-07-05 14:46:12 -04:00
Deluan
2a18863c06 feat(jellyfin): advertise Jellyfin-compatible version, brand ServerName with Navidrome version 2026-07-05 14:38:43 -04:00
Deluan
da9bb25313 test(jellyfin): cover GenreToBaseItem mapper 2026-07-05 14:27:14 -04:00
Deluan
2e0e3264c7 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.
2026-07-05 14:26:24 -04:00
Deluan
8a4b7d1221 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.
2026-07-05 14:18:39 -04:00
Deluan
73376f519d fix(jellyfin): memoize stable server Id and cover get-or-create path 2026-07-05 14:13:18 -04:00
Deluan
910c239e29 test(jellyfin): cover ping and quickConnectEnabled handlers
Both were added alongside the System/Info/Public handshake endpoint but
had no direct test coverage.
2026-07-05 14:05:09 -04:00
Deluan
b38750fd5b 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.
2026-07-05 14:03:40 -04:00
Deluan
95e33c92ca fix(jellyfin): default Jellyfin ServerName to "Navidrome" 2026-07-05 13:57:21 -04:00
Deluan
36240d4582 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.
2026-07-05 13:53:16 -04:00
Deluan
5e913ae6a4 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.
2026-07-04 19:49:19 -04:00
Deluan Quintão
d4387c5502
perf(db): index media_file album/artist sort orders (#5706)
* perf(db): add composite indexes for song list album/artist sorts

The media_file sort mappings for album, artist and albumArtist expand to
multi-column ORDER BY clauses that no existing index could satisfy, so SQLite
fell back to a full table scan plus a temp B-tree sort of every row (including
the large lyrics/tags/full_text columns) even for a single 15-item page. On a
96K-track library this made /api/song?_sort=album take 3.6s on a cold cache.

Add composite indexes matching the three sort mappings, allowing the query to
walk the index and stop at the page size, in both directions. Drop the now
redundant single-column order_album_name/order_artist_name indexes (strict
prefixes of the new composites) and three indexes with no query path:
birth_time is only read in Go code, and artist/album_artist text column
lookups go through the media_file_artists table instead.

* fix(ui): make composer and track number columns non-sortable in song list

Clicking the Composer header was a silent no-op: composer is not a media_file
column, so the native API's sanitizeSort drops the sort and returns rows in
table order. Track number sorting across the whole library is not meaningful
and cannot use an index (the existing index leads with disc_number). Mark both
columns sortable={false}, like quality and mood.

* test(persistence): add sort index coverage test for large tables

Guard against sort options silently losing index support: every sort mapping
on media_file, album and artist is now verified with EXPLAIN QUERY PLAN to be
satisfiable by an index (both directions), so adding a mapping or dropping an
index that reintroduces a full-table temp B-tree sort fails the test. Sorts
that genuinely cannot use an index (random, annotation-join columns, JSON
expressions) must be declared in an exceptions list with the reason, keeping
the trade-off visible in review.

To make the sort mappings the complete declared sort surface, add identity
mappings for the media_file columns the UI sorts by without a mapping (year,
genre, duration, channels, bpm, path, comment, play_count, play_date, rating).
These are behaviorally no-ops: the same ORDER BY was previously produced by
the field whitelist fallback.

* perf(db): drop PreferSortTags expression indexes from media_file

The media_file sort_title/sort_artist_name/sort_album_name expression indexes
are only usable when PreferSortTags is enabled - a config reported by ~0.1% of
installations (insights, week of 2026-06-22) - yet every install pays their
storage (~8.6MB on a 96K-track library) and scanner write overhead. Drop them:
PreferSortTags installs fall back to a full sort for title/artist/album orders,
everyone else gets smaller DBs and cheaper writes. The order_album_name and
order_artist_name collation checks remain valid, now satisfied by the composite
sort indexes.

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-07-03 08:58:55 -04:00
Deluan Quintão
ae9b8a5fe6
feat(search): rank exact matches above prefix matches (#5704)
* feat(search): boost exact token matches over prefix matches

buildFTS5Query now emits (word OR word*) instead of word* for plain tokens. The match set is unchanged (exact is a subset of prefix), but bm25 gives the rare exact token a high-IDF contribution, so rows containing the literal query word rank above prefix-only matches. The degraded-query check keeps evaluating the plain prefix form, preserving the LIKE fallback for queries like "1+" and "C++".

* feat(search): weight artist search_normalized equal to name in bm25

For the artist table, search_normalized holds only the artist's name in alternate spelling (transliterated/punctuation-stripped), so a hit there is as meaningful as a name hit. Combined with exact-token boosting, artists like MØ now rank in the top results for the query "MO" instead of dead last. media_file and album keep weight 1.0 because their search_normalized mixes title, album, and artist variants.

* test(persistence): add exact-match ranking regression test

Seeds MØ, Morrissey, and Modest Mouse and asserts MØ ranks first for the queries "MO" and "MØ": the exact transliterated hit in search_normalized must outrank name-prefix matches. The corpus deliberately has no competing exact-word names, since exact-vs-exact ordering depends on corpus statistics rather than the guaranteed exact-over-prefix property. Rows are inserted per-test (with their library_artist associations) and cleaned up to avoid disturbing the shared seed fixtures and their count assertions.

* docs(search): document exact-token OR emission in buildFTS5Query

* test(persistence): harden exact-match ranking test fixtures

Register the corpus cleanup before the insert loop so a mid-loop assertion failure cannot leak fts-rank-% rows into the shared integration DB, and reuse the existing createArtistWithLibrary helper instead of hand-rolling Put+AddArtist (which also replaces the ad-hoc context.TODO with the helper's GinkgoT().Context).

* fix(search): flag multi-word degraded queries for the LIKE fallback

The degradation probe was joined with explicit " AND " like the real query, so ftsQueryDegraded counted the literal AND as a long token and never flagged queries where every term degrades to a short token (e.g. "1+ 2+"). This predates this branch (the old code passed the same AND-joined string), but the probe now exists separately, so join it with spaces — it only feeds ftsQueryDegraded, which needs no explicit operators.
2026-07-02 15:51:03 -04:00