mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
147 lines
5.0 KiB
Go
147 lines
5.0 KiB
Go
// Package service — one-time cleanup for episode metadata polluted by an older
|
|
// scraper bug that wrote per-episode TMDB ids / episode names into the
|
|
// series-level fields (tm_db_id / original_name).
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/ShukeBta/MediaStationGo/internal/model"
|
|
)
|
|
|
|
// pollutedEpisodeCleanupSettingKey marks that the one-time normalization has run.
|
|
const pollutedEpisodeCleanupSettingKey = "media.polluted_episode_cleanup_v2_done"
|
|
|
|
// seasonFolderTailRE 去掉路径末尾的「季文件夹 + 文件名」,得到整剧目录(show_dir)。
|
|
// 例: /tv/国漫/遮天 (2023)/Season 01/遮天 - S01E01.mkv → /tv/国漫/遮天 (2023)
|
|
var seasonFolderTailRE = regexp.MustCompile(`(?i)[\\/](?:season[\s._-]*\d+|s\d{1,2}|specials?|sp|ova|oad|extra|extras|第\s*[0-9一二三四五六七八九十百零两]+\s*季|特别篇|特別篇|番外|特典)[\\/][^\\/]*$`)
|
|
|
|
// showDirFromEpisodePath 从单集路径推出整剧目录, 作为「同一部剧」的聚合键。
|
|
// 若没有季文件夹, 则退而去掉文件名取其父目录。
|
|
func showDirFromEpisodePath(path string) string {
|
|
path = strings.TrimSpace(path)
|
|
if path == "" {
|
|
return ""
|
|
}
|
|
if loc := seasonFolderTailRE.FindStringIndex(path); loc != nil {
|
|
return path[:loc[0]]
|
|
}
|
|
// 无季文件夹: 取父目录(去掉最后一段文件名)。
|
|
sep := strings.LastIndexAny(path, `/\`)
|
|
if sep <= 0 {
|
|
return ""
|
|
}
|
|
return path[:sep]
|
|
}
|
|
|
|
// NormalizePollutedEpisodeMetadata 一次性清洗历史脏数据:老版本刮削曾把
|
|
// 【单集 episode id】写进整剧 tm_db_id、把单集名写进 original_name, 导致同一部剧
|
|
// 每集 id/原名各不相同, 被前端 / Emby 拆成 N 张单集卡(实测「遮天」90 集 = 89 个
|
|
// 不同 tm_db_id)。
|
|
//
|
|
// 处理方式: 按「library_id + 整剧目录(show_dir)」聚合有季集号的行;当一组内出现
|
|
// 「多个不同的非零 tm_db_id」或「多个不同的 original_name」时, 判定该组被单集级
|
|
// 数据污染, 将该组所有行的 tm_db_id 清零、original_name 清空, 并把 scrape_status
|
|
// 重置为 pending —— 让其借「修复+重刮」写回正确的整剧 id/原名。即便在重刮前,
|
|
// 前端 / Emby 也已改为「路径剧名优先」分组, 清空脏字段后即可正确合并成一张卡。
|
|
//
|
|
// 幂等: 完成后写入 setting 标记;已标记则直接跳过。仅处理剧集类(有季集号)行,
|
|
// 不触碰电影。
|
|
func (c *Container) NormalizePollutedEpisodeMetadata(ctx context.Context) (int, error) {
|
|
if c == nil || c.Repo == nil || c.Repo.DB == nil {
|
|
return 0, nil
|
|
}
|
|
if c.Repo.Setting != nil {
|
|
if v, err := c.Repo.Setting.Get(ctx, pollutedEpisodeCleanupSettingKey); err == nil && strings.TrimSpace(v) == "true" {
|
|
return 0, nil
|
|
}
|
|
}
|
|
|
|
db := c.Repo.DB.WithContext(ctx)
|
|
|
|
// 聚合状态: 每个 show_dir 收集 行 id、非零 tmdb 集合、original_name 集合。
|
|
type agg struct {
|
|
ids []string
|
|
tmdbSet map[int]struct{}
|
|
origSet map[string]struct{}
|
|
}
|
|
groups := map[string]*agg{}
|
|
|
|
var rows []model.Media
|
|
err := db.Model(&model.Media{}).
|
|
Select("id, library_id, original_name, season_num, episode_num, tm_db_id, path").
|
|
Where("season_num > 0 OR episode_num > 0").
|
|
FindInBatches(&rows, 1000, func(_ *gorm.DB, _ int) error {
|
|
for i := range rows {
|
|
row := rows[i]
|
|
showDir := showDirFromEpisodePath(row.Path)
|
|
if showDir == "" {
|
|
continue
|
|
}
|
|
key := strings.ToLower(strings.TrimSpace(row.LibraryID)) + "|" + strings.ToLower(showDir)
|
|
g := groups[key]
|
|
if g == nil {
|
|
g = &agg{tmdbSet: map[int]struct{}{}, origSet: map[string]struct{}{}}
|
|
groups[key] = g
|
|
}
|
|
g.ids = append(g.ids, row.ID)
|
|
if row.TMDbID > 0 {
|
|
g.tmdbSet[row.TMDbID] = struct{}{}
|
|
}
|
|
if name := strings.TrimSpace(row.OriginalName); name != "" {
|
|
g.origSet[name] = struct{}{}
|
|
}
|
|
}
|
|
return nil
|
|
}).Error
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
cleaned := 0
|
|
for _, g := range groups {
|
|
// 被单集数据污染的判据: 同一部剧出现多个不同的非零 tmdb id,
|
|
// 或多个不同的 original_name(整剧原名本应全组一致)。
|
|
polluted := len(g.tmdbSet) > 1 || len(g.origSet) > 1
|
|
if !polluted || len(g.ids) == 0 {
|
|
continue
|
|
}
|
|
// 分批更新(避免 IN 列表过长)。
|
|
const chunk = 400
|
|
for start := 0; start < len(g.ids); start += chunk {
|
|
end := start + chunk
|
|
if end > len(g.ids) {
|
|
end = len(g.ids)
|
|
}
|
|
res := db.Model(&model.Media{}).
|
|
Where("id IN ?", g.ids[start:end]).
|
|
Updates(map[string]any{
|
|
"tm_db_id": 0,
|
|
"original_name": "",
|
|
"scrape_status": "pending",
|
|
})
|
|
if res.Error != nil {
|
|
return cleaned, res.Error
|
|
}
|
|
cleaned += int(res.RowsAffected)
|
|
}
|
|
}
|
|
|
|
if c.Repo.Setting != nil {
|
|
if err := c.Repo.Setting.Set(ctx, pollutedEpisodeCleanupSettingKey, "true"); err != nil && c.Log != nil {
|
|
c.Log.Warn("mark polluted episode cleanup done failed", zap.Error(err))
|
|
}
|
|
}
|
|
if cleaned > 0 && c.Log != nil {
|
|
c.Log.Info("polluted episode metadata normalized",
|
|
zap.Int("rows", cleaned),
|
|
zap.Int("groups", len(groups)))
|
|
}
|
|
return cleaned, nil
|
|
}
|