mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-06 21:36:37 +08:00
fix(media): 修复emby电影库混入剧集/特别篇识别/剧集合集 + 新增手动编辑元数据
emby 兼容层(hills/yamby 等客户端): - 电影库不再混入剧集:filterLikelyEpisodicPathsFromMovieQuery 按路径 特征(Season/S0x/Specials/电视剧/日番/国漫等)剔除误入电影库的剧集 - itemPayload 改用 mediaShouldBeEpisode:有季集号且(属剧集库或路径像剧集) 才标记为 Episode,修正电影库里剧集被当成 Movie 特别篇识别: - episode_parser 新增 patSpecialSeason,识别 S0/Season 0/Specials/SP/OVA/ OAD/番外/特别篇/特典 文件夹 → 第0季;seasonFromParents 返回 found 标志, 区分"明确第0季"与"未识别默认第1季" - emby seasonDir 正则同步支持 Specials/SP/OVA/特别篇 等 剧集合集: - groupSeries 抽出 isSeriesCard,卡片链接按是否剧集判定,修复剧集卡片 跳转;每集分离的根因(季集号/分组key)随入库识别修复一并改善 手动编辑元数据(自采集视频无TMDB信息时可自定义封面/标题/简介/分类): - 新增 MediaService.UpdateMetadata + PATCH /media/:id/metadata(需admin) - 前端 MetadataEditDialog 接入 LibraryPage 与 MediaDetailPage 其他:scanner/scraper/nfo/local+cloud_metadata/adult 元数据识别配套改进; .gitignore 忽略本地 .dev-cache/.dev-data 运行产物。
This commit is contained in:
@@ -377,28 +377,31 @@ func (r *MediaRepository) upsert(ctx context.Context, m *model.Media) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
if existing.ScrapeStatus == "pending" || existing.ScrapeStatus == "" || existing.ScrapeStatus == "no_match" {
|
||||
backfilledExternalID := false
|
||||
if m.TMDbID > 0 && existing.TMDbID <= 0 {
|
||||
status := strings.TrimSpace(existing.ScrapeStatus)
|
||||
canRefreshExternalIDs := status == "pending" || status == "" || status == "no_match" ||
|
||||
m.ScrapeStatus == "matched" || strings.HasPrefix(strings.ToLower(strings.TrimSpace(m.Path)), "cloud://")
|
||||
if canRefreshExternalIDs {
|
||||
changedExternalID := false
|
||||
if m.TMDbID > 0 && existing.TMDbID != m.TMDbID {
|
||||
updates["tm_db_id"] = m.TMDbID
|
||||
backfilledExternalID = true
|
||||
changedExternalID = true
|
||||
}
|
||||
if m.BangumiID > 0 && existing.BangumiID <= 0 {
|
||||
if m.BangumiID > 0 && existing.BangumiID != m.BangumiID {
|
||||
updates["bangumi_id"] = m.BangumiID
|
||||
backfilledExternalID = true
|
||||
changedExternalID = true
|
||||
}
|
||||
if m.DoubanID != "" && existing.DoubanID == "" {
|
||||
if m.DoubanID != "" && strings.TrimSpace(existing.DoubanID) != strings.TrimSpace(m.DoubanID) {
|
||||
updates["douban_id"] = m.DoubanID
|
||||
backfilledExternalID = true
|
||||
changedExternalID = true
|
||||
}
|
||||
if m.TheTVDBID != "" && existing.TheTVDBID == "" {
|
||||
if m.TheTVDBID != "" && strings.TrimSpace(existing.TheTVDBID) != strings.TrimSpace(m.TheTVDBID) {
|
||||
updates["thetvdb_id"] = m.TheTVDBID
|
||||
backfilledExternalID = true
|
||||
changedExternalID = true
|
||||
}
|
||||
if m.Year > 0 && existing.Year <= 0 {
|
||||
updates["year"] = m.Year
|
||||
}
|
||||
if backfilledExternalID && existing.ScrapeStatus == "no_match" {
|
||||
if changedExternalID && (status == "no_match" || status == "matched") && m.ScrapeStatus != "matched" {
|
||||
updates["scrape_status"] = "pending"
|
||||
}
|
||||
}
|
||||
@@ -456,7 +459,7 @@ func (r *MediaRepository) upsert(ctx context.Context, m *model.Media) error {
|
||||
if lib := m.LibraryID; lib != "" && lib != existing.LibraryID {
|
||||
updates["library_id"] = m.LibraryID
|
||||
}
|
||||
if m.SeasonNum > 0 && existing.SeasonNum != m.SeasonNum {
|
||||
if (m.SeasonNum > 0 || m.EpisodeNum > 0) && existing.SeasonNum != m.SeasonNum {
|
||||
updates["season_num"] = m.SeasonNum
|
||||
}
|
||||
if m.EpisodeNum > 0 && existing.EpisodeNum != m.EpisodeNum {
|
||||
|
||||
@@ -72,6 +72,52 @@ func TestMediaUpsertSkipsUnchangedExistingRow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaUpsertRefreshesCloudExternalIDFromPathHint(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := database.AutoMigrate(db); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
repos := New(db)
|
||||
lib := model.Library{Name: "OpenList · 国产剧", Path: "cloud://openlist/国产剧", Type: "tv", Enabled: true}
|
||||
if err := repos.Library.Create(t.Context(), &lib); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := "cloud://openlist/国产剧/折腰 (2025) {tmdb-296753}/Season 1/折腰.S01E01.mkv"
|
||||
existing := model.Media{
|
||||
LibraryID: lib.ID,
|
||||
Title: "折腰",
|
||||
Path: path,
|
||||
SeasonNum: 1,
|
||||
EpisodeNum: 1,
|
||||
TMDbID: 220269,
|
||||
ScrapeStatus: "matched",
|
||||
}
|
||||
if err := repos.Media.Upsert(t.Context(), &existing); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
next := model.Media{
|
||||
LibraryID: lib.ID,
|
||||
Title: "折腰",
|
||||
Path: path,
|
||||
SeasonNum: 1,
|
||||
EpisodeNum: 1,
|
||||
TMDbID: 296753,
|
||||
}
|
||||
if err := repos.Media.Upsert(t.Context(), &next); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got model.Media
|
||||
if err := repos.DB.Where("path = ?", path).First(&got).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.TMDbID != 296753 || got.ScrapeStatus != "pending" {
|
||||
t.Fatalf("cloud path hint should refresh tmdb and retry scrape, got tmdb=%d status=%q", got.TMDbID, got.ScrapeStatus)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeMediaSearchBackend struct {
|
||||
ids []string
|
||||
err error
|
||||
|
||||
Reference in New Issue
Block a user