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

183 lines
8.4 KiB
Markdown
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.
# Алгоритм конвейера
Обработка одной книги проходит через несколько последовательных шагов внутри функции `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()
```