mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 03:26:37 +08:00
fix(telegram): improve media notification template
This commit is contained in:
@@ -72,7 +72,22 @@ func TestTelegramDispatchUsesPhotoAndFormattedCaption(t *testing.T) {
|
||||
Title: "MediaStationGo 下载完成",
|
||||
Message: "任务:示例影片\n保存路径:/downloads/movie\nHash:abcdef",
|
||||
Data: map[string]interface{}{
|
||||
"poster_url": server.URL + "/poster.jpg",
|
||||
"poster_url": server.URL + "/poster.jpg",
|
||||
"media_type": "tv",
|
||||
"media_category": "纪录片剧集",
|
||||
"title": "美国甜心:达拉斯牛仔啦啦队",
|
||||
"original_title": "AMERICA'S SWEETHEARTS: Dallas Cowboys Cheerleaders",
|
||||
"original_language": "en",
|
||||
"year": 2024,
|
||||
"season_episode": "S03E07",
|
||||
"size": "3.0GB / 5.7Mbps",
|
||||
"version": "H264.NF.FHD-HHWEB",
|
||||
"rating": 8.2,
|
||||
"genres": "纪录",
|
||||
"overview": "从试镜到训练营再到 NFL 赛季,一路跟随达拉斯牛仔队啦啦队队员们追逐梦想。",
|
||||
"tmdb_url": server.URL + "/tmdb",
|
||||
"imdb_url": server.URL + "/imdb",
|
||||
"douban_url": server.URL + "/douban",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -85,9 +100,30 @@ func TestTelegramDispatchUsesPhotoAndFormattedCaption(t *testing.T) {
|
||||
t.Fatalf("telegram form = %#v", gotForm)
|
||||
}
|
||||
caption := gotForm["caption"]
|
||||
for _, want := range []string{"<b>MediaStationGo</b>", "<b>下载完成</b>", "<b>任务</b>: 示例影片", "<code>/downloads/movie</code>", "<code>abcdef</code>"} {
|
||||
for _, want := range []string{
|
||||
"🐈⬛🐈⬛ MediaStationGo 更新啦 🐈⬛🐈⬛",
|
||||
"--------------------------------",
|
||||
"#剧集",
|
||||
"📺 中文片名:美国甜心:达拉斯牛仔啦啦队",
|
||||
"🧿 原始片名:AMERICA'S SWEETHEARTS: Dallas Cowboys Cheerleaders",
|
||||
"🌐 原始语言:英语",
|
||||
"📅 发行年份:2024",
|
||||
"🐈⬛ 类别:纪录片剧集",
|
||||
"🫧 季集:S03E07",
|
||||
"🔎 大小:3.0GB / 5.7Mbps",
|
||||
"📁 版本:H264.NF.FHD-HHWEB",
|
||||
"⭐️ 评分:8.2",
|
||||
"💎 类型:纪录",
|
||||
"🪬 简介:",
|
||||
`🔗 外链:<a href="` + server.URL + `/tmdb">TMDB</a> / <a href="` + server.URL + `/imdb">IMDB</a> / <a href="` + server.URL + `/douban">豆瓣</a>`,
|
||||
} {
|
||||
if !strings.Contains(caption, want) {
|
||||
t.Fatalf("caption missing %q: %s", want, caption)
|
||||
}
|
||||
}
|
||||
for _, unwanted := range []string{"✅ <b>下载完成</b>", "🎯 订阅命中新资源", "保存路径", "abcdef"} {
|
||||
if strings.Contains(caption, unwanted) {
|
||||
t.Fatalf("caption should not include %q: %s", unwanted, caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,12 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// TelegramProvider 通过 Telegram Bot API 发送通知。
|
||||
@@ -31,7 +34,7 @@ func (p *TelegramProvider) Send(ctx context.Context, cfg map[string]string, even
|
||||
|
||||
var firstErr error
|
||||
for _, chatID := range chatIDs {
|
||||
if photoURL != "" && len(text) <= 1024 {
|
||||
if photoURL != "" && utf8.RuneCountInString(text) <= 1024 {
|
||||
payload := map[string]string{
|
||||
"chat_id": chatID,
|
||||
"photo": photoURL,
|
||||
@@ -43,6 +46,20 @@ func (p *TelegramProvider) Send(ctx context.Context, cfg map[string]string, even
|
||||
} else if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
if photo, _, err := telegramFetchRemotePhoto(ctx, cfg, photoURL, 15*time.Second); err == nil {
|
||||
fields := map[string]string{
|
||||
"chat_id": chatID,
|
||||
"caption": text,
|
||||
"parse_mode": parseMode,
|
||||
}
|
||||
if err := telegramPostMultipart(ctx, cfg, "sendPhoto", fields, "photo", "poster.jpg", photo, 20*time.Second); err == nil {
|
||||
continue
|
||||
} else if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
} else if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
payload := map[string]string{
|
||||
"chat_id": chatID,
|
||||
@@ -85,26 +102,32 @@ func formatTelegramMessage(event NotifyEvent, parseMode string) string {
|
||||
}
|
||||
|
||||
func formatTelegramNotification(event NotifyEvent) string {
|
||||
if text := formatTelegramMediaNotification(event); text != "" {
|
||||
return text
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
title := strings.TrimSpace(event.Title)
|
||||
if strings.HasPrefix(title, "MediaStationGo ") {
|
||||
sb.WriteString("<b>MediaStationGo</b>\n")
|
||||
sb.WriteString(fmt.Sprintf("<b>%s</b>", escapeHTML(strings.TrimSpace(strings.TrimPrefix(title, "MediaStationGo ")))))
|
||||
} else if title != "" {
|
||||
sb.WriteString(fmt.Sprintf("<b>%s</b>", escapeHTML(title)))
|
||||
} else {
|
||||
sb.WriteString("<b>MediaStationGo</b>")
|
||||
if tag := telegramEventTag(event); tag != "" {
|
||||
sb.WriteString(tag)
|
||||
if telegramShouldShowEventHeading(event) {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
if telegramShouldShowEventHeading(event) {
|
||||
sb.WriteString(telegramEventHeading(event))
|
||||
}
|
||||
|
||||
message := strings.TrimSpace(event.Message)
|
||||
if message != "" {
|
||||
sb.WriteString("\n\n")
|
||||
if sb.Len() > 0 {
|
||||
sb.WriteString("\n\n")
|
||||
}
|
||||
sb.WriteString(formatTelegramBody(message))
|
||||
}
|
||||
|
||||
fields := telegramDisplayData(event.Data)
|
||||
if len(fields) > 0 {
|
||||
if message != "" {
|
||||
if sb.Len() > 0 {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
@@ -113,10 +136,189 @@ func formatTelegramNotification(event NotifyEvent) string {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
if links := telegramExternalLinks(event.Data); links != "" {
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(links)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(sb.String())
|
||||
}
|
||||
|
||||
const (
|
||||
telegramMediaTemplateHeader = "🐈⬛🐈⬛ MediaStationGo 更新啦 🐈⬛🐈⬛"
|
||||
telegramMediaTemplateSeparator = "--------------------------------"
|
||||
)
|
||||
|
||||
var telegramSeasonEpisodePattern = regexp.MustCompile(`(?i)S\d{1,2}E\d{1,3}(?:[\-.~_ ]?E?\d{1,3})?`)
|
||||
|
||||
func formatTelegramMediaNotification(event NotifyEvent) string {
|
||||
tag := telegramMediaTag(event.Data)
|
||||
if tag == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
title := telegramFirstValue(event.Data, "chinese_title", "title", "name", "media_title")
|
||||
if title == "" {
|
||||
title = telegramMessageFieldValue(event.Message, "任务", "订阅", "媒体", "资源")
|
||||
}
|
||||
originalTitle := telegramFirstValue(event.Data, "original_title", "original_name")
|
||||
originalLanguage := telegramLanguageName(telegramFirstValue(event.Data, "original_language", "language", "languages"))
|
||||
year := telegramFirstValue(event.Data, "year", "release_year")
|
||||
category := telegramMediaCategory(event.Data)
|
||||
seasonEpisode := telegramSeasonEpisodeValue(event)
|
||||
size := telegramSizeValue(event.Data)
|
||||
version := telegramVersionValue(event, seasonEpisode)
|
||||
rating := telegramFirstValue(event.Data, "rating", "score")
|
||||
genres := telegramGenresValue(telegramFirstValue(event.Data, "genres", "genre", "type"))
|
||||
overview := telegramFirstValue(event.Data, "overview", "summary", "description")
|
||||
links := telegramExternalLinks(event.Data)
|
||||
|
||||
topFields := []string{}
|
||||
if title != "" {
|
||||
topFields = append(topFields, "📺 中文片名:"+escapeHTML(title))
|
||||
}
|
||||
if originalTitle != "" && !strings.EqualFold(originalTitle, title) {
|
||||
topFields = append(topFields, "🧿 原始片名:"+escapeHTML(originalTitle))
|
||||
}
|
||||
if originalLanguage != "" {
|
||||
topFields = append(topFields, "🌐 原始语言:"+escapeHTML(originalLanguage))
|
||||
}
|
||||
if year != "" && year != "0" {
|
||||
topFields = append(topFields, "📅 发行年份:"+escapeHTML(year))
|
||||
}
|
||||
|
||||
mediaFields := []string{}
|
||||
if category != "" {
|
||||
mediaFields = append(mediaFields, "🐈⬛ 类别:"+escapeHTML(category))
|
||||
}
|
||||
if seasonEpisode != "" {
|
||||
mediaFields = append(mediaFields, "🫧 季集:"+escapeHTML(seasonEpisode))
|
||||
}
|
||||
if size != "" {
|
||||
mediaFields = append(mediaFields, "🔎 大小:"+escapeHTML(size))
|
||||
}
|
||||
if version != "" {
|
||||
mediaFields = append(mediaFields, "📁 版本:"+escapeHTML(version))
|
||||
}
|
||||
|
||||
infoFields := []string{}
|
||||
if rating != "" && rating != "0" {
|
||||
infoFields = append(infoFields, "⭐️ 评分:"+escapeHTML(rating))
|
||||
}
|
||||
if genres != "" {
|
||||
infoFields = append(infoFields, "💎 类型:"+escapeHTML(genres))
|
||||
}
|
||||
if overview != "" {
|
||||
infoFields = append(infoFields, "🪬 简介:\n"+escapeHTML(overview))
|
||||
}
|
||||
|
||||
if len(topFields) == 0 && len(mediaFields) == 0 && len(infoFields) == 0 && links == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
lines := []string{telegramMediaTemplateHeader, telegramMediaTemplateSeparator, tag}
|
||||
lines = append(lines, topFields...)
|
||||
if len(mediaFields) > 0 {
|
||||
if len(topFields) > 0 {
|
||||
lines = append(lines, "")
|
||||
}
|
||||
lines = append(lines, mediaFields...)
|
||||
}
|
||||
if len(infoFields) > 0 {
|
||||
if len(topFields) > 0 || len(mediaFields) > 0 {
|
||||
lines = append(lines, "")
|
||||
}
|
||||
lines = append(lines, infoFields...)
|
||||
}
|
||||
if links != "" {
|
||||
lines = append(lines, "", telegramMediaTemplateSeparator, links)
|
||||
}
|
||||
return strings.TrimSpace(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
func telegramShouldShowEventHeading(event NotifyEvent) bool {
|
||||
if telegramMediaTag(event.Data) != "" {
|
||||
return false
|
||||
}
|
||||
switch strings.TrimSpace(event.Type) {
|
||||
case EventSubscriptionHit, EventDownloadComplete:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func telegramEventTag(event NotifyEvent) string {
|
||||
if tag := telegramMediaTag(event.Data); tag != "" {
|
||||
return tag
|
||||
}
|
||||
switch strings.TrimSpace(event.Type) {
|
||||
case EventSubscriptionHit:
|
||||
return "#订阅"
|
||||
case EventDownloadComplete:
|
||||
return "#下载完成"
|
||||
case EventScrapeFailed:
|
||||
return "#刮削失败"
|
||||
case EventSystemAlert:
|
||||
return "#系统提醒"
|
||||
case EventLibraryIngest:
|
||||
return "#入库"
|
||||
default:
|
||||
title := strings.TrimSpace(strings.TrimPrefix(event.Title, "MediaStationGo "))
|
||||
if title == "" {
|
||||
return "#MediaStationGo"
|
||||
}
|
||||
return "#" + escapeHTML(strings.ReplaceAll(title, " ", ""))
|
||||
}
|
||||
}
|
||||
|
||||
func telegramMediaTag(data map[string]interface{}) string {
|
||||
for _, key := range []string{"media_category", "category", "media_type"} {
|
||||
value := strings.TrimSpace(telegramDataString(data, key))
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
lower := strings.ToLower(value)
|
||||
switch {
|
||||
case strings.Contains(value, "电影") || lower == "movie":
|
||||
return "#电影"
|
||||
case strings.Contains(value, "剧") || lower == "tv" || lower == "series" || lower == "show":
|
||||
return "#剧集"
|
||||
case strings.Contains(value, "动漫") || strings.Contains(value, "动画") || lower == "anime":
|
||||
return "#动漫"
|
||||
case strings.Contains(value, "综艺") || lower == "variety":
|
||||
return "#综艺"
|
||||
case strings.Contains(value, "纪录") || lower == "documentary":
|
||||
return "#纪录片"
|
||||
default:
|
||||
return "#" + escapeHTML(strings.ReplaceAll(value, " ", ""))
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func telegramEventHeading(event NotifyEvent) string {
|
||||
title := strings.TrimSpace(event.Title)
|
||||
title = strings.TrimSpace(strings.TrimPrefix(title, "MediaStationGo "))
|
||||
if title == "" {
|
||||
title = "MediaStationGo 通知"
|
||||
}
|
||||
icon := "🔔"
|
||||
switch strings.TrimSpace(event.Type) {
|
||||
case EventSubscriptionHit:
|
||||
icon = "🎯"
|
||||
case EventDownloadComplete:
|
||||
icon = "✅"
|
||||
case EventScrapeFailed:
|
||||
icon = "⚠️"
|
||||
case EventSystemAlert:
|
||||
icon = "🚨"
|
||||
case EventLibraryIngest:
|
||||
icon = "📚"
|
||||
}
|
||||
return fmt.Sprintf("%s <b>%s</b>", icon, escapeHTML(title))
|
||||
}
|
||||
|
||||
func formatTelegramBody(message string) string {
|
||||
lines := strings.Split(message, "\n")
|
||||
out := make([]string, 0, len(lines))
|
||||
@@ -130,6 +332,10 @@ func formatTelegramBody(message string) string {
|
||||
out = append(out, "- "+escapeHTML(strings.TrimSpace(strings.TrimPrefix(line, "- "))))
|
||||
continue
|
||||
}
|
||||
if key, ok := trimTelegramEmptyField(line); ok {
|
||||
out = append(out, fmt.Sprintf("%s <b>%s</b>:", telegramFieldIcon(telegramFieldLabel(key)), escapeHTML(telegramFieldLabel(key))))
|
||||
continue
|
||||
}
|
||||
if key, value, ok := splitTelegramField(line); ok {
|
||||
out = append(out, formatTelegramField(key, value))
|
||||
continue
|
||||
@@ -139,6 +345,19 @@ func formatTelegramBody(message string) string {
|
||||
return strings.TrimSpace(strings.Join(out, "\n"))
|
||||
}
|
||||
|
||||
func trimTelegramEmptyField(line string) (string, bool) {
|
||||
line = strings.TrimSpace(line)
|
||||
for _, suffix := range []string{":", ":"} {
|
||||
if strings.HasSuffix(line, suffix) {
|
||||
key := strings.TrimSpace(strings.TrimSuffix(line, suffix))
|
||||
if key != "" && len([]rune(key)) <= 16 {
|
||||
return key, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func splitTelegramField(line string) (string, string, bool) {
|
||||
idx := strings.Index(line, ":")
|
||||
sepLen := len(":")
|
||||
@@ -163,7 +382,7 @@ func formatTelegramField(key, value string) string {
|
||||
if telegramCodeField(key) {
|
||||
escapedValue = "<code>" + escapedValue + "</code>"
|
||||
}
|
||||
return fmt.Sprintf("<b>%s</b>: %s", escapeHTML(key), escapedValue)
|
||||
return fmt.Sprintf("%s <b>%s</b>:%s", telegramFieldIcon(key), escapeHTML(key), escapedValue)
|
||||
}
|
||||
|
||||
func telegramCodeField(key string) bool {
|
||||
@@ -174,6 +393,262 @@ func telegramCodeField(key string) bool {
|
||||
strings.Contains(key, "id")
|
||||
}
|
||||
|
||||
func telegramFieldIcon(key string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(key)) {
|
||||
case "中文片名", "标题", "任务", "媒体", "资源":
|
||||
return "📺"
|
||||
case "原始片名":
|
||||
return "🧿"
|
||||
case "原始语言", "语言":
|
||||
return "🌐"
|
||||
case "发行年份", "年份":
|
||||
return "📅"
|
||||
case "类别", "分类", "媒体类型":
|
||||
return "🐈⬛"
|
||||
case "季集", "集数":
|
||||
return "🫧"
|
||||
case "大小", "质量", "规格":
|
||||
return "🔎"
|
||||
case "版本", "保存路径":
|
||||
return "📁"
|
||||
case "评分":
|
||||
return "⭐️"
|
||||
case "类型":
|
||||
return "💎"
|
||||
case "简介":
|
||||
return "🪬"
|
||||
case "订阅":
|
||||
return "🎯"
|
||||
case "新增资源":
|
||||
return "✨"
|
||||
case "Hash":
|
||||
return "🧿"
|
||||
case "错误":
|
||||
return "⚠️"
|
||||
default:
|
||||
return "•"
|
||||
}
|
||||
}
|
||||
|
||||
func telegramFirstValue(data map[string]interface{}, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if value := telegramDataString(data, key); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func telegramMessageFieldValue(message string, keys ...string) string {
|
||||
if strings.TrimSpace(message) == "" {
|
||||
return ""
|
||||
}
|
||||
for _, line := range strings.Split(message, "\n") {
|
||||
key, value, ok := splitTelegramField(line)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, want := range keys {
|
||||
if strings.EqualFold(strings.TrimSpace(key), strings.TrimSpace(want)) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func telegramMediaCategory(data map[string]interface{}) string {
|
||||
if category := telegramFirstValue(data, "media_category", "category"); category != "" {
|
||||
return category
|
||||
}
|
||||
switch strings.ToLower(telegramFirstValue(data, "media_type")) {
|
||||
case "movie":
|
||||
return "电影"
|
||||
case "tv", "series", "show":
|
||||
return "剧集"
|
||||
case "anime":
|
||||
return "动漫"
|
||||
case "variety":
|
||||
return "综艺"
|
||||
case "documentary":
|
||||
return "纪录片"
|
||||
default:
|
||||
return telegramFirstValue(data, "media_type")
|
||||
}
|
||||
}
|
||||
|
||||
func telegramLanguageName(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.FieldsFunc(raw, func(r rune) bool {
|
||||
return r == ',' || r == ',' || r == '/' || r == '|' || r == '、'
|
||||
})
|
||||
if len(parts) == 0 {
|
||||
parts = []string{raw}
|
||||
}
|
||||
seen := map[string]struct{}{}
|
||||
out := []string{}
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(strings.Trim(part, "[]"))
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
lower := strings.ToLower(strings.ReplaceAll(part, "_", "-"))
|
||||
name := part
|
||||
switch {
|
||||
case strings.HasPrefix(lower, "zh") || lower == "cn" || lower == "cmn":
|
||||
name = "中文"
|
||||
case lower == "en" || strings.HasPrefix(lower, "en-"):
|
||||
name = "英语"
|
||||
case lower == "ja" || lower == "jp" || strings.HasPrefix(lower, "ja-"):
|
||||
name = "日语"
|
||||
case lower == "ko" || lower == "kr" || strings.HasPrefix(lower, "ko-"):
|
||||
name = "韩语"
|
||||
case lower == "fr" || strings.HasPrefix(lower, "fr-"):
|
||||
name = "法语"
|
||||
case lower == "de" || strings.HasPrefix(lower, "de-"):
|
||||
name = "德语"
|
||||
case lower == "es" || strings.HasPrefix(lower, "es-"):
|
||||
name = "西班牙语"
|
||||
case lower == "it" || strings.HasPrefix(lower, "it-"):
|
||||
name = "意大利语"
|
||||
case lower == "ru" || strings.HasPrefix(lower, "ru-"):
|
||||
name = "俄语"
|
||||
case lower == "th" || strings.HasPrefix(lower, "th-"):
|
||||
name = "泰语"
|
||||
}
|
||||
if _, ok := seen[name]; ok {
|
||||
continue
|
||||
}
|
||||
seen[name] = struct{}{}
|
||||
out = append(out, name)
|
||||
}
|
||||
return strings.Join(out, "、")
|
||||
}
|
||||
|
||||
func telegramSeasonEpisodeValue(event NotifyEvent) string {
|
||||
if value := telegramFirstValue(event.Data, "season_episode", "episode_tag"); value != "" {
|
||||
return strings.ToUpper(value)
|
||||
}
|
||||
for _, raw := range []string{
|
||||
telegramFirstValue(event.Data, "resource_title", "torrent_title", "release_title"),
|
||||
telegramFirstValue(event.Data, "title", "name"),
|
||||
event.Message,
|
||||
} {
|
||||
if value := telegramExtractSeasonEpisode(raw); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
season := telegramFirstValue(event.Data, "season")
|
||||
episode := telegramFirstValue(event.Data, "episode")
|
||||
if season != "" && episode != "" {
|
||||
return fmt.Sprintf("S%02dE%02d", telegramEpisodeNumber(season), telegramEpisodeNumber(episode))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func telegramEpisodeNumber(raw string) int {
|
||||
raw = strings.TrimSpace(strings.TrimLeft(strings.ToUpper(raw), "SE"))
|
||||
raw = strings.TrimLeft(raw, "0")
|
||||
if raw == "" {
|
||||
return 0
|
||||
}
|
||||
n, _ := strconv.Atoi(raw)
|
||||
return n
|
||||
}
|
||||
|
||||
func telegramExtractSeasonEpisode(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
return strings.ToUpper(telegramSeasonEpisodePattern.FindString(raw))
|
||||
}
|
||||
|
||||
func telegramSizeValue(data map[string]interface{}) string {
|
||||
size := telegramFirstValue(data, "size")
|
||||
bitrate := telegramFirstValue(data, "bitrate")
|
||||
if size != "" && bitrate != "" {
|
||||
return size + " / " + bitrate
|
||||
}
|
||||
if size != "" {
|
||||
return size
|
||||
}
|
||||
return bitrate
|
||||
}
|
||||
|
||||
func telegramVersionValue(event NotifyEvent, seasonEpisode string) string {
|
||||
if version := telegramFirstValue(event.Data, "version", "release_group"); version != "" && !strings.EqualFold(version, "best") {
|
||||
return version
|
||||
}
|
||||
return telegramVersionFromResourceTitle(
|
||||
telegramFirstValue(event.Data, "resource_title", "torrent_title", "release_title"),
|
||||
seasonEpisode,
|
||||
telegramFirstValue(event.Data, "year", "release_year"),
|
||||
)
|
||||
}
|
||||
|
||||
func telegramVersionFromResourceTitle(raw, seasonEpisode, year string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
tail := ""
|
||||
if seasonEpisode != "" {
|
||||
upperRaw := strings.ToUpper(raw)
|
||||
upperEpisode := strings.ToUpper(seasonEpisode)
|
||||
if idx := strings.Index(upperRaw, upperEpisode); idx >= 0 {
|
||||
tail = raw[idx+len(seasonEpisode):]
|
||||
}
|
||||
}
|
||||
if tail == "" && year != "" {
|
||||
if idx := strings.LastIndex(raw, year); idx >= 0 {
|
||||
tail = raw[idx+len(year):]
|
||||
}
|
||||
}
|
||||
tail = strings.Trim(tail, " \t\r\n._-[]()【】")
|
||||
if tail == "" {
|
||||
return ""
|
||||
}
|
||||
tail = strings.TrimSuffix(tail, ".torrent")
|
||||
tail = strings.TrimSuffix(tail, ".mkv")
|
||||
tail = strings.TrimSuffix(tail, ".mp4")
|
||||
tail = strings.Join(strings.Fields(tail), ".")
|
||||
if len([]rune(tail)) > 72 {
|
||||
tail = string([]rune(tail)[:72]) + "..."
|
||||
}
|
||||
return tail
|
||||
}
|
||||
|
||||
func telegramGenresValue(raw string) string {
|
||||
raw = strings.TrimSpace(strings.Trim(raw, "[]"))
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.FieldsFunc(raw, func(r rune) bool {
|
||||
return r == ',' || r == ',' || r == '/' || r == '|' || r == '、'
|
||||
})
|
||||
if len(parts) <= 1 {
|
||||
return raw
|
||||
}
|
||||
out := make([]string, 0, len(parts))
|
||||
seen := map[string]struct{}{}
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[part]; ok {
|
||||
continue
|
||||
}
|
||||
seen[part] = struct{}{}
|
||||
out = append(out, part)
|
||||
}
|
||||
return strings.Join(out, "、")
|
||||
}
|
||||
|
||||
type telegramDataField struct {
|
||||
key string
|
||||
value string
|
||||
@@ -204,7 +679,9 @@ func telegramDisplayData(data map[string]interface{}) []telegramDataField {
|
||||
|
||||
func telegramHiddenDataKey(key string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(key)) {
|
||||
case "photo_url", "poster_url", "poster", "image_url", "backdrop_url":
|
||||
case "photo_url", "poster_url", "poster", "image_url", "backdrop_url",
|
||||
"tmdb_url", "imdb_url", "douban_url", "detail_url", "external_url",
|
||||
"resource_title", "torrent_title", "release_title":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -215,6 +692,12 @@ func telegramFieldLabel(key string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(key)) {
|
||||
case "title", "name":
|
||||
return "标题"
|
||||
case "original_title":
|
||||
return "原始片名"
|
||||
case "original_language":
|
||||
return "原始语言"
|
||||
case "year", "release_year":
|
||||
return "发行年份"
|
||||
case "save_path":
|
||||
return "保存路径"
|
||||
case "hash":
|
||||
@@ -222,7 +705,19 @@ func telegramFieldLabel(key string) string {
|
||||
case "media_type":
|
||||
return "媒体类型"
|
||||
case "media_category":
|
||||
return "分类"
|
||||
return "类别"
|
||||
case "season_episode":
|
||||
return "季集"
|
||||
case "size", "bitrate":
|
||||
return "大小"
|
||||
case "version", "release_group":
|
||||
return "版本"
|
||||
case "rating":
|
||||
return "评分"
|
||||
case "genres":
|
||||
return "类型"
|
||||
case "overview":
|
||||
return "简介"
|
||||
case "subscription":
|
||||
return "订阅"
|
||||
case "queued":
|
||||
@@ -232,6 +727,30 @@ func telegramFieldLabel(key string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func telegramExternalLinks(data map[string]interface{}) string {
|
||||
if len(data) == 0 {
|
||||
return ""
|
||||
}
|
||||
links := []string{}
|
||||
for _, item := range []struct {
|
||||
key string
|
||||
name string
|
||||
}{
|
||||
{key: "tmdb_url", name: "TMDB"},
|
||||
{key: "imdb_url", name: "IMDB"},
|
||||
{key: "douban_url", name: "豆瓣"},
|
||||
} {
|
||||
value := telegramDataString(data, item.key)
|
||||
if isTelegramRemotePhotoURL(value) {
|
||||
links = append(links, fmt.Sprintf(`<a href="%s">%s</a>`, escapeHTML(value), escapeHTML(item.name)))
|
||||
}
|
||||
}
|
||||
if len(links) == 0 {
|
||||
return ""
|
||||
}
|
||||
return "🔗 外链:" + strings.Join(links, " / ")
|
||||
}
|
||||
|
||||
func telegramEventPhotoURL(event NotifyEvent) string {
|
||||
for _, key := range []string{"photo_url", "poster_url", "poster", "image_url", "backdrop_url"} {
|
||||
value := telegramDataString(event.Data, key)
|
||||
@@ -248,12 +767,41 @@ func telegramDataString(data map[string]interface{}, key string) string {
|
||||
}
|
||||
for k, value := range data {
|
||||
if strings.EqualFold(strings.TrimSpace(k), key) {
|
||||
return strings.TrimSpace(fmt.Sprint(value))
|
||||
return telegramValueString(value)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func telegramValueString(value interface{}) string {
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
return ""
|
||||
case string:
|
||||
return strings.TrimSpace(v)
|
||||
case []string:
|
||||
return strings.TrimSpace(strings.Join(v, ","))
|
||||
case []interface{}:
|
||||
out := make([]string, 0, len(v))
|
||||
for _, item := range v {
|
||||
if s := telegramValueString(item); s != "" {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return strings.Join(out, ",")
|
||||
case float32:
|
||||
return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.1f", v), "0"), ".")
|
||||
case float64:
|
||||
return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.1f", v), "0"), ".")
|
||||
default:
|
||||
text := strings.TrimSpace(fmt.Sprint(value))
|
||||
if text == "<nil>" {
|
||||
return ""
|
||||
}
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
func isTelegramRemotePhotoURL(raw string) bool {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -120,6 +122,81 @@ func telegramPostJSON(ctx context.Context, cfg map[string]string, method string,
|
||||
return telegramDoWithFallback(ctx, cfg, http.MethodPost, apiURL, string(body), "application/json", timeout)
|
||||
}
|
||||
|
||||
func telegramPostMultipart(ctx context.Context, cfg map[string]string, method string, fields map[string]string, fileField, fileName string, file []byte, timeout time.Duration) error {
|
||||
apiURL, err := telegramMethodURL(cfg, cfg["bot_token"], method)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
for key, value := range fields {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
continue
|
||||
}
|
||||
if err := writer.WriteField(key, value); err != nil {
|
||||
_ = writer.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
part, err := writer.CreateFormFile(fileField, fileName)
|
||||
if err != nil {
|
||||
_ = writer.Close()
|
||||
return err
|
||||
}
|
||||
if _, err := part.Write(file); err != nil {
|
||||
_ = writer.Close()
|
||||
return err
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return telegramDoWithFallback(ctx, cfg, http.MethodPost, apiURL, body.String(), writer.FormDataContentType(), timeout)
|
||||
}
|
||||
|
||||
func telegramFetchRemotePhoto(ctx context.Context, cfg map[string]string, rawURL string, timeout time.Duration) ([]byte, string, error) {
|
||||
rawURL = strings.TrimSpace(rawURL)
|
||||
if rawURL == "" {
|
||||
return nil, "", errors.New("telegram photo url required")
|
||||
}
|
||||
var lastErr error
|
||||
for _, client := range telegramHTTPClients(timeout, cfg) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
req.Header.Set("User-Agent", "MediaStationGo/1.0")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
lastErr = sanitizeTelegramError(err)
|
||||
continue
|
||||
}
|
||||
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 10*1024*1024+1))
|
||||
_ = resp.Body.Close()
|
||||
if readErr != nil {
|
||||
lastErr = readErr
|
||||
continue
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
lastErr = fmt.Errorf("photo fetch error %d", resp.StatusCode)
|
||||
continue
|
||||
}
|
||||
if len(body) == 0 {
|
||||
lastErr = errors.New("photo fetch returned empty body")
|
||||
continue
|
||||
}
|
||||
if len(body) > 10*1024*1024 {
|
||||
lastErr = errors.New("photo too large")
|
||||
continue
|
||||
}
|
||||
contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
|
||||
return body, contentType, nil
|
||||
}
|
||||
if lastErr != nil {
|
||||
return nil, "", lastErr
|
||||
}
|
||||
return nil, "", errors.New("photo fetch failed")
|
||||
}
|
||||
|
||||
func deleteTelegramWebhook(ctx context.Context, cfg map[string]string) error {
|
||||
payload := map[string]any{
|
||||
"drop_pending_updates": false,
|
||||
|
||||
Reference in New Issue
Block a user