1
Algorithm
wiki-sync-bot edited this page 2026-02-23 14:28:35 +03:00
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()