* feat(jellycompat): add Filters2, LocalTrailers, UserImage and ClientLog endpoints
Four endpoints that real Jellyfin clients call were unregistered and fell
through to chi's default 404 (or, for Filters2, were swallowed by /Items/{id}).
All are additive and contract-faithful to the Jellyfin C# server:
- GET /Items/Filters2 -> 200 QueryFilters v2 shape (Genres NameGuidPair[],
Tags, Audio/SubtitleLanguages), empty arrays. Fladder's filter UI 404'd before.
- GET /Items/{id}/LocalTrailers (+ /Users/{userId}/... alias) -> 200 bare
BaseItemDto[] ([]); Silo indexes no local trailers. Infuse/Moonfin hit this
on every item-detail load.
- GET|HEAD /UserImage?userId= -> the same anonymous palette avatar as the
legacy /Users/{id}/Images/Primary route; HandleUserImage now reads the id
from the query param when the path segment is absent (modern Jellyfin route).
- POST /ClientLog/Document -> 200 {FileName} after draining/discarding the
body (Silo has no client-log store); 413 over 1 MiB, matching MaxDocumentSize.
Stops recurring 404 noise and lets clients that depend on these (filter sheets,
avatars, crash-log upload) work. Adds handler unit tests for each.
* feat(jellycompat): add GET /Sessions returning a contract-shaped session list
Wholphin and other jellyfin-sdk clients poll GET /Sessions (optionally
?deviceId=) every few seconds during playback; the route was unregistered, so
each poll hit a chi 404 the SDK could not deserialize — a ~289-per-4h 404 storm
in production. Register it under the same [Authorize] group Jellyfin uses and
return a correctly-typed SessionInfoDto[] (currently empty, consistent with the
existing compat stub handlers). This stops the storm and lets clients degrade
cleanly; populating live session/now-playing state from the playback store is a
follow-up.
* refactor(jellycompat): match Jellyfin client-log size limit exactly
Use 1,000,000 bytes (Jellyfin's ClientLogController.MaxDocumentSize, decimal)
instead of 1<<20, and fix the comment that wrongly called it 1 MiB. Behavior is
functionally identical (the body is discarded); this is contract-fidelity only.
Review follow-up.
* test(jellycompat): add router-level coverage for the new endpoints
The per-handler tests call handlers directly and never exercise NewRouter, so
route registration, chi static-vs-{id} ordering, and auth-group placement were
untested — the one thing this change is actually about. Add a full
NewRouter/ServeHTTP test asserting the session-auth-group routes (Filters2,
LocalTrailers x2, Sessions, ClientLog/Document) return 401 unauthenticated
(registered + behind auth, not 404 or accidentally anonymous), /UserImage serves
its anonymous palette avatar, and an authenticated Filters2 reaches the v2
filters handler (not shadowed by /Items/{id}) with /Sessions returning [].
36 lines
1.5 KiB
Go
36 lines
1.5 KiB
Go
package jellycompat
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
)
|
|
|
|
// maxClientLogBytes bounds how much of a client log upload we read before
|
|
// discarding it, matching Jellyfin's ClientLogController.MaxDocumentSize
|
|
// (1,000,000 bytes). Anything larger is rejected with 413.
|
|
const maxClientLogBytes = 1_000_000
|
|
|
|
// clientLogDocumentResponse mirrors Jellyfin's ClientLogDocumentResponseDto.
|
|
// Some clients parse FileName from the 200 response, so it must be present.
|
|
type clientLogDocumentResponse struct {
|
|
FileName string `json:"FileName"`
|
|
}
|
|
|
|
// HandleClientLogDocument accepts POST /ClientLog/Document. Clients (Jellyfin
|
|
// Android TV, Wholphin, Fire TV apps) upload crash/diagnostic bundles here; with
|
|
// no route they hit a chi 404 and "upload logs" silently fails. Silo has no
|
|
// client-log store, so the body is drained and discarded, but we answer 200 with
|
|
// a generated FileName to match Jellyfin's contract (it returns the stored file
|
|
// name, never 204). Oversized uploads get 413 like Jellyfin's MaxDocumentSize.
|
|
func HandleClientLogDocument(w http.ResponseWriter, r *http.Request) {
|
|
if r.ContentLength > maxClientLogBytes {
|
|
writeError(w, http.StatusRequestEntityTooLarge, "PayloadTooLarge", "Client log document is too large")
|
|
return
|
|
}
|
|
// Drain (bounded) and discard: Silo does not persist client logs, but the
|
|
// body must be consumed so the client's upload completes cleanly.
|
|
_, _ = io.Copy(io.Discard, io.LimitReader(r.Body, maxClientLogBytes))
|
|
|
|
writeJSON(w, http.StatusOK, clientLogDocumentResponse{FileName: uuidNewString() + ".log"})
|
|
}
|