mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-04 20:46:37 +08:00
b44c7f860b
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 运行产物。
159 lines
5.1 KiB
Go
159 lines
5.1 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// ExternalMediaResult is a metadata candidate from an online catalog. It is
|
|
// intentionally separate from model.Media because the item may not exist in
|
|
// the local library yet.
|
|
type ExternalMediaResult struct {
|
|
Source string `json:"source"`
|
|
MediaType string `json:"media_type,omitempty"`
|
|
Title string `json:"title"`
|
|
OriginalName string `json:"original_name,omitempty"`
|
|
Overview string `json:"overview,omitempty"`
|
|
PosterURL string `json:"poster_url,omitempty"`
|
|
BackdropURL string `json:"backdrop_url,omitempty"`
|
|
Year int `json:"year,omitempty"`
|
|
Rating float32 `json:"rating,omitempty"`
|
|
TMDbID int `json:"tmdb_id,omitempty"`
|
|
BangumiID int `json:"bangumi_id,omitempty"`
|
|
DoubanID string `json:"douban_id,omitempty"`
|
|
TheTVDBID string `json:"thetvdb_id,omitempty"`
|
|
SubscribeKeyword string `json:"subscribe_keyword"`
|
|
TotalEpisodes int `json:"total_episodes,omitempty"`
|
|
DownloadedEpisodes int `json:"downloaded_episodes,omitempty"`
|
|
LocalMediaCount int `json:"local_media_count,omitempty"`
|
|
MissingEpisodes []int `json:"missing_episodes,omitempty"`
|
|
InLibrary bool `json:"in_library"`
|
|
Languages []string `json:"languages,omitempty"`
|
|
Countries []string `json:"countries,omitempty"`
|
|
Genres []string `json:"genres,omitempty"`
|
|
NSFW bool `json:"nsfw,omitempty"`
|
|
}
|
|
|
|
// SearchExternalMedia fans out one normalized search intent to TMDb, Douban
|
|
// and Bangumi. This mirrors MoviePilot's separation of "metadata discovery"
|
|
// from later tracker searching/downloading, but keeps our Go service small.
|
|
func SearchExternalMedia(ctx context.Context, query string, year int, mediaType string, tmdb *TMDbProvider, douban *DoubanProvider, bangumi *BangumiProvider) []ExternalMediaResult {
|
|
query = strings.TrimSpace(query)
|
|
if query == "" {
|
|
return []ExternalMediaResult{}
|
|
}
|
|
|
|
results := make([]ExternalMediaResult, 0, 6)
|
|
addMatch := func(source, typ string, m *Match) {
|
|
if m == nil || strings.TrimSpace(m.Title) == "" {
|
|
return
|
|
}
|
|
totalEpisodes := 0
|
|
if source == "tmdb" && typ == "tv" && m.TMDbID > 0 && tmdb != nil {
|
|
totalEpisodes, _ = tmdb.GetTVEpisodeCount(ctx, m.TMDbID)
|
|
}
|
|
results = append(results, ExternalMediaResult{
|
|
Source: source,
|
|
MediaType: typ,
|
|
Title: m.Title,
|
|
OriginalName: m.OriginalName,
|
|
Overview: m.Overview,
|
|
PosterURL: m.PosterURL,
|
|
BackdropURL: m.BackdropURL,
|
|
Year: m.Year,
|
|
Rating: m.Rating,
|
|
TMDbID: m.TMDbID,
|
|
BangumiID: m.BangumiID,
|
|
SubscribeKeyword: buildSubscribeKeyword(m.Title, m.Year),
|
|
TotalEpisodes: totalEpisodes,
|
|
Languages: m.Languages,
|
|
Countries: m.Countries,
|
|
Genres: m.Genres,
|
|
NSFW: m.NSFW,
|
|
})
|
|
}
|
|
|
|
if tmdb != nil {
|
|
if mediaType == "" || mediaType == "movie" {
|
|
if m, err := tmdb.SearchMovie(ctx, query, year); err == nil {
|
|
addMatch("tmdb", "movie", m)
|
|
}
|
|
}
|
|
if mediaType == "" || mediaType == "tv" || mediaType == "anime" {
|
|
if m, err := tmdb.SearchTV(ctx, query, year); err == nil {
|
|
addMatch("tmdb", "tv", m)
|
|
}
|
|
}
|
|
}
|
|
|
|
if bangumi != nil && (mediaType == "" || mediaType == "anime") {
|
|
if m, err := bangumi.Search(ctx, query); err == nil {
|
|
addMatch("bangumi", "anime", m)
|
|
}
|
|
}
|
|
|
|
if douban != nil {
|
|
if m, err := douban.Search(ctx, query); err == nil && m != nil {
|
|
yearValue, _ := strconv.Atoi(m.Year)
|
|
typ := normalizeDoubanType(m.Type, mediaType)
|
|
results = append(results, ExternalMediaResult{
|
|
Source: "douban",
|
|
MediaType: typ,
|
|
Title: m.Title,
|
|
PosterURL: m.Img,
|
|
Year: yearValue,
|
|
Rating: m.Rating,
|
|
DoubanID: m.DoubanID,
|
|
SubscribeKeyword: buildSubscribeKeyword(m.Title, yearValue),
|
|
})
|
|
}
|
|
}
|
|
|
|
return dedupeExternalMedia(results)
|
|
}
|
|
|
|
func buildSubscribeKeyword(title string, year int) string {
|
|
title = strings.TrimSpace(title)
|
|
if year > 0 {
|
|
return fmt.Sprintf("%s %d", title, year)
|
|
}
|
|
return title
|
|
}
|
|
|
|
func normalizeDoubanType(doubanType, fallback string) string {
|
|
doubanType = strings.ToLower(strings.TrimSpace(doubanType))
|
|
switch doubanType {
|
|
case "movie":
|
|
return "movie"
|
|
case "tv", "tvshow", "drama":
|
|
return "tv"
|
|
}
|
|
if fallback != "" {
|
|
return fallback
|
|
}
|
|
return "movie"
|
|
}
|
|
|
|
func dedupeExternalMedia(in []ExternalMediaResult) []ExternalMediaResult {
|
|
seen := map[string]struct{}{}
|
|
out := make([]ExternalMediaResult, 0, len(in))
|
|
for _, item := range in {
|
|
key := item.Source + ":" + strings.ToLower(item.Title)
|
|
if item.TMDbID > 0 {
|
|
key = fmt.Sprintf("tmdb:%d:%s", item.TMDbID, item.MediaType)
|
|
} else if item.BangumiID > 0 {
|
|
key = fmt.Sprintf("bangumi:%d", item.BangumiID)
|
|
} else if item.DoubanID != "" {
|
|
key = "douban:" + item.DoubanID
|
|
}
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
out = append(out, item)
|
|
}
|
|
return out
|
|
}
|