fix: implement watch media group handler and enhance media processing logic

This commit is contained in:
krau
2026-01-17 13:23:47 +08:00
parent cabe1189e2
commit f17a380579
3 changed files with 169 additions and 4 deletions

2
.gitignore vendored
View File

@@ -10,3 +10,5 @@ temp/
.hugo_build.lock
playwright/
testplugins/
*.exe
tmp-*

View File

@@ -26,7 +26,7 @@ func BuildDirHelpStyling(dirs []database.Dir) []styling.StyledTextOption {
styling.Blockquote(func() string {
var sb strings.Builder
for _, dir := range dirs {
sb.WriteString(fmt.Sprintf("%d: ", dir.ID))
fmt.Fprintf(&sb, "%d: ", dir.ID)
sb.WriteString(dir.StorageName)
sb.WriteString(" - ")
sb.WriteString(dir.Path)

View File

@@ -5,7 +5,9 @@ import (
"path"
"regexp"
"strings"
"sync"
"text/template"
"time"
"github.com/celestix/gotgproto/dispatcher"
"github.com/celestix/gotgproto/ext"
@@ -16,10 +18,12 @@ import (
"github.com/krau/SaveAny-Bot/common/i18n"
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
"github.com/krau/SaveAny-Bot/config"
"github.com/krau/SaveAny-Bot/core"
"github.com/krau/SaveAny-Bot/core/tasks/tfile"
coretfile "github.com/krau/SaveAny-Bot/core/tasks/tfile"
"github.com/krau/SaveAny-Bot/database"
"github.com/krau/SaveAny-Bot/pkg/enums/fnamest"
"github.com/krau/SaveAny-Bot/pkg/tfile"
"github.com/krau/SaveAny-Bot/storage"
"github.com/rs/xid"
)
@@ -151,6 +155,53 @@ func handleUnwatchCmd(ctx *ext.Context, update *ext.Update) error {
return dispatcher.EndGroups
}
type watchMediaGroupHandler struct {
groups map[int64]map[uint][]tfile.TGFileMessage // chatID -> userID -> files
timers map[int64]map[uint]*time.Timer
mu sync.Mutex
}
var watchMediaGroupMgr = &watchMediaGroupHandler{
groups: make(map[int64]map[uint][]tfile.TGFileMessage),
timers: make(map[int64]map[uint]*time.Timer),
}
func (w *watchMediaGroupHandler) addFile(chatID int64, userID uint, file tfile.TGFileMessage, timeout time.Duration, callback func([]tfile.TGFileMessage)) {
w.mu.Lock()
defer w.mu.Unlock()
if w.groups[chatID] == nil {
w.groups[chatID] = make(map[uint][]tfile.TGFileMessage)
}
if w.timers[chatID] == nil {
w.timers[chatID] = make(map[uint]*time.Timer)
}
if timer, exists := w.timers[chatID][userID]; exists {
timer.Stop()
}
w.groups[chatID][userID] = append(w.groups[chatID][userID], file)
w.timers[chatID][userID] = time.AfterFunc(timeout, func() {
w.mu.Lock()
files := w.groups[chatID][userID]
delete(w.groups[chatID], userID)
delete(w.timers[chatID], userID)
if len(w.groups[chatID]) == 0 {
delete(w.groups, chatID)
}
if len(w.timers[chatID]) == 0 {
delete(w.timers, chatID)
}
w.mu.Unlock()
if len(files) > 0 {
callback(files)
}
})
}
func listenMediaMessageEvent(ch chan userclient.MediaMessageEvent) {
if userclient.GetCtx() == nil {
return
@@ -224,6 +275,24 @@ func listenMediaMessageEvent(ch chan userclient.MediaMessageEvent) {
}
file.SetName(sb.String())
}
// Check if this is a media group and if rules specify NEW-FOR-ALBUM
groupID, isGroup := file.Message().GetGroupedID()
needAlbumHandling := false
if isGroup && groupID != 0 && user.ApplyRule && user.Rules != nil {
_, _, matchedDirPath := ruleutil.ApplyRule(ctx, user.Rules, ruleutil.NewInput(file))
needAlbumHandling = matchedDirPath.NeedNewForAlbum()
}
if needAlbumHandling {
// For media groups with NEW-FOR-ALBUM rule, collect all files of the same group
watchMediaGroupMgr.addFile(event.ChatID, user.ID, file, time.Duration(config.C().Telegram.MediaGroupTimeout)*time.Second, func(files []tfile.TGFileMessage) {
processWatchMediaGroup(ctx, user, stor, "", files)
})
continue
}
// Process single file or media group without album folder creation
var dirPath string
if user.ApplyRule && user.Rules != nil {
matched, matchedStorageName, matchedDirPath := ruleutil.ApplyRule(ctx, user.Rules, ruleutil.NewInput(file))
@@ -243,7 +312,7 @@ func listenMediaMessageEvent(ch chan userclient.MediaMessageEvent) {
storagePath := stor.JoinStoragePath(path.Join(dirPath, file.Name()))
injectCtx := tgutil.ExtWithContext(ctx.Context, ctx)
taskid := xid.New().String()
task, err := tfile.NewTGFileTask(taskid, injectCtx, file, stor, storagePath, nil)
task, err := coretfile.NewTGFileTask(taskid, injectCtx, file, stor, storagePath, nil)
if err != nil {
logger.Errorf("create task failed: %s", err)
continue
@@ -256,3 +325,97 @@ func listenMediaMessageEvent(ch chan userclient.MediaMessageEvent) {
}
}
}
func processWatchMediaGroup(ctx *ext.Context, user *database.User, stor storage.Storage, dirPath string, files []tfile.TGFileMessage) {
logger := log.FromContext(ctx)
if len(files) == 0 {
return
}
useRule := user.ApplyRule && user.Rules != nil
applyRule := func(file tfile.TGFileMessage) (string, ruleutil.MatchedDirPath) {
if !useRule {
return stor.Name(), ruleutil.MatchedDirPath(dirPath)
}
matched, storName, dirP := ruleutil.ApplyRule(ctx, user.Rules, ruleutil.NewInput(file))
if !matched {
return stor.Name(), ruleutil.MatchedDirPath(dirPath)
}
storname := storName.String()
if !storName.Usable() {
storname = stor.Name()
}
return storname, dirP
}
type albumFile struct {
file tfile.TGFileMessage
storage storage.Storage
}
albumFiles := make(map[int64][]albumFile)
// Collect files by group ID
for _, file := range files {
storName, ruleDirPath := applyRule(file)
fileStor := stor
if storName != stor.Name() && storName != "" {
var err error
fileStor, err = storage.GetStorageByUserIDAndName(ctx, user.ChatID, storName)
if err != nil {
logger.Errorf("Failed to get storage by user ID and name: %s", err)
continue
}
}
groupId, isGroup := file.Message().GetGroupedID()
if !isGroup || groupId == 0 {
logger.Warnf("File %s is not in a group, skipping", file.Name())
continue
}
if !ruleDirPath.NeedNewForAlbum() {
logger.Warnf("File %s does not need album folder, skipping", file.Name())
continue
}
if _, ok := albumFiles[groupId]; !ok {
albumFiles[groupId] = make([]albumFile, 0)
}
albumFiles[groupId] = append(albumFiles[groupId], albumFile{
file: file,
storage: fileStor,
})
}
// Process album files with folder creation
injectCtx := tgutil.ExtWithContext(ctx.Context, ctx)
totalTasks := 0
for groupID, afiles := range albumFiles {
if len(afiles) <= 1 {
continue
}
// Use first file's name (without extension) as album folder name
albumDir := strings.TrimSuffix(path.Base(afiles[0].file.Name()), path.Ext(afiles[0].file.Name()))
albumStor := afiles[0].storage
logger.Infof("Creating album folder for group %d: %s with %d files", groupID, albumDir, len(afiles))
for _, af := range afiles {
afstorPath := af.storage.JoinStoragePath(path.Join(dirPath, albumDir, af.file.Name()))
taskid := xid.New().String()
task, err := coretfile.NewTGFileTask(taskid, injectCtx, af.file, albumStor, afstorPath, nil)
if err != nil {
logger.Errorf("create task failed for album file: %s", err)
continue
}
if err := core.AddTask(injectCtx, task); err != nil {
logger.Errorf("add task failed: %s", err)
continue
}
totalTasks++
}
}
logger.Infof("Added %d watch media tasks for user %d", totalTasks, user.ChatID)
}