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