From baad7ad21f0293f8c5bee48c235d7e109b7e0953 Mon Sep 17 00:00:00 2001 From: expary <259320579+expary@users.noreply.github.com> Date: Mon, 1 Jun 2026 07:20:27 +0800 Subject: [PATCH] feat(dl): propagate grouped file captions Use the first non-empty caption in a grouped message as the fallback for group members that do not carry their own caption, so FileCaption-based templates can name every file in the album. Closes #1208 --- app/dl/iter.go | 29 ++++++++++++++++++++++-- app/dl/iter_test.go | 37 +++++++++++++++++++++++++++++++ docs/content/en/guide/template.md | 2 +- docs/content/zh/guide/template.md | 2 +- 4 files changed, 66 insertions(+), 4 deletions(-) diff --git a/app/dl/iter.go b/app/dl/iter.go index 44503e248a..1d61e62ade 100644 --- a/app/dl/iter.go +++ b/app/dl/iter.go @@ -206,6 +206,10 @@ func (i *iter) process(ctx context.Context) (ret bool, skip bool) { } func (i *iter) processSingle(ctx context.Context, message *tg.Message, from peers.Peer, logicalPos int) (bool, bool) { + return i.processSingleWithCaption(ctx, message, from, logicalPos, message.Message) +} + +func (i *iter) processSingleWithCaption(ctx context.Context, message *tg.Message, from peers.Peer, logicalPos int, fileCaption string) (bool, bool) { item, ok := tmedia.GetMedia(message) if !ok { logctx.From(ctx).Warn("Message has no media", @@ -231,7 +235,7 @@ func (i *iter) processSingle(ctx context.Context, message *tg.Message, from peer MessageID: message.ID, MessageDate: int64(message.Date), FileName: item.Name, - FileCaption: message.Message, + FileCaption: fileCaption, FileSize: utils.Byte.FormatBinaryBytes(item.Size), DownloadDate: time.Now().Unix(), }) @@ -288,6 +292,7 @@ func (i *iter) processGrouped(ctx context.Context, message *tg.Message, from pee } hasValid := false + groupCaption := groupedCaption(grouped) for idx, msg := range grouped { logicalPos := startLogicalPos + idx @@ -297,7 +302,7 @@ func (i *iter) processGrouped(ctx context.Context, message *tg.Message, from pee continue } - ret, skip := i.processSingle(ctx, msg, from, logicalPos) + ret, skip := i.processSingleWithCaption(ctx, msg, from, logicalPos, fileCaption(msg, groupCaption)) // if processSingle encounters a fatal error (not just skip), propagate it if !ret && !skip { @@ -316,6 +321,26 @@ func (i *iter) processGrouped(ctx context.Context, message *tg.Message, from pee return hasValid, !hasValid } +func groupedCaption(grouped []*tg.Message) string { + for _, msg := range grouped { + if msg == nil || msg.Message == "" { + continue + } + + return msg.Message + } + + return "" +} + +func fileCaption(message *tg.Message, fallback string) string { + if message.Message != "" { + return message.Message + } + + return fallback +} + func (i *iter) Value() downloader.Elem { return <-i.elem } diff --git a/app/dl/iter_test.go b/app/dl/iter_test.go index 97deadfe19..d906ccd26e 100644 --- a/app/dl/iter_test.go +++ b/app/dl/iter_test.go @@ -6,10 +6,47 @@ import ( "testing" "time" + "github.com/gotd/td/tg" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func TestGroupedCaption(t *testing.T) { + t.Run("uses first non-empty grouped message caption", func(t *testing.T) { + caption := groupedCaption([]*tg.Message{ + {ID: 1, Message: ""}, + {ID: 2, Message: "album caption"}, + {ID: 3, Message: "later caption"}, + }) + + require.Equal(t, "album caption", caption) + }) + + t.Run("returns empty caption when group has no caption", func(t *testing.T) { + caption := groupedCaption([]*tg.Message{ + nil, + {ID: 1, Message: ""}, + {ID: 2, Message: ""}, + }) + + require.Empty(t, caption) + }) +} + +func TestFileCaption(t *testing.T) { + t.Run("keeps message caption when present", func(t *testing.T) { + caption := fileCaption(&tg.Message{Message: "file caption"}, "album caption") + + require.Equal(t, "file caption", caption) + }) + + t.Run("falls back to grouped caption", func(t *testing.T) { + caption := fileCaption(&tg.Message{Message: ""}, "album caption") + + require.Equal(t, "album caption", caption) + }) +} + // TestIterDeletedMessageHandling verifies that deleted messages are handled correctly // without causing the program to crash. This test simulates the scenario where GetSingleMessage // returns a "may be deleted" error and verifies that the iterator: diff --git a/docs/content/en/guide/template.md b/docs/content/en/guide/template.md index 7d27074fe1..455dc5a20b 100644 --- a/docs/content/en/guide/template.md +++ b/docs/content/en/guide/template.md @@ -20,7 +20,7 @@ Template syntax is based on [Go's text/template](https://golang.org/pkg/text/tem | `MessageID` | Telegram message id | | `MessageDate` | Telegram message date(timestamp) | | `FileName` | Telegram file name | -| `FileCaption` | Telegram file caption, aka. text message | +| `FileCaption` | Telegram file caption, aka. text message. In grouped media, files without their own caption use the first non-empty group caption. | | `FileSize` | Human-readable file size, like `1GB` | | `DownloadDate` | Download date(timestamp) | diff --git a/docs/content/zh/guide/template.md b/docs/content/zh/guide/template.md index e3c1374821..45f02492d4 100644 --- a/docs/content/zh/guide/template.md +++ b/docs/content/zh/guide/template.md @@ -20,7 +20,7 @@ bookToC: false | `MessageID` | Telegram 消息ID | | `MessageDate` | Telegram 消息日期(时间戳) | | `FileName` | Telegram 文件名 | -| `FileCaption` | Telegram 文件说明,也就是文本消息 | +| `FileCaption` | Telegram 文件说明,也就是文本消息。组合消息中没有自身 caption 的文件会使用同组第一个非空 caption。 | | `FileSize` | 可读的文件大小,例如 `1GB` | | `DownloadDate` | 下载日期(时间戳) |