App v0.2.27 and gateway 0.1.23
Skip Intro from Emby's own chapter markers, trickplay seek previews from BIF files, a server-composed home hero ranked on Radarr/Sonarr dates and review scores, and My Alerts as its own page behind the user picker. Related titles now degrade at every step instead of returning empty, and the "+" is back on Manage users so a second viewer can be added from the launcher. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4a4df7a73c
commit
80c304d86b
@@ -0,0 +1,149 @@
|
||||
package trickplay
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// buildBIF assembles a file in the shape Emby writes one, so the tests exercise the same
|
||||
// arithmetic the real parser will meet rather than a convenient fiction.
|
||||
func buildBIF(count int, multiplier uint32, frameSizes []int) []byte {
|
||||
length := IndexLength(count)
|
||||
file := make([]byte, length)
|
||||
copy(file, magic)
|
||||
binary.LittleEndian.PutUint32(file[12:16], uint32(count))
|
||||
binary.LittleEndian.PutUint32(file[16:20], multiplier)
|
||||
|
||||
offset := length
|
||||
for entry := 0; entry < count; entry++ {
|
||||
at := HeaderSize + entry*entrySize
|
||||
binary.LittleEndian.PutUint32(file[at:at+4], uint32(entry))
|
||||
binary.LittleEndian.PutUint32(file[at+4:at+8], uint32(offset))
|
||||
size := 100
|
||||
if entry < len(frameSizes) {
|
||||
size = frameSizes[entry]
|
||||
}
|
||||
offset += size
|
||||
file = append(file, make([]byte, size)...)
|
||||
}
|
||||
at := HeaderSize + count*entrySize
|
||||
binary.LittleEndian.PutUint32(file[at:at+4], 0xFFFFFFFF)
|
||||
binary.LittleEndian.PutUint32(file[at+4:at+8], uint32(offset))
|
||||
return file
|
||||
}
|
||||
|
||||
func TestParseIndexReadsEmbysLayout(t *testing.T) {
|
||||
// Emby 4.10 writes a multiplier of 10000 with timestamps counting 0, 1, 2 — so the
|
||||
// interval is ten seconds, and reading the multiplier as the interval would be right
|
||||
// only by accident. The multiplication is the part worth pinning.
|
||||
file := buildBIF(3, 10_000, []int{500, 600, 700})
|
||||
|
||||
index, err := ParseIndex(file)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseIndex: %v", err)
|
||||
}
|
||||
if index.Count != 3 {
|
||||
t.Fatalf("Count = %d, want 3", index.Count)
|
||||
}
|
||||
if index.IntervalMs != 10_000 {
|
||||
t.Fatalf("IntervalMs = %d, want 10000", index.IntervalMs)
|
||||
}
|
||||
start, end, ok := index.Frame(1)
|
||||
if !ok {
|
||||
t.Fatal("Frame(1) not ok")
|
||||
}
|
||||
if want := int64(IndexLength(3) + 500); start != want {
|
||||
t.Fatalf("frame 1 starts at %d, want %d", start, want)
|
||||
}
|
||||
if end-start != 600 {
|
||||
t.Fatalf("frame 1 is %d bytes, want 600", end-start)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIndexAcceptsATitleWithNoThumbnails(t *testing.T) {
|
||||
// This is what Emby serves for a title it has not generated previews for: a
|
||||
// well-formed 72-byte header with a count of zero. It must read as "this title has
|
||||
// none", never as a broken file, or every such title logs an error.
|
||||
file := buildBIF(0, 10_000, nil)
|
||||
|
||||
index, err := ParseIndex(file)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseIndex: %v", err)
|
||||
}
|
||||
if index.Available() {
|
||||
t.Fatal("a zero-frame BIF reported itself as available")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIndexRejectsWhatIsNotABIF(t *testing.T) {
|
||||
if _, err := ParseIndex([]byte("<html>not found</html>")); !errors.Is(err, ErrNotBIF) &&
|
||||
!errors.Is(err, ErrShort) {
|
||||
t.Fatalf("err = %v, want ErrNotBIF or ErrShort", err)
|
||||
}
|
||||
file := buildBIF(2, 10_000, nil)
|
||||
file[3] = 'X'
|
||||
if _, err := ParseIndex(file); !errors.Is(err, ErrNotBIF) {
|
||||
t.Fatalf("err = %v, want ErrNotBIF", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIndexRejectsFramesInsideTheIndex(t *testing.T) {
|
||||
// An offset pointing back into the index would have the gateway serve a slice of the
|
||||
// index itself as a JPEG. Refuse the file rather than hand a television nonsense.
|
||||
file := buildBIF(2, 10_000, nil)
|
||||
binary.LittleEndian.PutUint32(file[HeaderSize+4:HeaderSize+8], 8)
|
||||
if _, err := ParseIndex(file); !errors.Is(err, ErrNotBIF) {
|
||||
t.Fatalf("err = %v, want ErrNotBIF", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIndexAsksForMoreRatherThanFailing(t *testing.T) {
|
||||
// A caller reads a fixed window off the front of the file, so a long title legitimately
|
||||
// arrives with the index cut short. That must be answerable — fetch more — rather than
|
||||
// looking like a bad file.
|
||||
file := buildBIF(40, 10_000, nil)
|
||||
if _, err := ParseIndex(file[:HeaderSize+16]); !errors.Is(err, ErrShort) {
|
||||
t.Fatalf("err = %v, want ErrShort", err)
|
||||
}
|
||||
if _, _, err := ParseHeader(file[:20]); !errors.Is(err, ErrShort) {
|
||||
t.Fatalf("ParseHeader err = %v, want ErrShort", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrameAtClampsRatherThanRefusing(t *testing.T) {
|
||||
// The preview is drawn while somebody is still moving a seek target about, so a
|
||||
// position past the last frame must show the last frame. Nothing is worse here than
|
||||
// the thumbnail blanking at exactly the end of a film.
|
||||
index, err := ParseIndex(buildBIF(3, 10_000, nil))
|
||||
if err != nil {
|
||||
t.Fatalf("ParseIndex: %v", err)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
positionMs int64
|
||||
want int
|
||||
}{
|
||||
{-5_000, 0},
|
||||
{0, 0},
|
||||
{9_999, 0},
|
||||
{10_000, 1},
|
||||
{25_000, 2},
|
||||
{9_000_000, 2},
|
||||
} {
|
||||
if got := index.FrameAt(tc.positionMs); got != tc.want {
|
||||
t.Fatalf("FrameAt(%d) = %d, want %d", tc.positionMs, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrameRefusesAnIndexOutOfRange(t *testing.T) {
|
||||
index, err := ParseIndex(buildBIF(2, 10_000, nil))
|
||||
if err != nil {
|
||||
t.Fatalf("ParseIndex: %v", err)
|
||||
}
|
||||
for _, n := range []int{-1, 2, 99} {
|
||||
if _, _, ok := index.Frame(n); ok {
|
||||
t.Fatalf("Frame(%d) was served", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user