From 6e61c8b5451e6de402c57db1e31679fd0c63b566 Mon Sep 17 00:00:00 2001
From: ShukeBta <272197458+ShukeBta@users.noreply.github.com>
Date: Sat, 20 Jun 2026 01:45:40 +0800
Subject: [PATCH] =?UTF-8?q?feat(telegram):=20=E8=AE=A2=E9=98=85/=E4=B8=8B?=
=?UTF-8?q?=E8=BD=BD=E8=A1=A5=E9=BD=90=20year/rating/genres/=E5=8E=9F?=
=?UTF-8?q?=E5=90=8D/=E8=AF=AD=E8=A8=80=20=E5=AF=8C=E9=80=9A=E7=9F=A5?=
=?UTF-8?q?=E5=AD=97=E6=AE=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 模型: Subscription/DownloadTask 新增 OriginalName/OriginalLanguage/Year/
Rating/Genres 列(GORM AutoMigrate 自动建列, 无需手写迁移)
- enrich 链路: displayMetadata 与 DownloadTaskMeta 携带新字段, 从
SearchExternalMedia(TMDb/豆瓣/Bangumi)结果回填并持久化; 放宽提前返回
门槛, 已有海报但缺新字段时仍补齐
- 通知: 订阅/下载完成事件 data 注入 year/rating/genres/original_title/
original_language, 喂给作者富模板 formatTelegramMediaNotification
- 测试: 新增端到端断言, 验证补齐字段透传进 Telegram caption
---
internal/handler/metadata_enrich.go | 148 ++++++++++++++++++++---
internal/model/model.go | 26 ++--
internal/service/downloads.go | 25 ++++
internal/service/notify_channels_test.go | 39 ++++++
internal/service/subscription.go | 20 ++-
5 files changed, 229 insertions(+), 29 deletions(-)
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
}