Files
silo-server/internal/jellycompat/mapping_sort_test.go
cf4e080bf4 fix(jellycompat): Wholphin (jellyfin-sdk-kotlin) playback & genre compatibility (#100)
* fix(jellycompat): match MediaSourceId across UUID formats (compact vs dashed)

* fix(jellycompat): honor ImageTypes=Backdrop as a filter on /Items

Wholphin genre cards request /Items?imageTypes=Backdrop&limit=1&sortBy=Random
and assume every returned item has a backdrop. Silo ignored ImageTypes, so a
random pick could lack a backdrop (BackdropImageTags: null), crashing Wholphin.

Push the filter down to the catalog browse SQL
(NULLIF(BTRIM(backdrop_path),'') IS NOT NULL) so random/limited selections only
ever consider backdrop-having items; empty genres correctly return [].

* fix(jellycompat): case-insensitive PlaySessionId + api_key in stream auth

Wholphin's jellyfin-sdk-kotlin builds its own direct-play URL
(/Videos/{id}/stream?static=true&playSessionId=...&mediaSourceId=...) with a
lowercase 'playSessionId', no api_key, and no auth header (ExoPlayer's data
source drops it). PlaybackSessionAuth read 'PlaySessionId'/'PlaySessionID'
case-sensitively, so the fallback never matched -> 401 on every direct-play
stream -> forced (often failing) transcode fallback. Resolve PlaySessionId via
newCaseInsensitiveQuery, and likewise accept case-variant api_key in
ExtractToken.

* fix(jellycompat): support Wholphin season item queries

---------

Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com>
2026-06-08 21:05:33 -04:00

74 lines
2.1 KiB
Go

package jellycompat
import (
"testing"
"github.com/Silo-Server/silo-server/internal/config"
)
func TestItemDetailSortNamePrefersSortTitle(t *testing.T) {
m := newMapper(NewResourceIDCodec(), &config.Config{})
dto := m.itemFromDetail(upstreamItemDetail{
ContentID: "movie-1",
Type: "movie",
Title: "The Matrix",
SortTitle: "Matrix, The",
OriginalTitle: "Matrix Original",
}, false, nil)
if dto.SortName != "Matrix, The" {
t.Fatalf("SortName = %q, want %q", dto.SortName, "Matrix, The")
}
if dto.ForcedSortName != "Matrix, The" {
t.Fatalf("ForcedSortName = %q, want %q", dto.ForcedSortName, "Matrix, The")
}
}
func TestItemListSortNamePrefersSortTitle(t *testing.T) {
m := newMapper(NewResourceIDCodec(), &config.Config{})
dto := m.itemFromList(upstreamListItem{
ContentID: "movie-1",
Type: "movie",
Title: "The Matrix",
SortTitle: "Matrix, The",
}, false, nil, map[string]bool{"sortname": true})
if dto.SortName != "Matrix, The" {
t.Fatalf("SortName = %q, want %q", dto.SortName, "Matrix, The")
}
}
func TestSeriesListIncludesSeasonCount(t *testing.T) {
m := newMapper(NewResourceIDCodec(), &config.Config{})
seasonCount := 4
dto := m.itemFromList(upstreamListItem{
ContentID: "series-1",
Type: "series",
Title: "Snowpiercer",
SeasonCount: &seasonCount,
}, false, nil, nil)
if dto.SeasonCount != 4 {
t.Fatalf("SeasonCount = %d, want 4", dto.SeasonCount)
}
if dto.ChildCount != 4 || dto.RecursiveItemCount != 4 {
t.Fatalf("ChildCount/RecursiveItemCount = %d/%d, want 4/4", dto.ChildCount, dto.RecursiveItemCount)
}
}
func TestSeriesDetailIncludesSeasonCount(t *testing.T) {
m := newMapper(NewResourceIDCodec(), &config.Config{})
seasonCount := 4
dto := m.itemFromDetail(upstreamItemDetail{
ContentID: "series-1",
Type: "series",
Title: "Snowpiercer",
SeasonCount: &seasonCount,
}, false, nil)
if dto.SeasonCount != 4 {
t.Fatalf("SeasonCount = %d, want 4", dto.SeasonCount)
}
if dto.ChildCount != 4 || dto.RecursiveItemCount != 4 {
t.Fatalf("ChildCount/RecursiveItemCount = %d/%d, want 4/4", dto.ChildCount, dto.RecursiveItemCount)
}
}