Files
Dmitriy Fofanov 41fb62f62e Добавлены страницы вики для GenAudioBookInfo: Home, Installation, Makefile, OpenRouter, Output Structure, TorrAPI и Sidebar.
Создана структура документации, описывающая функциональность, установку, использование CLI, архитектуру и интеграции с TorrAPI и OpenRouter.
Добавлены примеры конфигурации и метаданных, а также описание структуры выходных данных.
2026-02-23 13:19:39 +03:00

8.4 KiB
Raw Permalink Blame History

Алгоритм конвейера

Обработка одной книги проходит через несколько последовательных шагов внутри функции processOneBook.
Все книги обрабатываются параллельно в Worker Pool (Fan-Out / Fan-In).


Высокоуровневая схема

  Входная папка
       │
       ▼
┌─────────────────────────────────────────┐
│  Шаг 1: Сканирование подпапок           │
│  FolderLister.ListSubfolders()          │
└─────────────────────────────────────────┘
       │
       ▼ (список папок → channel)
┌─────────────────────────────────────────┐
│  Fan-Out: N воркеров (Worker Pool)      │
│  каждый воркер → processOneBook()       │
└─────────────────────────────────────────┘
       │
       ▼
┌─────────────────────────────────────────┐
│  Шаг 2: Извлечение метаданных           │
│  MetadataExtractor.Extract()            │
│  + nameparser (из имени папки)          │
└─────────────────────────────────────────┘
       │
       ▼
┌─────────────────────────────────────────┐
│  Шаг 3: Поиск на трекерах               │
│  (до N попыток с паузой)                │
│  TorrentSearcher.Search()               │
│  + TorrentSearcher.GetDetail()          │
│    ├─ Найдено → Шаг 4                   │
│    └─ Не найдено → moveToErrorFolder()  │
└─────────────────────────────────────────┘
       │
       ▼
┌─────────────────────────────────────────┐
│  Шаг 4: LLM нормализация (опционально) │
│  LLMClient.NormalizeMetadata()          │
│  (исправляет автора и название)         │
└─────────────────────────────────────────┘
       │
       ▼
┌─────────────────────────────────────────┐
│  Шаг 5: Запись результата               │
│  ResultWriter.WriteResult()             │
│  + CoverDownloader.Download()           │
│    ├─ Новая папка → result/<Б>/<Авт>/   │
│    └─ Дубликат → DUPLICATE/<Б>/<Авт>/  │
└─────────────────────────────────────────┘
       │
       ▼
  Fan-In: сбор ProcessResult

Шаг 1 — Сканирование папок

FSFolderLister обходит переданный корневой каталог и возвращает список непосредственных подпапок.
Папки result, ERROR, DUPLICATE пропускаются автоматически (их имена совпадают с именами системных выходных папок).


Шаг 2 — Извлечение метаданных

TagMetadataExtractor находит первый аудиофайл в папке и читает теги:

  • ID3v1/ID3v2 для .mp3
  • Vorbis Comment для .ogg, .flac, .opus
  • MP4 atoms для .m4b, .m4a, .aac

Параллельноnameparser разбирает имя папки по шаблонам:

  • Фамилия И.О. - Название
  • Автор — Название
  • [Серия] Название - Автор

Приоритет: теги из файла > nameparser > пустые строки.


Шаг 3 — Поиск на трекерах (с ретраями)

для попытки := 1..SearchRetries:
    результаты = TorrAPI.Search(title)
    если результаты пусты и title != author:
        результаты = TorrAPI.Search(author)
    если найдено:
        detail = TorrAPI.GetDetail(лучший_результат)
        выйти из цикла
    иначе:
        подождать SearchRetryDelay (по умолчанию 3s)

если после всех попыток ничего не найдено:
    записать _error.txt с описанием
    переместить папку в result/ERROR/<имя_папки>/
    вернуть ProcessResult{Status: "error"}

Приоритет трекеров: Rutracker → Rutor → Kinozal → остальные.

Параллелизм запросов к TorrAPI ограничен семафором searchSem (buffer = search_concurrency).


Шаг 4 — LLM нормализация (опционально)

Если задан openrouter.api_key, отправляется промпт:

Входные данные: {raw_author}, {raw_title}
Ожидаемый ответ: {"author": "Фамилия Имя", "title": "Название"}

LLM-ответ применяется только если оба поля непусты.
При ошибке API книга обрабатывается с исходными тегами (не блокирует конвейер).


Шаг 5 — Запись результата

FSResultWriter создаёт структуру:

result/
  <первая_буква_автора>/
    <Автор>/
      <Автор> — <Название> [<Год>]/
        metadata.json
        cover.jpg            (если найдена обложка)
        <аудиофайлы...>

Проверка на DUPLICATE

Перед созданием папки проверяется, не существует ли уже destDir.
Если существует — книга идёт в:

result/DUPLICATE/<первая_буква>/<Автор>/<Название>/
result/DUPLICATE/<первая_буква>/<Автор>/<Название>_2/   # если уже есть
result/DUPLICATE/<первая_буква>/<Автор>/<Название>_3/   # и т.д.

Обработка ошибок

Ситуация Действие
Папка не найдена на трекерах после N попыток result/ERROR/<имя>/ + _error.txt
Папка с таким именем уже существует в результатах result/DUPLICATE/.../ с суффиксом _2, _3...
Ошибка LLM API Лог WARN, обработка продолжается с исходными тегами
Ошибка скачивания обложки Лог WARN, книга сохраняется без cover.jpg
Отмена контекста (Ctrl+C / таймаут) Текущий воркер завершается, остальные — graceful stop

Параллелизм

main goroutine          processing goroutine
     │                         │
     │  go ExecuteForFolders()  │
     │──────────────────────────►│
     │                         │ Fan-Out: N воркеров
     │  tuiLog.Run(cancel)     │──(jobs channel)──►│worker1│
     │  (блокирует main)       │                   │worker2│
     │                         │                   │worker3│
     │                         │ Fan-In: results channel
     │                         │◄─────────────────────────
     │    outcomeCh ◄──────────│
     │◄──────────────────────────
     │  presenter.RenderResults()