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:
ponzischeme89
2026-08-07 10:44:17 +12:00
co-authored by Claude Opus 5
parent 4a4df7a73c
commit 80c304d86b
62 changed files with 6095 additions and 255 deletions
+91
View File
@@ -8,6 +8,7 @@ import (
"io"
"log/slog"
"net/url"
"slices"
"strconv"
"strings"
"sync"
@@ -875,3 +876,93 @@ func rowTitles(rows []Row) []string {
}
return out
}
func testSeeds(count int) []Seed {
seeds := make([]Seed, 0, count)
for index := 0; index < count; index++ {
id := "s" + strconv.Itoa(index)
seeds = append(seeds, Seed{ID: id, Name: "Title " + id})
}
return seeds
}
func seedIDs(seeds []Seed) []string {
out := make([]string, 0, len(seeds))
for _, seed := range seeds {
out = append(out, seed.ID)
}
return out
}
// The head of a household's history barely moves while they work through one series, so
// the seeds must move without it.
func TestSelectSeedsRotatesWithoutNewHistory(t *testing.T) {
seeds := testSeeds(12)
days := map[string]bool{}
for day := 1; day <= 14; day++ {
chosen := selectSeeds(seeds, 12, 3, "u1:2026-08-"+strconv.Itoa(day))
if len(chosen) != 3 {
t.Fatalf("day %d chose %d seeds", day, len(chosen))
}
days[strings.Join(seedIDs(chosen), ",")] = true
}
if len(days) < 4 {
t.Fatalf("a fortnight produced only %d distinct seed sets: %v", len(days), days)
}
}
// Rotation is within recency bands, never a shuffle of the whole window: the first row is
// still anchored to something watched lately.
func TestSelectSeedsKeepsRecencyBands(t *testing.T) {
seeds := testSeeds(12)
for day := 1; day <= 30; day++ {
chosen := selectSeeds(seeds, 12, 3, "u1:day-"+strconv.Itoa(day))
for index, seed := range chosen {
position, err := strconv.Atoi(strings.TrimPrefix(seed.ID, "s"))
if err != nil {
t.Fatalf("unexpected seed id %q", seed.ID)
}
if position < index*4 || position >= (index+1)*4 {
t.Fatalf("row %d drew %s from outside its band", index, seed.ID)
}
}
}
}
// Rows are rebuilt on every cache miss, so the same day must always yield the same seeds.
func TestSelectSeedsIsStableWithinADay(t *testing.T) {
seeds := testSeeds(20)
first := seedIDs(selectSeeds(seeds, 12, 3, "u1:2026-08-07"))
for attempt := 0; attempt < 5; attempt++ {
if got := seedIDs(selectSeeds(seeds, 12, 3, "u1:2026-08-07")); !slices.Equal(got, first) {
t.Fatalf("same day produced %v then %v", first, got)
}
}
other := seedIDs(selectSeeds(seeds, 12, 3, "u2:2026-08-07"))
if slices.Equal(other, first) {
t.Log("two users may coincide; only a smoke check")
}
}
// A short history has nothing to rotate: take what there is, newest first.
func TestSelectSeedsFallsBackToRecencyWhenShort(t *testing.T) {
if got := seedIDs(selectSeeds(testSeeds(2), 12, 3, "u1:day")); !slices.Equal(got, []string{"s0", "s1"}) {
t.Fatalf("short history = %v", got)
}
if got := selectSeeds(nil, 12, 3, "u1:day"); len(got) != 0 {
t.Fatalf("no history should seed nothing, got %v", got)
}
}
// A pool that does not divide evenly must not strand its oldest entries.
func TestSelectSeedsLastBandTakesTheRemainder(t *testing.T) {
seeds := testSeeds(11)
reached := map[string]bool{}
for day := 1; day <= 40; day++ {
chosen := selectSeeds(seeds, 11, 3, "u1:day-"+strconv.Itoa(day))
reached[chosen[2].ID] = true
}
if !reached["s10"] {
t.Fatalf("the oldest seed was never reachable: %v", reached)
}
}