mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 19:36:36 +08:00
234 lines
8.2 KiB
Go
234 lines
8.2 KiB
Go
package service
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/ShukeBta/MediaStationGo/internal/config"
|
|
"github.com/ShukeBta/MediaStationGo/internal/model"
|
|
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
|
)
|
|
|
|
func TestRepairAndRescrapeLibraryForceRematchesThenReclassifies(t *testing.T) {
|
|
scraper, repos, closeServer := newTestScraper(t)
|
|
defer closeServer()
|
|
|
|
root := t.TempDir()
|
|
wrongRoot := filepath.Join(root, "media", "电视剧", "国产剧")
|
|
mediaPath := filepath.Join(wrongRoot, "Spy Family", "Season 01", "Spy Family - S01E01.mkv")
|
|
if err := os.MkdirAll(filepath.Dir(mediaPath), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(mediaPath, []byte("episode"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lib := model.Library{Name: "国产剧", Path: wrongRoot, Type: "tv", Enabled: true}
|
|
if err := repos.Library.Create(t.Context(), &lib); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
media := model.Media{
|
|
LibraryID: lib.ID,
|
|
Title: "错误旧匹配",
|
|
Path: mediaPath,
|
|
SeasonNum: 1,
|
|
EpisodeNum: 1,
|
|
TMDbID: 999,
|
|
Countries: "CN",
|
|
Languages: "zh",
|
|
Genres: "Drama",
|
|
ScrapeStatus: "matched",
|
|
}
|
|
if err := repos.DB.Create(&media).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cfg := &config.Config{}
|
|
cfg.Organizer.SmartClassify = true
|
|
organizer := NewOrganizerService(cfg, zap.NewNop(), repos)
|
|
organizer.SetScraper(scraper)
|
|
container := &Container{Cfg: cfg, Log: zap.NewNop(), Repo: repos, Scraper: scraper, Organizer: organizer}
|
|
result, err := container.RepairAndRescrapeLibrary(t.Context(), lib.ID)
|
|
if err != nil {
|
|
t.Fatalf("repair and rescrape: %v", err)
|
|
}
|
|
if result.Reclassified != 1 {
|
|
t.Fatalf("result=%+v, want one corrected classification", result)
|
|
}
|
|
want := filepath.Join(root, "media", "动漫", "日番", "间谍过家家", "Season 01", "间谍过家家 - S01E01.mkv")
|
|
if _, err := os.Stat(want); err != nil {
|
|
t.Fatalf("corrected media missing at %q: %v", want, err)
|
|
}
|
|
var got model.Media
|
|
if err := repos.DB.First(&got, "id = ?", media.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.TMDbID != 12345 || got.Countries != "JP" || got.Path != want {
|
|
t.Fatalf("repaired media=%#v, want rematched Japanese anime at corrected path", got)
|
|
}
|
|
}
|
|
|
|
func TestRepairRescrapeOptionsDefaultSkipsEpisodeArtwork(t *testing.T) {
|
|
options := repairRescrapeOptions()
|
|
if !options.RetryNoMatch {
|
|
t.Fatal("repair rescrape should retry no_match rows")
|
|
}
|
|
if !options.IncludeMatched {
|
|
t.Fatal("repair rescrape should refresh already matched rows")
|
|
}
|
|
if !options.ForceRematch {
|
|
t.Fatal("repair rescrape should ignore stale external IDs and rematch by path/title")
|
|
}
|
|
if options.EpisodeArtwork == nil {
|
|
t.Fatal("repair rescrape should set an explicit episode artwork option")
|
|
}
|
|
if *options.EpisodeArtwork {
|
|
t.Fatal("repair rescrape should skip episode artwork by default")
|
|
}
|
|
}
|
|
|
|
func TestRepairRescrapeOptionsCanEnableEpisodeArtwork(t *testing.T) {
|
|
episodeArtwork := true
|
|
options := repairRescrapeOptions(ScrapeOptions{EpisodeArtwork: &episodeArtwork})
|
|
if !options.RetryNoMatch {
|
|
t.Fatal("repair rescrape should force retry no_match rows")
|
|
}
|
|
if !options.IncludeMatched {
|
|
t.Fatal("repair rescrape should force refreshing already matched rows")
|
|
}
|
|
if options.EpisodeArtwork == nil || !*options.EpisodeArtwork {
|
|
t.Fatal("repair rescrape should keep explicit episode artwork=true")
|
|
}
|
|
}
|
|
|
|
func TestRepairRescrapeOptionsKeepsExplicitEpisodeArtworkFalse(t *testing.T) {
|
|
episodeArtwork := false
|
|
options := repairRescrapeOptions(ScrapeOptions{EpisodeArtwork: &episodeArtwork})
|
|
if !options.RetryNoMatch {
|
|
t.Fatal("repair rescrape should force retry no_match rows")
|
|
}
|
|
if !options.IncludeMatched {
|
|
t.Fatal("repair rescrape should force refreshing already matched rows")
|
|
}
|
|
if options.EpisodeArtwork == nil {
|
|
t.Fatal("repair rescrape should keep explicit episode artwork option")
|
|
}
|
|
if *options.EpisodeArtwork {
|
|
t.Fatal("repair rescrape should keep explicit episode artwork=false")
|
|
}
|
|
}
|
|
|
|
// TestResetEpisodicMatchedForRescrape 验证「修复+重刮」会把脏的 matched 剧集行
|
|
// 重置为 pending(让 EnrichLibrary 能重新刮削),而电影行与其它库不受影响。
|
|
func TestResetEpisodicMatchedForRescrape(t *testing.T) {
|
|
db := newServiceTestDB(t, &model.Library{}, &model.Media{})
|
|
container := &Container{Repo: repository.New(db), Log: zap.NewNop()}
|
|
|
|
rows := []model.Media{
|
|
// 目标库的剧集行(matched, 有季集号)→ 应被重置。
|
|
{Base: model.Base{ID: "ep1"}, LibraryID: "lib-a", SeasonNum: 1, EpisodeNum: 1, ScrapeStatus: "matched", Path: "/a/show/S01/ep1.mkv"},
|
|
{Base: model.Base{ID: "ep2"}, LibraryID: "lib-a", SeasonNum: 1, EpisodeNum: 2, ScrapeStatus: "matched", Path: "/a/show/S01/ep2.mkv"},
|
|
// 目标库的电影行(无季集号)→ 不应被重置。
|
|
{Base: model.Base{ID: "movie1"}, LibraryID: "lib-a", ScrapeStatus: "matched", Path: "/a/movie.mkv"},
|
|
// 目标库已是 pending 的剧集行 → 不计入重置数。
|
|
{Base: model.Base{ID: "ep3"}, LibraryID: "lib-a", SeasonNum: 1, EpisodeNum: 3, ScrapeStatus: "pending", Path: "/a/show/S01/ep3.mkv"},
|
|
// 其它库的剧集行(matched)→ 单库重置时不应受影响。
|
|
{Base: model.Base{ID: "ep-other"}, LibraryID: "lib-b", SeasonNum: 1, EpisodeNum: 1, ScrapeStatus: "matched", Path: "/b/show/S01/ep1.mkv"},
|
|
}
|
|
for i := range rows {
|
|
if err := db.Create(&rows[i]).Error; err != nil {
|
|
t.Fatalf("create media %s: %v", rows[i].ID, err)
|
|
}
|
|
}
|
|
|
|
reset, err := container.resetEpisodicMatchedForRescrape(t.Context(), "lib-a")
|
|
if err != nil {
|
|
t.Fatalf("reset: %v", err)
|
|
}
|
|
if reset != 2 {
|
|
t.Fatalf("reset = %d, want 2 (only matched episodic rows in lib-a)", reset)
|
|
}
|
|
|
|
status := func(id string) string {
|
|
var m model.Media
|
|
if err := db.First(&m, "id = ?", id).Error; err != nil {
|
|
t.Fatalf("load %s: %v", id, err)
|
|
}
|
|
return m.ScrapeStatus
|
|
}
|
|
if status("ep1") != "pending" || status("ep2") != "pending" {
|
|
t.Fatalf("episodic matched rows should be pending, got ep1=%q ep2=%q", status("ep1"), status("ep2"))
|
|
}
|
|
if status("movie1") != "matched" {
|
|
t.Fatalf("movie row should stay matched, got %q", status("movie1"))
|
|
}
|
|
if status("ep-other") != "matched" {
|
|
t.Fatalf("other library row should stay matched, got %q", status("ep-other"))
|
|
}
|
|
}
|
|
|
|
func TestRepairAndRescrapeLibraryExpandsMergedCloudLibraries(t *testing.T) {
|
|
db := newServiceTestDB(t, &model.Library{}, &model.Media{})
|
|
container := &Container{Repo: repository.New(db), Log: zap.NewNop()}
|
|
|
|
local := model.Library{Name: "国产剧", Path: "/media/电视剧/国产剧", Type: "tv", Enabled: true}
|
|
cloud := model.Library{
|
|
Name: "OpenList · 国产剧",
|
|
Path: BuildCloudLibraryPath("openlist", "/国产剧", "/国产剧"),
|
|
Type: "tv",
|
|
Enabled: true,
|
|
}
|
|
if err := container.Repo.Library.Create(t.Context(), &local); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := container.Repo.Library.Create(t.Context(), &cloud); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
repairMedia := model.Media{
|
|
LibraryID: cloud.ID,
|
|
Title: "主角",
|
|
Path: "cloud://openlist/国产剧/主角 (2026) {tmdb-284110}/Season 1/主角.S01E01.mkv",
|
|
SeasonNum: 1,
|
|
EpisodeNum: 1,
|
|
ScrapeStatus: "matched",
|
|
}
|
|
resetMedia := model.Media{
|
|
LibraryID: cloud.ID,
|
|
Title: "无占位符剧集",
|
|
Path: "cloud://openlist/国产剧/无占位符剧集/Season 1/无占位符剧集.S01E01.mkv",
|
|
SeasonNum: 1,
|
|
EpisodeNum: 1,
|
|
ScrapeStatus: "matched",
|
|
}
|
|
if err := db.Create(&repairMedia).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Create(&resetMedia).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
result, err := container.RepairAndRescrapeLibrary(t.Context(), local.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Repaired != 1 || result.Reset != 1 {
|
|
t.Fatalf("result = %+v, want repaired/reset for merged cloud row", result)
|
|
}
|
|
var repaired model.Media
|
|
if err := db.First(&repaired, "id = ?", repairMedia.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if repaired.TMDbID != 284110 || repaired.ScrapeStatus != "pending" {
|
|
t.Fatalf("merged cloud row not repaired/reset: tmdb=%d status=%q", repaired.TMDbID, repaired.ScrapeStatus)
|
|
}
|
|
var reset model.Media
|
|
if err := db.First(&reset, "id = ?", resetMedia.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if reset.ScrapeStatus != "pending" {
|
|
t.Fatalf("merged cloud row not reset: status=%q", reset.ScrapeStatus)
|
|
}
|
|
}
|