mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-11 07:46:37 +08:00
fix(series): 彻底修复剧集被按单集显示且无法修复
根因(用真实数据确诊, 两个互相独立): - 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 中依赖旧错误行为的用例。
This commit is contained in:
@@ -340,6 +340,102 @@ func TestEmbyMovieLibraryFiltersMisplacedSeriesPaths(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestEmbyMovieLibraryGroupsEpisodicContentIntoSeries 验证方案 B: 电影类型库里
|
||||
// 混入的「剧集结构」内容(如整合成剧集的剧场版/合集动画, 路径含 Season/SxxE)
|
||||
// 在常规浏览(未指定 IncludeItemTypes)时应聚成 Series 卡片, 而不是以散装单集
|
||||
// (Episode)漏出; 同库里真正的电影仍按 Movie 显示。两类并存于同一电影库视图。
|
||||
func TestEmbyMovieLibraryGroupsEpisodicContentIntoSeries(t *testing.T) {
|
||||
svc := newTestEmbyService(t)
|
||||
lib := model.Library{Name: "动画电影", Path: `/media/动画电影`, Type: "movie", Enabled: true}
|
||||
if err := svc.repo.Library.Create(t.Context(), &lib); err != nil {
|
||||
t.Fatalf("create library: %v", err)
|
||||
}
|
||||
base := time.Now()
|
||||
// 剧集结构内容: 同一部「高达剧场版」的两集, 单集 tmdb 各不相同(模拟 NFO 单集 id 污染)。
|
||||
rows := []model.Media{
|
||||
{
|
||||
Base: model.Base{ID: "gundam-e13", CreatedAt: base.Add(2 * time.Minute)},
|
||||
LibraryID: lib.ID,
|
||||
Title: "高达剧场版",
|
||||
Path: `/media/动画电影/高达剧场版/Season 01/高达剧场版 - S01E13.mkv`,
|
||||
PosterURL: `/poster.jpg`,
|
||||
TMDbID: 4375419,
|
||||
SeasonNum: 1,
|
||||
EpisodeNum: 13,
|
||||
},
|
||||
{
|
||||
Base: model.Base{ID: "gundam-e14", CreatedAt: base.Add(3 * time.Minute)},
|
||||
LibraryID: lib.ID,
|
||||
Title: "高达剧场版",
|
||||
Path: `/media/动画电影/高达剧场版/Season 01/高达剧场版 - S01E14.mkv`,
|
||||
PosterURL: `/poster.jpg`,
|
||||
TMDbID: 4375461,
|
||||
SeasonNum: 1,
|
||||
EpisodeNum: 14,
|
||||
},
|
||||
// 真正的电影。
|
||||
{
|
||||
Base: model.Base{ID: "real-movie", CreatedAt: base.Add(1 * time.Minute)},
|
||||
LibraryID: lib.ID,
|
||||
Title: "普通动画电影",
|
||||
Path: `/media/动画电影/普通动画电影.2024.mkv`,
|
||||
PosterURL: `/poster.jpg`,
|
||||
},
|
||||
}
|
||||
for i := range rows {
|
||||
if err := svc.repo.DB.Create(&rows[i]).Error; err != nil {
|
||||
t.Fatalf("create media: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
out, err := svc.Items(t.Context(), ItemsParams{ParentID: lib.ID, Limit: 50})
|
||||
if err != nil {
|
||||
t.Fatalf("items: %v", err)
|
||||
}
|
||||
items := out["Items"].([]map[string]any)
|
||||
|
||||
var seriesCount, movieCount, episodeCount int
|
||||
var seriesPayload map[string]any
|
||||
for _, item := range items {
|
||||
switch item["Type"] {
|
||||
case "Series":
|
||||
seriesCount++
|
||||
seriesPayload = item
|
||||
case "Movie":
|
||||
movieCount++
|
||||
case "Episode":
|
||||
episodeCount++
|
||||
}
|
||||
}
|
||||
if episodeCount != 0 {
|
||||
t.Fatalf("movie library must not leak flat episodes, got %d episode items: %#v", episodeCount, items)
|
||||
}
|
||||
if seriesCount != 1 {
|
||||
t.Fatalf("episodic content should collapse into exactly one Series card, got %d: %#v", seriesCount, items)
|
||||
}
|
||||
if movieCount != 1 {
|
||||
t.Fatalf("real movie should stay as one Movie item, got %d: %#v", movieCount, items)
|
||||
}
|
||||
// 两集 tmdb 不同, 但按路径剧名聚成同一 Series, 集数应为 2。
|
||||
if got := seriesPayload["RecursiveItemCount"]; got != 2 {
|
||||
t.Fatalf("series should contain both episodes despite differing tmdb ids, got RecursiveItemCount=%v", got)
|
||||
}
|
||||
|
||||
// Series 卡片可下钻: 解析其 season → episodes。
|
||||
seriesID, _ := seriesPayload["Id"].(string)
|
||||
if seriesID == "" {
|
||||
t.Fatalf("series payload missing Id: %#v", seriesPayload)
|
||||
}
|
||||
drill, err := svc.Items(t.Context(), ItemsParams{ParentID: seriesID, Recursive: true, Limit: 50})
|
||||
if err != nil {
|
||||
t.Fatalf("series drill-down: %v", err)
|
||||
}
|
||||
drillItems := drill["Items"].([]map[string]any)
|
||||
if len(drillItems) != 2 {
|
||||
t.Fatalf("series drill-down should list both episodes, got %#v", drillItems)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbyMergedLocalCloudMovieVersionsShareMediaSources(t *testing.T) {
|
||||
svc := newTestEmbyService(t)
|
||||
local := model.Library{Name: "国产电影", Path: `/media/国产电影`, Type: "movie", Enabled: true}
|
||||
|
||||
Reference in New Issue
Block a user