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 }