Files
yamusic-bot/internal/admin/handler.go
Vladimir Zagainov 14c54ee737
All checks were successful
continuous-integration/drone/push Build is passing
feat(admin): add cache warming from local directory via /warm --from-dir
2025-06-23 21:52:57 +03:00

207 lines
7.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package admin
import (
"context"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
"time"
"gitea.mrixs.me/Mrixs/yamusic-bot/internal/interfaces"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
// Handler обрабатывает команды администратора.
type Handler struct {
storage interfaces.TrackStorage
telegram interfaces.TelegramClient
yandex interfaces.YandexMusicClient
startTime time.Time
}
// NewHandler создает новый обработчик команд администратора.
func NewHandler(storage interfaces.TrackStorage, telegram interfaces.TelegramClient, yandex interfaces.YandexMusicClient, startTime time.Time) *Handler {
return &Handler{
storage: storage,
telegram: telegram,
yandex: yandex,
startTime: startTime,
}
}
// HandleCommand обрабатывает входящую команду.
func (h *Handler) HandleCommand(ctx context.Context, message *tgbotapi.Message) {
command := message.Command()
args := message.CommandArguments()
slog.Info("Handling admin command", "user_id", message.From.ID, "command", command, "args", args)
switch command {
case "help":
h.handleHelp(ctx, message.Chat.ID)
case "stats":
h.handleStats(ctx, message.Chat.ID)
case "find":
h.handleFind(ctx, message.Chat.ID, args)
case "warm":
h.handleWarm(ctx, message.Chat.ID, args)
default:
if err := h.telegram.SendMessage(ctx, message.Chat.ID, "Неизвестная команда. Используйте /help для списка команд."); err != nil {
slog.Error("Failed to send 'unknown command' message", "error", err, "chat_id", message.Chat.ID)
}
}
}
func (h *Handler) handleHelp(ctx context.Context, chatID int64) {
helpText := "Команды администратора:\n" +
"/help - Показать это сообщение\n" +
"/stats - Показать статистику бота\n" +
"/find <yandex_track_id> - Найти трек в кэше по ID\n" +
"/warm <URL> - \"Прогреть\" кэш для альбома или исполнителя (в разработке)\n" +
"/warm --from-dir <path> - Прогреть кэш из локальной директории внутри контейнера"
if err := h.telegram.SendMessage(ctx, chatID, helpText); err != nil {
slog.Error("Failed to send help message", "error", err, "chat_id", chatID)
}
}
func (h *Handler) handleStats(ctx context.Context, chatID int64) {
cachedTracks, err := h.storage.Count(ctx)
if err != nil {
slog.Error("Failed to get stats from storage", "error", err)
if err := h.telegram.SendMessage(ctx, chatID, "Не удалось получить статистику из хранилища."); err != nil {
slog.Error("Failed to send stats error message", "error", err, "chat_id", chatID)
}
return
}
uptime := time.Since(h.startTime).Round(time.Second)
statsText := fmt.Sprintf(
"📊 Статистика бота\n\n"+
"Время работы: %s\n"+
"Треков в кэше: %d",
uptime,
cachedTracks,
)
if err := h.telegram.SendMessage(ctx, chatID, statsText); err != nil {
slog.Error("Failed to send stats message", "error", err, "chat_id", chatID)
}
}
func (h *Handler) handleFind(ctx context.Context, chatID int64, trackID string) {
if trackID == "" {
if err := h.telegram.SendMessage(ctx, chatID, "Пожалуйста, укажите Yandex Track ID. Пример: /find 123456"); err != nil {
slog.Error("Failed to send 'missing args' message for /find", "error", err, "chat_id", chatID)
}
return
}
fileID, err := h.storage.Get(ctx, trackID)
if err != nil {
msg := fmt.Sprintf("Трек с ID `%s` не найден в кэше.", trackID)
if err := h.telegram.SendMessage(ctx, chatID, msg); err != nil {
slog.Error("Failed to send 'track not found' message for /find", "error", err, "chat_id", chatID)
}
return
}
msg := fmt.Sprintf("Трек найден! Telegram File ID: `%s`", fileID)
if err := h.telegram.SendMessage(ctx, chatID, msg); err != nil {
slog.Error("Failed to send 'track found' message for /find", "error", err, "chat_id", chatID)
}
}
func (h *Handler) handleWarm(ctx context.Context, chatID int64, args string) {
const fromDirPrefix = "--from-dir "
if strings.HasPrefix(args, fromDirPrefix) {
dirPath := strings.TrimPrefix(args, fromDirPrefix)
h.handleWarmFromDir(ctx, chatID, dirPath)
return
}
// Здесь будет логика для прогрева по URL
if err := h.telegram.SendMessage(ctx, chatID, "Прогрев по URL находится в разработке."); err != nil {
slog.Error("Failed to send 'warm in development' message", "error", err, "chat_id", chatID)
}
}
// handleWarmFromDir запускает фоновую задачу прогрева кэша из локальной директории.
func (h *Handler) handleWarmFromDir(ctx context.Context, chatID int64, dirPath string) {
msg := fmt.Sprintf("Принято в обработку. Начинаю прогрев кэша из директории: `%s`", dirPath)
if err := h.telegram.SendMessage(ctx, chatID, msg); err != nil {
slog.Error("Failed to send 'warm from dir started' message", "error", err, "chat_id", chatID)
return
}
go func() {
slog.Info("Starting cache warming from directory", "path", dirPath)
files, err := os.ReadDir(dirPath)
if err != nil {
slog.Error("Failed to read directory for warming", "path", dirPath, "error", err)
errMsg := fmt.Sprintf("Ошибка: не удалось прочитать директорию `%s`. Убедитесь, что она существует и доступна.", dirPath)
_ = h.telegram.SendMessage(context.Background(), chatID, errMsg)
return
}
var addedCount, skippedCount, errorCount int
totalFiles := len(files)
for i, file := range files {
if file.IsDir() || !strings.HasSuffix(file.Name(), ".mp3") {
continue
}
trackID := strings.TrimSuffix(file.Name(), ".mp3")
fullPath := filepath.Join(dirPath, file.Name())
// 1. Проверяем, есть ли трек в кэше
_, err := h.storage.Get(ctx, trackID)
if err == nil {
slog.Debug("Skipping already cached track", "track_id", trackID)
skippedCount++
continue
}
// 2. Загружаем в Telegram
// Поскольку метатеги уже вшиты, для отображения в кэш-канале можно использовать простые title/performer
slog.Debug("Uploading track to cache channel", "track_id", trackID, "path", fullPath)
fileID, err := h.telegram.SendAudioToCacheChannel(ctx, fullPath, trackID, "Pre-cached")
if err != nil {
slog.Error("Failed to upload pre-cached file", "track_id", trackID, "error", err)
errorCount++
continue
}
// 3. Сохраняем в БД
err = h.storage.Set(ctx, trackID, fileID)
if err != nil {
slog.Error("Failed to save pre-cached file to storage", "track_id", trackID, "error", err)
errorCount++
continue
}
addedCount++
slog.Info("Successfully cached track from local file", "track_id", trackID, "file_id", fileID)
// Опционально: отправляем прогресс каждые N файлов
if (i+1)%1000 == 0 {
progressMsg := fmt.Sprintf("Прогресс: обработано %d из %d файлов...", i+1, totalFiles)
_ = h.telegram.SendMessage(context.Background(), chatID, progressMsg)
}
}
finalMsg := fmt.Sprintf(
"✅ Прогрев кэша из директории `%s` завершен.\n\n"+
"Новых треков добавлено: %d\n"+
"Треков пропущено (уже в кэше): %d\n"+
"Ошибок при обработке: %d",
dirPath, addedCount, skippedCount, errorCount,
)
_ = h.telegram.SendMessage(context.Background(), chatID, finalMsg)
slog.Info("Finished cache warming from directory", "path", dirPath, "added", addedCount, "skipped", skippedCount, "errors", errorCount)
}()
}