implemennted downloader
This commit is contained in:
1
go.mod
1
go.mod
@@ -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
2
go.sum
@@ -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
103
internal/processor/track.go
Normal 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
|
||||
}
|
||||
64
pkg/downloader/downloader.go
Normal file
64
pkg/downloader/downloader.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user