mirror of
https://github.com/navidrome/navidrome.git
synced 2026-11-02 16:37:49 +01:00
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.
108 lines
3.8 KiB
Go
108 lines
3.8 KiB
Go
package jellyfin
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"sync"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/navidrome/navidrome/core"
|
|
"github.com/navidrome/navidrome/core/artwork"
|
|
"github.com/navidrome/navidrome/core/playlists"
|
|
"github.com/navidrome/navidrome/core/scrobbler"
|
|
"github.com/navidrome/navidrome/core/stream"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
)
|
|
|
|
type Router struct {
|
|
http.Handler
|
|
ds model.DataStore
|
|
artwork artwork.Artwork
|
|
streamer stream.MediaStreamer
|
|
transcodeDecider stream.TranscodeDecider
|
|
players core.Players
|
|
scrobbler scrobbler.PlayTracker
|
|
playlists playlists.Playlists
|
|
serverIDOnce sync.Once
|
|
serverIDVal string
|
|
}
|
|
|
|
func New(ds model.DataStore, artwork artwork.Artwork, streamer stream.MediaStreamer,
|
|
transcodeDecider stream.TranscodeDecider, players core.Players,
|
|
scrobbler scrobbler.PlayTracker, playlists playlists.Playlists) *Router {
|
|
r := &Router{
|
|
ds: ds, artwork: artwork, streamer: streamer, transcodeDecider: transcodeDecider,
|
|
players: players, scrobbler: scrobbler, playlists: playlists,
|
|
}
|
|
r.Handler = r.routes()
|
|
return r
|
|
}
|
|
|
|
func (api *Router) routes() http.Handler {
|
|
r := chi.NewRouter()
|
|
|
|
// Public (no auth): handshake + login.
|
|
r.Get("/System/Info/Public", api.getPublicSystemInfo)
|
|
r.Get("/System/Ping", api.ping)
|
|
r.Post("/System/Ping", api.ping)
|
|
r.Get("/QuickConnect/Enabled", api.quickConnectEnabled)
|
|
r.Post("/Users/AuthenticateByName", api.authenticateByName)
|
|
r.Get("/Users/Public", api.getPublicUsers)
|
|
|
|
// Images are intentionally public and not library-scoped: artwork isn't sensitive media
|
|
// content, and clients (e.g. Finamp) load it via <img> tags with only ?api_key= in the URL.
|
|
r.Get("/Items/{itemId}/Images/{type}", api.getItemImage)
|
|
r.Get("/Items/{itemId}/Images/{type}/{index}", api.getItemImage)
|
|
|
|
r.Group(func(r chi.Router) {
|
|
r.Use(api.authenticate)
|
|
r.Get("/UserViews", api.getUserViews)
|
|
r.Get("/Users/{userId}/Views", api.getUserViews)
|
|
r.Get("/Users/Me", api.getCurrentUser)
|
|
r.Get("/Users/{userId}", api.getCurrentUser)
|
|
|
|
r.Get("/Items", api.getItems)
|
|
r.Get("/Users/{userId}/Items", api.getItems)
|
|
r.Get("/Items/{itemId}", api.getItem)
|
|
r.Get("/Users/{userId}/Items/{itemId}", api.getItem)
|
|
r.Get("/Users/{userId}/Items/Latest", api.getLatest)
|
|
|
|
r.Post("/Users/{userId}/FavoriteItems/{itemId}", api.markFavorite)
|
|
r.Delete("/Users/{userId}/FavoriteItems/{itemId}", api.unmarkFavorite)
|
|
r.Post("/Users/{userId}/Items/{itemId}/Rating", api.setRating)
|
|
r.Delete("/Users/{userId}/Items/{itemId}/Rating", api.removeRating)
|
|
|
|
r.Get("/Artists", api.getArtists)
|
|
r.Get("/Artists/AlbumArtists", api.getArtists)
|
|
r.Get("/Genres", api.getGenres)
|
|
r.Get("/MusicGenres", api.getGenres)
|
|
|
|
r.Get("/Audio/{itemId}/stream", api.streamAudio)
|
|
r.Get("/Audio/{itemId}/stream.{container}", api.streamAudio)
|
|
r.Get("/Audio/{itemId}/universal", api.streamAudio)
|
|
r.Get("/Items/{itemId}/PlaybackInfo", api.getPlaybackInfo)
|
|
r.Post("/Items/{itemId}/PlaybackInfo", api.getPlaybackInfo)
|
|
|
|
// Playback reports carry only the caller's own play data (see reportPlaybackStart
|
|
// doc comment), so no library-access gate is needed here.
|
|
r.Group(func(r chi.Router) {
|
|
r.Use(api.withPlayer)
|
|
r.Post("/Sessions/Playing", api.reportPlaybackStart)
|
|
r.Post("/Sessions/Playing/Progress", api.reportPlaybackProgress)
|
|
r.Post("/Sessions/Playing/Stopped", api.reportPlaybackStopped)
|
|
})
|
|
r.Post("/Sessions/Capabilities", api.postCapabilities)
|
|
r.Post("/Sessions/Capabilities/Full", api.postCapabilities)
|
|
})
|
|
|
|
return r
|
|
}
|
|
|
|
// ok writes payload as JSON.
|
|
func (api *Router) ok(w http.ResponseWriter, r *http.Request, payload any) {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
|
log.Error(r.Context(), "Jellyfin API: error encoding response", err)
|
|
}
|
|
}
|