package usecases import ( "fmt" "path/filepath" "audio-catalyst/internal/domain/entities" "audio-catalyst/internal/domain/repositories" "audio-catalyst/internal/domain/services" ) // ProcessAudioBooksUseCase обрабатывает аудиокниги type ProcessAudioBooksUseCase struct { audioBookRepo repositories.AudioBookRepository rutrackerRepo repositories.RuTrackerRepository logger repositories.Logger audioBookSvc *services.AudioBookService metadataSvc *services.MetadataService // Репортер прогресса progress func(entities.ProcessingStatus) } // NewProcessAudioBooksUseCase создает новый use case func NewProcessAudioBooksUseCase( audioBookRepo repositories.AudioBookRepository, rutrackerRepo repositories.RuTrackerRepository, logger repositories.Logger, ) *ProcessAudioBooksUseCase { return &ProcessAudioBooksUseCase{ audioBookRepo: audioBookRepo, rutrackerRepo: rutrackerRepo, logger: logger, audioBookSvc: services.NewAudioBookService(), metadataSvc: services.NewMetadataService(), } } // SetProgressReporter задает функцию репортинга прогресса func (uc *ProcessAudioBooksUseCase) SetProgressReporter(f func(entities.ProcessingStatus)) { uc.progress = f } // Execute выполняет обработку аудиокниг func (uc *ProcessAudioBooksUseCase) Execute(config *entities.Config) error { uc.logger.Info("🚀 Начало процесса обработки аудиокниг") uc.logger.Info("📁 Исходная директория: %s", config.Scanner.SourceDirectory) // Сканирование папок с аудиокнигами audioBooks, err := uc.audioBookRepo.ScanDirectory(config.Scanner.SourceDirectory) if err != nil { uc.logger.Error("❌ Критическая ошибка сканирования: %v", err) if uc.progress != nil { uc.progress(entities.ProcessingStatus{Current: 0, Total: 0, Status: "Ошибка сканирования", Error: err}) } return fmt.Errorf("ошибка сканирования: %w", err) } total := len(audioBooks) uc.logger.Info("📊 Найдено %d аудиокниг", total) if total == 0 { uc.logger.Warning("⚠️ Аудиокниги не найдены") if uc.progress != nil { uc.progress(entities.ProcessingStatus{Current: 0, Total: 0, Status: "Книги не найдены"}) } return nil } // Сообщаем общее количество до начала обработки if uc.progress != nil { uc.progress(entities.ProcessingStatus{Current: 0, Total: total, Status: "Начинаем обработку"}) } // Авторизация в RuTracker if err := uc.rutrackerRepo.Login(); err != nil { uc.logger.Error("❌ Ошибка авторизации в RuTracker: %v", err) if uc.progress != nil { uc.progress(entities.ProcessingStatus{Current: 0, Total: total, Status: "Ошибка авторизации", Error: err}) } return fmt.Errorf("ошибка авторизации: %w", err) } uc.logger.Success("✅ Авторизация в RuTracker успешна") // Обработка каждой аудиокниги for i, book := range audioBooks { uc.logger.Info("📋 Обработка книги %d/%d: \"%s\"", i+1, total, book.Title) if err := uc.processAudioBook(book); err != nil { uc.logger.Error("❌ Ошибка обработки книги \"%s\": %v", book.Title, err) } // Увеличиваем прогресс после завершения обработки книги (успешно или нет) if uc.progress != nil { uc.progress(entities.ProcessingStatus{Current: i + 1, Total: total, Status: fmt.Sprintf("Готово %d/%d", i+1, total)}) } uc.logger.Success("✅ Книга \"%s\" обработана", book.Title) } if uc.progress != nil { uc.progress(entities.ProcessingStatus{Current: total, Total: total, Status: "Обработка завершена"}) } uc.logger.Success("🎉 Обработка завершена успешно!") return nil } // processAudioBook обрабатывает одну аудиокнигу func (uc *ProcessAudioBooksUseCase) processAudioBook(book entities.AudioBook) error { // Поиск торрентов torrents, err := uc.rutrackerRepo.Search(book.Title, 1) if err != nil { uc.logger.Warning("⚠️ Ошибка поиска торрентов для \"%s\": %v", book.Title, err) return nil // переходим к следующей книге } if len(torrents) == 0 { uc.logger.Warning("⚠️ Не найдено на RuTracker: \"%s\" — пропускаем", book.Title) return nil // переходим к следующей книге } // Используем первый торрент bestTorrent := torrents[0] uc.logger.Info("📦 Выбран торрент: %s", bestTorrent.Title) // Получаем метаданные rutrackerResult, err := uc.rutrackerRepo.GetTopicMetadata(bestTorrent.ID) if err != nil { uc.logger.Warning("⚠️ Ошибка получения метаданных: %v", err) return nil // переходим к следующей книге } if rutrackerResult.CoverURL != "" { uc.logger.Info("🖼️ Найдена обложка: %s", rutrackerResult.CoverURL) } else { uc.logger.Debug("Обложка на странице не найдена") } // Создаем метаданные metadata := uc.createMetadataFromRuTracker(book, rutrackerResult) // Переименуем папку книги под Subtitle, если он есть bookPath := book.Path if metadata.Subtitle != "" && metadata.Subtitle != filepath.Base(book.Path) { if newPath, err := uc.audioBookRepo.RenameBookFolder(book.Path, metadata.Subtitle); err != nil { uc.logger.Warning("⚠️ Не удалось переименовать папку: %v", err) } else { uc.logger.Success("📁 Папка переименована: %s", filepath.Base(newPath)) bookPath = newPath } } // Если есть CoverURL — пробуем скачать обложку if rutrackerResult.CoverURL != "" { if err := uc.audioBookRepo.DownloadCover(rutrackerResult.CoverURL, bookPath); err != nil { uc.logger.Warning("⚠️ Не удалось загрузить обложку: %v", err) } else { uc.logger.Success("🖼️ Обложка загружена") } } // Сохраняем метаданные if err := uc.audioBookRepo.SaveMetadata(bookPath, metadata); err != nil { return fmt.Errorf("ошибка сохранения метаданных: %w", err) } uc.logger.Success("💾 Метаданные сохранены") // Организация в библиотеке organized if len(metadata.Authors) > 0 { author := metadata.Authors[0] targetRoot := "./organized" if newPath, err := uc.audioBookRepo.OrganizeBookFolder(bookPath, author, targetRoot); err != nil { uc.logger.Warning("⚠️ Не удалось организовать папку: %v", err) } else { uc.logger.Success("📚 Папка перемещена в библиотеку: %s", newPath) } } return nil } // createBasicMetadata создает базовые метаданные func (uc *ProcessAudioBooksUseCase) createBasicMetadata(book entities.AudioBook) error { metadata := &entities.AudioBookMetadata{ Tags: []string{}, Chapters: uc.audioBookSvc.CreateChapters(book.MP3Files), Title: book.Title, Subtitle: book.Title, Authors: []string{}, Narrators: []string{}, Series: []string{}, Genres: []string{}, Description: book.Description, Language: "ru", Explicit: false, Abridged: false, } return uc.audioBookRepo.SaveMetadata(book.Path, metadata) } // createMetadataFromRuTracker создает метаданные на основе данных RuTracker func (uc *ProcessAudioBooksUseCase) createMetadataFromRuTracker(book entities.AudioBook, rutrackerResult *entities.RuTrackerResult) *entities.AudioBookMetadata { metadata := &entities.AudioBookMetadata{ Tags: []string{}, Chapters: uc.audioBookSvc.CreateChapters(book.MP3Files), Title: rutrackerResult.Title, Subtitle: rutrackerResult.Subtitle, Authors: rutrackerResult.Authors, Narrators: rutrackerResult.Narrators, Series: rutrackerResult.Series, Genres: rutrackerResult.Genres, Description: rutrackerResult.Description, Language: "ru", Explicit: false, Abridged: false, } if rutrackerResult.Year != nil { metadata.PublishedYear = rutrackerResult.Year } if rutrackerResult.Publisher != nil { metadata.Publisher = rutrackerResult.Publisher } if metadata.Title == "" { metadata.Title = book.Title } if metadata.Subtitle == "" { metadata.Subtitle = metadata.Title } if metadata.Description == "" && book.Description != "" { metadata.Description = book.Description } return metadata }