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
@@ -0,0 +1,182 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ponzischeme89/memby/server/internal/bazarr"
|
||||
)
|
||||
|
||||
// Matching an Emby item onto a Bazarr id is where a wrong answer is worst: it downloads a
|
||||
// subtitle for one film and writes it beside another. These pin the rule.
|
||||
|
||||
func TestBazarrMovieMatchesOnTitleAndYear(t *testing.T) {
|
||||
movies := []bazarr.Movie{
|
||||
{RadarrID: 1, Title: "The Thing", Year: "1982"},
|
||||
{RadarrID: 2, Title: "The Thing", Year: "2011"},
|
||||
{RadarrID: 3, Title: "Arrival", Year: "2016"},
|
||||
}
|
||||
if got := bazarrMovieFor("The Thing", 2011, movies); got == nil || got.RadarrID != 2 {
|
||||
t.Fatalf("remake matched %+v", got)
|
||||
}
|
||||
if got := bazarrMovieFor("the thing!", 1982, movies); got == nil || got.RadarrID != 1 {
|
||||
t.Fatalf("punctuation and case should not matter, got %+v", got)
|
||||
}
|
||||
if got := bazarrMovieFor("Arrival", 0, movies); got == nil || got.RadarrID != 3 {
|
||||
t.Fatalf("an unknown year should still match a unique title, got %+v", got)
|
||||
}
|
||||
if got := bazarrMovieFor("Dune", 2021, movies); got != nil {
|
||||
t.Fatalf("expected no match, got %+v", got)
|
||||
}
|
||||
if got := bazarrMovieFor("", 2016, movies); got != nil {
|
||||
t.Fatalf("a blank title must never match, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Bazarr carries the year as a string and leaves it empty often enough that treating an
|
||||
// empty one as a disagreement would reject correct matches.
|
||||
func TestBazarrMovieAcceptsAnEntryWithNoYear(t *testing.T) {
|
||||
movies := []bazarr.Movie{{RadarrID: 4, Title: "Arrival", Year: ""}}
|
||||
if got := bazarrMovieFor("Arrival", 2016, movies); got == nil || got.RadarrID != 4 {
|
||||
t.Fatalf("got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// With two entries under one title, a year that both sides know must win over the first
|
||||
// row that merely failed to disagree.
|
||||
func TestBazarrMoviePrefersAnExactYearOverAYearlessFallback(t *testing.T) {
|
||||
movies := []bazarr.Movie{
|
||||
{RadarrID: 5, Title: "The Thing", Year: ""},
|
||||
{RadarrID: 6, Title: "The Thing", Year: "1982"},
|
||||
}
|
||||
if got := bazarrMovieFor("The Thing", 1982, movies); got == nil || got.RadarrID != 6 {
|
||||
t.Fatalf("got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBazarrSeriesMatchesOnTitleAndYear(t *testing.T) {
|
||||
series := []bazarr.Series{
|
||||
{SonarrSeriesID: 1, Title: "Battlestar Galactica", Year: "1978"},
|
||||
{SonarrSeriesID: 2, Title: "Battlestar Galactica", Year: "2004"},
|
||||
}
|
||||
if got := bazarrSeriesFor("Battlestar Galactica", 2004, series); got == nil || got.SonarrSeriesID != 2 {
|
||||
t.Fatalf("got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBazarrEpisodeMatchesOnNumbersNotTitles(t *testing.T) {
|
||||
episodes := []bazarr.Episode{
|
||||
{SonarrEpisodeID: 10, Season: 3, Episode: 3, Title: "Something"},
|
||||
{SonarrEpisodeID: 11, Season: 3, Episode: 4, Title: "Quite different in Sonarr"},
|
||||
{SonarrEpisodeID: 12, Season: 0, Episode: 4, Title: "A special"},
|
||||
}
|
||||
if got := bazarrEpisodeFor(episodes, 3, 4); got == nil || got.SonarrEpisodeID != 11 {
|
||||
t.Fatalf("got %+v", got)
|
||||
}
|
||||
// Season 0 is specials, a real season — it must not be confused with "no season".
|
||||
if got := bazarrEpisodeFor(episodes, 0, 4); got == nil || got.SonarrEpisodeID != 12 {
|
||||
t.Fatalf("specials should match, got %+v", got)
|
||||
}
|
||||
if got := bazarrEpisodeFor(episodes, 3, 9); got != nil {
|
||||
t.Fatalf("expected no match, got %+v", got)
|
||||
}
|
||||
if got := bazarrEpisodeFor(episodes, 3, 0); got != nil {
|
||||
t.Fatalf("a missing episode number must never match, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The viewer's language comes first because it is the only thing they asked for; within a
|
||||
// language a plain track beats a forced or hearing-impaired one, for the same reason
|
||||
// selectSubtitle ranks them that way.
|
||||
func TestRankSubtitleCandidatesPutsTheChosenLanguageFirst(t *testing.T) {
|
||||
found := []bazarr.Subtitle{
|
||||
{Language: "eng", Score: 99, Token: "en-plain"},
|
||||
{Language: "ita", Score: 40, Token: "it-plain"},
|
||||
{Language: "ita", Score: 98, Token: "it-forced", Forced: true},
|
||||
{Language: "ita", Score: 95, Token: "it-sdh", HearingImpaired: true},
|
||||
}
|
||||
results := rankSubtitleCandidates(found, "it")
|
||||
if len(results) != 4 {
|
||||
t.Fatalf("results = %+v", results)
|
||||
}
|
||||
order := []string{results[0].Token, results[1].Token, results[2].Token, results[3].Token}
|
||||
want := []string{"it-plain", "it-sdh", "it-forced", "en-plain"}
|
||||
for i := range want {
|
||||
if order[i] != want[i] {
|
||||
t.Fatalf("order = %v, want %v", order, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRankSubtitleCandidatesFallsBackToScoreWithNoPreference(t *testing.T) {
|
||||
found := []bazarr.Subtitle{
|
||||
{Language: "eng", Score: 55, Token: "low"},
|
||||
{Language: "eng", Score: 92, Token: "high"},
|
||||
}
|
||||
for _, language := range []string{"", "auto"} {
|
||||
results := rankSubtitleCandidates(found, language)
|
||||
if len(results) != 2 || results[0].Token != "high" {
|
||||
t.Fatalf("language %q gave %+v", language, results)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A row with no token cannot be downloaded, so offering it would be a button that does
|
||||
// nothing.
|
||||
func TestRankSubtitleCandidatesDropsTokenlessRowsAndCaps(t *testing.T) {
|
||||
found := []bazarr.Subtitle{{Language: "eng", Token: " "}}
|
||||
for i := 0; i < maxSubtitleResults+5; i++ {
|
||||
found = append(found, bazarr.Subtitle{Language: "eng", Score: i, Token: "t"})
|
||||
}
|
||||
results := rankSubtitleCandidates(found, "en")
|
||||
if len(results) > maxSubtitleResults {
|
||||
t.Fatalf("len = %d", len(results))
|
||||
}
|
||||
for _, result := range results {
|
||||
if result.Token == " " {
|
||||
t.Fatal("a tokenless row was offered")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubtitleCandidateLabelNamesTheLanguageAndKind(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
subtitle bazarr.Subtitle
|
||||
want string
|
||||
}{
|
||||
{bazarr.Subtitle{Language: "ita", Score: 97}, "Italian · 97% match"},
|
||||
{bazarr.Subtitle{Language: "eng", Forced: true, Score: 80}, "English · Forced · 80% match"},
|
||||
{bazarr.Subtitle{Language: "eng", HearingImpaired: true}, "English · Hearing impaired"},
|
||||
// A language Memby has no entry for must still name itself rather than go blank.
|
||||
{bazarr.Subtitle{Language: "mi"}, "MI"},
|
||||
{bazarr.Subtitle{}, "Unknown"},
|
||||
// Bazarr has been seen to return scores above 100; a "112% match" reads as a bug.
|
||||
{bazarr.Subtitle{Language: "eng", Score: 112}, "English · 100% match"},
|
||||
} {
|
||||
if got := subtitleCandidateLabel(testCase.subtitle); got != testCase.want {
|
||||
t.Errorf("label = %q, want %q", got, testCase.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The downloaded track has no marker saying it is new, so it is identified by what was
|
||||
// asked for. Failing to identify it is allowed — the client then applies its own rule.
|
||||
func TestNewestSubtitleIDPrefersTheMatchingExternalTrack(t *testing.T) {
|
||||
subtitles := []playableSubtitle{
|
||||
{ID: "1", Language: "eng", DeliveryMethod: "External"},
|
||||
{ID: "2", Language: "ita", DeliveryMethod: "Encode"},
|
||||
{ID: "3", Language: "ita", DeliveryMethod: "External", IsForced: true},
|
||||
{ID: "4", Language: "ita", DeliveryMethod: "External"},
|
||||
}
|
||||
if got := newestSubtitleID(subtitles, subtitleCandidate{Language: "it"}); got != "4" {
|
||||
t.Errorf("plain Italian = %q, want 4", got)
|
||||
}
|
||||
if got := newestSubtitleID(subtitles, subtitleCandidate{Language: "it", Forced: true}); got != "3" {
|
||||
t.Errorf("forced Italian = %q, want 3", got)
|
||||
}
|
||||
if got := newestSubtitleID(subtitles, subtitleCandidate{Language: "de"}); got != "" {
|
||||
t.Errorf("absent language = %q, want empty", got)
|
||||
}
|
||||
if got := newestSubtitleID(subtitles, subtitleCandidate{}); got != "" {
|
||||
t.Errorf("no language = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user