Files

288 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package api
import "testing"
// The credits rule, pinned against the same cases as the television's copy (`CreditsTest`
// in app/src/test). The two exist separately because with no gateway there is nobody to
// ask, and the picture must not start shrinking at a different moment depending on whether
// the container is up — so when one of these changes, the other has to change with it.
//
// The cases come from a survey of a real 20,000-item library, and the shape of that survey is
// why the rule looks the way it does. Emby 4.10 wrote **no `CreditsStart` at all** — only
// `Chapter`, `IntroStart` and `IntroEnd` — while 216 items carried a chapter *named* like
// credits at 9098% of runtime. Two of them carried "Opening Credits" at 01%, which is the
// case the position floor exists for and the one worth never regressing.
func named(seconds int64, name string) embyChapter {
return embyChapter{
StartPositionTicks: seconds * 1_000 * ticksPerMillisecond,
MarkerType: "Chapter",
Name: name,
}
}
// A two-thousand-second episode, so a percentage of runtime reads as a round number.
const testRuntimeMs = 2_000_000
func TestCreditsFromChapters(t *testing.T) {
cases := []struct {
name string
chapters []embyChapter
runtimeMs int64
want int64
ok bool
}{
{
// A real episode as the server holds it: chapters, the intro pair, more
// chapters, and the credits marker near the end.
name: "a real episode",
chapters: []embyChapter{
chapter(0, "Chapter"),
chapter(463, "IntroStart"),
chapter(583, "IntroEnd"),
chapter(1200, "Chapter"),
chapter(1900, "CreditsStart"),
},
want: 1_900_000,
ok: true,
},
{
// A film, which commonly has the credits marker and no intro at all. This is
// the case that would have been lost had the two features shared one flag.
name: "credits with no intro",
chapters: []embyChapter{
chapter(0, "Chapter"),
chapter(1400, "Chapter"),
chapter(1850, "CreditsStart"),
},
want: 1_850_000,
ok: true,
},
{
name: "no markers at all",
chapters: []embyChapter{chapter(0, "Chapter"), chapter(300, "Chapter")},
},
{
name: "no chapters at all",
chapters: nil,
},
{
// An intro pair is a different feature and must never be read as credits.
name: "intro markers are not credits",
chapters: []embyChapter{
chapter(463, "IntroStart"),
chapter(583, "IntroEnd"),
},
},
{
// A marker at zero says the whole file is credits, which is not something Emby
// means and not something worth shrinking a picture for.
name: "a marker at the very beginning",
chapters: []embyChapter{chapter(0, "CreditsStart"), chapter(300, "Chapter")},
},
{
// The last marker wins, where the intro rule takes the first. Two starts mean
// the markers are untrustworthy, and the two features are damaged in opposite
// directions: an intro skip that fires late throws somebody past the story, so
// the earlier marker is safer there; the credits pane firing early runs the last
// scene past somebody at double speed, so the later marker is safer here.
name: "two starts, the later one wins",
chapters: []embyChapter{
chapter(1700, "CreditsStart"),
chapter(1900, "CreditsStart"),
},
want: 1_900_000,
ok: true,
},
{
// Order in the array is not trusted to be sorted, so a later marker earlier in
// the list still loses to the one further into the film.
name: "the later marker wins whatever order they arrive in",
chapters: []embyChapter{
chapter(1900, "CreditsStart"),
chapter(1700, "CreditsStart"),
},
want: 1_700_000,
ok: true,
},
{
// Squid Game, as the library actually holds it: no marker of any kind, one
// ordinary chapter named "Credits" at 90% of runtime. This is where every bit of
// this feature's coverage comes from today.
name: "a chapter named Credits, which is all Emby 4.10 gives",
chapters: []embyChapter{
chapter(0, "Chapter"),
chapter(463, "IntroStart"),
chapter(583, "IntroEnd"),
named(1800, "Credits"),
},
want: 1_800_000,
ok: true,
},
{
name: "a chapter named End Credits",
chapters: []embyChapter{named(1920, "End Credits")},
want: 1_920_000,
ok: true,
},
{
// THE case. Belfast carries "Opening Credits" at 1% of runtime and Game of Thrones
// at 0%. Matching a name without testing the position starts the pane in the first
// minute of a film and runs its opening at double speed — the worst thing this
// feature could do, and the reason creditsMinimumPositionFraction exists.
name: "Opening Credits at the start of a film is never the credit roll",
chapters: []embyChapter{
named(20, "Opening Credits"),
chapter(600, "Chapter"),
},
},
{
// Belfast in full: both chapters present. The opening one must be rejected and the
// closing one found, which the position floor does on its own.
name: "an opening and a closing credit chapter in one film",
chapters: []embyChapter{
named(20, "Opening Credits"),
chapter(600, "Chapter"),
named(1900, "End Credits"),
},
want: 1_900_000,
ok: true,
},
{
// "The Pitt" carries both, seconds apart, describing one roll. The *earliest*
// qualifying chapter wins here — the opposite of the marker rule above — because
// the roll begins at the first of them and taking the last would skip part of it.
name: "two credits chapters describing one roll take the earlier",
chapters: []embyChapter{
named(1920, "End Credits"),
named(1900, "Credits"),
},
want: 1_900_000,
ok: true,
},
{
// Anything in the first three quarters is refused however it is worded. A position
// test catches wordings nobody thought of; a list of words only catches the listed.
name: "a credits-named chapter too early to be the roll",
chapters: []embyChapter{named(900, "Credits")},
},
{
// An explicit marker outranks a name, and is honoured even with no runtime to
// measure against: it is Emby asserting a position rather than this inferring one.
name: "a marker is honoured when the runtime is unknown",
chapters: []embyChapter{chapter(1900, "CreditsStart")},
runtimeMs: -1,
want: 1_900_000,
ok: true,
},
{
// A name is not. Without a runtime there is no way to tell an opening credit
// sequence from a closing one, and guessing is what this whole guard refuses.
name: "a name is refused when the runtime is unknown",
chapters: []embyChapter{named(1900, "End Credits")},
runtimeMs: -1,
},
{
// A marker below the floor is a mis-detection whoever wrote it, so it gives way to
// a name that does qualify rather than being honoured on authority.
name: "a marker below the floor falls through to a name that qualifies",
chapters: []embyChapter{
chapter(200, "CreditsStart"),
named(1900, "End Credits"),
},
want: 1_900_000,
ok: true,
},
{
// The intro's own chapters are named "Intro Start"/"Intro End" in this library, and
// an episode whose titles run late must never have them read as a credit roll.
name: "chapters named for the intro are never credits",
chapters: []embyChapter{
named(1800, "Intro Start"),
named(1900, "Intro End"),
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
// 0 means "the ordinary case, use the default"; -1 means "deliberately
// unknown", which is a case of its own rather than an absent field.
runtime := tc.runtimeMs
switch runtime {
case 0:
runtime = testRuntimeMs
case -1:
runtime = 0
}
got, ok := creditsFromChapters(tc.chapters, runtime)
if ok != tc.ok {
t.Fatalf("available = %v, want %v (start %d)", ok, tc.ok, got)
}
if ok && got != tc.want {
t.Fatalf("start = %d, want %d", got, tc.want)
}
})
}
}
// The two halves of one reading are independent. A film with credits and no intro must not
// report an intro starting at zero, which is what a shared flag would have produced.
func TestMarkersResponseKeepsTheHalvesApart(t *testing.T) {
response := markersResponse(chapterMarkers{creditsStart: 6_840_000, creditsFound: true})
if response.Available || response.StartMs != 0 || response.EndMs != 0 {
t.Fatalf("intro = %+v, want an absent intro on a title that has none", response)
}
if !response.CreditsAvailable || response.CreditsStartMs != 6_840_000 {
t.Fatalf("credits = %+v, want the marker that was found", response)
}
response = markersResponse(chapterMarkers{
intro: introSegment{StartMs: 463_000, EndMs: 583_000}, introFound: true,
})
if response.CreditsAvailable || response.CreditsStartMs != 0 {
t.Fatalf("credits = %+v, want none on a title with only an intro", response)
}
}
// An operator turning one feature off must not take the other with it, and must not poison
// the cache: masking happens on the way out, so the entry behind it still holds the truth.
func TestMaskMarkersWithholdsOnlyTheDisabledHalf(t *testing.T) {
full := introResponse{
Available: true, StartMs: 463_000, EndMs: 583_000,
CreditsAvailable: true, CreditsStartMs: 2_704_000,
}
withoutIntro := maskMarkers(full, false, true)
if withoutIntro.Available || withoutIntro.StartMs != 0 || withoutIntro.EndMs != 0 {
t.Fatalf("intro = %+v, want it withheld", withoutIntro)
}
if !withoutIntro.CreditsAvailable || withoutIntro.CreditsStartMs != 2_704_000 {
t.Fatal("turning the skip button off must not cost the credits pane as well")
}
withoutCredits := maskMarkers(full, true, false)
if withoutCredits.CreditsAvailable || withoutCredits.CreditsStartMs != 0 {
t.Fatalf("credits = %+v, want them withheld", withoutCredits)
}
if !withoutCredits.Available || withoutCredits.StartMs != 463_000 {
t.Fatal("turning the credits pane off must not cost the skip button as well")
}
}
// The credits pane is not a viewer preference any more, and this pins the removal from both
// ends: the key is gone from the catalogue, and a document still carrying it — written by a
// television or an operator before the removal — has it dropped rather than round-tripped.
// Without the second half a stored `false` would survive every sync and quietly keep the
// feature off on the one set that had turned it off.
func TestSpeedUpCreditsIsNoLongerAPreference(t *testing.T) {
if _, ok := preferenceDefinitionFor("speedUpCredits"); ok {
t.Fatal("speedUpCredits is still in the preference catalogue")
}
normalised := normalizePreferences(map[string]any{"speedUpCredits": false})
if _, present := normalised["speedUpCredits"]; present {
t.Fatal("a stored speedUpCredits must be dropped, not carried forward")
}
}