mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-30 03:36:37 +08:00
dff19801b6
根因(用真实数据确诊, 两个互相独立): - Web 端拆集: episode NFO 的 <uniqueid type=tmdb> 是【单集 episode id】、 <title> 是【单集名】, mergeEpisodeMetadata 却无条件覆盖了整剧 tm_db_id / original_name → 同剧每集 id/原名各不相同, 被 getSeriesKey 按 tmdb_id 拆成 N 张单集卡(实测「遮天」90 集 = 89 个不同 tmdb_id)。 - Emby 散装单集: 电影库混入按 Season/SxxE 整理的剧集结构内容, 被判成 Episode 却因 movie 库不聚 Series 而漏成散装单集。 - 无法修复: EnrichLibrary 只处理 pending/no_match, 跳过 matched 脏行。 修复: - local_metadata.mergeEpisodeMetadata: 单集 NFO 的外部 id 不再写入整剧字段, 单集名不再污染 original_name; 整剧 id/原名只认 tvshow.nfo。 - groupSeries.ts: 剧集分组改「库+路径剧名」优先(整剧目录对全剧一致), 外部 id 仅作回退, 对存量脏数据也能合并。 - emby_compat: 电影库常规浏览新增 movieLibraryItems, 把剧集结构内容聚成 Series 卡片与真电影并列(方案 B), 不再漏散装单集。 - cloud_path_repair: 「修复+重刮」先把脏的 matched 剧集行重置为 pending 再重刮, 使其能被重新刮削; 结果新增 reset 计数。 - 新增 NormalizePollutedEpisodeMetadata 启动迁移(幂等): 按整剧目录聚合, 清洗 tmdb/原名散乱的剧组并重置 pending。 测试: local_metadata/emby_compat/cloud_path_repair/episode_metadata_cleanup 新增或更新断言; 修正 scanner_cloud_test 中依赖旧错误行为的用例。
68 lines
2.6 KiB
Go
68 lines
2.6 KiB
Go
package service
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/glebarez/sqlite"
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/ShukeBta/MediaStationGo/internal/model"
|
|
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
|
)
|
|
|
|
// TestResetEpisodicMatchedForRescrape 验证「修复+重刮」会把脏的 matched 剧集行
|
|
// 重置为 pending(让 EnrichLibrary 能重新刮削),而电影行与其它库不受影响。
|
|
func TestResetEpisodicMatchedForRescrape(t *testing.T) {
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(&model.Library{}, &model.Media{}); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
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"))
|
|
}
|
|
}
|