Files
jellybit/docs/architecture.md
av 42d5b73a04 docs: перевод документации на канон av-dev
- Раскладка docs/ приведена к канону 2: заведены passport/architecture/
  database/security/review и research; docs/specs, drafts, backlog, review/
  и BRIEF.md разобраны и удалены, беклог переехал в docs/tasks (34 задачи,
  6 целей, слаги на английский).
- Нарративы specs удалены как дубли openspec-спек после поимённой сверки;
  остаток заведён задачами (редактор маппинга ревью, крайние случаи
  именования), отказ от сущности title промоутнут в ADR.
- Проектные копии агентов и скиллов ревью/пайплайна удалены в пользу
  плагинов av-dev-pm и av-dev-pipeline; в task gate добавлен шаг canon
  вместо er-schema.
2026-08-04 09:27:26 +03:00

170 lines
15 KiB
Markdown
Raw Permalink 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.
# Архитектура
Обзор: как сложено и где что работает. **Поведение системы здесь не
описывается** — его нормативный дом `openspec/specs/`; ниже компоненты только
ссылаются на свои capability. Инварианты и их severity — в
[CLAUDE.md](../CLAUDE.md), схема хранилища — в [database.md](database.md),
периметр — в [security.md](security.md).
## Принципы
- **Один статический бинарь.** Доставка — образом с готовым бинарём внутри. См.
[ADR-2026-06-13-go-single-binary](adr/ADR-2026-06-13-go-single-binary.md).
- **Источник неприкосновенен.** Только `mkdir`, `link(2)` и `unlink` *своих*
целевых ссылок. См. [ADR-2026-06-13-hardlinks](adr/ADR-2026-06-13-hardlinks.md).
- **Выход распознавания недоверенный.** Безопасность держится на валидации
целевого пути, а не на промпте — [security.md](security.md).
- **Единое ядро, тонкие транспорты.** Логика приёма — в use-case `ingest`;
переходами состояний владеет `worker`. HTTP API, веб-UI, Telegram и CLI лишь
складывают команды, `worker` их сериализует.
- **Опциональные внешние зависимости.** Метабазы (TMDB/TVDB/TVMaze) и триггер
Jellyfin включаются конфигом; без них сервис работает, но авто-раскладка без
подтверждённого матча не делается —
[ADR-2026-06-13-auto-link-requires-db-match](adr/ADR-2026-06-13-auto-link-requires-db-match.md).
- **Минимум компонентов.** В духе umbar — без зоопарка сервисов.
## Компоненты
`cmd/jellybit` — точка входа и сборка зависимостей; всё остальное — `internal/*`.
| Пакет | Ответственность | Capability |
| --- | --- | --- |
| `ingest` | use-case приёма загрузки, общий для всех транспортов | [ingest](../openspec/specs/ingest/spec.md) |
| `magnet`, `torrent` | разбор magnet-ссылки и байтов `.torrent`, извлечение инфохэшей | [ingest](../openspec/specs/ingest/spec.md) |
| `worker` | владелец машины состояний: поллинг qBittorrent, сериализация команд, фоновая сверка | [download-tracking](../openspec/specs/download-tracking/spec.md), [state-reconciliation](../openspec/specs/state-reconciliation/spec.md), [live-status](../openspec/specs/live-status/spec.md) |
| `qbt` | клиент qBittorrent WebUI API (сессия, добавление, опрос, удаление) | [download-tracking](../openspec/specs/download-tracking/spec.md) |
| `recognize` | пред-парс имени, вызов LLM, разбор плана, модель уверенности | [recognition](../openspec/specs/recognition/spec.md) |
| `llm` | провайдер LLM за интерфейсом (дискриминатор `[llm].type`) | [recognition](../openspec/specs/recognition/spec.md) |
| `metadata` | интерфейс метабаз + TMDB/TVDB/TVMaze (опц.) | [metadata-match](../openspec/specs/metadata-match/spec.md) |
| `naming` | единая логика целевых имён и отображаемого имени раздачи | [file-layout](../openspec/specs/file-layout/spec.md), [ingest](../openspec/specs/ingest/spec.md) |
| `layout` | санитизация путей, хардлинкер, copy-fallback, undo, владение путём | [file-layout](../openspec/specs/file-layout/spec.md), [state-reconciliation](../openspec/specs/state-reconciliation/spec.md) |
| `store` | SQLite: загрузки, распознавания, подсказки, кандидаты, ссылки | [identity](../openspec/specs/identity/spec.md) |
| `ident` | генерация и нормализация ULID | [identity](../openspec/specs/identity/spec.md) |
| `httpapi` | REST + веб-UI на htmx (server-rendered партиалы) | [web-ui](../openspec/specs/web-ui/spec.md), [review](../openspec/specs/review/spec.md) |
| `tgbot` | Telegram: приём, парсер сообщений торрент-бота, карточки, пинги | [notifications](../openspec/specs/notifications/spec.md), [review](../openspec/specs/review/spec.md) |
| `jellyfin` | триггер пересканирования медиатеки (опц.) | [file-layout](../openspec/specs/file-layout/spec.md) |
| `config` | загрузка и валидация TOML на старте | — |
| `logging`, `logctx` | slog-настройка и протяжка корреляции через контекст | [identity](../openspec/specs/identity/spec.md) |
| `archrules` | собственный анализатор архитектурных правил (часть гейта) | — |
Транспорты приёма — HTTP API, веб-UI, Telegram и CLI (`jellybit add`) — ведут в
один `ingest`; действия пользователя (apply / refine / reject / defer / undo /
retry / delete / dismiss) идут командами к `worker`.
## Внешние границы и форматы
- **qBittorrent WebUI API** — единственный способ качать: источник (magnet, URL,
`.torrent`) **отдаём ему**, сами по пользовательскому URL не ходим (SSRF
исключён). Пути берём из API (`save_path` + относительные имена из
`/torrents/files`), не из константы.
- **LLM** — OpenAI-совместимый Chat Completions (`[llm].type = "openai-compat"`),
структурированный вывод через `response_format: json_object`; валидация ответа
своя, в Go.
- **Метабазы** — TMDB, TVDB, TVMaze (последняя без ключа, только сериалы).
- **Jellyfin** — один вызов `POST /Library/Refresh`, авторизация заголовком
`X-Emby-Token`.
- **Telegram Bot API** — приём сообщений и исходящие карточки/пинги.
- **Сообщение торрент-бота** — чужой текстовый формат, разбирается парсером
`tgbot`; наблюдения по формату — в
[research/torrent-bot-message.md](research/torrent-bot-message.md).
## Эксплуатация
- **Где работает, что рядом, кто перезапускает:** домашний медиа-сервер umbar
(Intel N150), docker в общей сети с qBittorrent и Jellyfin. Перезапускает
docker по `restart`-политике и плейбук umbar при редеплое; человек — руками,
когда всё плохо. Оператор один и он же владелец.
- **Внешние зависимости поимённо и чем каждая отказывает:**
| Зависимость | Обязательна | Как отказывает |
| --- | --- | --- |
| qBittorrent | да | недоступен (весь цикл встаёт, тик поллинга краснеет); отдаёт раздачу без файлов; теряет раздачу (пропажа источника); переходные состояния `moving`/`checking*` выглядят как готовность |
| LLM-эндпоинт | да | недоступен; отвечает медленно (минуты); отдаёт не-JSON или JSON не по схеме; отдаёт правдоподобную выдумку — самый неприятный случай, потому что молчаливый |
| TMDB/TVDB/TVMaze | нет | недоступны; лимит запросов; пустой результат (норма для русского контента); несколько равнозначных кандидатов |
| Jellyfin | нет | недоступен — скан просто не случится, состояние задачи не страдает |
| Telegram Bot API | нет | недоступен — уведомление теряется, состояние задачи не страдает |
| Диск `/srv/media` | да | переполнен (особенно на copy-fallback); ФС без хардлинков; файл исчез между проверкой и `link(2)` |
| SQLite | да | `database is locked` при конкурентной записи; файл тома не смонтирован |
- **Кто заметит отказ и когда:** владелец — по отсутствию ожидаемого пинга и по
задаче, застрявшей в промежуточном состоянии; логи в stdout контейнера.
Автоматического алертинга нет, метрик нет — только уведомления в Telegram о
падении загрузки и о рассинхроне.
- **Характер потока:** непрерывный фон (тик поллинга qBittorrent, по умолчанию
5 с, и периодическая сверка) плюс редкие события по запросу человека. Объём —
единицы загрузок в день, десятки одновременно; ориентир масштаба и его аудит —
задача в беклоге.
## Единые точки проекта
Материал для вопроса «не появился ли второй способ делать то, что уже делается».
| Что | Где |
| --- | --- |
| Время | `store.Now()` — единственный источник, всегда UTC; формат хранения — RFC 3339 |
| Идентификаторы | `internal/ident` — генерация и нормализация ULID; `ident.Parse` на каждой входной границе |
| Целевые имена и превью раскладки | `internal/naming` — одна логика для превью в UI и для реального применения |
| Разбор источника | `internal/magnet` и `internal/torrent`; инфохэш извлекается только здесь |
| Приём | use-case `ingest` — общий путь для HTTP, веб-UI, Telegram и CLI |
| Переходы состояний | `worker` под per-download блокировкой; легальность перехода задаётся декларативным графом |
| Хардлинки и удаление своих ссылок | `internal/layout` — единственное место, которое пишет в файловую систему библиотеки |
| Трансляция доменной ошибки в код ответа | внешняя граница транспорта (`httpapi`, `tgbot`); правило — [conventions/errors.md](conventions/errors.md) |
| Логирующий чекпоинт | доменная граница, один на операцию; правило — [conventions/logging.md](conventions/logging.md) |
| Настройки | один TOML-файл, валидируется на старте; образец `config.example.toml` — источник истины по полям |
## Деплой
Работает в docker в одной среде с qBittorrent и Jellyfin — см.
[ADR-2026-07-24-local-image-build](adr/ADR-2026-07-24-local-image-build.md).
Сборка: статический бинарь (`GOOS=linux GOARCH=amd64 CGO_ENABLED=0`) и **полный
образ** собираются локально на control-хосте (`task image` упаковывает бинарь в
`distroless/static`). Образ едет на сервер через `docker save`/`load` (роль
`app_image` в umbar), там и запускается. Go-тулчейн и `docker build` на сервере
не нужны.
Разделение ответственности: **jellybit** (этот репозиторий) даёт бинарь и
`Dockerfile`; **umbar** — оркестрацию (доставка, docker compose,
`playbook-jellybit.yml`, рендер секретов).
Параметры запуска:
- **Общая docker-сеть** (external, напр. `media-net`) — адресация по именам
(`http://qbit:8989`, `http://jellyfin:8096`). Веб-UI публикуется на хост
(`8080:8080`) для LAN. qBit валидирует Host-заголовок — в umbar выставлен
`WebUI\ServerDomains=*`; LLM на хосте достаётся через `host.docker.internal`.
- **`user: "1000:1000"`**, UMASK 022 — единый системный пользователь umbar.
- **mount `/srv/media`** — единая песочница (см. ниже).
- **mount конфига** `/srv/applications/jellybit/config``/config` (ro),
`config.toml` с правами `0600`; рендерится плейбуком umbar, бекапу не подлежит.
- **mount данных** `/srv/applications/jellybit/data``/data`, SQLite
`/data/jellybit.db`. **Бекапить обязательно** — без него редеплой стирает всё
in-flight состояние.
- **healthcheck** зовёт сам бинарь (`jellybit healthcheck`): в distroless нет
shell и curl.
### Единая песочница `/srv/media`
Весь медиа-стек лежит под одним каталогом и монтируется **идентично**
(`/srv/media:/srv/media`) во все медиа-приложения:
```
/srv/media/
incomplete/ ← qBit качает сюда
downloads/ ← готовые раздачи (источник хардлинка)
movies/ series/ ← библиотека Jellyfin (цель хардлинка)
```
Так как всё под одним mount'ом, работают и **хардлинк** (downloads →
movies/series), и **мгновенный move** qBit (incomplete → downloads) — границ
между точками монтирования (`EXDEV`) нет. Путь из qBittorrent уже равен
хост-пути, трансляция не нужна (`path_map` — фолбэк, обычно пуст). Секреты и
чужие приложения (`/srv/applications`) в песочницу не попадают. Библиотеки
Jellyfin указывают на `movies`/`series`, а не на корень — иначе в индекс попадут
`downloads`/`incomplete`.
## Открытые вопросы
- Пока нет. Решённое разъехалось по ADR и capability-спекам; то, что требует
работы, живёт задачами в [tasks/BACKLOG.md](tasks/BACKLOG.md).