Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1852c5025e | |||
| afacaf4f14 |
@@ -1,3 +1,5 @@
|
|||||||
|
// yamusic-bot/cmd/bot/main.go
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -15,6 +17,7 @@ import (
|
|||||||
"gitea.mrixs.me/Mrixs/yamusic-bot/pkg/tagger"
|
"gitea.mrixs.me/Mrixs/yamusic-bot/pkg/tagger"
|
||||||
"gitea.mrixs.me/Mrixs/yamusic-bot/pkg/yamusic"
|
"gitea.mrixs.me/Mrixs/yamusic-bot/pkg/yamusic"
|
||||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -54,11 +57,24 @@ func main() {
|
|||||||
// 4. Инициализация компонентов
|
// 4. Инициализация компонентов
|
||||||
downloaderComponent := downloader.NewHTTPDownloader()
|
downloaderComponent := downloader.NewHTTPDownloader()
|
||||||
taggerComponent := tagger.NewID3Tagger()
|
taggerComponent := tagger.NewID3Tagger()
|
||||||
telegramClient := bot.NewTelegramClientAdapter(tgAPI, cfg.TelegramCacheChatID)
|
|
||||||
|
// Создаем БЫСТРЫЙ лимитер для общих вызовов API (в секунду)
|
||||||
|
fastLimiter := rate.NewLimiter(rate.Limit(cfg.TelegramAPIRateLimit), cfg.TelegramAPIRateLimit)
|
||||||
|
slog.Info("General Telegram API rate limit set", "requests_per_second", cfg.TelegramAPIRateLimit)
|
||||||
|
|
||||||
|
// Создаем МЕДЛЕННЫЙ лимитер для кэш-канала (в минуту)
|
||||||
|
// rate.Limit измеряется в событиях/секунду, поэтому конвертируем
|
||||||
|
// Burst size = 1, чтобы избежать отправки пачки сообщений и долгого ожидания
|
||||||
|
cacheRPS := float64(cfg.TelegramCacheRateLimitPerMinute) / 60.0
|
||||||
|
cacheLimiter := rate.NewLimiter(rate.Limit(cacheRPS), 1)
|
||||||
|
slog.Info("Cache channel Telegram API rate limit set", "requests_per_minute", cfg.TelegramCacheRateLimitPerMinute)
|
||||||
|
|
||||||
|
// Передаем оба лимитера в адаптер
|
||||||
|
telegramClient := bot.NewTelegramClientAdapter(tgAPI, cfg.TelegramCacheChatID, fastLimiter, cacheLimiter)
|
||||||
|
|
||||||
trackProcessor := processor.NewTrackProcessor(db, yandexClient, downloaderComponent, taggerComponent, telegramClient)
|
trackProcessor := processor.NewTrackProcessor(db, yandexClient, downloaderComponent, taggerComponent, telegramClient)
|
||||||
|
|
||||||
adminHandler := admin.NewHandler(db, telegramClient, yandexClient, startTime)
|
adminHandler := admin.NewHandler(db, telegramClient, yandexClient, taggerComponent, startTime)
|
||||||
inlineHandler := bot.NewInlineHandler(yandexClient, trackProcessor, telegramClient)
|
inlineHandler := bot.NewInlineHandler(yandexClient, trackProcessor, telegramClient)
|
||||||
|
|
||||||
// 5. Создание и запуск приложения
|
// 5. Создание и запуск приложения
|
||||||
|
|||||||
@@ -18,15 +18,17 @@ type Handler struct {
|
|||||||
storage interfaces.TrackStorage
|
storage interfaces.TrackStorage
|
||||||
telegram interfaces.TelegramClient
|
telegram interfaces.TelegramClient
|
||||||
yandex interfaces.YandexMusicClient
|
yandex interfaces.YandexMusicClient
|
||||||
|
tagger interfaces.Tagger
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler создает новый обработчик команд администратора.
|
// NewHandler создает новый обработчик команд администратора.
|
||||||
func NewHandler(storage interfaces.TrackStorage, telegram interfaces.TelegramClient, yandex interfaces.YandexMusicClient, startTime time.Time) *Handler {
|
func NewHandler(storage interfaces.TrackStorage, telegram interfaces.TelegramClient, yandex interfaces.YandexMusicClient, tagger interfaces.Tagger, startTime time.Time) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
telegram: telegram,
|
telegram: telegram,
|
||||||
yandex: yandex,
|
yandex: yandex,
|
||||||
|
tagger: tagger,
|
||||||
startTime: startTime,
|
startTime: startTime,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,17 +167,24 @@ func (h *Handler) handleWarmFromDir(ctx context.Context, chatID int64, dirPath s
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Загружаем в Telegram
|
// 2. Читаем метатеги из файла
|
||||||
// Поскольку метатеги уже вшиты, для отображения в кэш-канале можно использовать простые title/performer
|
title, artist, err := h.tagger.ReadMetadata(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("Failed to read metadata from file, using fallback", "path", fullPath, "error", err)
|
||||||
|
title = "" // Используем ID как заголовок
|
||||||
|
artist = "" // Используем заглушку как исполнителя
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Загружаем в Telegram с корректными метаданными
|
||||||
slog.Debug("Uploading track to cache channel", "track_id", trackID, "path", fullPath)
|
slog.Debug("Uploading track to cache channel", "track_id", trackID, "path", fullPath)
|
||||||
fileID, err := h.telegram.SendAudioToCacheChannel(ctx, fullPath, trackID, "Pre-cached")
|
fileID, err := h.telegram.SendAudioToCacheChannel(ctx, fullPath, title, artist)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to upload pre-cached file", "track_id", trackID, "error", err)
|
slog.Error("Failed to upload pre-cached file", "track_id", trackID, "error", err)
|
||||||
errorCount++
|
errorCount++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Сохраняем в БД
|
// 4. Сохраняем в БД
|
||||||
err = h.storage.Set(ctx, trackID, fileID)
|
err = h.storage.Set(ctx, trackID, fileID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to save pre-cached file to storage", "track_id", trackID, "error", err)
|
slog.Error("Failed to save pre-cached file to storage", "track_id", trackID, "error", err)
|
||||||
|
|||||||
@@ -6,24 +6,35 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TelegramClientAdapter адаптирует библиотеку tgbotapi под наш интерфейс interfaces.TelegramClient.
|
// TelegramClientAdapter адаптирует библиотеку tgbotapi под наш интерфейс interfaces.TelegramClient.
|
||||||
type TelegramClientAdapter struct {
|
type TelegramClientAdapter struct {
|
||||||
api *tgbotapi.BotAPI
|
api *tgbotapi.BotAPI
|
||||||
cacheChatID int64
|
cacheChatID int64
|
||||||
|
fastLimiter *rate.Limiter // Для общих быстрых запросов
|
||||||
|
cacheLimiter *rate.Limiter // Для медленных запросов в кэш-канал
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTelegramClientAdapter создает новый адаптер.
|
// NewTelegramClientAdapter создает новый адаптер.
|
||||||
func NewTelegramClientAdapter(api *tgbotapi.BotAPI, cacheChatID int64) *TelegramClientAdapter {
|
func NewTelegramClientAdapter(api *tgbotapi.BotAPI, cacheChatID int64, fastLimiter, cacheLimiter *rate.Limiter) *TelegramClientAdapter {
|
||||||
return &TelegramClientAdapter{
|
return &TelegramClientAdapter{
|
||||||
api: api,
|
api: api,
|
||||||
cacheChatID: cacheChatID,
|
cacheChatID: cacheChatID,
|
||||||
|
fastLimiter: fastLimiter,
|
||||||
|
cacheLimiter: cacheLimiter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendAudioToCacheChannel загружает аудиофайл в кэш-канал и возвращает его FileID.
|
// SendAudioToCacheChannel загружает аудиофайл в кэш-канал и возвращает его FileID.
|
||||||
|
// ИСПОЛЬЗУЕТ МЕДЛЕННЫЙ ЛИМИТЕР.
|
||||||
func (t *TelegramClientAdapter) SendAudioToCacheChannel(ctx context.Context, audioPath, title, performer string) (string, error) {
|
func (t *TelegramClientAdapter) SendAudioToCacheChannel(ctx context.Context, audioPath, title, performer string) (string, error) {
|
||||||
|
// Ждем, пока МЕДЛЕННЫЙ лимитер разрешит выполнить запрос
|
||||||
|
if err := t.cacheLimiter.Wait(ctx); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
audio := tgbotapi.NewAudio(t.cacheChatID, tgbotapi.FilePath(audioPath))
|
audio := tgbotapi.NewAudio(t.cacheChatID, tgbotapi.FilePath(audioPath))
|
||||||
audio.Title = title
|
audio.Title = title
|
||||||
audio.Performer = performer
|
audio.Performer = performer
|
||||||
@@ -41,11 +52,17 @@ func (t *TelegramClientAdapter) SendAudioToCacheChannel(ctx context.Context, aud
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AnswerInlineQuery отвечает на inline-запрос.
|
// AnswerInlineQuery отвечает на inline-запрос.
|
||||||
|
// ИСПОЛЬЗУЕТ БЫСТРЫЙ ЛИМИТЕР.
|
||||||
func (t *TelegramClientAdapter) AnswerInlineQuery(ctx context.Context, queryID string, results []interface{}) error {
|
func (t *TelegramClientAdapter) AnswerInlineQuery(ctx context.Context, queryID string, results []interface{}) error {
|
||||||
|
// Ждем, пока БЫСТРЫЙ лимитер разрешит выполнить запрос
|
||||||
|
if err := t.fastLimiter.Wait(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
inlineConfig := tgbotapi.InlineConfig{
|
inlineConfig := tgbotapi.InlineConfig{
|
||||||
InlineQueryID: queryID,
|
InlineQueryID: queryID,
|
||||||
Results: results,
|
Results: results,
|
||||||
CacheTime: 1, // Кэшируем результат на стороне Telegram на 1 секунду
|
CacheTime: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := t.api.Request(inlineConfig); err != nil {
|
if _, err := t.api.Request(inlineConfig); err != nil {
|
||||||
@@ -55,7 +72,13 @@ func (t *TelegramClientAdapter) AnswerInlineQuery(ctx context.Context, queryID s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SendMessage отправляет текстовое сообщение.
|
// SendMessage отправляет текстовое сообщение.
|
||||||
|
// ИСПОЛЬЗУЕТ БЫСТРЫЙ ЛИМИТЕР.
|
||||||
func (t *TelegramClientAdapter) SendMessage(ctx context.Context, chatID int64, text string) error {
|
func (t *TelegramClientAdapter) SendMessage(ctx context.Context, chatID int64, text string) error {
|
||||||
|
// Ждем, пока БЫСТРЫЙ лимитер разрешит выполнить запрос
|
||||||
|
if err := t.fastLimiter.Wait(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
msg := tgbotapi.NewMessage(chatID, text)
|
msg := tgbotapi.NewMessage(chatID, text)
|
||||||
if _, err := t.api.Send(msg); err != nil {
|
if _, err := t.api.Send(msg); err != nil {
|
||||||
return fmt.Errorf("failed to send message: %w", err)
|
return fmt.Errorf("failed to send message: %w", err)
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ type Config struct {
|
|||||||
LogLevel string `env:"LOG_LEVEL" envDefault:"info"`
|
LogLevel string `env:"LOG_LEVEL" envDefault:"info"`
|
||||||
ProcessorWorkers int `env:"PROCESSOR_WORKERS" envDefault:"4"`
|
ProcessorWorkers int `env:"PROCESSOR_WORKERS" envDefault:"4"`
|
||||||
YandexAPIRateLimit int `env:"YANDEX_API_RATE_LIMIT" envDefault:"5"`
|
YandexAPIRateLimit int `env:"YANDEX_API_RATE_LIMIT" envDefault:"5"`
|
||||||
TelegramAdminIDs []int64 `env:"-"` // Это поле будет заполнено после парсинга
|
TelegramAPIRateLimit int `env:"TELEGRAM_API_RATE_LIMIT" envDefault:"25"` // Общий лимит в секунду
|
||||||
|
TelegramCacheRateLimitPerMinute int `env:"TELEGRAM_CACHE_RATE_LIMIT_PER_MINUTE" envDefault:"20"` // Лимит для кэш-канала в минуту
|
||||||
|
TelegramAdminIDs []int64 `env:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// New загружает конфигурацию из переменных окружения и парсит необходимые поля.
|
// New загружает конфигурацию из переменных окружения и парсит необходимые поля.
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ type TelegramClient interface {
|
|||||||
// Tagger определяет методы для работы с метаданными аудиофайлов.
|
// Tagger определяет методы для работы с метаданными аудиофайлов.
|
||||||
type Tagger interface {
|
type Tagger interface {
|
||||||
WriteTags(filePath string, coverPath string, info *model.TrackInfo) error
|
WriteTags(filePath string, coverPath string, info *model.TrackInfo) error
|
||||||
|
ReadMetadata(filePath string) (title, artist string, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileDownloader определяет метод для скачивания файла.
|
// FileDownloader определяет метод для скачивания файла.
|
||||||
|
|||||||
@@ -17,6 +17,24 @@ func NewID3Tagger() *ID3Tagger {
|
|||||||
return &ID3Tagger{}
|
return &ID3Tagger{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadMetadata читает основные метаданные (название, исполнитель) из аудиофайла.
|
||||||
|
func (t *ID3Tagger) ReadMetadata(filePath string) (string, string, error) {
|
||||||
|
tag, err := id3v2.Open(filePath, id3v2.Options{Parse: true})
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("failed to open mp3 file for reading tags: %w", err)
|
||||||
|
}
|
||||||
|
defer tag.Close()
|
||||||
|
|
||||||
|
title := tag.Title()
|
||||||
|
artist := tag.Artist()
|
||||||
|
|
||||||
|
if title == "" || artist == "" {
|
||||||
|
return "", "", fmt.Errorf("title or artist tag is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return title, artist, nil
|
||||||
|
}
|
||||||
|
|
||||||
// WriteTags записывает метаданные и обложку в указанный аудиофайл.
|
// WriteTags записывает метаданные и обложку в указанный аудиофайл.
|
||||||
func (t *ID3Tagger) WriteTags(filePath string, coverPath string, info *model.TrackInfo) error {
|
func (t *ID3Tagger) WriteTags(filePath string, coverPath string, info *model.TrackInfo) error {
|
||||||
tag, err := id3v2.Open(filePath, id3v2.Options{Parse: true})
|
tag, err := id3v2.Open(filePath, id3v2.Options{Parse: true})
|
||||||
|
|||||||
Reference in New Issue
Block a user