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

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

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

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

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

214 lines
6.9 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 main — точка входа. Собирает зависимости (Composition Root)
// и запускает полный конвейер обработки аудиокниг.
package main
import (
"context"
"flag"
"fmt"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"github.com/fofanov/genaudiobookinfo/internal/domain"
"github.com/fofanov/genaudiobookinfo/internal/infrastructure"
"github.com/fofanov/genaudiobookinfo/internal/presentation"
"github.com/fofanov/genaudiobookinfo/internal/usecase"
"gopkg.in/yaml.v3"
)
var version = "2.0.0"
type appConfig struct {
Dir struct {
In string `yaml:"in"`
Out string `yaml:"out"`
} `yaml:"dir"`
InDir string `yaml:"in_dir"`
OutDir string `yaml:"out_dir"`
TorrAPI struct {
URL string `yaml:"url"`
} `yaml:"torrapi"`
OpenRouter struct {
APIKey string `yaml:"api_key"`
BaseURL string `yaml:"base_url"`
Timeout time.Duration `yaml:"timeout"`
Model string `yaml:"model"`
Prompt string `yaml:"prompt"`
MaxRetries int `yaml:"max_retries"`
RetryBackoff time.Duration `yaml:"retry_backoff"`
RetryBackoffMax time.Duration `yaml:"retry_backoff_max"`
} `yaml:"openrouter"`
}
func main() {
// Устанавливаем UTF-8 кодировку для консоли Windows (исправляет кракозябры с кириллицей)
infrastructure.SetConsoleUTF8()
// --- CLI-флаги ---
workers := flag.Int("workers", 2, "количество параллельных воркеров")
timeout := flag.Duration("timeout", 5*time.Minute, "таймаут обработки")
apiURL := flag.String("api", "", "адрес TorrAPI сервера (переопределяет config.yaml)")
resultDir := flag.String("result", "", "каталог для результатов (по умолчанию: <входная папка>/result)")
showVersion := flag.Bool("version", false, "показать версию")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Использование: %s [опции] [путь к каталогу с аудиокнигами]\n\n", os.Args[0])
fmt.Fprintln(os.Stderr, "Сканирует каталог, находит аудиокниги, обогащает данными с трекеров,")
fmt.Fprintln(os.Stderr, "создаёт metadata.json и переносит файлы в структурированные папки.")
fmt.Fprintln(os.Stderr, "Если путь не передан, используется dir.in из config.yaml.")
fmt.Fprintln(os.Stderr, "Опции:")
flag.PrintDefaults()
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Пример:")
fmt.Fprintf(os.Stderr, " %s -workers 4 -api http://localhost:9200 D:\\Audiobooks\n", os.Args[0])
}
flag.Parse()
cfg := loadConfig("config.yaml")
if *showVersion {
fmt.Printf("GenAudioBookInfo v%s\n", version)
os.Exit(0)
}
rootPath := ""
if flag.NArg() >= 1 {
rootPath = flag.Arg(0)
} else {
rootPath = cfg.Dir.In
if rootPath == "" {
rootPath = cfg.InDir
}
}
if rootPath == "" {
flag.Usage()
fmt.Fprintln(os.Stderr, "Ошибка: не задан входной каталог (укажите аргумент или dir.in в config.yaml).")
os.Exit(1)
}
effectiveAPIURL := cfg.TorrAPI.URL
if effectiveAPIURL == "" {
effectiveAPIURL = "http://localhost:9200"
}
if *apiURL != "" {
effectiveAPIURL = *apiURL
}
// Папка результатов
resultRoot := *resultDir
if resultRoot == "" {
resultRoot = cfg.Dir.Out
if resultRoot == "" {
resultRoot = cfg.OutDir
}
if resultRoot == "" {
resultRoot = filepath.Join(rootPath, "result")
} else {
resultRoot = filepath.Clean(resultRoot)
}
}
// --- Context с отменой по сигналу (graceful shutdown) ---
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
defer cancel()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigCh
fmt.Fprintln(os.Stderr, "\nПрерывание... завершаем работу.")
cancel()
}()
// --- Composition Root: сборка зависимостей (DI без фреймворка) ---
lister := infrastructure.NewFSFolderLister()
extractor := infrastructure.NewTagMetadataExtractor()
searcher := infrastructure.NewTorrAPIClient(effectiveAPIURL)
writer := infrastructure.NewFSResultWriter()
downloader := infrastructure.NewHTTPCoverDownloader()
presenter := presentation.NewConsolePresenter()
// Инициализация OpenRouter клиента (если указан API ключ)
var llmClient domain.LLMClient
if cfg.OpenRouter.APIKey != "" {
orClient := infrastructure.NewOpenRouterClient(infrastructure.OpenRouterConfig{
APIKey: cfg.OpenRouter.APIKey,
BaseURL: cfg.OpenRouter.BaseURL,
Timeout: cfg.OpenRouter.Timeout,
Model: cfg.OpenRouter.Model,
Prompt: cfg.OpenRouter.Prompt,
MaxRetries: cfg.OpenRouter.MaxRetries,
RetryBackoff: cfg.OpenRouter.RetryBackoff,
RetryBackoffMax: cfg.OpenRouter.RetryBackoffMax,
})
llmClient = orClient
modelName := cfg.OpenRouter.Model
if modelName == "" {
modelName = "openai/gpt-3.5-turbo"
}
fmt.Printf("OpenRouter: включен (модель: %s)\n", modelName)
} else {
fmt.Printf("OpenRouter: выключен (не указан API ключ)\n")
}
// --- Выполнение ---
fmt.Printf("Сканирование: %s\n", rootPath)
fmt.Printf("TorrAPI: %s\n", effectiveAPIURL)
fmt.Printf("Результат: %s\n", resultRoot)
fmt.Printf("Воркеры: %d\n\n", *workers)
start := time.Now()
// Получаем список папок для прогресс-бара
folders, err := lister.ListSubfolders(ctx, rootPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Ошибка при сканировании папок: %v\n", err)
os.Exit(1)
}
if len(folders) == 0 {
fmt.Println("Аудиокниги не найдены.")
os.Exit(0)
}
fmt.Printf("Найдено аудиокниг: %d\n\n", len(folders))
// Создаём логгер с прогресс-баром
logger := presentation.NewConsoleLoggerWithProgress(len(folders))
defer logger.Finish()
processUC := usecase.NewProcessAudioBooksUseCase(
lister, extractor, searcher, writer, downloader, llmClient, logger, *workers,
)
results, err := processUC.ExecuteForFolders(ctx, folders, resultRoot)
if err != nil {
fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
os.Exit(1)
}
presenter.RenderProcessResults(results)
fmt.Printf("\nВремя выполнения: %s\n", time.Since(start).Round(time.Millisecond))
}
func loadConfig(path string) appConfig {
data, err := os.ReadFile(path)
if err != nil {
return appConfig{}
}
var cfg appConfig
if err := yaml.Unmarshal(data, &cfg); err != nil {
return appConfig{}
}
return cfg
}