распознавание: большие раздачи размечаются целиком, файлы вне плана видны
- модель адресует файл номером строки нашего списка вместо копии пути: ответ на 180 файлов вместо ~15k токенов стоит ~2.5k, усечение сотней снято, max_files и max_tokens ушли в [recognition], correction-ретрай больше не переприсылает список - негодный элемент ответа отбрасывается поимённой причиной, обрыв генерации и отказ по размеру запроса названы своими причинами, покрытие плана блокирует авто только при непокрытом видеофайле - раскладка показывает все файлы раздачи со строками «не в плане» и полным порядком сортировки; снимок списка файлов лёг рядом с планом (миграция 0012)
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
package recognize
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func intp(n int) *int { return &n }
|
||||
|
||||
func idxp(n int) *FileIndex { i := FileIndex(n); return &i }
|
||||
|
||||
func inputWith(paths ...string) Input {
|
||||
files := make([]File, len(paths))
|
||||
for i, p := range paths {
|
||||
@@ -25,9 +28,17 @@ func TestValidateSchema_OK(t *testing.T) {
|
||||
{Src: "b.mkv", Role: RoleEpisode, Season: intp(1), Episode: intp(2)},
|
||||
},
|
||||
}
|
||||
if err := validateSchema(&p, in); err != nil {
|
||||
problems, err := validateSchema(&p, in)
|
||||
if err != nil {
|
||||
t.Fatalf("validateSchema: %v", err)
|
||||
}
|
||||
if len(problems) != 0 {
|
||||
t.Errorf("чистый план не должен давать претензий: %v", problems)
|
||||
}
|
||||
// Запасной формат (src без номера) резолвится в свой же номер.
|
||||
if idxOf(p.Files[0]) != 1 || idxOf(p.Files[1]) != 2 {
|
||||
t.Errorf("индексы не проставлены: %+v", p.Files)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSchema_Errors(t *testing.T) {
|
||||
@@ -42,13 +53,13 @@ func TestValidateSchema_Errors(t *testing.T) {
|
||||
{"empty title", Plan{Type: MediaMovie, Files: []PlanFile{{Src: "a.mkv", Role: RoleMain}}}, "title is empty"},
|
||||
{"no files", Plan{Type: MediaMovie, Title: "x"}, "files list is empty"},
|
||||
{"bad role", Plan{Type: MediaMovie, Title: "x", Files: []PlanFile{{Src: "a.mkv", Role: "boss"}}}, "unknown role"},
|
||||
{"empty src", Plan{Type: MediaMovie, Title: "x", Files: []PlanFile{{Src: "", Role: RoleMain}}}, "empty src"},
|
||||
{"unknown src", Plan{Type: MediaMovie, Title: "x", Files: []PlanFile{{Src: "z.mkv", Role: RoleMain}}}, "not found among torrent files"},
|
||||
{"no addressing at all", Plan{Type: MediaMovie, Title: "x", Files: []PlanFile{{Src: "", Role: RoleMain}}}, "no files[] element addresses"},
|
||||
{"unknown src only", Plan{Type: MediaMovie, Title: "x", Files: []PlanFile{{Src: "z.mkv", Role: RoleMain}}}, "no files[] element addresses"},
|
||||
{"episode no num", Plan{Type: MediaSeries, Title: "x", Files: []PlanFile{{Src: "a.mkv", Role: RoleEpisode, Season: intp(1)}}}, "has no episode number"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateSchema(&tt.p, in)
|
||||
_, err := validateSchema(&tt.p, in)
|
||||
if err == nil || !strings.Contains(err.Error(), tt.want) {
|
||||
t.Errorf("err = %v, want contains %q", err, tt.want)
|
||||
}
|
||||
@@ -60,7 +71,7 @@ func TestParsePlan_FencedJSON(t *testing.T) {
|
||||
in := inputWith("film.mkv")
|
||||
raw := "Вот результат:\n```json\n{\"type\":\"movie\",\"title\":\"Film\"," +
|
||||
"\"files\":[{\"src\":\"film.mkv\",\"role\":\"main\"}]}\n```"
|
||||
p, err := parsePlan(raw, in, testLogger())
|
||||
p, _, err := parsePlan(raw, in, testLogger())
|
||||
if err != nil {
|
||||
t.Fatalf("parsePlan: %v", err)
|
||||
}
|
||||
@@ -73,7 +84,7 @@ func TestParsePlan_UnknownFieldTolerated(t *testing.T) {
|
||||
in := inputWith("film.mkv")
|
||||
raw := `{"type":"movie","title":"Film","extra_field":123,
|
||||
"files":[{"src":"film.mkv","role":"main"}]}`
|
||||
if _, err := parsePlan(raw, in, testLogger()); err != nil {
|
||||
if _, _, err := parsePlan(raw, in, testLogger()); err != nil {
|
||||
t.Fatalf("unknown field should be tolerated: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -106,10 +117,10 @@ func TestSeriesWarnings_GapAndDup(t *testing.T) {
|
||||
w := seriesWarnings(files)
|
||||
var dup, gap bool
|
||||
for _, s := range w {
|
||||
if strings.Contains(s, "дубль") {
|
||||
if strings.Contains(s, "дубли серий") {
|
||||
dup = true
|
||||
}
|
||||
if strings.Contains(s, "пропуск") {
|
||||
if strings.Contains(s, "не хватает серий") {
|
||||
gap = true
|
||||
}
|
||||
}
|
||||
@@ -157,7 +168,7 @@ func TestConsistencyWarnings(t *testing.T) {
|
||||
|
||||
func TestDecide_MetadataDisabled(t *testing.T) {
|
||||
p := Plan{Type: MediaMovie, Title: "X", Confidence: 0.99, Files: []PlanFile{{Role: RoleMain}}}
|
||||
d := decide(p, PreParse{}, nil, false, 0.85)
|
||||
d := decide(p, PreParse{}, nil, false, 0.85, planIssues{})
|
||||
if d.Auto {
|
||||
t.Error("без метабаз авто недопустимо")
|
||||
}
|
||||
@@ -168,7 +179,7 @@ func TestDecide_MetadataDisabled(t *testing.T) {
|
||||
|
||||
func TestDecide_NoMatch(t *testing.T) {
|
||||
p := Plan{Type: MediaMovie, Title: "X", Confidence: 0.99, Files: []PlanFile{{Role: RoleMain}}}
|
||||
d := decide(p, PreParse{}, nil, true, 0.85)
|
||||
d := decide(p, PreParse{}, nil, true, 0.85, planIssues{})
|
||||
if d.Auto || !strings.Contains(d.Reasons[0], "не найдено в базе") {
|
||||
t.Errorf("reasons = %v", d.Reasons)
|
||||
}
|
||||
@@ -178,7 +189,7 @@ func TestDecide_AutoMovie(t *testing.T) {
|
||||
p := Plan{Type: MediaMovie, Title: "The Matrix", Year: 1999, Confidence: 0.95,
|
||||
Files: []PlanFile{{Role: RoleMain}, {Role: RoleSample}}}
|
||||
match := &Match{Provider: "tmdb", ProviderID: "603", Title: "The Matrix", Year: 1999}
|
||||
d := decide(p, PreParse{Year: 1999}, match, true, 0.85)
|
||||
d := decide(p, PreParse{Year: 1999}, match, true, 0.85, planIssues{})
|
||||
if !d.Auto {
|
||||
t.Errorf("clean movie with match must be auto, reasons: %v", d.Reasons)
|
||||
}
|
||||
@@ -188,7 +199,7 @@ func TestDecide_LowConfidenceBlocksAuto(t *testing.T) {
|
||||
p := Plan{Type: MediaMovie, Title: "X", Year: 2000, Confidence: 0.5,
|
||||
Files: []PlanFile{{Role: RoleMain}}}
|
||||
match := &Match{Provider: "tmdb", ProviderID: "1", Title: "X", Year: 2000}
|
||||
d := decide(p, PreParse{}, match, true, 0.85)
|
||||
d := decide(p, PreParse{}, match, true, 0.85, planIssues{})
|
||||
if d.Auto || !hasReason(d.Reasons, "уверенность") {
|
||||
t.Errorf("low confidence must block auto, reasons: %v", d.Reasons)
|
||||
}
|
||||
@@ -201,20 +212,20 @@ func TestDecide_AutoSeriesEpisodeCount(t *testing.T) {
|
||||
Files: []PlanFile{mk(1), mk(2), mk(3)}}
|
||||
match := &Match{Provider: "tmdb", ProviderID: "1", Title: "Fargo", Year: 2014,
|
||||
SeasonEpisodeCounts: map[int]int{2: 3}}
|
||||
if d := decide(p, PreParse{Year: 2014}, match, true, 0.85); !d.Auto {
|
||||
if d := decide(p, PreParse{Year: 2014}, match, true, 0.85, planIssues{}); !d.Auto {
|
||||
t.Errorf("full season must be auto, reasons: %v", d.Reasons)
|
||||
}
|
||||
|
||||
// Неполный пак (в базе 10) — авто блокируется.
|
||||
match.SeasonEpisodeCounts = map[int]int{2: 10}
|
||||
if d := decide(p, PreParse{Year: 2014}, match, true, 0.85); d.Auto ||
|
||||
if d := decide(p, PreParse{Year: 2014}, match, true, 0.85, planIssues{}); d.Auto ||
|
||||
!hasReason(d.Reasons, "распознано серий 3, в базе 10") {
|
||||
t.Errorf("partial pack must block auto, reasons: %v", d.Reasons)
|
||||
}
|
||||
|
||||
// Нет данных о числе серий — авто блокируется.
|
||||
match.SeasonEpisodeCounts = nil
|
||||
if d := decide(p, PreParse{Year: 2014}, match, true, 0.85); d.Auto ||
|
||||
if d := decide(p, PreParse{Year: 2014}, match, true, 0.85, planIssues{}); d.Auto ||
|
||||
!hasReason(d.Reasons, "нет данных о числе серий") {
|
||||
t.Errorf("missing counts must block auto, reasons: %v", d.Reasons)
|
||||
}
|
||||
@@ -234,3 +245,256 @@ func TestPreParse(t *testing.T) {
|
||||
t.Errorf("season/episode = %d/%d, want 2/5", series.Season, series.Episode)
|
||||
}
|
||||
}
|
||||
|
||||
// Перечень видеорасширений — единственное место, где решается, что считать
|
||||
// потерянной серией: незнакомое расширение уедет в спутники и авто не заблокирует.
|
||||
func TestIsVideoFile(t *testing.T) {
|
||||
video := []string{
|
||||
"a.mkv", "a.MKV", "s1/e01.avi", "film.mp4", "x.m4v", "x.mov", "x.wmv",
|
||||
"x.mpg", "x.mpeg", "BDMV/STREAM/00001.m2ts", "x.mts", "x.ts", "x.vob",
|
||||
"x.flv", "x.webm", "x.ogm", "x.divx", "x.rmvb", "x.3gp", "disc.iso", "disc.img",
|
||||
}
|
||||
for _, p := range video {
|
||||
if !IsVideoFile(p) {
|
||||
t.Errorf("IsVideoFile(%q) = false, want true", p)
|
||||
}
|
||||
}
|
||||
other := []string{
|
||||
"a.srt", "a.ass", "a.sub", "a.idx", "a.nfo", "a.txt", "a.jpg", "a.png",
|
||||
"a.md5", "a.sfv", "cover", "a.mkv.txt", "",
|
||||
}
|
||||
for _, p := range other {
|
||||
if IsVideoFile(p) {
|
||||
t.Errorf("IsVideoFile(%q) = true, want false", p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Сезон с тремя дырами даёт ровно одну причину, перечисляющую недостающие серии.
|
||||
func TestSeriesWarnings_GapsCollapsedPerSeason(t *testing.T) {
|
||||
var files []PlanFile
|
||||
for _, e := range []int{1, 2, 3, 4, 6, 8, 9, 10, 12} { // нет 5, 7, 11
|
||||
files = append(files, PlanFile{Role: RoleEpisode, Season: intp(1), Episode: intp(e)})
|
||||
}
|
||||
w := seriesWarnings(files)
|
||||
if len(w) != 1 {
|
||||
t.Fatalf("ожидалась одна причина на сезон, получено %d: %v", len(w), w)
|
||||
}
|
||||
if !strings.Contains(w[0], "E05") || !strings.Contains(w[0], "E07") || !strings.Contains(w[0], "E11") {
|
||||
t.Errorf("причина не перечисляет недостающие серии: %q", w[0])
|
||||
}
|
||||
}
|
||||
|
||||
// Мультисезонный пак: на сезон — не больше одной причины о пропусках.
|
||||
func TestSeriesWarnings_OneReasonPerSeason(t *testing.T) {
|
||||
var files []PlanFile
|
||||
for s := 1; s <= 8; s++ {
|
||||
for _, e := range []int{1, 3, 5} { // дыры в каждом сезоне
|
||||
files = append(files, PlanFile{Role: RoleEpisode, Season: intp(s), Episode: intp(e)})
|
||||
}
|
||||
}
|
||||
if w := seriesWarnings(files); len(w) != 8 {
|
||||
t.Errorf("причин %d при 8 сезонах, ожидалось 8: %v", len(w), w)
|
||||
}
|
||||
}
|
||||
|
||||
// Непокрытый видеофайл означает потерянную серию — авто блокируется.
|
||||
func TestDecide_UncoveredVideoBlocksAuto(t *testing.T) {
|
||||
in := inputWith("s1/e01.mkv", "s1/e02.mkv")
|
||||
p := Plan{Type: MediaSeries, Title: "Show", Year: 2020, Confidence: 0.95,
|
||||
Files: []PlanFile{{Src: "s1/e01.mkv", Role: RoleEpisode, Season: intp(1), Episode: intp(1)}}}
|
||||
match := &Match{Provider: "tmdb", ProviderID: "1", Title: "Show", Year: 2020,
|
||||
SeasonEpisodeCounts: map[int]int{1: 1}}
|
||||
|
||||
d := decide(p, PreParse{}, match, true, 0.85,
|
||||
planIssues{files: in.Files, all: in.Files})
|
||||
if d.Auto {
|
||||
t.Errorf("непокрытый видеофайл обязан блокировать авто: %v", d.Reasons)
|
||||
}
|
||||
if !hasReason(d.Reasons, "вне плана осталось видеофайлов: 1") {
|
||||
t.Errorf("reasons = %v", d.Reasons)
|
||||
}
|
||||
if !hasReason(d.Reasons, "в план попало 1 файлов из 2") {
|
||||
t.Errorf("сводка покрытия обязана быть названа: %v", d.Reasons)
|
||||
}
|
||||
}
|
||||
|
||||
// Непокрытые спутники (.nfo, скриншоты, тексты) видны в покрытии, но авто не
|
||||
// отменяют: модель вправе не перечислять то, что не раскладывается.
|
||||
func TestDecide_UncoveredCompanionsKeepAuto(t *testing.T) {
|
||||
in := inputWith("film.mkv", "film.nfo", "screens/01.jpg", "readme.txt")
|
||||
p := Plan{Type: MediaMovie, Title: "Film", Year: 1999, Confidence: 0.95,
|
||||
Files: []PlanFile{{Src: "film.mkv", Role: RoleMain}}}
|
||||
match := &Match{Provider: "tmdb", ProviderID: "1", Title: "Film", Year: 1999}
|
||||
|
||||
d := decide(p, PreParse{Year: 1999}, match, true, 0.85,
|
||||
planIssues{files: in.Files, all: in.Files})
|
||||
if !d.Auto {
|
||||
t.Errorf("непокрытые спутники не должны отменять авто: %v", d.Reasons)
|
||||
}
|
||||
if !hasReason(d.Reasons, "в план попало 1 файлов из 4") {
|
||||
t.Errorf("покрытие обязано быть показано человеку: %v", d.Reasons)
|
||||
}
|
||||
}
|
||||
|
||||
// Отброшенный элемент — блокирующая причина, а полное покрытие сводкой не шумит.
|
||||
func TestDecide_FullCoverageIsSilent(t *testing.T) {
|
||||
in := inputWith("film.mkv")
|
||||
p := Plan{Type: MediaMovie, Title: "Film", Year: 1999, Confidence: 0.95,
|
||||
Files: []PlanFile{{Src: "film.mkv", Role: RoleMain}}}
|
||||
match := &Match{Provider: "tmdb", ProviderID: "1", Title: "Film", Year: 1999}
|
||||
|
||||
d := decide(p, PreParse{Year: 1999}, match, true, 0.85,
|
||||
planIssues{files: in.Files, all: in.Files})
|
||||
if !d.Auto || len(d.Reasons) != 0 {
|
||||
t.Errorf("полное покрытие не должно давать причин: %+v", d)
|
||||
}
|
||||
|
||||
withDrop := decide(p, PreParse{Year: 1999}, match, true, 0.85,
|
||||
planIssues{files: in.Files, all: in.Files, dropped: []string{"элемент отброшен"}})
|
||||
if withDrop.Auto || !hasReason(withDrop.Reasons, "элемент отброшен") {
|
||||
t.Errorf("отбраковка обязана блокировать авто: %+v", withDrop)
|
||||
}
|
||||
}
|
||||
|
||||
// Датовая нумерация серий (обычная для ежедневных шоу) рядом с обычной даёт
|
||||
// разрыв в миллионы номеров. Перечисление такого разрыва материализовало бы
|
||||
// сотни мегабайт int при пределе печати в 12 номеров, поэтому пропуски
|
||||
// считаются арифметикой, а не перечислением.
|
||||
func TestSeriesWarnings_HugeGapNotMaterialized(t *testing.T) {
|
||||
files := []PlanFile{
|
||||
{Role: RoleEpisode, Season: intp(1), Episode: intp(1)},
|
||||
{Role: RoleEpisode, Season: intp(1), Episode: intp(20260902)},
|
||||
}
|
||||
|
||||
var before, after runtime.MemStats
|
||||
runtime.ReadMemStats(&before)
|
||||
w := seriesWarnings(files)
|
||||
runtime.ReadMemStats(&after)
|
||||
|
||||
// Бюджет — на строки причины, а не на 20 млн номеров: старая реализация
|
||||
// брала здесь ~160 МиБ.
|
||||
if grew := after.TotalAlloc - before.TotalAlloc; grew > 1<<20 {
|
||||
t.Errorf("разрыв материализован: выделено %d байт", grew)
|
||||
}
|
||||
if len(w) != 1 {
|
||||
t.Fatalf("ожидалась одна причина на сезон, получено %v", w)
|
||||
}
|
||||
if !strings.Contains(w[0], "E02, E03") || !strings.Contains(w[0], "E13") {
|
||||
t.Errorf("причина обязана перечислять начало разрыва: %q", w[0])
|
||||
}
|
||||
if !strings.Contains(w[0], "и ещё 20260888") {
|
||||
t.Errorf("хвост обязан быть назван числом: %q", w[0])
|
||||
}
|
||||
}
|
||||
|
||||
// Длинный хвост пропусков сворачивается: печатается предел, остальное — счётчик.
|
||||
func TestSeriesWarnings_LongTailFolded(t *testing.T) {
|
||||
files := []PlanFile{
|
||||
{Role: RoleEpisode, Season: intp(1), Episode: intp(1)},
|
||||
{Role: RoleEpisode, Season: intp(1), Episode: intp(20)}, // нет 2..19 — 18 штук
|
||||
}
|
||||
w := seriesWarnings(files)
|
||||
if len(w) != 1 {
|
||||
t.Fatalf("причины = %v", w)
|
||||
}
|
||||
if strings.Count(w[0], "E") != episodeListMax {
|
||||
t.Errorf("напечатано номеров: %d, ожидался предел %d: %q",
|
||||
strings.Count(w[0], "E"), episodeListMax, w[0])
|
||||
}
|
||||
if !strings.Contains(w[0], "E13") || strings.Contains(w[0], "E14") {
|
||||
t.Errorf("предел печати сдвинулся: %q", w[0])
|
||||
}
|
||||
if !strings.Contains(w[0], "и ещё 6") {
|
||||
t.Errorf("свёрнутый хвост не назван числом: %q", w[0])
|
||||
}
|
||||
}
|
||||
|
||||
// Претензии к элементам не растут без предела, а внешние значения в них
|
||||
// усечены: причины оседают в БД навсегда и уезжают в карточку Telegram.
|
||||
func TestValidateSchema_ProblemsCappedAndValuesShortened(t *testing.T) {
|
||||
long := "сезон 1/" + strings.Repeat("длинное-имя-", 30) + "серия.mkv"
|
||||
in := inputWith(long, "b.mkv")
|
||||
p := Plan{Type: MediaSeries, Title: "Show", Files: []PlanFile{
|
||||
{Src: long, Role: RoleEpisode, Season: intp(1), Episode: intp(1)},
|
||||
}}
|
||||
// Тридцать элементов, каждый повторно адресующий первый файл.
|
||||
for range 30 {
|
||||
p.Files = append(p.Files,
|
||||
PlanFile{Src: long, Role: RoleEpisode, Season: intp(1), Episode: intp(2)})
|
||||
}
|
||||
problems, err := validateSchema(&p, in)
|
||||
if err != nil {
|
||||
t.Fatalf("validateSchema: %v", err)
|
||||
}
|
||||
if len(problems) != maxProblems+1 {
|
||||
t.Fatalf("претензий %d, ожидались %d поимённых плюс свёрнутый хвост: %v",
|
||||
len(problems), maxProblems, problems)
|
||||
}
|
||||
if !strings.Contains(problems[len(problems)-1], "и ещё 18") {
|
||||
t.Errorf("хвост претензий не назван числом: %q", problems[len(problems)-1])
|
||||
}
|
||||
for _, s := range problems {
|
||||
if strings.Contains(s, long) {
|
||||
t.Errorf("внешнее значение не усечено: %q", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Отказ по неизвестной роли называет файл так, как его адресовала модель:
|
||||
// проверка стоит до резолва, и pf.Src там ещё пуст.
|
||||
func TestValidateSchema_UnknownRoleNamesAddressing(t *testing.T) {
|
||||
in := inputWith("a.mkv", "b.mkv")
|
||||
p := Plan{Type: MediaMovie, Title: "Film", Files: []PlanFile{
|
||||
{Index: idxp(2), Role: "trailer"},
|
||||
}}
|
||||
_, err := validateSchema(&p, in)
|
||||
if err == nil || !strings.Contains(err.Error(), "#2") {
|
||||
t.Errorf("err = %v, ожидался номер адресации", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Ноль — не «номера нет»: так адресует модель, посчитавшая список с нуля, и
|
||||
// остальные её номера уезжают на файл. Диагноз обязан быть самоописывающимся.
|
||||
func TestResolveFile_ZeroIndexIsItsOwnCase(t *testing.T) {
|
||||
in := inputWith("a.mkv", "b.mkv")
|
||||
byPath := map[string]int{"a.mkv": 0, "b.mkv": 1}
|
||||
|
||||
_, problem, ok := resolveFile(PlanFile{Index: idxp(0)}, in.Files, byPath)
|
||||
if ok || !strings.Contains(problem, "нумеруется с 1") {
|
||||
t.Errorf("явный ноль: ok=%v, problem=%q", ok, problem)
|
||||
}
|
||||
_, problem, ok = resolveFile(PlanFile{}, in.Files, byPath)
|
||||
if ok || !strings.Contains(problem, "без номера файла и без пути") {
|
||||
t.Errorf("поля нет вовсе: ok=%v, problem=%q", ok, problem)
|
||||
}
|
||||
// Ноль рядом с путём остаётся запасным форматом: путь резолвится.
|
||||
idx, problem, ok := resolveFile(PlanFile{Index: idxp(0), Src: "b.mkv"}, in.Files, byPath)
|
||||
if !ok || idx != 1 || problem != "" {
|
||||
t.Errorf("путь при нулевом номере обязан резолвиться: idx=%d ok=%v %q", idx, ok, problem)
|
||||
}
|
||||
}
|
||||
|
||||
// Структурное предупреждение и рассинхрон года — блокирующие причины, а не
|
||||
// информационные заметки: развод «блокирует / не блокирует» обязан исполняться.
|
||||
func TestDecide_StructuralAndConsistencyBlockAuto(t *testing.T) {
|
||||
in := inputWith("a.mkv", "b.mkv")
|
||||
p := Plan{Type: MediaMovie, Title: "Film", Year: 1999, Confidence: 0.95,
|
||||
Files: []PlanFile{
|
||||
{Src: "a.mkv", Role: RoleMain},
|
||||
{Src: "b.mkv", Role: RoleMain}, // два main у фильма
|
||||
}}
|
||||
match := &Match{Provider: "tmdb", ProviderID: "1", Title: "Film", Year: 1999}
|
||||
|
||||
d := decide(p, PreParse{Year: 1998}, match, true, 0.85,
|
||||
planIssues{files: in.Files, all: in.Files})
|
||||
if d.Auto {
|
||||
t.Errorf("структурное предупреждение обязано блокировать авто: %v", d.Reasons)
|
||||
}
|
||||
if !hasReason(d.Reasons, "основных видеофайлов 2") {
|
||||
t.Errorf("структурная причина не названа: %v", d.Reasons)
|
||||
}
|
||||
if !hasReason(d.Reasons, "год расходится: пред-парс=1998, LLM=1999") {
|
||||
t.Errorf("рассинхрон года не назван: %v", d.Reasons)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user