Files
GenAudioBookInfo/internal/presentation/console_presenter.go
Dmitriy Fofanov 402ce7f4f1 Функция: реализованы консольный логгер и презентер для обработки аудиокниг
- Добавлен ConsoleLogger для подробного логирования этапов обработки аудиокниг в консоли.

- Введен ConsolePresenter для форматированного вывода результатов сканирования в консоль.

- Создан ProcessAudioBooksUseCase для обработки полного конвейера обработки аудиокниг, включая сканирование папок, извлечение метаданных, поиск торрентов и запись результатов.

- Реализована проверка LLM для улучшения метаданных.

- Добавлена ​​обработка ошибок и логирование на всех этапах обработки.
2026-02-20 00:35:43 +03:00

195 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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]) + "…"
}