mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-08 22:36: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:
@@ -118,6 +118,40 @@ type RepairAndRescrapeResult struct {
|
||||
Repaired int `json:"repaired"` // 从路径占位符回填外部 ID 的媒体数
|
||||
Libraries int `json:"libraries"` // 参与重刮的媒体库数
|
||||
Matched int `json:"matched"` // 重刮后成功匹配的媒体数
|
||||
Reset int `json:"reset"` // 被重置为 pending 以便重刮的剧集行数
|
||||
}
|
||||
|
||||
// resetEpisodicMatchedForRescrape 把剧集类(有季集号)且已 matched 的行重置为
|
||||
// pending,使 EnrichLibrary(只处理 pending/no_match)能重新刮削它们。
|
||||
//
|
||||
// 背景: 历史版本(commit b44c7f8)曾把【单集 episode id】写进整剧 tm_db_id、把
|
||||
// 单集名写进 original_name,污染了合集分组键 —— 同一部剧每集 id/原名各不相同,
|
||||
// 被前端 / Emby 拆成 N 张单集卡。这些行 scrape_status 多为 matched,常规「修复+
|
||||
// 重刮」会跳过,导致「无法修复」。源头已在 local_metadata.go 修正,这里把脏的
|
||||
// matched 剧集行放回 pending,借重刮写回正确的整剧 ID / 原名。
|
||||
//
|
||||
// libraryID 为空时处理全库;非空时仅该库。返回被重置的行数。
|
||||
func (c *Container) resetEpisodicMatchedForRescrape(ctx context.Context, libraryID string) (int, error) {
|
||||
if c == nil || c.Repo == nil || c.Repo.DB == nil {
|
||||
return 0, nil
|
||||
}
|
||||
q := c.Repo.DB.WithContext(ctx).Model(&model.Media{}).
|
||||
Where("(season_num > 0 OR episode_num > 0)").
|
||||
Where("LOWER(scrape_status) = ?", "matched")
|
||||
if id := strings.TrimSpace(libraryID); id != "" {
|
||||
q = q.Where("library_id = ?", id)
|
||||
}
|
||||
res := q.Update("scrape_status", "pending")
|
||||
if res.Error != nil {
|
||||
return 0, res.Error
|
||||
}
|
||||
reset := int(res.RowsAffected)
|
||||
if reset > 0 && c.Log != nil {
|
||||
c.Log.Info("episodic matched rows reset to pending for rescrape",
|
||||
zap.String("library", strings.TrimSpace(libraryID)),
|
||||
zap.Int("reset", reset))
|
||||
}
|
||||
return reset, nil
|
||||
}
|
||||
|
||||
// RepairAndRescrapeAllLibraries 修复并重刮所有媒体库:先从媒体路径中的
|
||||
@@ -135,6 +169,13 @@ func (c *Container) RepairAndRescrapeAllLibraries(ctx context.Context) (RepairAn
|
||||
}
|
||||
result.Repaired = repaired
|
||||
|
||||
// 重置全库脏的 matched 剧集行(单集 id 污染整剧字段),让其下方重刮一并修正。
|
||||
if reset, err := c.resetEpisodicMatchedForRescrape(ctx, ""); err != nil {
|
||||
return result, err
|
||||
} else {
|
||||
result.Reset = reset
|
||||
}
|
||||
|
||||
if c.Scraper == nil || c.Repo.Library == nil {
|
||||
return result, nil
|
||||
}
|
||||
@@ -187,6 +228,13 @@ func (c *Container) RepairAndRescrapeLibrary(ctx context.Context, libraryID stri
|
||||
}
|
||||
result.Repaired = repaired
|
||||
|
||||
// 重置该库脏的 matched 剧集行,让下方重刮修正被单集 id 污染的整剧字段。
|
||||
if reset, err := c.resetEpisodicMatchedForRescrape(ctx, libraryID); err != nil {
|
||||
return result, err
|
||||
} else {
|
||||
result.Reset = reset
|
||||
}
|
||||
|
||||
if c.Scraper == nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user