192 lines
6.3 KiB
Go
192 lines
6.3 KiB
Go
package api
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"github.com/ponzischeme89/memby/server/internal/config"
|
||
|
|
"github.com/ponzischeme89/memby/server/internal/store"
|
||
|
|
)
|
||
|
|
|
||
|
|
// The intro rule, pinned against the same cases as the television's copy
|
||
|
|
// (`IntroTest` in app/src/test). The two exist separately because with no gateway there is
|
||
|
|
// nobody to ask, and a skip must not land somewhere different depending on whether the
|
||
|
|
// container is up — so when one of these changes, the other has to change with it.
|
||
|
|
//
|
||
|
|
// The cases are taken from what Emby actually writes: markers arrive as ordinary chapters
|
||
|
|
// carrying a MarkerType, interleaved with the real ones in playback order.
|
||
|
|
|
||
|
|
func chapter(seconds int64, marker string) embyChapter {
|
||
|
|
return embyChapter{
|
||
|
|
StartPositionTicks: seconds * 1_000 * ticksPerMillisecond,
|
||
|
|
MarkerType: marker,
|
||
|
|
Name: marker,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestIntroFromChapters(t *testing.T) {
|
||
|
|
cases := []struct {
|
||
|
|
name string
|
||
|
|
chapters []embyChapter
|
||
|
|
want introSegment
|
||
|
|
ok bool
|
||
|
|
}{
|
||
|
|
{
|
||
|
|
// FROM S01E01 as the server actually holds it: two ordinary chapters, then the
|
||
|
|
// pair, then the rest of the chapters.
|
||
|
|
name: "a real episode",
|
||
|
|
chapters: []embyChapter{
|
||
|
|
chapter(0, "Chapter"),
|
||
|
|
chapter(300, "Chapter"),
|
||
|
|
chapter(463, "IntroStart"),
|
||
|
|
chapter(583, "IntroEnd"),
|
||
|
|
chapter(600, "Chapter"),
|
||
|
|
},
|
||
|
|
want: introSegment{StartMs: 463_000, EndMs: 583_000},
|
||
|
|
ok: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "an intro that starts at the very beginning",
|
||
|
|
chapters: []embyChapter{
|
||
|
|
chapter(0, "IntroStart"),
|
||
|
|
chapter(95, "IntroEnd"),
|
||
|
|
},
|
||
|
|
want: introSegment{StartMs: 0, EndMs: 95_000},
|
||
|
|
ok: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "no markers at all",
|
||
|
|
chapters: []embyChapter{chapter(0, "Chapter"), chapter(300, "Chapter")},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "no chapters at all",
|
||
|
|
chapters: nil,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
// Half a pair is not a segment. There is no honest end to skip to, and
|
||
|
|
// guessing one is how a viewer lands in the middle of a scene.
|
||
|
|
name: "a start with no end",
|
||
|
|
chapters: []embyChapter{chapter(112, "IntroStart"), chapter(300, "Chapter")},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "an end with no start",
|
||
|
|
chapters: []embyChapter{chapter(0, "Chapter"), chapter(246, "IntroEnd")},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "an end before its start",
|
||
|
|
chapters: []embyChapter{chapter(300, "IntroStart"), chapter(120, "IntroEnd")},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
// Emby occasionally writes a pair seconds apart on a title whose opening it
|
||
|
|
// half-recognised. A button that moves the picture imperceptibly reads as
|
||
|
|
// broken, so there is deliberately no button at all.
|
||
|
|
name: "a segment too short to be an intro",
|
||
|
|
chapters: []embyChapter{chapter(100, "IntroStart"), chapter(103, "IntroEnd")},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
// Far more likely two unrelated markers read as a range than a ten-minute
|
||
|
|
// title sequence, and honouring it would throw the viewer past the story.
|
||
|
|
name: "a segment too long to be an intro",
|
||
|
|
chapters: []embyChapter{chapter(60, "IntroStart"), chapter(660, "IntroEnd")},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
// The first start wins. Two starts mean the markers are already untrustworthy,
|
||
|
|
// and taking the later one would pick the larger, more damaging skip.
|
||
|
|
name: "two starts before one end",
|
||
|
|
chapters: []embyChapter{
|
||
|
|
chapter(100, "IntroStart"),
|
||
|
|
chapter(160, "IntroStart"),
|
||
|
|
chapter(220, "IntroEnd"),
|
||
|
|
},
|
||
|
|
want: introSegment{StartMs: 100_000, EndMs: 220_000},
|
||
|
|
ok: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "a later pair is ignored once one has been found",
|
||
|
|
chapters: []embyChapter{
|
||
|
|
chapter(100, "IntroStart"),
|
||
|
|
chapter(220, "IntroEnd"),
|
||
|
|
chapter(1800, "IntroStart"),
|
||
|
|
chapter(1900, "IntroEnd"),
|
||
|
|
},
|
||
|
|
want: introSegment{StartMs: 100_000, EndMs: 220_000},
|
||
|
|
ok: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
// A credits marker is a different feature that does not exist here yet, and
|
||
|
|
// must never be mistaken for an intro.
|
||
|
|
name: "credit markers are not intros",
|
||
|
|
chapters: []embyChapter{
|
||
|
|
chapter(2800, "CreditsStart"),
|
||
|
|
chapter(2900, "CreditsEnd"),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tc := range cases {
|
||
|
|
t.Run(tc.name, func(t *testing.T) {
|
||
|
|
got, ok := introFromChapters(tc.chapters)
|
||
|
|
if ok != tc.ok {
|
||
|
|
t.Fatalf("available = %v, want %v (segment %+v)", ok, tc.ok, got)
|
||
|
|
}
|
||
|
|
if ok && got != tc.want {
|
||
|
|
t.Fatalf("segment = %+v, want %+v", got, tc.want)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// A gateway with no Emby behind it — and one whose operator has turned the feature off —
|
||
|
|
// answers "no intro" rather than an error. The button is an optional convenience on a film
|
||
|
|
// that is already playing, and a 502 here would be a red line in the log for every episode
|
||
|
|
// anybody watched.
|
||
|
|
func TestIntroWithoutEmbyAnswersUnavailable(t *testing.T) {
|
||
|
|
server := testServer(config.Config{})
|
||
|
|
|
||
|
|
request := httptest.NewRequest(http.MethodGet, "/v1/items/1304864/intro", nil)
|
||
|
|
request.SetPathValue("id", "1304864")
|
||
|
|
rec := httptest.NewRecorder()
|
||
|
|
server.handleIntro(rec, request, store.Session{})
|
||
|
|
|
||
|
|
if rec.Code != http.StatusOK {
|
||
|
|
t.Fatalf("status = %d, want 200 — trouble is answered with silence here", rec.Code)
|
||
|
|
}
|
||
|
|
var body introResponse
|
||
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||
|
|
t.Fatalf("body: %v", err)
|
||
|
|
}
|
||
|
|
if body.Available {
|
||
|
|
t.Fatal("a gateway with nothing to ask must never claim an intro")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// The mode a television is told to use has to be one this build knows, or a set could be
|
||
|
|
// pushed a value it silently does nothing with.
|
||
|
|
func TestSkipIntroPreferenceIsCatalogued(t *testing.T) {
|
||
|
|
definition, ok := preferenceDefinitionFor("skipIntroMode")
|
||
|
|
if !ok {
|
||
|
|
t.Fatal("skipIntroMode is missing from the preference catalogue")
|
||
|
|
}
|
||
|
|
if definition.Default != skipIntroPrompt {
|
||
|
|
t.Fatalf("default = %v, want %q — a button is the asked-for behaviour, not a silent seek",
|
||
|
|
definition.Default, skipIntroPrompt)
|
||
|
|
}
|
||
|
|
wanted := []string{skipIntroPrompt, skipIntroAuto, skipIntroOff}
|
||
|
|
if len(definition.Options) != len(wanted) {
|
||
|
|
t.Fatalf("options = %+v, want the three modes", definition.Options)
|
||
|
|
}
|
||
|
|
for i, value := range wanted {
|
||
|
|
if definition.Options[i].Value != value {
|
||
|
|
t.Fatalf("option %d = %q, want %q", i, definition.Options[i].Value, value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// An illegal value must come back as the default rather than reaching a player.
|
||
|
|
normalised := normalizePreferences(map[string]any{"skipIntroMode": "immediately"})
|
||
|
|
if normalised["skipIntroMode"] != skipIntroPrompt {
|
||
|
|
t.Fatalf("normalised = %v, want %q", normalised["skipIntroMode"], skipIntroPrompt)
|
||
|
|
}
|
||
|
|
}
|