Files
jellybit/docs/specs/architecture.md
T

195 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Архитектура
## Назначение
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**, образ собирается на сервере из готового
бинаря (см. «Деплой»).
## Открытые вопросы
Существенных пока нет.