docs(wiki): sync from main repo
+182
@@ -0,0 +1,182 @@
|
||||
# Алгоритм конвейера
|
||||
|
||||
Обработка одной книги проходит через несколько последовательных шагов внутри функции `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()
|
||||
```
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
# Архитектура
|
||||
|
||||
GenAudioBookInfo построен по принципам **Чистой архитектуры** (Clean Architecture / Hexagonal Architecture).
|
||||
Бизнес-логика не зависит от внешних библиотек, баз данных или UI-фреймворков.
|
||||
|
||||
---
|
||||
|
||||
## Слои
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ cmd/genaudiobookinfo/main.go (Composition Root / DI) │
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ presentation/ TUI, ConsoleLogger, Presenter │
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ usecase/ Бизнес-логика. Нет зависимостей │
|
||||
│ от infra, только интерфейсы │
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ domain/ Сущности + интерфейсы (порты) │
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ infrastructure/ Адаптеры: FS, HTTP, теги, YAML │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Зависимости направлены **внутрь**: `main → usecase → domain ← infrastructure`.
|
||||
|
||||
---
|
||||
|
||||
## Структура папок
|
||||
|
||||
```
|
||||
genaudiobookinfo/
|
||||
├── cmd/
|
||||
│ └── genaudiobookinfo/
|
||||
│ └── main.go # Точка входа, DI, TUI запуск
|
||||
├── internal/
|
||||
│ ├── domain/
|
||||
│ │ ├── audiobook.go # Агрегат AudioBookInfo, EnrichedBookInfo
|
||||
│ │ ├── llm.go # Интерфейс LLMClient
|
||||
│ │ ├── ports.go # Все порты: FolderLister, MetadataExtractor,
|
||||
│ │ │ # TorrentSearcher, ResultWriter, CoverDownloader,
|
||||
│ │ │ # ProcessLogger, ProcessResult
|
||||
│ │ └── torrent.go # Сущности TorrentInfo, TorrentDetail
|
||||
│ ├── usecase/
|
||||
│ │ └── scan_audiobooks.go # Конвейер (Pipeline + Fan-Out/Fan-In)
|
||||
│ ├── infrastructure/ # Адаптеры (реализации портов)
|
||||
│ │ ├── folder_lister.go # FSFolderLister
|
||||
│ │ ├── metadata_extractor.go # TagMetadataExtractor (dhowden/tag)
|
||||
│ │ ├── torrapi_client.go # TorrAPIClient (HTTP)
|
||||
│ │ ├── result_writer.go # FSResultWriter (создание папок, JSON, копирование)
|
||||
│ │ ├── cover_downloader.go # HTTPCoverDownloader
|
||||
│ │ ├── openrouter_client.go # OpenRouterClient (LLM через REST)
|
||||
│ │ ├── audio_utils.go # Утилиты: длительность, формат, список файлов
|
||||
│ │ ├── console_windows.go # SetConsoleUTF8 для Windows
|
||||
│ │ └── console_other.go # Заглушка для non-Windows
|
||||
│ ├── nameparser/
|
||||
│ │ └── parser.go # Разбор "Автор - Название" из имени папки
|
||||
│ └── presentation/
|
||||
│ ├── tui_logger.go # TUILogger (Bubbletea, Dracula)
|
||||
│ ├── console_logger.go # ConsoleLogger (fallback без TUI)
|
||||
│ └── console_presenter.go # Финальная сводка в stdout
|
||||
├── config.yaml
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
├── Makefile
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Интерфейсы (порты)
|
||||
|
||||
Определены в `internal/domain/ports.go`:
|
||||
|
||||
| Интерфейс | Назначение | Адаптер |
|
||||
|---|---|---|
|
||||
| `FolderLister` | Перечислить подпапки | `FSFolderLister` |
|
||||
| `MetadataExtractor` | Извлечь теги из аудиофайла | `TagMetadataExtractor` |
|
||||
| `TorrentSearcher` | Поиск + детали раздачи | `TorrAPIClient` |
|
||||
| `ResultWriter` | Записать результат на диск | `FSResultWriter` |
|
||||
| `CoverDownloader` | Скачать обложку | `HTTPCoverDownloader` |
|
||||
| `LLMClient` | Нормализовать автора/название | `OpenRouterClient` |
|
||||
| `ProcessLogger` | Отчитываться о прогрессе | `TUILogger` / `ConsoleLogger` |
|
||||
|
||||
---
|
||||
|
||||
## Паттерны
|
||||
|
||||
| Паттерн | Где применён |
|
||||
|---|---|
|
||||
| Pipeline | `ExecuteForFolders` → `processOneBook` → writer |
|
||||
| Fan-Out / Fan-In | Worker Pool в `ExecuteForFolders` |
|
||||
| Semaphore | `searchSem` — ограничение параллелизма к TorrAPI |
|
||||
| Dependency Injection | `main.go` — Composition Root без фреймворка |
|
||||
| Repository / Port-Adapter | Все `infrastructure/` адаптеры |
|
||||
| Strategy | `ProcessLogger` — TUI или Console выбирается в `main.go` |
|
||||
| Graceful Shutdown | `context.WithTimeout` + `signal.Notify` (SIGINT, SIGTERM) |
|
||||
|
||||
---
|
||||
|
||||
## TUI (Bubbletea)
|
||||
|
||||
`presentation/tui_logger.go` реализует полноэкранный интерфейс:
|
||||
|
||||
- **Верхняя панель**: текущая книга, статус, прогресс-бар `N / Total`, спиннер, последние 5 событий
|
||||
- **Нижняя панель**: полный лог с цветами (Dracula palette)
|
||||
|
||||
Цветовая схема:
|
||||
|
||||
| Тип | Цвет |
|
||||
|---|---|
|
||||
| INFO | `#00FFFF` cyan |
|
||||
| WARN | `#FFFF00` yellow |
|
||||
| ERROR | `#FF5555` red |
|
||||
| OK | `#50FA7B` green |
|
||||
| DEBUG | `#6272A4` gray |
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
# Использование CLI
|
||||
|
||||
## Синтаксис
|
||||
|
||||
```
|
||||
genaudiobookinfo [опции] [путь к каталогу с аудиокнигами]
|
||||
```
|
||||
|
||||
Если путь не передан, используется `dir.in` из `config.yaml`.
|
||||
|
||||
---
|
||||
|
||||
## Флаги
|
||||
|
||||
| Флаг | Тип | По умолчанию | Описание |
|
||||
|---|---|---|---|
|
||||
| `-workers N` | int | `0` (из config) | Количество параллельных воркеров. Переопределяет `processing.workers`. |
|
||||
| `-timeout T` | duration | `0` (из config) | Таймаут всей сессии. Формат: `5m`, `1h30m`. Переопределяет `processing.timeout`. |
|
||||
| `-api URL` | string | из config | URL TorrAPI сервера. Переопределяет `torrapi.url`. |
|
||||
| `-result DIR` | string | `<вход>/result` | Папка для результатов. Переопределяет `dir.out`. |
|
||||
| `-version` | flag | — | Вывести версию и выйти. |
|
||||
|
||||
### Приоритет параметров
|
||||
|
||||
```
|
||||
CLI-флаг > config.yaml > встроенные defaults
|
||||
```
|
||||
|
||||
Флаги `-workers` и `-timeout` со значением `0` (умолчание) не перетирают конфиг.
|
||||
|
||||
---
|
||||
|
||||
## Примеры
|
||||
|
||||
### Минимальный запуск (всё из config.yaml)
|
||||
|
||||
```bash
|
||||
./genaudiobookinfo
|
||||
```
|
||||
|
||||
### Указать входной каталог явно
|
||||
|
||||
```bash
|
||||
./genaudiobookinfo D:\Audiobooks
|
||||
```
|
||||
|
||||
### Переопределить количество воркеров
|
||||
|
||||
```bash
|
||||
./genaudiobookinfo -workers 6 D:\Audiobooks
|
||||
```
|
||||
|
||||
### Другой сервер TorrAPI
|
||||
|
||||
```bash
|
||||
./genaudiobookinfo -api http://192.168.1.10:9200 D:\Audiobooks
|
||||
```
|
||||
|
||||
### Указать нестандартную папку результатов
|
||||
|
||||
```bash
|
||||
./genaudiobookinfo -result E:\ProcessedBooks D:\Audiobooks
|
||||
```
|
||||
|
||||
### Полный набор параметров
|
||||
|
||||
```bash
|
||||
./genaudiobookinfo \
|
||||
-workers 4 \
|
||||
-timeout 30m \
|
||||
-api http://localhost:9200 \
|
||||
-result D:\Books\result \
|
||||
D:\Books\Incoming
|
||||
```
|
||||
|
||||
### Проверить версию
|
||||
|
||||
```bash
|
||||
./genaudiobookinfo -version
|
||||
# GenAudioBookInfo v2.0.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
| Переменная | Описание |
|
||||
|---|---|
|
||||
| `OPENROUTER_API_KEY` | API ключ OpenRouter. Используется если `openrouter.api_key` в config.yaml пуст. |
|
||||
|
||||
```bash
|
||||
export OPENROUTER_API_KEY=sk-or-v1-xxxx
|
||||
./genaudiobookinfo D:\Audiobooks
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Управление в TUI
|
||||
|
||||
Во время обработки работает полноэкранный TUI:
|
||||
|
||||
| Клавиша | Действие |
|
||||
|---|---|
|
||||
| `q` | Мягкое завершение (graceful stop, ждёт текущий воркер) |
|
||||
| `Ctrl+C` | Прерывание (cancel context) |
|
||||
|
||||
---
|
||||
|
||||
## Сигналы ОС
|
||||
|
||||
| Сигнал | Действие |
|
||||
|---|---|
|
||||
| `SIGINT` (Ctrl+C) | Отмена контекста → воркеры завершают текущую книгу |
|
||||
| `SIGTERM` | То же |
|
||||
|
||||
---
|
||||
|
||||
## Код выхода
|
||||
|
||||
| Код | Значение |
|
||||
|---|---|
|
||||
| `0` | Успешное завершение |
|
||||
| `1` | Ошибка запуска (нет входного каталога, ошибка сканирования) |
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
# Конфигурация (`.env`)
|
||||
|
||||
Все параметры хранятся в файле `.env` в корне проекта.
|
||||
Копируется из `.env.example` при первой настройке. Файл `.env` не коммитится в Git.
|
||||
Параметры командной строки (где применимо) **переопределяют** значения из `.env`.
|
||||
|
||||
---
|
||||
|
||||
## Пути
|
||||
|
||||
```env
|
||||
DIR_IN=D:\Audiobooks
|
||||
DIR_OUT=D:\Audiobooks\result
|
||||
```
|
||||
|
||||
| Переменная | Тип | По умолчанию | Описание |
|
||||
|---|---|---|---|
|
||||
| `DIR_IN` | string | — | Корневой каталог для сканирования. Можно переопределить аргументом CLI. |
|
||||
| `DIR_OUT` | string | `<DIR_IN>/result` | Куда складывать обработанные книги. Можно переопределить флагом `-result`. |
|
||||
|
||||
---
|
||||
|
||||
## TorrAPI сервер
|
||||
|
||||
```env
|
||||
TORRAPI_URL=http://localhost:9200
|
||||
```
|
||||
|
||||
| Переменная | Тип | По умолчанию | Описание |
|
||||
|---|---|---|---|
|
||||
| `TORRAPI_URL` | string | `http://localhost:9200` | URL TorrAPI-совместимого сервера. Переопределяется флагом `-api`. |
|
||||
|
||||
---
|
||||
|
||||
## Параметры конвейера
|
||||
|
||||
```env
|
||||
PROCESSING_WORKERS=2
|
||||
PROCESSING_TIMEOUT=5m
|
||||
PROCESSING_SEARCH_RETRIES=3
|
||||
PROCESSING_SEARCH_RETRY_DELAY=3s
|
||||
PROCESSING_SEARCH_CONCURRENCY=2
|
||||
```
|
||||
|
||||
| Переменная | Тип | По умолчанию | Описание |
|
||||
|---|---|---|---|
|
||||
| `PROCESSING_WORKERS` | int | `2` | Число параллельных горутин-воркеров (Fan-Out). Переопределяется `-workers`. |
|
||||
| `PROCESSING_TIMEOUT` | duration | `5m` | Дедлайн для всей сессии обработки. Переопределяется `-timeout`. |
|
||||
| `PROCESSING_SEARCH_RETRIES` | int | `3` | Сколько попыток найти книгу на трекерах перед перемещением в `ERROR/`. |
|
||||
| `PROCESSING_SEARCH_RETRY_DELAY` | duration | `3s` | Задержка между повторными попытками поиска. |
|
||||
| `PROCESSING_SEARCH_CONCURRENCY` | int | `2` | Ограничение числа одновременных HTTP-запросов к TorrAPI (семафор). |
|
||||
|
||||
### Приоритет параметров
|
||||
|
||||
```
|
||||
CLI-флаг (-workers/-timeout) > .env > встроенные defaults
|
||||
```
|
||||
|
||||
Флаги `-workers` и `-timeout` со значением `0` (умолчание) не перетирают значение из `.env`.
|
||||
|
||||
---
|
||||
|
||||
## OpenRouter LLM (опционально)
|
||||
|
||||
```env
|
||||
OPENROUTER_API_KEY=sk-or-v1-your-key
|
||||
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
|
||||
OPENROUTER_TIMEOUT=120s
|
||||
OPENROUTER_MODEL=openai/gpt-4o-mini
|
||||
OPENROUTER_MAX_RETRIES=3
|
||||
OPENROUTER_RETRY_BACKOFF=1s
|
||||
OPENROUTER_RETRY_BACKOFF_MAX=8s
|
||||
# OPENROUTER_PROMPT= # опционально, переопределяет встроенный промпт
|
||||
```
|
||||
|
||||
| Переменная | Тип | По умолчанию | Описание |
|
||||
|---|---|---|---|
|
||||
| `OPENROUTER_API_KEY` | string | `""` | API ключ. Если пуст — LLM выключен. Можно передать через переменную окружения ОС. |
|
||||
| `OPENROUTER_BASE_URL` | string | `https://openrouter.ai/api/v1` | Базовый URL API. |
|
||||
| `OPENROUTER_TIMEOUT` | duration | `120s` | Таймаут одного запроса к API. |
|
||||
| `OPENROUTER_MODEL` | string | `openai/gpt-3.5-turbo` | Идентификатор модели в формате `provider/model`. |
|
||||
| `OPENROUTER_MAX_RETRIES` | int | `3` | Количество ретраев при ошибках API. |
|
||||
| `OPENROUTER_RETRY_BACKOFF` | duration | `1s` | Начальная задержка между ретраями (экспоненциальный backoff). |
|
||||
| `OPENROUTER_RETRY_BACKOFF_MAX` | duration | `8s` | Максимальная задержка backoff. |
|
||||
| `OPENROUTER_PROMPT` | string | (встроенный) | Системный промпт. Если не задан — используется дефолтный из кода. |
|
||||
|
||||
### Ключ через переменную окружения ОС
|
||||
|
||||
`OPENROUTER_API_KEY` можно не писать в `.env` — достаточно задать в окружении ОС. Переменные ОС имеют приоритет над `.env`:
|
||||
|
||||
```bash
|
||||
export OPENROUTER_API_KEY=sk-or-...
|
||||
./genaudiobookinfo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Форматы продолжительностей
|
||||
|
||||
Go-синтаксис: `300ms`, `1.5s`, `2m30s`, `1h`.
|
||||
Примеры для `*_TIMEOUT`: `5m`, `10m`, `1h30m`.
|
||||
|
||||
---
|
||||
|
||||
## Полный пример `.env`
|
||||
|
||||
```env
|
||||
# Пути
|
||||
DIR_IN=D:\Audiobooks
|
||||
DIR_OUT=D:\Audiobooks\result
|
||||
|
||||
# TorrAPI
|
||||
TORRAPI_URL=http://localhost:9200
|
||||
|
||||
# Конвейер
|
||||
PROCESSING_WORKERS=4
|
||||
PROCESSING_TIMEOUT=15m
|
||||
PROCESSING_SEARCH_RETRIES=3
|
||||
PROCESSING_SEARCH_RETRY_DELAY=3s
|
||||
PROCESSING_SEARCH_CONCURRENCY=2
|
||||
|
||||
# OpenRouter
|
||||
OPENROUTER_API_KEY=sk-or-v1-xxxxxxxxxxxxxxxxxxxx
|
||||
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
|
||||
OPENROUTER_TIMEOUT=120s
|
||||
OPENROUTER_MODEL=openai/gpt-4o-mini
|
||||
OPENROUTER_MAX_RETRIES=3
|
||||
OPENROUTER_RETRY_BACKOFF=1s
|
||||
OPENROUTER_RETRY_BACKOFF_MAX=8s
|
||||
```
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
# GenAudioBookInfo
|
||||
|
||||
> Автоматический каталогизатор аудиокниг с обогащением метаданных из торрент-трекеров и LLM.
|
||||
|
||||
## Что делает инструмент
|
||||
|
||||
1. **Сканирует** каталог с аудиокнигами — находит все подпапки с аудиофайлами (mp3, m4b, ogg, flac, opus, aac и др.).
|
||||
2. **Извлекает метаданные** из аудиофайлов: автор, название, год, жанр, длительность через ID3/Vorbis теги.
|
||||
3. **Ищет книгу на трекерах** через [TorrAPI](TorrAPI) по имени папки и/или тегам — получает расширенное описание.
|
||||
4. **Опционально** уточняет автора и название через LLM ([OpenRouter](OpenRouter)).
|
||||
5. **Создаёт структуру** `result/<Буква>/<Автор>/<Автор — Книга [Год]>/` с `metadata.json` и обложкой.
|
||||
6. Папки, которые не удалось найти, уходят в `ERROR/`; дубликаты — в `DUPLICATE/`.
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
```bash
|
||||
# Сборка
|
||||
go build -o genaudiobookinfo ./cmd/genaudiobookinfo
|
||||
|
||||
# Первичная настройка
|
||||
cp .env.example .env
|
||||
# Отредактировать .env: задать DIR_IN, TORRAPI_URL, OPENROUTER_API_KEY
|
||||
|
||||
# Запуск (входная папка из DIR_IN в .env)
|
||||
./genaudiobookinfo
|
||||
|
||||
# Явное указание каталога
|
||||
./genaudiobookinfo D:\Audiobooks
|
||||
|
||||
# С переопределением параметров
|
||||
./genaudiobookinfo -workers 4 -timeout 10m D:\Audiobooks
|
||||
```
|
||||
|
||||
Полная документация по флагам: [[CLI-Usage]].
|
||||
|
||||
## Структура вики
|
||||
|
||||
| Страница | Содержание |
|
||||
|---|---|
|
||||
| [[Installation]] | Требования, сборка из исходников |
|
||||
| [[Configuration]] | Справочник `.env` (все переменные конфигурации) |
|
||||
| [[CLI-Usage]] | Флаги командной строки, примеры |
|
||||
| [[Architecture]] | Чистая архитектура, слои, паттерны |
|
||||
| [[Algorithm]] | Шаги конвейера, ретрай, ERROR, DUPLICATE |
|
||||
| [[Output-Structure]] | Структура `result/`, формат `metadata.json` |
|
||||
| [[TorrAPI]] | Интеграция с торрент-трекерами |
|
||||
| [[OpenRouter]] | LLM-интеграция для нормализации метаданных |
|
||||
|
||||
## Поддерживаемые форматы
|
||||
|
||||
`mp3` · `m4b` · `m4a` · `ogg` · `opus` · `flac` · `aac` · `wma` · `wav` · `aiff`
|
||||
|
||||
## Требования
|
||||
|
||||
- Go 1.22+
|
||||
- [TorrAPI](https://github.com/yourok/TorrServer) (локальный или удалённый сервер)
|
||||
- Ключ [OpenRouter](https://openrouter.ai/) — **опционально**, для нормализации автора/названия через LLM
|
||||
|
||||
## Ссылки
|
||||
|
||||
- Репозиторий: `https://github.dfv24.com/fofanov/genaudiobookinfo`
|
||||
- Документация OpenRouter: [[OpenRouter]]
|
||||
|
||||
Sync test 2026-02-23T14:27:00
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
# Установка и сборка
|
||||
|
||||
## Требования
|
||||
|
||||
| Компонент | Версия | Обязательно |
|
||||
|---|---|---|
|
||||
| Go | 1.22+ | ✅ |
|
||||
| TorrAPI-совместимый сервер | любая | ✅ |
|
||||
| OpenRouter API ключ | — | ❌ (только для LLM) |
|
||||
|
||||
## Сборка из исходников
|
||||
|
||||
```bash
|
||||
# Клонировать репозиторий
|
||||
git clone https://github.dfv24.com/fofanov/genaudiobookinfo.git
|
||||
cd genaudiobookinfo
|
||||
|
||||
# Загрузить зависимости
|
||||
go mod tidy
|
||||
|
||||
# Сборка
|
||||
go build -o genaudiobookinfo ./cmd/genaudiobookinfo
|
||||
|
||||
# Или через Makefile
|
||||
make build
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```powershell
|
||||
go build -o genaudiobookinfo.exe ./cmd/genaudiobookinfo
|
||||
```
|
||||
|
||||
## Зависимости
|
||||
|
||||
Все зависимости управляются через Go Modules. Основные:
|
||||
|
||||
| Модуль | Назначение |
|
||||
|---|---|
|
||||
| `charmbracelet/bubbletea` | TUI-фреймворк (интерактивный вывод) |
|
||||
| `charmbracelet/lipgloss` | Цветовое оформление терминала (Dracula scheme) |
|
||||
| `charmbracelet/bubbles` | Компоненты TUI: прогресс, спиннер |
|
||||
| `dhowden/tag` | Чтение ID3/Vorbis/MP4 тегов из аудиофайлов |
|
||||
| `tcolgate/mp3` | Расчёт длительности MP3 с VBR |
|
||||
| `schollz/progressbar/v3` | Прогресс-бар для ConsoleLogger |
|
||||
| `gopkg.in/yaml.v3` | ~~Парсинг `config.yaml`~~ удалён |
|
||||
|
||||
## Первичная настройка
|
||||
|
||||
1. Скопировать шаблон конфигурации:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. Изменить пути в `.env`:
|
||||
|
||||
```env
|
||||
DIR_IN=/path/to/audiobooks # входная папка
|
||||
DIR_OUT=/path/to/result # папка результатов (опционально)
|
||||
```
|
||||
|
||||
3. Настроить TorrAPI:
|
||||
|
||||
```env
|
||||
TORRAPI_URL=http://localhost:9200 # адрес TorrServer
|
||||
```
|
||||
|
||||
4. Запустить:
|
||||
|
||||
```bash
|
||||
./genaudiobookinfo
|
||||
```
|
||||
|
||||
## Проверка сборки
|
||||
|
||||
```bash
|
||||
go vet ./...
|
||||
go test ./...
|
||||
./genaudiobookinfo -version
|
||||
```
|
||||
|
||||
## Обновление
|
||||
|
||||
```bash
|
||||
git pull
|
||||
go mod tidy
|
||||
make build
|
||||
```
|
||||
+398
@@ -0,0 +1,398 @@
|
||||
# Makefile — описание команд
|
||||
|
||||
В проекте используется `Makefile` как единая точка входа для сборки, тестирования, линтинга и управления релизами. Файл поддерживает **Windows (PowerShell)** и **Unix (sh)** — ветвление происходит автоматически через `ifeq ($(OS),Windows_NT)`.
|
||||
|
||||
Команда по умолчанию (`make` без аргументов) показывает справку.
|
||||
|
||||
---
|
||||
|
||||
## Переменные конфигурации
|
||||
|
||||
| Переменная | Значение по умолчанию | Назначение |
|
||||
|---------------|-----------------------------------|----------------------------------------------------|
|
||||
| `APP_NAME` | `genaudiobookinfo` | Имя исполняемого файла |
|
||||
| `VERSION` | `2.0.0` | Версия, встраивается в бинарник через `-ldflags` |
|
||||
| `BUILD_DIR` | `build` | Директория для артефактов сборки |
|
||||
| `CMD_PATH` | `./cmd/genaudiobookinfo` | Путь к `main.go` |
|
||||
| `GO` | `go` | Команда Go-компилятора |
|
||||
| `GOFLAGS` | `-trimpath` | Флаги компилятора (убирает пути хоста из бинарника)|
|
||||
| `LDFLAGS` | `-s -w -X main.version=$(VERSION)`| Линковщик: strip debug, strip DWARF, встроить версию |
|
||||
| `OUTPUT_DIR` | `./result` | Директория с результатами работы программы |
|
||||
| `GITEA_URL` | `https://github.dfv24.com` | Адрес Gitea-инстанции |
|
||||
| `GITEA_REPO` | `fofanov/genaudiobookinfo` | Владелец/репозиторий в Gitea |
|
||||
| `GITEA_TOKEN` | *(пусто)* | Токен API Gitea (нужен только для `ci-release`) |
|
||||
|
||||
Переменные можно переопределять при вызове:
|
||||
|
||||
```bash
|
||||
make build VERSION=2.1.0
|
||||
make ci-release VERSION=2.1.0 GITEA_TOKEN=abc123
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Основные команды
|
||||
|
||||
### `make` / `make help`
|
||||
Вывод справки со списком всех доступных команд. Цель по умолчанию — не требует аргументов.
|
||||
|
||||
```bash
|
||||
make
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `make all`
|
||||
Последовательно выполняет `build` и `vet`. Удобен как первичная проверка после изменений.
|
||||
|
||||
```bash
|
||||
make all
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `make build`
|
||||
Компилирует бинарник для **текущей платформы** и помещает его в `build/`.
|
||||
|
||||
| ОС | Результирующий файл |
|
||||
|---------|----------------------------------|
|
||||
| Windows | `build/genaudiobookinfo.exe` |
|
||||
| Linux | `build/genaudiobookinfo` |
|
||||
| macOS | `build/genaudiobookinfo` |
|
||||
|
||||
Флаги компилятора: `-trimpath -ldflags "-s -w -X main.version=2.0.0"`.
|
||||
|
||||
```bash
|
||||
make build
|
||||
make build VERSION=2.1.0 # встроить другую версию
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `make build-windows-current`
|
||||
Компилирует `genaudiobookinfo.exe` (Windows/amd64) **в корневой директории проекта** (не в `build/`). Удобно для быстрой локальной проверки на Windows без отдельной папки.
|
||||
|
||||
```bash
|
||||
make build-windows-current
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `make run`
|
||||
Запускает программу напрямую через `go run` без предварительной компиляции с параметрами по умолчанию: `-workers 8 -timeout 60m`. Директории `DIR_IN` и `DIR_OUT` берутся из `.env`.
|
||||
|
||||
```bash
|
||||
make run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `make run-custom ARGS="..."`
|
||||
Запускает программу с произвольными CLI-флагами через переменную `ARGS`.
|
||||
|
||||
```bash
|
||||
make run-custom ARGS="-workers 4 -timeout 30m"
|
||||
make run-custom ARGS="-workers 2"
|
||||
```
|
||||
|
||||
> Полный список CLI-флагов: [[CLI-Usage|CLI-Usage]].
|
||||
|
||||
---
|
||||
|
||||
## Тестирование
|
||||
|
||||
### `make test`
|
||||
Запускает все unit-тесты с флагами `-v -race -count=1`.
|
||||
|
||||
- `-v` — podробный вывод
|
||||
- `-race` — детектор гонок (data race detector)
|
||||
- `-count=1` — отключает кэширование результатов тестов
|
||||
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `make test-cover`
|
||||
Запускает тесты с измерением покрытия кода и генерирует HTML-отчёт `coverage.html`.
|
||||
|
||||
```bash
|
||||
make test-cover
|
||||
# откройте coverage.html в браузере для просмотра отчёта
|
||||
```
|
||||
|
||||
Файлы:
|
||||
- `coverage.out` — сырые данные покрытия
|
||||
- `coverage.html` — визуальный отчёт (удаляется через `make clean`)
|
||||
|
||||
---
|
||||
|
||||
## Качество кода
|
||||
|
||||
### `make fmt`
|
||||
Форматирует весь Go-код согласно стандарту `gofmt`. Эквивалент `go fmt ./...`.
|
||||
|
||||
```bash
|
||||
make fmt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `make vet`
|
||||
Запускает статический анализ `go vet ./...`. Выявляет потенциальные ошибки: неправильный printf-формат, достижимые nil-dereference и т.п.
|
||||
|
||||
```bash
|
||||
make vet
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `make lint`
|
||||
Запускает `golangci-lint run ./...`. Если `golangci-lint` не установлен, выводит команду для установки.
|
||||
|
||||
```bash
|
||||
make lint
|
||||
# Установка линтера:
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `make deps`
|
||||
Загружает все зависимости модуля (`go mod download`). Полезно для первоначальной настройки или подготовки к офлайн-сборке.
|
||||
|
||||
```bash
|
||||
make deps
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `make tidy`
|
||||
Запускает `go mod tidy` — убирает неиспользуемые зависимости и обновляет `go.sum`.
|
||||
|
||||
```bash
|
||||
make tidy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Очистка
|
||||
|
||||
### `make clean`
|
||||
Удаляет артефакты сборки:
|
||||
- папку `build/`
|
||||
- файл `genaudiobookinfo.exe` в корне (от `build-windows-current`)
|
||||
- файлы `coverage.out` и `coverage.html`
|
||||
|
||||
```bash
|
||||
make clean
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `make clean-results`
|
||||
Удаляет и пересоздаёт директорию результатов (`./result/`). Используйте с осторожностью — удаляет все обработанные аудиокниги из выходной папки.
|
||||
|
||||
```bash
|
||||
make clean-results
|
||||
```
|
||||
|
||||
> Директория `OUTPUT_DIR` задаётся переменной Makefile (`./result` по умолчанию), но реальный путь при работе программы берётся из `DIR_OUT` в `.env`.
|
||||
|
||||
---
|
||||
|
||||
### `make clean-all`
|
||||
Выполняет `clean` + `clean-results` — полная очистка проекта.
|
||||
|
||||
```bash
|
||||
make clean-all
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Кросс-компиляция
|
||||
|
||||
### `make build-all`
|
||||
Компилирует бинарники для **всех 16 поддерживаемых платформ**. Сначала выполняет `clean`, затем `ensure-build-dir`, затем собирает каждую платформу последовательно. Все результаты помещаются в `build/`.
|
||||
|
||||
```bash
|
||||
make build-all
|
||||
make build-all VERSION=2.1.0
|
||||
```
|
||||
|
||||
Время выполнения: 1–3 минуты в зависимости от машины.
|
||||
|
||||
---
|
||||
|
||||
### Платформы и выходные файлы
|
||||
|
||||
| Команда make | GOOS | GOARCH | Доп. флаг | Файл |
|
||||
|-------------------------|---------|----------|---------------|---------------------------------------------|
|
||||
| `build-linux-amd64` | linux | amd64 | — | `genaudiobookinfo-linux-amd64` |
|
||||
| `build-linux-386` | linux | 386 | — | `genaudiobookinfo-linux-386` |
|
||||
| `build-linux-arm64` | linux | arm64 | — | `genaudiobookinfo-linux-arm64` |
|
||||
| `build-linux-arm7` | linux | arm | GOARM=7 | `genaudiobookinfo-linux-armv7` |
|
||||
| `build-darwin-amd64` | darwin | amd64 | — | `genaudiobookinfo-darwin-amd64` |
|
||||
| `build-darwin-arm64` | darwin | arm64 | — | `genaudiobookinfo-darwin-arm64` |
|
||||
| `build-windows-amd64` | windows | amd64 | — | `genaudiobookinfo-windows-amd64.exe` |
|
||||
| `build-windows-386` | windows | 386 | — | `genaudiobookinfo-windows-386.exe` |
|
||||
| `build-windows-arm64` | windows | arm64 | — | `genaudiobookinfo-windows-arm64.exe` |
|
||||
| `build-arm-mips` | linux | mips | GOMIPS=softfloat | `genaudiobookinfo-linux-mips` |
|
||||
| `build-arm-mipsle` | linux | mipsle | GOMIPS=softfloat | `genaudiobookinfo-linux-mipsle` |
|
||||
| `build-arm-riscv64` | linux | riscv64 | — | `genaudiobookinfo-linux-riscv64` |
|
||||
| `build-freebsd-amd64` | freebsd | amd64 | — | `genaudiobookinfo-freebsd-amd64` |
|
||||
| `build-freebsd-arm64` | freebsd | arm64 | — | `genaudiobookinfo-freebsd-arm64` |
|
||||
| `build-openbsd-amd64` | openbsd | amd64 | — | `genaudiobookinfo-openbsd-amd64` |
|
||||
| `build-netbsd-amd64` | netbsd | amd64 | — | `genaudiobookinfo-netbsd-amd64` |
|
||||
|
||||
Каждую платформу можно собирать независимо:
|
||||
|
||||
```bash
|
||||
make build-linux # Linux: все 4 варианта
|
||||
make build-darwin # macOS: Intel + Apple Silicon
|
||||
make build-windows # Windows: amd64 + 386 + arm64
|
||||
make build-arm # Embedded: MIPS + MIPSle + RISC-V
|
||||
make build-freebsd # FreeBSD: amd64 + arm64
|
||||
make build-openbsd # OpenBSD: amd64
|
||||
make build-netbsd # NetBSD: amd64
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Локальный релиз
|
||||
|
||||
### `make release`
|
||||
Выполняет `build-all`, затем копирует все скомпилированные бинарники из `build/` в `build/release/`.
|
||||
|
||||
```bash
|
||||
make release
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `make checksum`
|
||||
Выполняет `release`, затем генерирует файл `build/release/checksums-sha256.txt` с SHA256-хешами всех бинарников.
|
||||
|
||||
- **Windows**: использует `Get-FileHash` (PowerShell)
|
||||
- **Unix**: использует `sha256sum`
|
||||
|
||||
```bash
|
||||
make checksum
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `make release-local`
|
||||
**Рекомендуемая команда для полного локального релиза.** Псевдоним для `checksum` — собирает все платформы, создаёт архивную папку и файл контрольных сумм.
|
||||
|
||||
```bash
|
||||
make release-local
|
||||
make release-local VERSION=2.1.0
|
||||
```
|
||||
|
||||
Результат: папка `build/release/` с 16 бинарниками и `checksums-sha256.txt`.
|
||||
|
||||
---
|
||||
|
||||
## CI/CD через Gitea
|
||||
|
||||
Для автоматизации релизов используется пайплайн `.gitea/workflows/release.yml` (3 стадии: `quality → build → publish`). Подробнее: [[Architecture|Architecture]].
|
||||
|
||||
### `make ci-check`
|
||||
Показывает текущее состояние рабочего дерева (`git status --short`) и последние 5 коммитов. Рекомендуется запускать **перед созданием тега релиза**, чтобы убедиться, что все изменения закоммичены.
|
||||
|
||||
```bash
|
||||
make ci-check
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `make release-tag VERSION=X.Y.Z`
|
||||
Создаёт аннотированный git-тег `vX.Y.Z` и пушит его в `origin`. Это **автоматически запускает CI/CD pipeline** в Gitea (триггер `push tags v*.*.*`).
|
||||
|
||||
```bash
|
||||
make ci-check # 1. проверяем состояние
|
||||
make release-tag VERSION=2.1.0 # 2. создаём тег → CI запускается
|
||||
```
|
||||
|
||||
> **Внимание**: если `VERSION` не изменён (остался `2.0.0`), команда выведет предупреждение, но тег всё равно будет создан. Обязательно указывайте нужную версию явно.
|
||||
|
||||
После создания тега статус CI доступен по адресу:
|
||||
`https://github.dfv24.com/fofanov/genaudiobookinfo/actions`
|
||||
|
||||
---
|
||||
|
||||
### `make release-tag-delete VERSION=X.Y.Z`
|
||||
Удаляет тег `vX.Y.Z` локально и в `origin`. Используется для исправления ошибочно созданного тега.
|
||||
|
||||
```bash
|
||||
make release-tag-delete VERSION=2.1.0
|
||||
```
|
||||
|
||||
Затем можно выполнить коммит исправлений и повторно создать тег:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "fix: исправление перед релизом"
|
||||
make release-tag VERSION=2.1.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `make ci-release VERSION=X.Y.Z GITEA_TOKEN=<токен>`
|
||||
Запускает CI/CD workflow **вручную через Gitea REST API** (endpoint `workflow_dispatch`) — без создания git-тега. Удобно для тестирования пайплайна или выпуска сборки на уже существующей ветке.
|
||||
|
||||
```bash
|
||||
make ci-release VERSION=2.1.0 GITEA_TOKEN=abc123def456
|
||||
```
|
||||
|
||||
- **`GITEA_TOKEN`** обязателен — команда завершится с ошибкой, если токен не задан.
|
||||
- Токен получается в настройках Gitea: *Settings → Applications → Generate Token* (scope: `write:repository`).
|
||||
- **Windows**: использует `Invoke-RestMethod` (PowerShell)
|
||||
- **Unix**: использует `curl`
|
||||
|
||||
---
|
||||
|
||||
## Типичные рабочие сценарии
|
||||
|
||||
### Разработка (ежедневно)
|
||||
```bash
|
||||
make fmt # форматирование перед коммитом
|
||||
make vet # статический анализ
|
||||
make test # запуск тестов
|
||||
make build # локальная сборка
|
||||
```
|
||||
|
||||
### Выпуск новой версии через CI
|
||||
```bash
|
||||
make ci-check # проверить состояние
|
||||
make release-tag VERSION=2.1.0 # создать тег → CI запустится автоматически
|
||||
```
|
||||
|
||||
### Локальный релиз (без CI)
|
||||
```bash
|
||||
make release-local VERSION=2.1.0
|
||||
# результат: build/release/ — 16 бинарников + checksums-sha256.txt
|
||||
```
|
||||
|
||||
### Отладка пайплайна CI
|
||||
```bash
|
||||
make ci-release VERSION=2.1.0 GITEA_TOKEN=abc123 # ручной запуск без тега
|
||||
make release-tag-delete VERSION=2.1.0 # откат тега при ошибке
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Совместимость с платформами
|
||||
|
||||
| Функция | Windows (PowerShell) | Linux/macOS (sh) |
|
||||
|--------------------------|----------------------|------------------|
|
||||
| Установка `GOOS/GOARCH` | `$$env:GOOS='...'` | `GOOS=... $(GO)` |
|
||||
| Создание директорий | `New-Item -Force` | `mkdir -p` |
|
||||
| Удаление файлов | `Remove-Item -Force` | `rm -rf` |
|
||||
| SHA256 контрольные суммы | `Get-FileHash` | `sha256sum` |
|
||||
| Запрос к Gitea API | `Invoke-RestMethod` | `curl` |
|
||||
| Вывод сообщений | `$(info ...)` | `$(info ...)` |
|
||||
|
||||
> `$(info ...)` используется вместо `@echo "..."` для корректной работы в PowerShell, где двойные кавычки обрабатываются иначе.
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
# OpenRouter (LLM интеграция)
|
||||
|
||||
GenAudioBookInfo поддерживает опциональную нормализацию метаданных через LLM с помощью [OpenRouter](https://openrouter.ai/) — унифицированного API-шлюза к сотням AI-моделей.
|
||||
|
||||
---
|
||||
|
||||
## Зачем нужно LLM
|
||||
|
||||
Теги в аудиофайлах и имена папок часто содержат:
|
||||
- Инициалы вместо полного имени: `Акунин Б.` → `Акунин Борис`
|
||||
- Неправильный порядок: `Борис Акунин` → `Акунин Борис`
|
||||
- Лишние символы, транслитерацию, опечатки
|
||||
- Номера серий и части в названии
|
||||
|
||||
LLM нормализует автора в формат `Фамилия Имя` и очищает название книги.
|
||||
|
||||
---
|
||||
|
||||
## Настройка
|
||||
|
||||
### 1. Получить API ключ
|
||||
|
||||
Зарегистрируйтесь на [openrouter.ai](https://openrouter.ai/) и создайте API ключ в личном кабинете.
|
||||
|
||||
### 2. Добавить в `config.yaml`
|
||||
|
||||
```yaml
|
||||
openrouter:
|
||||
api_key: sk-or-v1-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
model: openai/gpt-4o-mini
|
||||
```
|
||||
|
||||
### Или через переменную окружения
|
||||
|
||||
```bash
|
||||
export OPENROUTER_API_KEY=sk-or-v1-xxxx
|
||||
./genaudiobookinfo
|
||||
```
|
||||
|
||||
Переменная `OPENROUTER_API_KEY` проверяется если `api_key` в конфиге пуст.
|
||||
|
||||
---
|
||||
|
||||
## Настройки в `config.yaml`
|
||||
|
||||
```yaml
|
||||
openrouter:
|
||||
api_key: sk-or-v1-xxxx # API ключ (или env OPENROUTER_API_KEY)
|
||||
base_url: https://openrouter.ai/api/v1
|
||||
timeout: 30s # Таймаут HTTP запроса
|
||||
model: openai/gpt-4o-mini # Идентификатор модели
|
||||
max_retries: 3 # Ретраи при ошибках API
|
||||
retry_backoff: 2s # Начальная задержка backoff
|
||||
retry_backoff_max: 30s # Максимальная задержка backoff
|
||||
prompt: | # Системный промпт
|
||||
Ты эксперт по русскоязычным аудиокнигам...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Промпт
|
||||
|
||||
Промпт определяет, как LLM должна обработать метаданные. Требования к промпту:
|
||||
|
||||
1. Отвечать **только JSON** без дополнительного текста.
|
||||
2. Формат ответа: `{"author": "Фамилия Имя", "title": "Название"}`.
|
||||
3. Автор в формате `Фамилия Имя` (не инициалы).
|
||||
|
||||
### Пример промпта
|
||||
|
||||
```yaml
|
||||
prompt: |
|
||||
Ты эксперт по русскоязычным аудиокнигам.
|
||||
Тебе дадут сырые данные об аудиокниге (автор и название).
|
||||
Твоя задача — нормализовать их:
|
||||
1. Автор: формат "Фамилия Имя" (полное имя, без инициалов)
|
||||
2. Название: убрать номера частей, лишние скобки, год
|
||||
Верни ответ строго в JSON формате:
|
||||
{"author": "Фамилия Имя", "title": "Чистое название"}
|
||||
Только JSON, никакого лишнего текста.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Выбор модели
|
||||
|
||||
OpenRouter предоставляет доступ к сотням моделей.
|
||||
Формат: `provider/model-name`.
|
||||
|
||||
Рекомендуемые модели для нормализации текста:
|
||||
|
||||
| Модель | Стоимость | Качество |
|
||||
|---|---|---|
|
||||
| `openai/gpt-4o-mini` | низкая | хорошее |
|
||||
| `openai/gpt-4o` | средняя | отличное |
|
||||
| `anthropic/claude-3-haiku` | низкая | хорошее |
|
||||
| `google/gemini-flash-1.5` | очень низкая | хорошее |
|
||||
| `meta-llama/llama-3.1-8b-instruct:free` | бесплатно | среднее |
|
||||
|
||||
Полный список: [openrouter.ai/models](https://openrouter.ai/models)
|
||||
|
||||
---
|
||||
|
||||
## Поведение при ошибках
|
||||
|
||||
| Ситуация | Действие |
|
||||
|---|---|
|
||||
| API ключ не задан | LLM выключен, книга обрабатывается с исходными тегами |
|
||||
| Ошибка HTTP | Ретрай до `max_retries` раз с backoff |
|
||||
| Невалидный JSON в ответе | WARN в лог, используются исходные теги |
|
||||
| Пустые поля в ответе | LLM результат игнорируется, используются исходные теги |
|
||||
| Таймаут запроса | WARN в лог, используются исходные теги |
|
||||
|
||||
LLM-ошибки **не прерывают** обработку книги — конвейер продолжается с оригинальными данными.
|
||||
|
||||
---
|
||||
|
||||
## Архитектура интеграции
|
||||
|
||||
```
|
||||
usecase/scan_audiobooks.go
|
||||
│
|
||||
▼ (если llmClient != nil)
|
||||
domain.LLMClient.NormalizeMetadata(rawAuthor, rawTitle)
|
||||
│
|
||||
▼
|
||||
infrastructure/openrouter_client.go
|
||||
OpenRouterClient.NormalizeMetadata()
|
||||
│
|
||||
├── HTTP POST /chat/completions
|
||||
│ Authorization: Bearer <api_key>
|
||||
│ {"model": "...", "messages": [...]}
|
||||
│
|
||||
└── разбор JSON {"author": "...", "title": "..."}
|
||||
```
|
||||
|
||||
`LLMClient` — интерфейс в `domain/llm.go`. Легко заменить на любой другой LLM провайдер.
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
# Структура результатов
|
||||
|
||||
После обработки все аудиокниги раскладываются по структурированным папкам.
|
||||
Корневая папка результатов — `result/` (или `dir.out` из `config.yaml`, или флаг `-result`).
|
||||
|
||||
---
|
||||
|
||||
## Основная структура
|
||||
|
||||
```
|
||||
result/
|
||||
├── А/
|
||||
│ └── Акунин Борис/
|
||||
│ ├── Акунин Борис — Азазель [2003]/
|
||||
│ │ ├── metadata.json
|
||||
│ │ ├── cover.jpg
|
||||
│ │ ├── 001.mp3
|
||||
│ │ └── 002.mp3
|
||||
│ └── Акунин Борис — Турецкий гамбит [2003]/
|
||||
│ ├── metadata.json
|
||||
│ └── ...
|
||||
├── Д/
|
||||
│ └── Достоевский Федор/
|
||||
│ └── ...
|
||||
├── ERROR/
|
||||
│ ├── Неизвестная_книга_123/
|
||||
│ │ ├── _error.txt
|
||||
│ │ └── <аудиофайлы...>
|
||||
│ └── ...
|
||||
└── DUPLICATE/
|
||||
└── А/
|
||||
└── Акунин Борис/
|
||||
├── Акунин Борис — Азазель [2003]/
|
||||
│ ├── metadata.json
|
||||
│ └── ...
|
||||
└── Акунин Борис — Азазель [2003]_2/
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Правила формирования имён
|
||||
|
||||
1. Первый уровень — первая буква фамилии автора (кириллица/латиница).
|
||||
2. Второй уровень — `Фамилия Имя` автора.
|
||||
3. Третий уровень — `Автор — Название [Год]/` (год добавляется только если известен).
|
||||
|
||||
Специальные символы (`/`, `\`, `:`, `*`, `?`, `"`, `<`, `>`, `|`) в именах папок заменяются безопасными аналогами.
|
||||
|
||||
---
|
||||
|
||||
## `metadata.json`
|
||||
|
||||
Создаётся в каждой папке с книгой. Пример:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Азазель",
|
||||
"authors": ["Акунин Борис"],
|
||||
"year": 2003,
|
||||
"genre": "Детектив",
|
||||
"comment": "Первый роман серии «Приключения Эраста Фандорина»...",
|
||||
"duration": "8h23m",
|
||||
"format": "mp3",
|
||||
"files_count": 24,
|
||||
"cover_found": true,
|
||||
"source": {
|
||||
"folder": "Акунин - Азазель",
|
||||
"file": "001.mp3"
|
||||
},
|
||||
"torrent": {
|
||||
"id": "123456",
|
||||
"tracker": "rutracker",
|
||||
"title": "Акунин Б. — Азазель (Б. Дьяченко) [2003, MP3, 96 kbps]",
|
||||
"seeds": 42,
|
||||
"size": "198 MB",
|
||||
"url": "https://rutracker.org/forum/viewtopic.php?t=123456"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Поля metadata.json
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `title` | string | Нормализованное название книги |
|
||||
| `authors` | []string | Список авторов |
|
||||
| `year` | int | Год издания (0 если неизвестен) |
|
||||
| `genre` | string | Жанр |
|
||||
| `comment` | string | Описание из тегов / трекера |
|
||||
| `duration` | string | Длительность первого трека |
|
||||
| `format` | string | Формат аудиофайлов |
|
||||
| `files_count` | int | Кол-во аудиофайлов в папке |
|
||||
| `cover_found` | bool | Найдена ли обложка |
|
||||
| `source.folder` | string | Исходное имя папки |
|
||||
| `source.file` | string | Файл, из которого читались теги |
|
||||
| `torrent.*` | object | Данные с трекера (если найдено) |
|
||||
|
||||
---
|
||||
|
||||
## Папка `ERROR/`
|
||||
|
||||
Книги, которые не удалось найти ни на одном трекере после всех попыток.
|
||||
|
||||
```
|
||||
result/ERROR/
|
||||
└── Исходное_имя_папки/
|
||||
├── _error.txt ← причина (не найдено / таймаут / API недоступен)
|
||||
├── 001.mp3
|
||||
└── 002.mp3
|
||||
```
|
||||
|
||||
### Файл `_error.txt`
|
||||
|
||||
```
|
||||
Ошибка обработки: aудиокнига не найдена на трекерах
|
||||
Папка: Автор - Название
|
||||
Попыток: 3
|
||||
Последняя ошибка: no results found for query "Название"
|
||||
Время: 2024-01-15 14:23:45
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Папка `DUPLICATE/`
|
||||
|
||||
Книга с таким же именем целевой папки уже существует в `result/`.
|
||||
|
||||
```
|
||||
result/DUPLICATE/
|
||||
└── А/
|
||||
└── Акунин Борис/
|
||||
├── Акунин Борис — Азазель [2003]/ ← первый дубликат
|
||||
├── Акунин Борис — Азазель [2003]_2/ ← второй
|
||||
└── Акунин Борис — Азазель [2003]_3/ ← третий
|
||||
```
|
||||
|
||||
Суффиксы `_2`, `_3`, ... добавляются итеративно до первого свободного имени.
|
||||
|
||||
---
|
||||
|
||||
## Обложка (`cover.jpg`)
|
||||
|
||||
Загружается из URL обложки в данных раздачи с трекера.
|
||||
Если URL недоступен или скачивание завершилось ошибкой — книга сохраняется без `cover.jpg`, в лог пишется `WARN`.
|
||||
|
||||
---
|
||||
|
||||
## Аудиофайлы
|
||||
|
||||
Аудиофайлы **перемещаются** (не копируются) из исходной папки в папку результата.
|
||||
Исходная папка после успешного переноса становится пустой (или удаляется, в зависимости от ОС).
|
||||
|
||||
Поддерживаемые расширения: `mp3`, `m4b`, `m4a`, `ogg`, `opus`, `flac`, `aac`, `wma`, `wav`, `aiff`.
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
# TorrAPI (Поиск на трекерах)
|
||||
|
||||
GenAudioBookInfo использует [TorrServer](https://github.com/YouROK/TorrServer) / TorrAPI-совместимый сервер для поиска аудиокниг на торрент-трекерах и получения метаданных раздач.
|
||||
|
||||
---
|
||||
|
||||
## Что такое TorrAPI
|
||||
|
||||
TorrServer — локальный сервис, который предоставляет JSON API для поиска по популярным торрент-трекерам:
|
||||
|
||||
- **Rutracker** — крупнейший русскоязычный трекер
|
||||
- **Rutor** — популярный трекер
|
||||
- **Kinozal** — кино и аудиокниги
|
||||
- И другие трекеры в зависимости от конфигурации TorrServer
|
||||
|
||||
---
|
||||
|
||||
## Установка TorrServer
|
||||
|
||||
TorrServer распространяется отдельно. Скачать: [github.com/YouROK/TorrServer/releases](https://github.com/YouROK/TorrServer/releases)
|
||||
|
||||
```bash
|
||||
# Linux/macOS
|
||||
./TorrServer -port 9200
|
||||
|
||||
# Windows
|
||||
TorrServer.exe -port 9200
|
||||
```
|
||||
|
||||
После запуска TorrServer доступен на `http://localhost:9200`.
|
||||
|
||||
---
|
||||
|
||||
## Настройка в `config.yaml`
|
||||
|
||||
```yaml
|
||||
torrapi:
|
||||
url: http://localhost:9200 # Адрес TorrAPI сервера
|
||||
```
|
||||
|
||||
Можно переопределить флагом:
|
||||
```bash
|
||||
./genaudiobookinfo -api http://192.168.1.10:9200 D:\Audiobooks
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Как выполняется поиск
|
||||
|
||||
### Алгоритм поиска одной книги
|
||||
|
||||
```
|
||||
1. Попытаться найти по title (названию из тегов / имени папки)
|
||||
2. Если результатов нет и title ≠ author:
|
||||
попытаться найти по author (автору)
|
||||
3. Если найдено: взять детали лучшего результата
|
||||
4. Если не найдено: следующая попытка (до search_retries раз)
|
||||
5. Если все попытки исчерпаны: переместить в ERROR/
|
||||
```
|
||||
|
||||
### Критерий выбора лучшего результата
|
||||
|
||||
Трекеры ранжируются в порядке приоритета:
|
||||
|
||||
1. **Rutracker** (наиболее полные метаданные)
|
||||
2. **Rutor**
|
||||
3. **Kinozal**
|
||||
4. Остальные (по количеству сидов)
|
||||
|
||||
Из результатов одного трекера выбирается раздача с наибольшим числом сидов.
|
||||
|
||||
---
|
||||
|
||||
## Ограничение параллелизма
|
||||
|
||||
Параметр `search_concurrency` (по умолчанию `2`) ограничивает число одновременных HTTP-запросов к TorrAPI через семафор. Это предотвращает перегрузку TorrServer при большом количестве воркеров.
|
||||
|
||||
```yaml
|
||||
processing:
|
||||
workers: 6 # воркеров до 6
|
||||
search_concurrency: 2 # но к TorrAPI ходят только 2 одновременно
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ретраи поиска
|
||||
|
||||
Если поиск вернул пустой результат или произошла ошибка сети:
|
||||
|
||||
```yaml
|
||||
processing:
|
||||
search_retries: 3 # попыток
|
||||
search_retry_delay: 3s # пауза между попытками
|
||||
```
|
||||
|
||||
Это полезно если TorrServer временно перегружен или трекер недоступен.
|
||||
|
||||
---
|
||||
|
||||
## Получаемые данные
|
||||
|
||||
После успешного поиска `GetDetail()` возвращает:
|
||||
|
||||
| Поле | Описание |
|
||||
|---|---|
|
||||
| `Title` | Полное название раздачи (трекерное) |
|
||||
| `Comment` | Описание раздачи (аннотация книги) |
|
||||
| `Year` | Год издания (если указан на трекере) |
|
||||
| `Genre` | Жанр |
|
||||
| `Authors` | Авторы в трекерном формате |
|
||||
| `CoverURL` | URL обложки для скачивания |
|
||||
| `Seeds` | Количество сидов |
|
||||
| `Size` | Размер раздачи |
|
||||
| `TrackerURL` | Ссылка на страницу раздачи |
|
||||
|
||||
Эти данные записываются в `metadata.json` в секцию `torrent.*`.
|
||||
|
||||
---
|
||||
|
||||
## Диагностика
|
||||
|
||||
### TorrServer недоступен
|
||||
|
||||
```
|
||||
WARN TorrAPI недоступен: dial tcp 127.0.0.1:9200: connection refused
|
||||
```
|
||||
|
||||
Решение: убедиться, что TorrServer запущен на указанном порту.
|
||||
|
||||
### Ничего не найдено
|
||||
|
||||
```
|
||||
WARN [Книга] не найдена на трекерах (попытка 1/3)
|
||||
...
|
||||
ERROR [Книга] перемещена в ERROR/ после 3 попыток
|
||||
```
|
||||
|
||||
Возможные причины:
|
||||
- Нестандартное имя папки (числа, латиница вместо кириллицы)
|
||||
- Книга действительно отсутствует на трекерах
|
||||
- Слишком короткое/общее название
|
||||
|
||||
### Проверка вручную
|
||||
|
||||
```bash
|
||||
curl "http://localhost:9200/api/v1/search?query=Акунин+Азазель" | jq .
|
||||
```
|
||||
Reference in New Issue
Block a user