diff --git a/internal/handler/metadata_enrich.go b/internal/handler/metadata_enrich.go index 1f5ae84..3fabfb0 100644 --- a/internal/handler/metadata_enrich.go +++ b/internal/handler/metadata_enrich.go @@ -27,7 +27,13 @@ type cachedDisplayMetadata struct { } func enrichSubscriptionArtwork(ctx context.Context, svc *service.Container, sub *model.Subscription) { - if svc == nil || sub == nil || (strings.TrimSpace(sub.PosterURL) != "" && strings.TrimSpace(sub.BackdropURL) != "") { + if svc == nil || sub == nil { + return + } + // 已有图片且媒体展示字段也齐全时才跳过;否则仍需查一次补 年份/评分/类型 等 + // 富通知字段(老订阅只存了 poster/overview 的情况)。 + if strings.TrimSpace(sub.PosterURL) != "" && strings.TrimSpace(sub.BackdropURL) != "" && + sub.Year > 0 && sub.Rating > 0 && strings.TrimSpace(sub.Genres) != "" { return } meta := lookupDisplayMetadata(ctx, svc, sub.Name, sub.Filter, sub.MediaType) @@ -46,6 +52,21 @@ func enrichSubscriptionArtwork(ctx context.Context, svc *service.Container, sub if strings.TrimSpace(sub.Overview) == "" { sub.Overview = meta.Overview } + if strings.TrimSpace(sub.OriginalName) == "" { + sub.OriginalName = meta.OriginalName + } + if strings.TrimSpace(sub.OriginalLanguage) == "" { + sub.OriginalLanguage = meta.OriginalLanguage + } + if sub.Year <= 0 && meta.Year > 0 { + sub.Year = meta.Year + } + if sub.Rating <= 0 && meta.Rating > 0 { + sub.Rating = meta.Rating + } + if strings.TrimSpace(sub.Genres) == "" { + sub.Genres = meta.Genres + } } func enrichAndPersistSubscriptions(ctx context.Context, svc *service.Container, items []model.Subscription) { @@ -55,7 +76,12 @@ func enrichAndPersistSubscriptions(ctx context.Context, svc *service.Container, if before.Source == items[i].Source && before.PosterURL == items[i].PosterURL && before.BackdropURL == items[i].BackdropURL && - before.Overview == items[i].Overview { + before.Overview == items[i].Overview && + before.OriginalName == items[i].OriginalName && + before.OriginalLanguage == items[i].OriginalLanguage && + before.Year == items[i].Year && + before.Rating == items[i].Rating && + before.Genres == items[i].Genres { continue } updates := map[string]any{} @@ -71,6 +97,21 @@ func enrichAndPersistSubscriptions(ctx context.Context, svc *service.Container, if before.Overview != items[i].Overview { updates["overview"] = items[i].Overview } + if before.OriginalName != items[i].OriginalName { + updates["original_name"] = items[i].OriginalName + } + if before.OriginalLanguage != items[i].OriginalLanguage { + updates["original_language"] = items[i].OriginalLanguage + } + if before.Year != items[i].Year { + updates["year"] = items[i].Year + } + if before.Rating != items[i].Rating { + updates["rating"] = items[i].Rating + } + if before.Genres != items[i].Genres { + updates["genres"] = items[i].Genres + } if len(updates) == 0 { continue } @@ -88,22 +129,41 @@ func enrichAndPersistDownloadRows(ctx context.Context, svc *service.Container, r title = downloadDisplayTitle(rows[i].URL) rows[i].Title = title } - if strings.TrimSpace(rows[i].PosterURL) == "" || strings.TrimSpace(rows[i].BackdropURL) == "" { + needsMeta := strings.TrimSpace(rows[i].PosterURL) == "" || strings.TrimSpace(rows[i].BackdropURL) == "" || + strings.TrimSpace(rows[i].Overview) == "" || rows[i].Year <= 0 || rows[i].Rating <= 0 || + strings.TrimSpace(rows[i].Genres) == "" || strings.TrimSpace(rows[i].OriginalName) == "" || + strings.TrimSpace(rows[i].OriginalLanguage) == "" + if needsMeta { meta := enrichDownloadTaskMeta(ctx, svc, service.DownloadTaskMeta{ - Title: rows[i].Title, - PosterURL: rows[i].PosterURL, - BackdropURL: rows[i].BackdropURL, - Overview: rows[i].Overview, - }, firstNonEmptyString(rows[i].Title, rows[i].URL), "") + Title: rows[i].Title, + PosterURL: rows[i].PosterURL, + BackdropURL: rows[i].BackdropURL, + Overview: rows[i].Overview, + OriginalName: rows[i].OriginalName, + OriginalLanguage: rows[i].OriginalLanguage, + Year: rows[i].Year, + Rating: rows[i].Rating, + Genres: rows[i].Genres, + }, firstNonEmptyString(rows[i].Title, rows[i].URL), rows[i].MediaType) rows[i].Title = firstNonEmptyString(rows[i].Title, meta.Title) rows[i].PosterURL = meta.PosterURL rows[i].BackdropURL = meta.BackdropURL rows[i].Overview = meta.Overview + rows[i].OriginalName = meta.OriginalName + rows[i].OriginalLanguage = meta.OriginalLanguage + rows[i].Year = meta.Year + rows[i].Rating = meta.Rating + rows[i].Genres = meta.Genres } if before.Title == rows[i].Title && before.PosterURL == rows[i].PosterURL && before.BackdropURL == rows[i].BackdropURL && - before.Overview == rows[i].Overview { + before.Overview == rows[i].Overview && + before.OriginalName == rows[i].OriginalName && + before.OriginalLanguage == rows[i].OriginalLanguage && + before.Year == rows[i].Year && + before.Rating == rows[i].Rating && + before.Genres == rows[i].Genres { continue } updates := map[string]any{} @@ -119,6 +179,21 @@ func enrichAndPersistDownloadRows(ctx context.Context, svc *service.Container, r if before.Overview != rows[i].Overview { updates["overview"] = rows[i].Overview } + if before.OriginalName != rows[i].OriginalName { + updates["original_name"] = rows[i].OriginalName + } + if before.OriginalLanguage != rows[i].OriginalLanguage { + updates["original_language"] = rows[i].OriginalLanguage + } + if before.Year != rows[i].Year { + updates["year"] = rows[i].Year + } + if before.Rating != rows[i].Rating { + updates["rating"] = rows[i].Rating + } + if before.Genres != rows[i].Genres { + updates["genres"] = rows[i].Genres + } if len(updates) == 0 { continue } @@ -160,7 +235,11 @@ func enrichDownloadTaskMeta(ctx context.Context, svc *service.Container, meta se if strings.TrimSpace(meta.Title) == "" { meta.Title = strings.TrimSpace(downloadDisplayTitle(fallbackTitle)) } - if strings.TrimSpace(meta.PosterURL) != "" && strings.TrimSpace(meta.BackdropURL) != "" { + // 仅当展示字段都齐时才跳过查询(含媒体富通知所需的年份/评分/类型/原始信息)。 + if strings.TrimSpace(meta.PosterURL) != "" && strings.TrimSpace(meta.BackdropURL) != "" && + strings.TrimSpace(meta.Overview) != "" && meta.Year > 0 && meta.Rating > 0 && + strings.TrimSpace(meta.Genres) != "" && strings.TrimSpace(meta.OriginalName) != "" && + strings.TrimSpace(meta.OriginalLanguage) != "" { return meta } found := lookupDisplayMetadata(ctx, svc, meta.Title, fallbackTitle, mediaType) @@ -176,6 +255,21 @@ func enrichDownloadTaskMeta(ctx context.Context, svc *service.Container, meta se if strings.TrimSpace(meta.Overview) == "" { meta.Overview = found.Overview } + if strings.TrimSpace(meta.OriginalName) == "" { + meta.OriginalName = found.OriginalName + } + if strings.TrimSpace(meta.OriginalLanguage) == "" { + meta.OriginalLanguage = found.OriginalLanguage + } + if meta.Year <= 0 && found.Year > 0 { + meta.Year = found.Year + } + if meta.Rating <= 0 && found.Rating > 0 { + meta.Rating = found.Rating + } + if strings.TrimSpace(meta.Genres) == "" { + meta.Genres = found.Genres + } return meta } @@ -203,11 +297,16 @@ func downloadDisplayTitle(raw string) string { } type displayMetadata struct { - Source string - Title string - PosterURL string - BackdropURL string - Overview string + Source string + Title string + OriginalName string + OriginalLanguage string + PosterURL string + BackdropURL string + Overview string + Year int + Rating float32 + Genres string } func lookupDisplayMetadata(ctx context.Context, svc *service.Container, title, fallback, mediaType string) displayMetadata { @@ -244,12 +343,21 @@ func lookupDisplayMetadata(ctx context.Context, svc *service.Container, title, f break } } + originalLanguage := "" + if len(best.Languages) > 0 { + originalLanguage = strings.TrimSpace(best.Languages[0]) + } meta := displayMetadata{ - Source: best.Source, - Title: best.Title, - PosterURL: best.PosterURL, - BackdropURL: best.BackdropURL, - Overview: best.Overview, + Source: best.Source, + Title: best.Title, + OriginalName: best.OriginalName, + OriginalLanguage: originalLanguage, + PosterURL: best.PosterURL, + BackdropURL: best.BackdropURL, + Overview: best.Overview, + Year: best.Year, + Rating: best.Rating, + Genres: strings.Join(best.Genres, ","), } displayMetadataCache.Store(cacheKey, cachedDisplayMetadata{value: meta, expiresAt: time.Now().Add(12 * time.Hour)}) return meta diff --git a/internal/model/model.go b/internal/model/model.go index dd9f802..1c483d5 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -211,8 +211,14 @@ type DownloadTask struct { SavePath string `gorm:"size:1024" json:"save_path"` MediaType string `gorm:"size:16" json:"media_type,omitempty"` MediaCategory string `gorm:"size:128" json:"media_category,omitempty"` - Status string `gorm:"size:32;default:queued" json:"status"` - Progress float32 `json:"progress"` + // 媒体展示元数据(用于 Telegram 富通知模板等):原始片名/语言/年份/评分/类型。 + OriginalName string `gorm:"size:512" json:"original_name,omitempty"` + OriginalLanguage string `gorm:"size:32" json:"original_language,omitempty"` + Year int `json:"year,omitempty"` + Rating float32 `json:"rating,omitempty"` + Genres string `gorm:"size:255" json:"genres,omitempty"` // comma separated + Status string `gorm:"size:32;default:queued" json:"status"` + Progress float32 `json:"progress"` // AllowExistingLibrary is true for subscription wash/upgrade tasks that are // allowed to replace an existing library item after download completion. @@ -235,11 +241,17 @@ type Subscription struct { PosterURL string `gorm:"size:2048" json:"poster_url,omitempty"` BackdropURL string `gorm:"size:2048" json:"backdrop_url,omitempty"` Overview string `gorm:"type:text" json:"overview,omitempty"` - Resolution string `gorm:"size:32" json:"resolution,omitempty"` // 2160p / 1080p / 720p / best - Quality string `gorm:"size:64" json:"quality,omitempty"` // remux / bluray / web-dl / hdtv - Effects string `gorm:"size:128" json:"effects,omitempty"` // hdr,dolby-vision,atmos - ReleaseGroups string `gorm:"size:255" json:"release_groups,omitempty"` // comma separated - ExcludeWords string `gorm:"size:255" json:"exclude_words,omitempty"` // comma separated + // 媒体展示元数据(用于 Telegram 富通知模板等):原始片名/语言/年份/评分/类型。 + OriginalName string `gorm:"size:512" json:"original_name,omitempty"` + OriginalLanguage string `gorm:"size:32" json:"original_language,omitempty"` + Year int `json:"year,omitempty"` + Rating float32 `json:"rating,omitempty"` + Genres string `gorm:"size:255" json:"genres,omitempty"` // comma separated + Resolution string `gorm:"size:32" json:"resolution,omitempty"` // 2160p / 1080p / 720p / best + Quality string `gorm:"size:64" json:"quality,omitempty"` // remux / bluray / web-dl / hdtv + Effects string `gorm:"size:128" json:"effects,omitempty"` // hdr,dolby-vision,atmos + ReleaseGroups string `gorm:"size:255" json:"release_groups,omitempty"` // comma separated + ExcludeWords string `gorm:"size:255" json:"exclude_words,omitempty"` // comma separated WashEnabled bool `gorm:"default:false" json:"wash_enabled"` WashPriority string `gorm:"size:32" json:"wash_priority,omitempty"` // balanced / resolution / quality / effects / seeders TotalEpisodes int `gorm:"default:0" json:"total_episodes,omitempty"` diff --git a/internal/service/downloads.go b/internal/service/downloads.go index b0ea9f2..e891d6f 100644 --- a/internal/service/downloads.go +++ b/internal/service/downloads.go @@ -110,6 +110,11 @@ type DownloadTaskMeta struct { MediaType string MediaCategory string SourceCategory string + OriginalName string + OriginalLanguage string + Year int + Rating float32 + Genres string AllowExistingLibrary bool } @@ -570,6 +575,11 @@ func (d *DownloadService) createTask(ctx context.Context, userID, urlStr, savePa SavePath: savePath, MediaType: meta.MediaType, MediaCategory: meta.MediaCategory, + OriginalName: meta.OriginalName, + OriginalLanguage: meta.OriginalLanguage, + Year: meta.Year, + Rating: meta.Rating, + Genres: meta.Genres, Status: "queued", AllowExistingLibrary: meta.AllowExistingLibrary, } @@ -1381,6 +1391,21 @@ func (d *DownloadService) notifyDownloadComplete(ctx context.Context, torrent QB if strings.TrimSpace(task.Overview) != "" { data["overview"] = task.Overview } + if strings.TrimSpace(task.OriginalName) != "" { + data["original_title"] = task.OriginalName + } + if strings.TrimSpace(task.OriginalLanguage) != "" { + data["original_language"] = task.OriginalLanguage + } + if task.Year > 0 { + data["year"] = task.Year + } + if task.Rating > 0 { + data["rating"] = task.Rating + } + if strings.TrimSpace(task.Genres) != "" { + data["genres"] = task.Genres + } } go func() { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) diff --git a/internal/service/notify_channels_test.go b/internal/service/notify_channels_test.go index 3f38405..f007085 100644 --- a/internal/service/notify_channels_test.go +++ b/internal/service/notify_channels_test.go @@ -127,3 +127,42 @@ func TestTelegramDispatchUsesPhotoAndFormattedCaption(t *testing.T) { } } } + +// TestTelegramMediaTemplateRendersEnrichedFields 验证补齐的媒体字段 +// (年份/评分/类型/原名/语言)确实透传进 Telegram 富模板 caption。 +// 这是「模型/刮削链路补齐字段」与「作者富模板」对接的端到端断言。 +func TestTelegramMediaTemplateRendersEnrichedFields(t *testing.T) { + caption := formatTelegramNotification(NotifyEvent{ + Type: EventSubscriptionHit, + Title: "MediaStationGo 订阅命中新资源", + Message: "订阅:遮天\n新增资源:1", + Data: map[string]interface{}{ + "media_category": "国漫", + "title": "遮天", + "original_title": "Shrouding the Heavens", + "original_language": "zh", + "year": 2023, + "rating": 8.6, + "genres": "动画,奇幻", + "overview": "荒古禁地中走出的少年。", + "tmdb_url": "https://www.themoviedb.org/tv/223911", + "imdb_url": "https://www.imdb.com/title/tt12345678/", + }, + }) + for _, want := range []string{ + "#国漫", + "📺 中文片名:遮天", + "🧿 原始片名:Shrouding the Heavens", + "🌐 原始语言:中文", + "📅 发行年份:2023", + "⭐️ 评分:8.6", + "💎 类型:动画、奇幻", + "🪬 简介:", + `TMDB`, + `IMDB`, + } { + if !strings.Contains(caption, want) { + t.Fatalf("caption missing %q:\n%s", want, caption) + } + } +} diff --git a/internal/service/subscription.go b/internal/service/subscription.go index 4878aa9..591148e 100644 --- a/internal/service/subscription.go +++ b/internal/service/subscription.go @@ -469,11 +469,27 @@ func (s *SubscriptionService) notifySubscriptionHit(sub *model.Subscription, que if strings.TrimSpace(sub.MediaCategory) != "" { data["media_category"] = sub.MediaCategory } - // 补充媒体通知模板(formatTelegramMediaNotification)所需字段:片名 / 简介 / - // 外链 / 资源标题(供模板提取季集 + 版本)。仅填现成可用的,缺失项模板会自动略过。 + // 补充媒体通知模板(formatTelegramMediaNotification)所需字段:片名 / 原名 / + // 语言 / 年份 / 评分 / 类型 / 简介 / 外链 / 资源标题(供模板提取季集 + 版本)。 + // 仅填现成可用的,缺失项模板会自动略过。 if strings.TrimSpace(sub.Name) != "" { data["title"] = sub.Name } + if strings.TrimSpace(sub.OriginalName) != "" { + data["original_title"] = sub.OriginalName + } + if strings.TrimSpace(sub.OriginalLanguage) != "" { + data["original_language"] = sub.OriginalLanguage + } + if sub.Year > 0 { + data["year"] = sub.Year + } + if sub.Rating > 0 { + data["rating"] = sub.Rating + } + if strings.TrimSpace(sub.Genres) != "" { + data["genres"] = sub.Genres + } if strings.TrimSpace(sub.Overview) != "" { data["overview"] = sub.Overview }