Commit graph

816 commits

Author SHA1 Message Date
Deluan
a2b8d8c0d0 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.
2026-07-06 07:45:25 -04:00
Deluan
b10ec9e9c9 test(jellyfin): e2e for streaming, sessions, and multi-user access control 2026-07-06 00:44:51 -04:00
Deluan
6aa0d6a2aa test(jellyfin): e2e for playlists (CRUD, expansion, cover) and item images 2026-07-06 00:36:04 -04:00
Deluan
121dd71ea1 test(jellyfin): e2e for system, auth, routing, browsing, annotations 2026-07-06 00:33:24 -04:00
Deluan
8ce3f0e9ef test(jellyfin): add e2e suite harness + smoke tests 2026-07-06 00:18:27 -04:00
Deluan
03d5ff12e5 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.
2026-07-06 00:03:35 -04:00
Deluan
00b9e26d36 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.
2026-07-05 23:58:43 -04:00
Deluan
d24ba9b5ea 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).
2026-07-05 23:22:37 -04:00
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
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