195 lines
9.3 KiB
Markdown
195 lines
9.3 KiB
Markdown
# Архитектура
|
||
|
||
## Назначение
|
||
|
||
Jellybit принимает торрент с текстовым контекстом, скачивает его через
|
||
qBittorrent, определяет содержимое (фильм или сериал с сезонами и
|
||
сериями) и раскладывает файлы по конвенциям Jellyfin — хардлинками, не
|
||
трогая исходную раздачу.
|
||
|
||
## Принципы
|
||
|
||
- **Один статический бинарь.** Доставка — копированием на сервер. См.
|
||
[ADR-2026-06-13-go-single-binary](../adr/ADR-2026-06-13-go-single-binary.md).
|
||
- **Источник не трогаем.** В библиотеку кладём хардлинки; qBittorrent
|
||
продолжает раздачу, место на диске не дублируется.
|
||
- **Единое ядро, тонкие транспорты.** Вся логика приёма загрузки — в
|
||
use-case `Ingest`. HTTP API, веб-UI и Telegram — обёртки над ним.
|
||
- **Опциональные внешние зависимости.** Базы метаданных (TMDB/TVDB)
|
||
включаются конфигом; без них сервис работает на одном LLM.
|
||
- **Минимум компонентов.** В духе umbar — без лишних сервисов.
|
||
|
||
## Компоненты
|
||
|
||
| Пакет | Ответственность |
|
||
| ----------- | ----------------------------------------------------- |
|
||
| `ingest` | use-case приёма загрузки, общий для всех транспортов |
|
||
| `qbt` | клиент qBittorrent WebUI API |
|
||
| `worker` | фоновый цикл: машина состояний, поллинг завершения |
|
||
| `recognize` | пред-парс имени + вызов LLM + модель уверенности |
|
||
| `llm` | провайдер LLM за интерфейсом (дискриминатор `type`) |
|
||
| `metadata` | интерфейс баз метаданных + TMDB/TVDB (опц.) |
|
||
| `layout` | конвенции Jellyfin + хардлинкер |
|
||
| `store` | SQLite: загрузки, распознавание, ссылки |
|
||
| `httpapi` | REST + веб-UI (server-rendered, htmx) |
|
||
| `tgbot` | Telegram-адаптер + парсер сообщений торрент-бота |
|
||
| `config` | загрузка TOML-конфига |
|
||
|
||
## Поток и машина состояний
|
||
|
||
```
|
||
ingest → downloading → completed → recognizing ─┬─ уверенно ───────→ linking → done
|
||
└─ сомнительно → review → linking → done
|
||
любой шаг при ошибке → failed
|
||
```
|
||
|
||
- **ingest** — приняли источник + контекст, поставили в qBittorrent
|
||
(категория `jellybit`), записали в БД.
|
||
- **downloading / completed** — `worker` поллит qBittorrent по категории
|
||
(интервал `worker.poll_interval`, по умолчанию 5 с).
|
||
- **recognizing** — `recognize` строит план раскладки и оценку
|
||
уверенности (см. [recognition.md](recognition.md)).
|
||
- **review** — план уходит человеку (веб-UI / Telegram), ждём решения;
|
||
сценарии — в [review-ux.md](review-ux.md).
|
||
- **linking** — `layout` создаёт хардлинки в библиотеке.
|
||
- **done** — опционально дёргаем скан библиотеки Jellyfin.
|
||
|
||
Состояние персистентно в SQLite — перезапуск сервиса безопасен, `worker`
|
||
продолжает с того же места.
|
||
|
||
## Транспорты
|
||
|
||
Все три ведут в один `Ingest(req)`:
|
||
|
||
- **HTTP API + веб-UI** — форма «добавить», список загрузок, экран
|
||
подтверждения раскладки (server-rendered + htmx, без JS-сборки).
|
||
- **Telegram-бот** — переслать magnet или сообщение торрент-бота прямо в
|
||
jellybit; текст становится контекстом распознавания.
|
||
- **CLI** — `jellybit add <magnet> --context "..."` для отладки.
|
||
|
||
## Хранилище
|
||
|
||
SQLite, минимум таблиц:
|
||
|
||
- `download` — источник, контекст, hash торрента, категория, состояние,
|
||
тайминги.
|
||
- `recognition` — тип, название, год, сезон, provider-id, оценка
|
||
уверенности, сырой ответ LLM.
|
||
- `file_link` — соответствие исходный файл → целевой путь, вид
|
||
(видео/субтитры), статус.
|
||
|
||
## Конфигурация
|
||
|
||
TOML. В репозитории — `config.example.toml` с placeholder'ами; реальный
|
||
`config.toml` рендерится при деплое Ansible-шаблоном из переменных umbar
|
||
(секреты — в `vars/secrets.yml` под ansible-vault) и не коммитится.
|
||
Пример:
|
||
|
||
```toml
|
||
[qbittorrent]
|
||
url = "http://127.0.0.1:8989"
|
||
username = "admin"
|
||
password = ""
|
||
category = "jellybit"
|
||
|
||
[paths]
|
||
downloads = "/srv/downloads"
|
||
movies = "/srv/media/movies"
|
||
series = "/srv/media/series"
|
||
|
||
[llm]
|
||
# type — дискриминатор реализации; пока поддерживается "openai-compat"
|
||
type = "openai-compat"
|
||
base_url = "http://127.0.0.1:1234/v1"
|
||
api_key = ""
|
||
model = "qwen2.5-32b-instruct"
|
||
|
||
[metadata.tmdb]
|
||
enabled = true
|
||
api_key = ""
|
||
|
||
[metadata.tvdb]
|
||
enabled = false
|
||
api_key = ""
|
||
|
||
[worker]
|
||
poll_interval = "5s" # как часто опрашивать qBittorrent
|
||
|
||
[recognition]
|
||
auto_confidence_threshold = 0.85
|
||
|
||
[telegram]
|
||
enabled = false
|
||
token = ""
|
||
allowed_user_ids = []
|
||
|
||
[http]
|
||
listen = ":8080"
|
||
|
||
[log]
|
||
level = "info"
|
||
format = "json"
|
||
```
|
||
|
||
## Логирование
|
||
|
||
Структурированный JSON через `log/slog`. Каждая загрузка проходит со
|
||
сквозным идентификатором; решения распознавания (почему авто/ревью)
|
||
логируются явно.
|
||
|
||
## Деплой
|
||
|
||
Jellybit работает в **docker** — в одной среде с qBittorrent и Jellyfin
|
||
(они тоже в контейнерах на umbar). Единая среда запуска перевешивает
|
||
простоту нативного systemd.
|
||
|
||
Сборка — дёшево и сердито: статический бинарь собирается здесь; на сервер
|
||
во временную build-папку кладутся бинарь + `Dockerfile` (он просто
|
||
копирует бинарь в минимальный образ), образ собирается прямо на сервере и
|
||
запускается. Go-тулчейн на сервере не нужен — только docker.
|
||
|
||
Разделение ответственности:
|
||
|
||
- **jellybit** (этот репозиторий) — производит статический бинарь и
|
||
`Dockerfile`.
|
||
- **umbar** — оркестрация деплоя: доставка артефактов, `docker build` и
|
||
запуск через docker compose (`playbook-jellybit.yml`).
|
||
|
||
## Раскладка файлов
|
||
|
||
Хардлинки в `paths.movies` / `paths.series` по конвенциям Jellyfin.
|
||
Детали и крайние случаи — в [jellyfin-layout.md](jellyfin-layout.md).
|
||
`/srv/downloads` и `/srv/media` — одна ФС (подтверждено), поэтому
|
||
хардлинки применимы. Так как jellybit в docker (см. «Деплой»), контейнеру
|
||
монтируем общего родителя `/srv` — чтобы внутри оба каталога остались на
|
||
одной ФС и хардлинк проходил.
|
||
|
||
## Предполагаемая структура репозитория
|
||
|
||
```
|
||
cmd/jellybit/ точка входа, сборка зависимостей
|
||
internal/
|
||
ingest/ qbt/ worker/ recognize/ llm/ metadata/
|
||
layout/ store/ httpapi/ tgbot/ config/
|
||
migrations/ миграции SQLite
|
||
web/templates/ шаблоны веб-UI
|
||
docs/ specs / adr / drafts
|
||
config.example.toml
|
||
```
|
||
|
||
## Решённые вопросы
|
||
|
||
- `/srv/downloads` и `/srv/media` — одна ФС (подтверждено); хардлинки
|
||
применимы.
|
||
- Детект завершения — поллинг qBittorrent раз в несколько секунд
|
||
(`worker.poll_interval`). Webhook — возможная оптимизация на будущее
|
||
([drafts/ideas.md](../drafts/ideas.md)).
|
||
- Секреты — в переменных umbar; `config.toml` рендерится Ansible-шаблоном
|
||
при деплое.
|
||
- Форма запуска — **docker**, образ собирается на сервере из готового
|
||
бинаря (см. «Деплой»).
|
||
|
||
## Открытые вопросы
|
||
|
||
Существенных пока нет.
|