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:
ShukeBta
2026-06-18 21:56:25 +00:00
parent cce88ce3b0
commit b44c7f860b
34 changed files with 1601 additions and 207 deletions
+46
View File
@@ -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