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
+50 -4
View File
@@ -30,7 +30,7 @@ func TestBuildSonarrRowIncludesScheduleAndAddedState(t *testing.T) {
{CoverType: "fanart"},
},
},
}}, time.Date(2026, 7, 27, 21, 0, 0, 0, location), location)
}}, time.Date(2026, 7, 27, 21, 0, 0, 0, location), location, nil)
if err != nil {
t.Fatal(err)
}
@@ -52,6 +52,50 @@ func TestBuildSonarrRowIncludesScheduleAndAddedState(t *testing.T) {
}
}
func TestBuildSonarrRowLinksTheEmbySeries(t *testing.T) {
location := time.UTC
now := time.Date(2026, 7, 27, 8, 0, 0, 0, location)
air := time.Date(2026, 7, 27, 20, 0, 0, 0, location)
index := seriesIndex{
"northbound": "emby-old",
"northbound|2024": "emby-2024",
"harbour": "emby-harbour",
}
episode := func(title string, year int) sonarr.Episode {
utc := air
return sonarr.Episode{
ID: 1, SeriesID: 1, SeasonNumber: 1, EpisodeNumber: 1, AirDateUTC: &utc,
Series: sonarr.Series{ID: 1, Title: title, Year: year},
}
}
cases := map[string]struct {
episode sonarr.Episode
want string
}{
// The year decides between a remake and its original.
"year qualified": {episode("Northbound", 2024), "emby-2024"},
// Sonarr and Emby disagree about a year more often than about a name.
"year mismatch falls back to the title": {episode("Northbound", 1999), "emby-old"},
"punctuation and case are ignored": {episode("Harbour!", 0), "emby-harbour"},
"not in the library": {episode("Unimported", 2026), ""},
}
for name, testCase := range cases {
t.Run(name, func(t *testing.T) {
row, err := buildSonarrRow([]sonarr.Episode{testCase.episode}, now, location, index)
if err != nil {
t.Fatal(err)
}
var item sonarrScheduleItem
if err := json.Unmarshal(row.Items[0], &item); err != nil {
t.Fatal(err)
}
if item.MembySeriesItemID != testCase.want {
t.Fatalf("series id = %q, want %q", item.MembySeriesItemID, testCase.want)
}
})
}
}
func TestBuildPrerollScheduleSplitsTodayAndWeek(t *testing.T) {
location := time.FixedZone("NZST", 12*60*60)
now := time.Date(2026, 7, 28, 10, 0, 0, 0, location)
@@ -93,11 +137,13 @@ func TestBuildSonarrRowCoversFiveDaysAndUsesRelativeAirLabels(t *testing.T) {
}
}
row, err := buildSonarrRow([]sonarr.Episode{
episode(1, time.Date(2026, 7, 29, 16, 0, 0, 0, location)),
episode(2, time.Date(2026, 7, 30, 16, 0, 0, 0, location)),
// Sonarr's response order is not part of its contract. Deliberately put the
// later episode first to ensure the row is authored chronologically.
episode(3, time.Date(2026, 8, 2, 16, 0, 0, 0, location)),
episode(2, time.Date(2026, 7, 30, 16, 0, 0, 0, location)),
episode(1, time.Date(2026, 7, 29, 16, 0, 0, 0, location)),
episode(4, time.Date(2026, 8, 3, 16, 0, 0, 0, location)),
}, now, location)
}, now, location, nil)
if err != nil {
t.Fatal(err)
}