Files
memby/server/internal/api/subtitle_download_test.go
2026-08-09 08:25:50 +12:00

186 lines
7.3 KiB
Go

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)
}
}