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 := rankMergedCandidates(bazarrCandidates(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 := rankMergedCandidates(bazarrCandidates(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 := rankMergedCandidates(bazarrCandidates(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 TestMergedCandidateLabelNamesTheLanguageAndKind(t *testing.T) { label := func(subtitle bazarr.Subtitle) string { return mergedCandidateLabel(bazarrCandidates([]bazarr.Subtitle{subtitle})[0]) } for _, testCase := range []struct { subtitle bazarr.Subtitle want string }{ {bazarr.Subtitle{Language: "ita", Score: 97, Token: "t"}, "Italian · 97% match"}, {bazarr.Subtitle{Language: "eng", Forced: true, Score: 80, Token: "t"}, "English · Forced · 80% match"}, {bazarr.Subtitle{Language: "eng", HearingImpaired: true, Token: "t"}, "English · Hearing impaired"}, // A language Memby has no entry for must still name itself rather than go blank. {bazarr.Subtitle{Language: "mi", Token: "t"}, "MI"}, {bazarr.Subtitle{Token: "t"}, "Unknown"}, // Bazarr has been seen to return scores above 100; a "112% match" reads as a bug. {bazarr.Subtitle{Language: "eng", Score: 112, Token: "t"}, "English · 100% match"}, } { if got := label(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) } }