# Архитектура ## Назначение 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 --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**, образ собирается на сервере из готового бинаря (см. «Деплой»). ## Открытые вопросы Существенных пока нет.