package httpapi import ( "net/http" "strings" "testing" "git.vakhrushev.me/av/jellybit/internal/layout" "git.vakhrushev.me/av/jellybit/internal/recognize" "git.vakhrushev.me/av/jellybit/internal/store" "git.vakhrushev.me/av/jellybit/internal/worker" ) func ip(n int) *int { return &n } // srcFiles — снимок списка файлов раздачи в порядке нумерации распознавания. func srcFiles(paths ...string) []recognize.File { out := make([]recognize.File, len(paths)) for i, p := range paths { out[i] = recognize.File{Path: p, Size: 1 << 20} } return out } // Файл, которого модель не разметила, виден отдельной строкой без цели. func TestBuildFileRows_UnplannedFileVisible(t *testing.T) { files := srcFiles("s1/e01.mkv", "s1/e02.mkv", "extras/screen.jpg") plan := recognize.Plan{Type: recognize.MediaSeries, Files: []recognize.PlanFile{ {Src: "s1/e01.mkv", Role: recognize.RoleEpisode, Season: ip(1), Episode: ip(1)}, }} preview := []layout.Link{{Src: "s1/e01.mkv", Dst: "/srv/series/Show/S01/E01.mkv"}} w := buildFileRows(plan, preview, files, true) if len(w.Rows) != 3 { t.Fatalf("строк %d, ожидалось по одной на файл раздачи: %+v", len(w.Rows), w.Rows) } if w.Planned != 1 || w.Total != 3 || w.SourceUnknown { t.Errorf("сводка = %+v", w) } bySrc := map[string]fileRow{} for _, r := range w.Rows { bySrc[r.Src] = r } for _, src := range []string{"s1/e02.mkv", "extras/screen.jpg"} { r := bySrc[src] if !r.Unplanned || r.Dst != "" || r.Linked { t.Errorf("%s: ожидалась строка «не в плане» без цели, получено %+v", src, r) } if r.RoleLabel != "не в плане" { t.Errorf("%s: роль = %q", src, r.RoleLabel) } } // Номер строки — номер адресации: позиция в списке распознавания. if bySrc["s1/e02.mkv"].Num != 2 || bySrc["extras/screen.jpg"].Num != 3 { t.Errorf("номера не совпадают с нумерацией распознавания: %+v", w.Rows) } } // Порядок строк не зависит от порядка plan.Files. func TestBuildFileRows_OrderIndependentOfPlan(t *testing.T) { files := srcFiles( "pack/s1e01.mkv", "pack/s1e02.mkv", "pack/s2e01.mkv", "pack/sample.mkv", "pack/notes.nfo") ep := func(src string, s, e int) recognize.PlanFile { return recognize.PlanFile{Src: src, Role: recognize.RoleEpisode, Season: ip(s), Episode: ip(e)} } preview := []layout.Link{ {Src: "pack/s1e01.mkv", Dst: "/srv/Show/S01/E01.mkv"}, {Src: "pack/s1e02.mkv", Dst: "/srv/Show/S01/E02.mkv"}, {Src: "pack/s2e01.mkv", Dst: "/srv/Show/S02/E01.mkv"}, } want := []string{"pack/s1e01.mkv", "pack/s1e02.mkv", "pack/s2e01.mkv", "pack/sample.mkv", "pack/notes.nfo"} orders := [][]recognize.PlanFile{ {ep("pack/s1e01.mkv", 1, 1), ep("pack/s1e02.mkv", 1, 2), ep("pack/s2e01.mkv", 2, 1), {Src: "pack/sample.mkv", Role: recognize.RoleSample}}, {{Src: "pack/sample.mkv", Role: recognize.RoleSample}, ep("pack/s2e01.mkv", 2, 1), ep("pack/s1e02.mkv", 1, 2), ep("pack/s1e01.mkv", 1, 1)}, } for _, pf := range orders { w := buildFileRows(recognize.Plan{Type: recognize.MediaSeries, Files: pf}, preview, files, true) got := make([]string, 0, len(w.Rows)) for _, r := range w.Rows { got = append(got, r.Src) } if strings.Join(got, "|") != strings.Join(want, "|") { t.Errorf("порядок = %v, want %v", got, want) } } } // Файлы без сезона и серии не поднимаются в начало списка. func TestBuildFileRows_UnnumberedNotFirst(t *testing.T) { files := srcFiles("a_sample.mkv", "z_s01e01.mkv") plan := recognize.Plan{Type: recognize.MediaSeries, Files: []recognize.PlanFile{ {Src: "a_sample.mkv", Role: recognize.RoleSample}, {Src: "z_s01e01.mkv", Role: recognize.RoleEpisode, Season: ip(1), Episode: ip(1)}, }} preview := []layout.Link{{Src: "z_s01e01.mkv", Dst: "/srv/Show/S01/E01.mkv"}} w := buildFileRows(plan, preview, files, true) if w.Rows[0].Src != "z_s01e01.mkv" { t.Errorf("первым обязан идти разложенный файл, получено %+v", w.Rows) } } // Запись без сохранённого списка файлов: строки по плану + честная пометка. func TestBuildFileRows_SourceUnknown(t *testing.T) { plan := recognize.Plan{Type: recognize.MediaMovie, Files: []recognize.PlanFile{ {Src: "film.mkv", Role: recognize.RoleMain}, }} w := buildFileRows(plan, []layout.Link{{Src: "film.mkv", Dst: "/srv/movies/F/f.mkv"}}, nil, false) if !w.SourceUnknown { t.Error("отсутствие снимка обязано быть названо явно") } if len(w.Rows) != 1 || w.Rows[0].Src != "film.mkv" { t.Errorf("rows = %+v", w.Rows) } if w.Rows[0].Num != 0 { t.Errorf("без снимка номер адресации неизвестен и показываться не должен: %+v", w.Rows[0]) } } // Страница загрузки говорит о недоступности списка файлов, а не показывает // план как полный перечень. func TestDownloadPage_SaysFileListUnavailable(t *testing.T) { dl := store.Download{ID: testULID, SourceRef: "Film", State: store.StateReview} plan := recognize.Plan{Type: recognize.MediaMovie, Title: "Film", Files: []recognize.PlanFile{{Src: "film.mkv", Role: recognize.RoleMain}}} rd := &worker.ReviewData{ Download: dl, Recognition: &store.Recognition{ID: testULID, DownloadID: testULID}, Plan: plan, } h := testRouter(t, stubReader{list: []store.Download{dl}, one: &dl}, stubReviewer{data: rd}) rr := get(t, h, "/download/"+testULID) if rr.Code != http.StatusOK { t.Fatalf("GET /download/{id} = %d", rr.Code) } if !strings.Contains(rr.Body.String(), "Список файлов раздачи не сохранён") { t.Errorf("страница молчит о неполноте перечня") } // Со снимком — пометка исчезает, а файл вне плана виден. rd.SourceFiles = srcFiles("film.mkv", "screens/01.jpg") rd.SourceFilesKnown = true rr = get(t, h, "/download/"+testULID) body := rr.Body.String() if strings.Contains(body, "Список файлов раздачи не сохранён") { t.Errorf("со снимком пометка о неполноте не нужна") } if !strings.Contains(body, "screens/01.jpg") || !strings.Contains(body, "не в плане") { t.Errorf("файл вне плана не показан:\n%s", body) } } // Легаси-план вправе разметить один файл двумя элементами (сдвоенный эпизод): // Apply создаст два хардлинка, и виджет обязан показать обе цели — иначе // предпросмотр перестаёт быть равен применению. func TestBuildFileRows_DuplicateSrcShownTwice(t *testing.T) { files := srcFiles("pack/s01e01e02.mkv", "pack/s01e03.mkv") plan := recognize.Plan{Type: recognize.MediaSeries, Files: []recognize.PlanFile{ {Src: "pack/s01e01e02.mkv", Role: recognize.RoleEpisode, Season: ip(1), Episode: ip(1)}, {Src: "pack/s01e01e02.mkv", Role: recognize.RoleEpisode, Season: ip(1), Episode: ip(2)}, {Src: "pack/s01e03.mkv", Role: recognize.RoleEpisode, Season: ip(1), Episode: ip(3)}, }} preview := []layout.Link{ {Src: "pack/s01e01e02.mkv", Dst: "/srv/Show/S01/E01.mkv"}, {Src: "pack/s01e01e02.mkv", Dst: "/srv/Show/S01/E02.mkv"}, {Src: "pack/s01e03.mkv", Dst: "/srv/Show/S01/E03.mkv"}, } w := buildFileRows(plan, preview, files, true) if len(w.Rows) != 3 { t.Fatalf("строк %d, ожидалась строка на каждый элемент плана: %+v", len(w.Rows), w.Rows) } var dsts []string for _, r := range w.Rows { if r.Src == "pack/s01e01e02.mkv" { dsts = append(dsts, r.Dst) } if r.Num == 0 { t.Errorf("номер адресации потерян: %+v", r) } } if len(dsts) != 2 || dsts[0] != "/srv/Show/S01/E01.mkv" || dsts[1] != "/srv/Show/S01/E02.mkv" { t.Errorf("обе цели сдвоенного эпизода = %v", dsts) } // Знаменатель покрытия — файлы раздачи, а не элементы плана. if w.Planned != 2 || w.Total != 2 { t.Errorf("сводка = %+v", w) } }