mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16: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 中依赖旧错误行为的用例。
103 lines
3.9 KiB
Go
103 lines
3.9 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"
|
|
)
|
|
|
|
func newCleanupTestContainer(t *testing.T) (*Container, *gorm.DB) {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
if sqlDB, err := db.DB(); err == nil {
|
|
sqlDB.SetMaxOpenConns(1)
|
|
}
|
|
if err := db.AutoMigrate(&model.Media{}, &model.Setting{}); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
repos := repository.New(db)
|
|
return &Container{Repo: repos, Log: zap.NewNop()}, db
|
|
}
|
|
|
|
func TestShowDirFromEpisodePath(t *testing.T) {
|
|
cases := []struct{ in, want string }{
|
|
{`/tv/国漫/遮天 (2023)/Season 01/遮天 - S01E01.mkv`, `/tv/国漫/遮天 (2023)`},
|
|
{`cloud://drive/国漫/武神主宰 (2020)/S01/武神主宰 - S01E05.mkv`, `cloud://drive/国漫/武神主宰 (2020)`},
|
|
{`/tv/某剧/第 2 季/某剧 第05集.mkv`, `/tv/某剧`},
|
|
// 无季文件夹: 取父目录。
|
|
{`/tv/某剧/某剧 - S01E01.mkv`, `/tv/某剧`},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := showDirFromEpisodePath(tc.in); got != tc.want {
|
|
t.Errorf("showDirFromEpisodePath(%q) = %q, want %q", tc.in, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestNormalizePollutedEpisodeMetadata 验证: 被单集 id/名污染的剧组被清洗,
|
|
// 干净的剧组不动, 且迁移幂等(写入标记后再跑不重复处理)。
|
|
func TestNormalizePollutedEpisodeMetadata(t *testing.T) {
|
|
container, db := newCleanupTestContainer(t)
|
|
|
|
rows := []model.Media{
|
|
// 污染组: 同一部剧每集 tmdb 不同、original_name 是单集名 → 应清洗。
|
|
{Base: model.Base{ID: "z1"}, LibraryID: "lib", SeasonNum: 1, EpisodeNum: 1, TMDbID: 4375419, OriginalName: "九龙拉棺", ScrapeStatus: "matched", Path: `/tv/国漫/遮天 (2023)/Season 01/遮天 - S01E01.mkv`},
|
|
{Base: model.Base{ID: "z2"}, LibraryID: "lib", SeasonNum: 1, EpisodeNum: 2, TMDbID: 4375461, OriginalName: "星空古路", ScrapeStatus: "matched", Path: `/tv/国漫/遮天 (2023)/Season 01/遮天 - S01E02.mkv`},
|
|
// 干净组: 全组共享同一整剧 tmdb、original_name 一致 → 不动。
|
|
{Base: model.Base{ID: "c1"}, LibraryID: "lib", SeasonNum: 1, EpisodeNum: 1, TMDbID: 1234, OriginalName: "Clean Show", ScrapeStatus: "matched", Path: `/tv/欧美剧/Clean Show (2020)/Season 01/Clean Show - S01E01.mkv`},
|
|
{Base: model.Base{ID: "c2"}, LibraryID: "lib", SeasonNum: 1, EpisodeNum: 2, TMDbID: 1234, OriginalName: "Clean Show", ScrapeStatus: "matched", Path: `/tv/欧美剧/Clean Show (2020)/Season 01/Clean Show - S01E02.mkv`},
|
|
}
|
|
for i := range rows {
|
|
if err := db.Create(&rows[i]).Error; err != nil {
|
|
t.Fatalf("create %s: %v", rows[i].ID, err)
|
|
}
|
|
}
|
|
|
|
cleaned, err := container.NormalizePollutedEpisodeMetadata(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("normalize: %v", err)
|
|
}
|
|
if cleaned != 2 {
|
|
t.Fatalf("cleaned = %d, want 2 (only the polluted show)", cleaned)
|
|
}
|
|
|
|
load := func(id string) model.Media {
|
|
var m model.Media
|
|
if err := db.First(&m, "id = ?", id).Error; err != nil {
|
|
t.Fatalf("load %s: %v", id, err)
|
|
}
|
|
return m
|
|
}
|
|
// 污染组被清空字段并重置 pending。
|
|
for _, id := range []string{"z1", "z2"} {
|
|
m := load(id)
|
|
if m.TMDbID != 0 || m.OriginalName != "" || m.ScrapeStatus != "pending" {
|
|
t.Fatalf("polluted row %s not cleaned: %+v", id, m)
|
|
}
|
|
}
|
|
// 干净组保持不变。
|
|
for _, id := range []string{"c1", "c2"} {
|
|
m := load(id)
|
|
if m.TMDbID != 1234 || m.OriginalName != "Clean Show" || m.ScrapeStatus != "matched" {
|
|
t.Fatalf("clean row %s should be untouched: %+v", id, m)
|
|
}
|
|
}
|
|
|
|
// 幂等: 第二次运行不再处理(已写标记)。
|
|
cleaned2, err := container.NormalizePollutedEpisodeMetadata(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("normalize again: %v", err)
|
|
}
|
|
if cleaned2 != 0 {
|
|
t.Fatalf("second run should be a no-op, cleaned=%d", cleaned2)
|
|
}
|
|
}
|