docs(wiki): sync from main repo

wiki-sync-bot
2026-02-23 14:28:35 +03:00
parent ce728799bb
commit 556b793e26
10 changed files with 1538 additions and 0 deletions
+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
```
Время выполнения: 13 минуты в зависимости от машины.
---
### Платформы и выходные файлы
| Команда 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 стадии: `qualitybuildpublish`). Подробнее: [[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: *SettingsApplicationsGenerate 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 .
```