navidrome/server/jellyfin
2026-07-06 00:33:24 -04:00
..
dto fix(jellyfin): display playlist cover art 2026-07-05 23:22:37 -04:00
e2e test(jellyfin): e2e for system, auth, routing, browsing, annotations 2026-07-06 00:33:24 -04:00
annotations.go fix(jellyfin): hex-encode item ids at the API boundary for Jellyfin client compatibility 2026-07-05 22:07:32 -04:00
annotations_test.go fix(jellyfin): hex-encode item ids at the API boundary for Jellyfin client compatibility 2026-07-05 22:07:32 -04:00
api.go fix(jellyfin): support playlist deletion via DELETE /Items/{id} 2026-07-06 00:03:35 -04:00
api_test.go feat(jellyfin): unknown-route logging, generic 500s, rating clamp, docs 2026-07-05 17:33:41 -04:00
auth.go fix(jellyfin): return User Policy and Configuration so Finamp completes login 2026-07-05 19:56:26 -04:00
auth_test.go fix(jellyfin): return User Policy and Configuration so Finamp completes login 2026-07-05 19:56:26 -04:00
browsing.go fix(jellyfin): hex-encode item ids at the API boundary for Jellyfin client compatibility 2026-07-05 22:07:32 -04:00
browsing_test.go refactor(jellyfin): share resolveLibraryScope between items and artists 2026-07-05 16:16:26 -04:00
images.go fix(jellyfin): display playlist cover art 2026-07-05 23:22:37 -04:00
images_test.go fix(jellyfin): display playlist cover art 2026-07-05 23:22:37 -04:00
items.go fix(jellyfin): support playlist deletion via DELETE /Items/{id} 2026-07-06 00:03:35 -04:00
items_test.go fix(jellyfin): support Ids batch-fetch in /Items so clients can fetch items by id 2026-07-05 22:24:28 -04:00
jellyfin_suite_test.go feat(jellyfin): package skeleton with System handshake endpoints 2026-07-05 14:03:40 -04:00
library.go fix(jellyfin): hex-encode item ids at the API boundary for Jellyfin client compatibility 2026-07-05 22:07:32 -04:00
middlewares.go fix(jellyfin): serve direct file at /Items/{id}/File and accept ApiKey query param 2026-07-05 21:06:28 -04:00
middlewares_test.go fix(jellyfin): serve direct file at /Items/{id}/File and accept ApiKey query param 2026-07-05 21:06:28 -04:00
playlists.go fix(jellyfin): expand album/artist/playlist ids when building playlists 2026-07-05 23:58:43 -04:00
playlists_test.go fix(jellyfin): support playlist deletion via DELETE /Items/{id} 2026-07-06 00:03:35 -04:00
README.md fix(jellyfin): support Playlist type, multi-type Items queries, and PlayCount/DatePlayed sort 2026-07-05 20:28:20 -04:00
routing_test.go refactor(server): extract case-insensitive path routing to a shared helper 2026-07-05 18:19:41 -04:00
sessions.go fix(jellyfin): hex-encode item ids at the API boundary for Jellyfin client compatibility 2026-07-05 22:07:32 -04:00
sessions_test.go feat(jellyfin): playback reporting and scrobbling 2026-07-05 17:05:43 -04:00
socket.go feat(jellyfin): implement /socket WebSocket for real-time client sessions 2026-07-05 20:43:03 -04:00
socket_test.go feat(jellyfin): implement /socket WebSocket for real-time client sessions 2026-07-05 20:43:03 -04:00
stream.go fix(jellyfin): hex-encode item ids at the API boundary for Jellyfin client compatibility 2026-07-05 22:07:32 -04:00
stream_test.go fix(jellyfin): hex-encode item ids at the API boundary for Jellyfin client compatibility 2026-07-05 22:07:32 -04:00
system.go feat(jellyfin): unknown-route logging, generic 500s, rating clamp, docs 2026-07-05 17:33:41 -04:00
system_test.go feat(jellyfin): unknown-route logging, generic 500s, rating clamp, docs 2026-07-05 17:33:41 -04:00
users.go fix(jellyfin): resolve library-view ids in getItem so clients can load the library 2026-07-05 20:55:09 -04:00
users_test.go fix(jellyfin): hex-encode item ids at the API boundary for Jellyfin client compatibility 2026-07-05 22:07:32 -04:00

Jellyfin API

This package implements a subset of the Jellyfin REST API on top of Navidrome's existing library, users, playlists and scrobbling infrastructure. It lets Jellyfin-compatible clients (e.g. Finamp, jftui) browse and stream a Navidrome library without requiring a real Jellyfin server.

It is not a full Jellyfin server implementation: only the endpoints needed to browse a music library, stream audio, manage favorites/ratings, report playback, and manage playlists are implemented. Video, live TV, plugins, and Jellyfin's admin/dashboard APIs are out of scope.

Enabling

The Jellyfin API is disabled by default. Enable it via navidrome.toml:

[Jellyfin]
Enabled = true
# Optional: override the server name reported to clients (defaults to "Navidrome <version>")
ServerName = "My Music Server"

or via environment variables:

ND_JELLYFIN_ENABLED=true
ND_JELLYFIN_SERVERNAME="My Music Server"

Once enabled, the API is mounted at:

http://<host>:<port>/jellyfin

All the paths below are relative to that base URL (e.g. System/Info/Public means http://localhost:4533/jellyfin/System/Info/Public).

Authentication

Jellyfin clients authenticate with POST /Users/AuthenticateByName using the user's Navidrome username/password, and get back an AccessToken (a Navidrome JWT). That token is then sent on every subsequent request as the X-Emby-Token header (or embedded in the X-Emby-Authorization/Authorization header's Token="..." field, or as an api_key query param — all three forms are accepted, matching what different clients do).

Multi-library behavior

Jellyfin has no native concept of multiple music libraries the way Navidrome does, so each Navidrome library the current user can access is exposed as its own top-level Jellyfin "CollectionFolder" view (GET /UserViews), instead of merging every library into a single view. Browsing (/Items), artists, and the "Latest" list are all scoped to the libraries the authenticated user has access to; a library (or item within it) the user cannot access returns 404, never 403, so ids can't be used as an existence oracle.

Implemented endpoints

Area Endpoints
Handshake / system GET System/Info/Public, GET/POST System/Ping, GET QuickConnect/Enabled
Auth POST Users/AuthenticateByName, GET Users/Public
Users GET UserViews, GET Users/{userId}/Views, GET Users/Me, GET Users/{userId}
Browsing GET Items, GET Users/{userId}/Items, GET Items/{itemId}, GET Users/{userId}/Items/{itemId}, GET Users/{userId}/Items/Latest
Artists / genres GET Artists, GET Artists/AlbumArtists, GET Genres, GET MusicGenres
Images GET Items/{itemId}/Images/{type}[/{index}] (public, not library-scoped)
Favorites / ratings POST/DELETE Users/{userId}/FavoriteItems/{itemId}, POST/DELETE Users/{userId}/Items/{itemId}/Rating
Streaming GET Audio/{itemId}/stream[.{container}], GET Audio/{itemId}/universal, GET/POST Items/{itemId}/PlaybackInfo
Playback reporting POST Sessions/Playing, POST Sessions/Playing/Progress, POST Sessions/Playing/Stopped, POST Sessions/Capabilities[/Full]
Playlists POST Playlists, GET Playlists/{playlistId}/Items, POST/DELETE Playlists/{playlistId}/Items

Any other path returns a 404 with a {} JSON body, and is logged server-side at Debug level as Jellyfin API: unhandled route (method + path). If a client you're testing needs an endpoint that isn't in the table above, check the server logs for these lines to see exactly what it's requesting.

curl walkthrough

This mirrors the sequence a real client (e.g. Finamp) follows: handshake, login, browse the library hierarchy, fetch playback info, stream, favorite, report playback, and manage a playlist.

BASE=http://localhost:4533/jellyfin

# 1. Handshake (no auth required)
curl -s "$BASE/System/Info/Public" | jq .

# 2. Login - capture the AccessToken
TOKEN=$(curl -s -X POST "$BASE/Users/AuthenticateByName" \
  -H 'Content-Type: application/json' \
  -d '{"Username":"admin","Pw":"password"}' | jq -r .AccessToken)

AUTH=(-H "X-Emby-Token: $TOKEN")

# 3. List the user's views (one per accessible library)
curl -s "${AUTH[@]}" "$BASE/UserViews" | jq .

# 4. Browse artists
curl -s "${AUTH[@]}" "$BASE/Items?IncludeItemTypes=MusicArtist" | jq .
ARTIST_ID=$(curl -s "${AUTH[@]}" "$BASE/Items?IncludeItemTypes=MusicArtist&Limit=1" | jq -r '.Items[0].Id')

# 5. Drill into that artist's albums (ParentId with no IncludeItemTypes defaults to MusicAlbum)
ALBUM_ID=$(curl -s "${AUTH[@]}" "$BASE/Items?ParentId=$ARTIST_ID" | jq -r '.Items[0].Id')

# 6. List the album's songs
USER_ID=$(curl -s "${AUTH[@]}" "$BASE/Users/Me" | jq -r .Id)
SONG_ID=$(curl -s "${AUTH[@]}" "$BASE/Users/$USER_ID/Items?ParentId=$ALBUM_ID&IncludeItemTypes=Audio" \
  | jq -r '.Items[0].Id')

# 7. Ask for playback info, then stream the song
curl -s -X POST "${AUTH[@]}" "$BASE/Items/$SONG_ID/PlaybackInfo" | jq .
curl -s "${AUTH[@]}" "$BASE/Audio/$SONG_ID/stream" -o /tmp/song.audio

# 8. Favorite the song
curl -s -X POST "${AUTH[@]}" "$BASE/Users/$USER_ID/FavoriteItems/$SONG_ID" | jq .

# 9. Report playback start/stop (also drives scrobbling)
curl -s -X POST "${AUTH[@]}" -H 'Content-Type: application/json' \
  -d "{\"ItemId\":\"$SONG_ID\",\"PositionTicks\":0}" "$BASE/Sessions/Playing"
curl -s -X POST "${AUTH[@]}" -H 'Content-Type: application/json' \
  -d "{\"ItemId\":\"$SONG_ID\",\"PositionTicks\":1200000000}" "$BASE/Sessions/Playing/Stopped"

# 10. Create a playlist, add the song, then remove it
PLAYLIST_ID=$(curl -s -X POST "${AUTH[@]}" -H 'Content-Type: application/json' \
  -d "{\"Name\":\"My Playlist\",\"Ids\":[\"$SONG_ID\"]}" "$BASE/Playlists" | jq -r .Id)
curl -s -X POST "${AUTH[@]}" "$BASE/Playlists/$PLAYLIST_ID/Items?Ids=$SONG_ID"
ENTRY_ID=$(curl -s "${AUTH[@]}" "$BASE/Playlists/$PLAYLIST_ID/Items" | jq -r '.Items[0].PlaylistItemId')
curl -s -X DELETE "${AUTH[@]}" "$BASE/Playlists/$PLAYLIST_ID/Items?EntryIds=$ENTRY_ID"

Known limitations

  • Genres are global. GET Genres/MusicGenres is not scoped to the current user's libraries (genre tags aren't per-library entities in Navidrome's model).
  • Artist item-access relies on list-time scoping. Unlike albums and songs (which each belong to exactly one library and are checked against user.HasLibraryAccess on every fetch), an artist can have content across multiple libraries via library_artist, so there's no single library id to gate a direct GET Items/{artistId} or favorite/rating call against. Access control for artists is enforced by scoping the Artists/Items?IncludeItemTypes=MusicArtist list to the user's libraries, plus the persistence layer's own defense-in-depth; a client that already has an artist id from elsewhere is not re-checked against library membership.
  • PlaylistItemId round-trip is untested against a live client. GET Playlists/{id}/Items tags each entry with PlaylistItemId (the playlist-track row id, not the song id) specifically so a client can echo it back via DELETE Playlists/{id}/Items?EntryIds=... to remove one occurrence of a song that appears more than once in the same playlist. This is exercised by unit tests, but hasn't been confirmed against a real Jellyfin client's actual request shape — worth a manual smoke test before relying on it.
  • ID handling is pass-through only. Navidrome ids are used verbatim as Jellyfin item ids; there's no hex/GUID-reversible id scheme. If a client turns out to mangle ids in a way that requires one (e.g. expecting a GUID shape), that will need a follow-up change.
  • Playlists never match Filters=IsFavorite. GET Items?IncludeItemTypes=Playlist (alone or mixed with other types, e.g. Finamp's favorites screen sending IncludeItemTypes=Audio,MusicAlbum,Playlist) is supported, but model.Playlist has no starred/annotation concept, so a favorites query always returns zero playlists rather than erroring.