mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-12 16:26:36 +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:
@@ -0,0 +1,148 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user