Функция: реализованы консольный логгер и презентер для обработки аудиокниг
- Добавлен ConsoleLogger для подробного логирования этапов обработки аудиокниг в консоли. - Введен ConsolePresenter для форматированного вывода результатов сканирования в консоль. - Создан ProcessAudioBooksUseCase для обработки полного конвейера обработки аудиокниг, включая сканирование папок, извлечение метаданных, поиск торрентов и запись результатов. - Реализована проверка LLM для улучшения метаданных. - Добавлена обработка ошибок и логирование на всех этапах обработки.
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user