Создана структура документации, описывающая функциональность, установку, использование CLI, архитектуру и интеграции с TorrAPI и OpenRouter. Добавлены примеры конфигурации и метаданных, а также описание структуры выходных данных.
8.4 KiB
Алгоритм конвейера
Обработка одной книги проходит через несколько последовательных шагов внутри функции 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()