App v0.2.26 and gateway 0.1.20

Client: seek controls, Bazarr subtitle download and cast panel in the
player; MDBList ratings strip; episode and schedule detail pages; series
pace estimate; what's new panel; install-permission onboarding step;
synced per-profile preferences; Emby outage banner.

Gateway: rebuilt admin console (one fragment per page), preference
history and restore, merged Continue Watching, Emby health probe,
subtitle selection and Bazarr download, structured request logging with
per-request identity, and embedded build version.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ponzischeme89
2026-08-06 22:33:56 +12:00
co-authored by Claude Opus 5
parent 2675e6d82b
commit 4a4df7a73c
257 changed files with 24868 additions and 3108 deletions
+59 -1
View File
@@ -1,6 +1,7 @@
package api
import (
"context"
"encoding/json"
"testing"
"time"
@@ -32,6 +33,29 @@ func TestPersonalizeHomeRowsGraduallyDemotesIgnoredRows(t *testing.T) {
}
}
func TestContinueWatchingPromotesShowsAiringToday(t *testing.T) {
raw := func(value string) json.RawMessage { return json.RawMessage(value) }
items := []json.RawMessage{
raw(`{"Id":"ended","Name":"Old episode","Type":"Episode","SeriesName":"Ended Rewatch"}`),
raw(`{"Id":"movie","Name":"Paused Movie","Type":"Movie"}`),
raw(`{"Id":"today-1","Name":"Episode 4","Type":"Episode","SeriesName":"The Bear"}`),
raw(`{"Id":"today-2","Name":"Episode 2","Type":"Episode","SeriesName":"Abbott Elementary"}`),
}
schedule := &recommend.Row{Items: []json.RawMessage{
raw(`{"Name":"The Bear","MembyAirDayLabel":"Today"}`),
raw(`{"Name":"Tomorrow Show","MembyAirDayLabel":"Tomorrow"}`),
raw(`{"Name":"Abbott Elementary","MembyAirDayLabel":"Today"}`),
}}
got := prioritizeAiringTodayContinue(items, schedule)
want := []string{"today-1", "today-2", "ended", "movie"}
for index, item := range recommend.Decode(got) {
if item.ID != want[index] {
t.Fatalf("item %d = %q, want %q", index, item.ID, want[index])
}
}
}
func TestDifferentHistoriesChangeRowAndPosterOrdering(t *testing.T) {
now := time.Date(2026, 7, 31, 20, 0, 0, 0, time.UTC)
item := func(id, name, genre string) recommend.Item {
@@ -90,8 +114,42 @@ func TestDifferentHistoriesChangeRowAndPosterOrdering(t *testing.T) {
}
}
// Continue Watching answers "what was I watching?", so its order belongs to Emby (and to
// mergeContinueWatching). Ranking it by taste buried episodes behind films and pushed a
// just-watched show past the visible cards, which read on a television as the series
// having vanished.
func TestProgressRowsKeepEmbyOrder(t *testing.T) {
raw := func(id, name, itemType string) json.RawMessage {
value, _ := json.Marshal(map[string]any{"Id": id, "Name": name, "Type": itemType})
return value
}
items := func() []json.RawMessage {
return []json.RawMessage{
raw("episode", "Zulu Company", "Episode"),
raw("movie", "A Film", "Movie"),
}
}
rows := (&Server{}).personalizeTitles(
context.Background(),
store.Session{EmbyUserID: "user"},
[]recommend.Row{
{ID: "continue", Items: items()},
{ID: "curated:movies:drama", Items: items()},
},
)
if got := recommend.Decode(rows[0].Items); len(got) != 2 ||
got[0].ID != "episode" || got[1].ID != "movie" {
t.Fatalf("continue row was reordered: %+v", got)
}
// The same items in a discovery shelf are still ranked, which is what makes the
// exemption above a deliberate choice rather than dead code.
if discovery := recommend.Decode(rows[1].Items); discovery[0].ID != "movie" {
t.Fatalf("discovery row was not ranked: %+v", discovery)
}
}
func TestPersonalizeHomeRowsPreservesColdStartDefaults(t *testing.T) {
rows := []recommend.Row{{ID: "continue"}, {ID: "next-up"}, {ID: "latest"}}
rows := []recommend.Row{{ID: "continue"}, {ID: "favorites"}, {ID: "latest"}}
got := personalizeHomeRows(rows, nil)
for i := range rows {
if got[i].ID != rows[i].ID {