Files
MeBox/internal/service/media_metadata.go
T
ShukeBta b44c7f860b 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 运行产物。
2026-06-19 12:23:56 +08:00

149 lines
3.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"errors"
"strings"
"github.com/ShukeBta/MediaStationGo/internal/model"
)
type MediaMetadataUpdate struct {
Title *string `json:"title"`
OriginalName *string `json:"original_name"`
Overview *string `json:"overview"`
PosterURL *string `json:"poster_url"`
BackdropURL *string `json:"backdrop_url"`
Year *int `json:"year"`
Rating *float32 `json:"rating"`
SeasonNum *int `json:"season_num"`
EpisodeNum *int `json:"episode_num"`
TMDbID *int `json:"tmdb_id"`
BangumiID *int `json:"bangumi_id"`
DoubanID *string `json:"douban_id"`
TheTVDBID *string `json:"thetvdb_id"`
Languages *string `json:"languages"`
Countries *string `json:"countries"`
Genres *string `json:"genres"`
NSFW *bool `json:"nsfw"`
}
func (s *MediaService) UpdateMetadata(ctx context.Context, id string, req MediaMetadataUpdate) (*model.Media, error) {
if s == nil || s.repo == nil || s.repo.DB == nil {
return nil, errors.New("media service unavailable")
}
id = strings.TrimSpace(id)
if id == "" {
return nil, errors.New("media id required")
}
if existing, err := s.repo.Media.FindByID(ctx, id); err != nil {
return nil, err
} else if existing == nil {
return nil, errors.New("media not found")
}
updates := map[string]any{"scrape_status": "matched"}
if req.Title != nil {
title := strings.TrimSpace(*req.Title)
if title == "" {
return nil, errors.New("title required")
}
updates["title"] = title
}
if req.OriginalName != nil {
updates["original_name"] = strings.TrimSpace(*req.OriginalName)
}
if req.Overview != nil {
updates["overview"] = strings.TrimSpace(*req.Overview)
}
if req.PosterURL != nil {
updates["poster_url"] = strings.TrimSpace(*req.PosterURL)
}
if req.BackdropURL != nil {
updates["backdrop_url"] = strings.TrimSpace(*req.BackdropURL)
}
if req.Year != nil {
updates["year"] = clampNonNegativeInt(*req.Year)
}
if req.Rating != nil {
updates["rating"] = clampRating(*req.Rating)
}
if req.SeasonNum != nil {
updates["season_num"] = clampNonNegativeInt(*req.SeasonNum)
}
if req.EpisodeNum != nil {
updates["episode_num"] = clampNonNegativeInt(*req.EpisodeNum)
}
if req.TMDbID != nil {
updates["tm_db_id"] = clampNonNegativeInt(*req.TMDbID)
}
if req.BangumiID != nil {
updates["bangumi_id"] = clampNonNegativeInt(*req.BangumiID)
}
if req.DoubanID != nil {
updates["douban_id"] = strings.TrimSpace(*req.DoubanID)
}
if req.TheTVDBID != nil {
updates["thetvdb_id"] = strings.TrimSpace(*req.TheTVDBID)
}
if req.Languages != nil {
updates["languages"] = normalizeMetadataCSV(*req.Languages)
}
if req.Countries != nil {
updates["countries"] = normalizeMetadataCSV(*req.Countries)
}
if req.Genres != nil {
updates["genres"] = normalizeMetadataCSV(*req.Genres)
}
if req.NSFW != nil {
updates["nsfw"] = *req.NSFW
}
if err := s.repo.DB.WithContext(ctx).Model(&model.Media{}).Where("id = ?", id).Updates(updates).Error; err != nil {
return nil, err
}
s.invalidateMediaCache(ctx)
return s.repo.Media.FindByID(ctx, id)
}
func normalizeMetadataCSV(value string) string {
parts := strings.FieldsFunc(value, func(r rune) bool {
switch r {
case ',', ',', ';', ';', '\n', '\r', '\t':
return true
default:
return false
}
})
out := make([]string, 0, len(parts))
seen := map[string]struct{}{}
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
key := strings.ToLower(part)
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out = append(out, part)
}
return strings.Join(out, ",")
}
func clampNonNegativeInt(value int) int {
if value < 0 {
return 0
}
return value
}
func clampRating(value float32) float32 {
if value < 0 {
return 0
}
if value > 10 {
return 10
}
return value
}