// 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]) + "…" }