- Добавлен ConsoleLogger для подробного логирования этапов обработки аудиокниг в консоли. - Введен ConsolePresenter для форматированного вывода результатов сканирования в консоль. - Создан ProcessAudioBooksUseCase для обработки полного конвейера обработки аудиокниг, включая сканирование папок, извлечение метаданных, поиск торрентов и запись результатов. - Реализована проверка LLM для улучшения метаданных. - Добавлена обработка ошибок и логирование на всех этапах обработки.
195 lines
5.4 KiB
Go
195 lines
5.4 KiB
Go
// Package presentation реализует порт Presenter —
|
||
// форматированный вывод результатов сканирования в консоль.
|
||
package presentation
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
"text/tabwriter"
|
||
|
||
"github.com/fofanov/genaudiobookinfo/internal/domain"
|
||
)
|
||
|
||
// ConsolePresenter реализует domain.Presenter для вывода в stdout.
|
||
type ConsolePresenter struct{}
|
||
|
||
// NewConsolePresenter создаёт новый экземпляр.
|
||
func NewConsolePresenter() *ConsolePresenter {
|
||
return &ConsolePresenter{}
|
||
}
|
||
|
||
// RenderResults выводит результаты первичного сканирования.
|
||
func (p *ConsolePresenter) RenderResults(results []domain.ScanResult) {
|
||
if len(results) == 0 {
|
||
fmt.Println("Аудиокниги не найдены.")
|
||
return
|
||
}
|
||
|
||
successCount := 0
|
||
errorCount := 0
|
||
|
||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||
|
||
for i, res := range results {
|
||
if res.Err != nil {
|
||
errorCount++
|
||
continue
|
||
}
|
||
successCount++
|
||
|
||
info := res.Info
|
||
fmt.Fprintf(w, "%s\n", strings.Repeat("─", 70))
|
||
fmt.Fprintf(w, " #%d\n", i+1)
|
||
fmt.Fprintf(w, " Папка:\t%s\n", info.FolderPath)
|
||
fmt.Fprintf(w, " Название:\t%s\n", info.Title)
|
||
|
||
if info.Author != "" {
|
||
fmt.Fprintf(w, " Автор:\t%s\n", info.Author)
|
||
}
|
||
if info.Album != "" {
|
||
fmt.Fprintf(w, " Альбом:\t%s\n", info.Album)
|
||
}
|
||
if info.Genre != "" {
|
||
fmt.Fprintf(w, " Жанр:\t%s\n", info.Genre)
|
||
}
|
||
if info.Year != 0 {
|
||
fmt.Fprintf(w, " Год:\t%d\n", info.Year)
|
||
}
|
||
if info.Comment != "" {
|
||
fmt.Fprintf(w, " Описание:\t%s\n", truncate(info.Comment, 200))
|
||
}
|
||
|
||
fmt.Fprintf(w, " Формат:\t%s\n", info.Format)
|
||
fmt.Fprintf(w, " Файл-источник:\t%s\n", info.SourceFile)
|
||
fmt.Fprintf(w, " Файлов:\t%d\n", info.FilesCount)
|
||
|
||
coverStatus := "нет"
|
||
if info.CoverFound {
|
||
coverStatus = "да"
|
||
}
|
||
fmt.Fprintf(w, " Обложка:\t%s\n", coverStatus)
|
||
}
|
||
|
||
fmt.Fprintf(w, "%s\n", strings.Repeat("─", 70))
|
||
w.Flush()
|
||
|
||
fmt.Printf("\nИтого: найдено %d аудиокниг", successCount)
|
||
if errorCount > 0 {
|
||
fmt.Printf(", ошибок: %d", errorCount)
|
||
}
|
||
fmt.Println()
|
||
|
||
if errorCount > 0 {
|
||
fmt.Println("\nОшибки:")
|
||
for _, res := range results {
|
||
if res.Err != nil {
|
||
fmt.Printf(" ⚠ %v\n", res.Err)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// RenderProcessResults выводит результаты полной обработки (с данными трекеров).
|
||
func (p *ConsolePresenter) RenderProcessResults(results []domain.ProcessResult) {
|
||
if len(results) == 0 {
|
||
fmt.Println("Нет результатов обработки.")
|
||
return
|
||
}
|
||
|
||
successCount := 0
|
||
errorCount := 0
|
||
partialCount := 0
|
||
|
||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||
|
||
for i, res := range results {
|
||
fmt.Fprintf(w, "%s\n", strings.Repeat("═", 80))
|
||
|
||
if res.Err != nil && res.Book == nil {
|
||
errorCount++
|
||
fmt.Fprintf(w, " #%d ✗ ОШИБКА: %v\n", i+1, res.Err)
|
||
continue
|
||
}
|
||
|
||
book := res.Book
|
||
info := book.AudioBook
|
||
|
||
if res.Err != nil {
|
||
partialCount++
|
||
fmt.Fprintf(w, " #%d ⚠ ЧАСТИЧНО\n", i+1)
|
||
} else {
|
||
successCount++
|
||
fmt.Fprintf(w, " #%d ✓ ОБРАБОТАНО\n", i+1)
|
||
}
|
||
|
||
// Базовая информация из тегов
|
||
fmt.Fprintf(w, " Исходная папка:\t%s\n", info.FolderPath)
|
||
fmt.Fprintf(w, " Название:\t%s\n", info.Title)
|
||
if info.Author != "" {
|
||
fmt.Fprintf(w, " Автор:\t%s\n", info.Author)
|
||
}
|
||
if info.Album != "" {
|
||
fmt.Fprintf(w, " Альбом:\t%s\n", info.Album)
|
||
}
|
||
if info.Year != 0 {
|
||
fmt.Fprintf(w, " Год:\t%d\n", info.Year)
|
||
}
|
||
fmt.Fprintf(w, " Файлов:\t%d\n", info.FilesCount)
|
||
fmt.Fprintf(w, " Формат:\t%s\n", info.Format)
|
||
|
||
// Данные трекера
|
||
if book.Detail != nil {
|
||
fmt.Fprintf(w, "\n --- Данные трекера (%s) ---\n", book.TrackerName)
|
||
fmt.Fprintf(w, " Название:\t%s\n", book.Detail.Name)
|
||
if book.Detail.Description != "" {
|
||
fmt.Fprintf(w, " Описание:\t%s\n", truncate(book.Detail.Description, 300))
|
||
}
|
||
if book.Detail.Type != "" {
|
||
fmt.Fprintf(w, " Жанр:\t%s\n", book.Detail.Type)
|
||
}
|
||
fmt.Fprintf(w, " URL:\t%s\n", book.Detail.URL)
|
||
if book.Detail.Poster != "" {
|
||
fmt.Fprintf(w, " Постер:\t%s\n", book.Detail.Poster)
|
||
}
|
||
if book.Detail.Hash != "" {
|
||
fmt.Fprintf(w, " Hash:\t%s\n", book.Detail.Hash)
|
||
}
|
||
} else {
|
||
fmt.Fprintf(w, "\n --- Трекер: данные не найдены ---\n")
|
||
}
|
||
|
||
// Результат
|
||
if book.DestFolder != "" {
|
||
fmt.Fprintf(w, "\n Результат:\t%s\n", book.DestFolder)
|
||
}
|
||
if book.ErrorMessage != "" {
|
||
fmt.Fprintf(w, " Предупреждение:\t%s\n", book.ErrorMessage)
|
||
}
|
||
if res.Err != nil {
|
||
fmt.Fprintf(w, " Ошибка:\t%v\n", res.Err)
|
||
}
|
||
}
|
||
|
||
fmt.Fprintf(w, "%s\n", strings.Repeat("═", 80))
|
||
w.Flush()
|
||
|
||
fmt.Printf("\nИтого: успешно %d", successCount)
|
||
if partialCount > 0 {
|
||
fmt.Printf(", частично %d", partialCount)
|
||
}
|
||
if errorCount > 0 {
|
||
fmt.Printf(", ошибок %d", errorCount)
|
||
}
|
||
fmt.Printf(" (всего %d)\n", len(results))
|
||
}
|
||
|
||
// truncate обрезает строку до maxLen рун, добавляя "…".
|
||
func truncate(s string, maxLen int) string {
|
||
runes := []rune(s)
|
||
if len(runes) <= maxLen {
|
||
return s
|
||
}
|
||
return string(runes[:maxLen]) + "…"
|
||
}
|