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:
co-authored by
Claude Opus 5
parent
2675e6d82b
commit
4a4df7a73c
@@ -1,9 +1,12 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ponzischeme89/memby/server/internal/mdblist"
|
||||
"github.com/ponzischeme89/memby/server/internal/store"
|
||||
)
|
||||
|
||||
func TestSelectedMovieRatingsFiltersOrdersAndLabelsAvailableValues(t *testing.T) {
|
||||
@@ -20,13 +23,34 @@ func TestSelectedMovieRatingsFiltersOrdersAndLabelsAvailableValues(t *testing.T)
|
||||
if len(got) != 3 {
|
||||
t.Fatalf("ratings = %+v", got)
|
||||
}
|
||||
if got[0] != (movieRating{Source: "letterboxd", Name: "Letterboxd", Score: "4.15", Scale: "/5"}) ||
|
||||
if got[0] != (movieRating{Source: "letterboxd", Name: "Letterboxd", Score: "4.2", Scale: "/5"}) ||
|
||||
got[1].Name != "IMDb" || got[1].Score != "8.2" || got[1].Scale != "/10" ||
|
||||
got[2].Name != "Rotten Tomatoes" || got[2].Score != "91" || got[2].Scale != "%" {
|
||||
t.Fatalf("formatted ratings = %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A round score is the case the strip is read on: "7" beside "8.2" looks like a score out
|
||||
// of a different scale, so a fractional scale keeps its decimal and a whole one never
|
||||
// gains one.
|
||||
func TestFormatRatingScoreKeepsTheDecimalOfAFractionalScale(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
source string
|
||||
value float64
|
||||
want string
|
||||
}{
|
||||
{"imdb", 7, "7.0"},
|
||||
{"imdb", 8.26, "8.3"},
|
||||
{"letterboxd", 4, "4.0"},
|
||||
{"tomatoes", 91, "91"},
|
||||
{"metacritic", 87.4, "87"},
|
||||
} {
|
||||
if got := formatRatingScore(tc.value, movieRatingSources[tc.source]); got != tc.want {
|
||||
t.Fatalf("%s %v = %q, want %q", tc.source, tc.value, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMovieProviderPrefersTMDBAndFallsBackToIMDb(t *testing.T) {
|
||||
provider, id := movieProvider(map[string]string{"ImDb": "tt0111161", "TmDb": "278"})
|
||||
if provider != "tmdb" || id != "278" {
|
||||
@@ -37,3 +61,99 @@ func TestMovieProviderPrefersTMDBAndFallsBackToIMDb(t *testing.T) {
|
||||
t.Fatalf("fallback provider = %q %q", provider, id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRatingsNeedRefreshServesStoredValuesForAMonthAndRetriesEmptyOnesSooner(t *testing.T) {
|
||||
now := time.Date(2026, 8, 5, 12, 0, 0, 0, time.UTC)
|
||||
scored := []mdblist.Rating{{Source: "imdb", Value: 8.2}}
|
||||
if ratingsNeedRefresh(scored, now.Add(-29*24*time.Hour), now) {
|
||||
t.Fatal("a month-old score should still be considered current")
|
||||
}
|
||||
if !ratingsNeedRefresh(scored, now.Add(-31*24*time.Hour), now) {
|
||||
t.Fatal("an expired score should be refreshed")
|
||||
}
|
||||
// Nothing found is worth asking about again sooner: a new release gains scores.
|
||||
if ratingsNeedRefresh(nil, now.Add(-2*24*time.Hour), now) {
|
||||
t.Fatal("a recent empty answer should be kept")
|
||||
}
|
||||
if !ratingsNeedRefresh(nil, now.Add(-4*24*time.Hour), now) {
|
||||
t.Fatal("an old empty answer should be retried")
|
||||
}
|
||||
if !ratingsNeedRefresh(scored, time.Time{}, now) {
|
||||
t.Fatal("an unknown fetch time should be refreshed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRatingKeyForRatesEpisodesAsTheirSeriesAndPrefersTMDB(t *testing.T) {
|
||||
key, ok := ratingKeyFor("Episode", map[string]string{"Tmdb": "1396", "Imdb": "tt0903747"})
|
||||
if !ok || key != (store.RatingKey{MediaType: "show", Provider: "tmdb", ProviderID: "1396"}) {
|
||||
t.Fatalf("episode key = %+v %v", key, ok)
|
||||
}
|
||||
key, ok = ratingKeyFor("Movie", map[string]string{"IMDB": "tt0111161"})
|
||||
if !ok || key != (store.RatingKey{MediaType: "movie", Provider: "imdb", ProviderID: "tt0111161"}) {
|
||||
t.Fatalf("movie key = %+v %v", key, ok)
|
||||
}
|
||||
if _, ok := ratingKeyFor("Movie", map[string]string{"Tvdb": "1234"}); ok {
|
||||
t.Fatal("an item MDBList cannot be asked about must not produce a key")
|
||||
}
|
||||
if _, ok := ratingKeyFor("BoxSet", map[string]string{"Tmdb": "9"}); ok {
|
||||
t.Fatal("only films and shows are rated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectItemRatingsKeepsEveryOtherFieldOfEmbysPayload(t *testing.T) {
|
||||
raw := json.RawMessage(`{"Id":"42","Name":"Arrival","UnknownToThisBuild":{"a":1}}`)
|
||||
out := injectItemRatings(raw, []movieRating{
|
||||
{Source: "imdb", Name: "IMDb", Score: "7.9", Scale: "/10"},
|
||||
})
|
||||
var decoded map[string]json.RawMessage
|
||||
if err := json.Unmarshal(out, &decoded); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if string(decoded["UnknownToThisBuild"]) != `{"a":1}` || string(decoded["Name"]) != `"Arrival"` {
|
||||
t.Fatalf("payload lost fields: %s", out)
|
||||
}
|
||||
var ratings []movieRating
|
||||
if json.Unmarshal(decoded[ratingsItemField], &ratings) != nil || len(ratings) != 1 ||
|
||||
ratings[0].Name != "IMDb" {
|
||||
t.Fatalf("ratings = %s", decoded[ratingsItemField])
|
||||
}
|
||||
// Nothing to say is said by saying nothing: an empty list must not add the field.
|
||||
if got := injectItemRatings(raw, nil); string(got) != string(raw) {
|
||||
t.Fatalf("empty ratings changed the payload: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestItemIDsInDeduplicatesAcrossRowsAndStopsAtTheLimit(t *testing.T) {
|
||||
item := func(id string) json.RawMessage { return json.RawMessage(`{"Id":"` + id + `"}`) }
|
||||
ids := itemIDsIn([][]json.RawMessage{
|
||||
{item("a"), item("b")},
|
||||
{item("b"), item("c"), json.RawMessage(`{"Name":"no id"}`)},
|
||||
}, 10)
|
||||
if len(ids) != 3 || ids[0] != "a" || ids[2] != "c" {
|
||||
t.Fatalf("ids = %v", ids)
|
||||
}
|
||||
if capped := itemIDsIn([][]json.RawMessage{{item("a"), item("b"), item("c")}}, 2); len(capped) != 2 {
|
||||
t.Fatalf("capped = %v", capped)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaimRatingsBudgetLimitsTheDayAndHonoursABackoff(t *testing.T) {
|
||||
s := &Server{}
|
||||
now := time.Date(2026, 8, 5, 9, 0, 0, 0, time.UTC)
|
||||
for spent := 0; spent < ratingsWarmDailyBudget; spent++ {
|
||||
if !s.claimRatingsBudget(now) {
|
||||
t.Fatalf("budget refused at %d", spent)
|
||||
}
|
||||
}
|
||||
if s.claimRatingsBudget(now) {
|
||||
t.Fatal("the daily budget should be spent")
|
||||
}
|
||||
// A new day restores it.
|
||||
if !s.claimRatingsBudget(now.Add(25 * time.Hour)) {
|
||||
t.Fatal("a new window should restore the budget")
|
||||
}
|
||||
s.blockRatingsWarming(now.Add(26 * time.Hour))
|
||||
if s.claimRatingsBudget(now.Add(25*time.Hour + time.Minute)) {
|
||||
t.Fatal("a refused provider should stop the warmer entirely")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user