implemennted downloader

This commit is contained in:
2025-06-23 09:45:42 +03:00
parent 4db033dc7d
commit 610fb3da11
4 changed files with 170 additions and 0 deletions

1
go.mod
View File

@@ -5,6 +5,7 @@ go 1.24
require (
github.com/bogem/id3v2 v1.2.0
github.com/caarlos0/env/v10 v10.0.0
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/mattn/go-sqlite3 v1.14.28
github.com/oapi-codegen/runtime v1.1.1
)

2
go.sum
View File

@@ -9,6 +9,8 @@ github.com/caarlos0/env/v10 v10.0.0/go.mod h1:ZfulV76NvVPw3tm591U4SwL3Xx9ldzBP9a
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=

103
internal/processor/track.go Normal file
View File

@@ -0,0 +1,103 @@
package processor
import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"gitea.mrixs.me/Mrixs/yamusic-bot/internal/interfaces"
"gitea.mrixs.me/Mrixs/yamusic-bot/internal/model"
"gitea.mrixs.me/Mrixs/yamusic-bot/internal/storage"
)
// TrackProcessor инкапсулирует всю логику обработки одного трека.
type TrackProcessor struct {
storage interfaces.TrackStorage
yandex interfaces.YandexMusicClient
downloader interfaces.FileDownloader
tagger interfaces.Tagger
telegram interfaces.TelegramClient
}
// NewTrackProcessor создает новый процессор.
func NewTrackProcessor(
storage interfaces.TrackStorage,
yandex interfaces.YandexMusicClient,
downloader interfaces.FileDownloader,
tagger interfaces.Tagger,
telegram interfaces.TelegramClient,
) *TrackProcessor {
return &TrackProcessor{
storage: storage,
yandex: yandex,
downloader: downloader,
tagger: tagger,
telegram: telegram,
}
}
// Process получает информацию о треке, обрабатывает его (если нужно) и возвращает Telegram File ID.
func (p *TrackProcessor) Process(ctx context.Context, trackInfo *model.TrackInfo) (string, error) {
const op = "processor.TrackProcessor.Process"
// 1. Проверяем кэш в БД
fileID, err := p.storage.Get(ctx, trackInfo.YandexTrackID)
if err == nil {
slog.Info("Cache hit", "track_id", trackInfo.YandexTrackID, "title", trackInfo.Title)
return fileID, nil
}
// Если ошибка - это не "не найдено", то это проблема
if !errors.Is(err, storage.ErrNotFound) {
return "", fmt.Errorf("%s: failed to get from storage: %w", op, err)
}
// 2. Cache Miss: начинаем полный цикл обработки
slog.Info("Cache miss, processing track", "track_id", trackInfo.YandexTrackID, "title", trackInfo.Title)
// 2a. Получаем URL для скачивания
downloadURL, err := p.yandex.GetDownloadURL(ctx, trackInfo.YandexTrackID)
if err != nil {
return "", fmt.Errorf("%s: failed to get download url: %w", op, err)
}
trackInfo.DownloadURL = downloadURL
// 2b. Скачиваем аудиофайл и обложку
audioPath, err := p.downloader.Download(ctx, trackInfo.DownloadURL)
if err != nil {
return "", fmt.Errorf("%s: failed to download audio: %w", op, err)
}
defer os.Remove(audioPath) // Гарантированное удаление временного аудиофайла
var coverPath string
if trackInfo.CoverURL != "" {
coverPath, err = p.downloader.Download(ctx, trackInfo.CoverURL)
if err != nil {
slog.Warn("Failed to download cover, proceeding without it", "url", trackInfo.CoverURL, "error", err)
} else {
defer os.Remove(coverPath) // Гарантированное удаление временной обложки
}
}
// 2c. Записываем теги
if err := p.tagger.WriteTags(audioPath, coverPath, trackInfo); err != nil {
// Не фатальная ошибка, просто логируем и продолжаем
slog.Warn("Failed to write tags, proceeding without them", "track_id", trackInfo.YandexTrackID, "error", err)
}
// 2d. Загружаем в Telegram (в кэш-канал)
newFileID, err := p.telegram.SendAudioToCacheChannel(ctx, audioPath, trackInfo.Title, trackInfo.Artist)
if err != nil {
return "", fmt.Errorf("%s: failed to upload to telegram: %w", op, err)
}
// 2e. Сохраняем в нашу БД
if err := p.storage.Set(ctx, trackInfo.YandexTrackID, newFileID); err != nil {
// Не фатальная ошибка для пользователя, но критичная для нас. Логируем как ошибку.
slog.Error("Failed to save track to cache storage", "track_id", trackInfo.YandexTrackID, "error", err)
}
return newFileID, nil
}

View File

@@ -0,0 +1,64 @@
package downloader
import (
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
)
// HTTPDownloader реализует интерфейс interfaces.FileDownloader.
type HTTPDownloader struct {
client *http.Client
}
// NewHTTPDownloader создает новый экземпляр загрузчика.
func NewHTTPDownloader() *HTTPDownloader {
return &HTTPDownloader{
client: &http.Client{},
}
}
// Download скачивает файл по URL и сохраняет его во временный файл.
// Возвращает путь к скачанному файлу.
func (d *HTTPDownloader) Download(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return "", fmt.Errorf("failed to create download request: %w", err)
}
resp, err := d.client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to execute download request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("bad status code on download: %s", resp.Status)
}
// Создаем временный файл с правильным расширением
tmpFile, err := os.CreateTemp("", "track-*.tmp")
if err != nil {
return "", fmt.Errorf("failed to create temp file: %w", err)
}
defer tmpFile.Close()
_, err = io.Copy(tmpFile, resp.Body)
if err != nil {
// Если произошла ошибка, удаляем временный файл
os.Remove(tmpFile.Name())
return "", fmt.Errorf("failed to write to temp file: %w", err)
}
// Переименовываем файл, чтобы убрать .tmp расширение (не обязательно, но красиво)
finalPath := strings.TrimSuffix(tmpFile.Name(), filepath.Ext(tmpFile.Name()))
if err := os.Rename(tmpFile.Name(), finalPath); err != nil {
return "", fmt.Errorf("failed to rename temp file: %w", err)
}
return finalPath, nil
}