- Раскладка 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.
15 KiB
Архитектура
Обзор: как сложено и где что работает. Поведение системы здесь не
описывается — его нормативный дом openspec/specs/; ниже компоненты только
ссылаются на свои capability. Инварианты и их severity — в
CLAUDE.md, схема хранилища — в database.md,
периметр — в security.md.
Принципы
- Один статический бинарь. Доставка — образом с готовым бинарём внутри. См. ADR-2026-06-13-go-single-binary.
- Источник неприкосновенен. Только
mkdir,link(2)иunlinkсвоих целевых ссылок. См. ADR-2026-06-13-hardlinks. - Выход распознавания недоверенный. Безопасность держится на валидации целевого пути, а не на промпте — 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.
- Минимум компонентов. В духе umbar — без зоопарка сервисов.
Компоненты
cmd/jellybit — точка входа и сборка зависимостей; всё остальное — internal/*.
| Пакет | Ответственность | Capability |
|---|---|---|
ingest |
use-case приёма загрузки, общий для всех транспортов | ingest |
magnet, torrent |
разбор magnet-ссылки и байтов .torrent, извлечение инфохэшей |
ingest |
worker |
владелец машины состояний: поллинг qBittorrent, сериализация команд, фоновая сверка | download-tracking, state-reconciliation, live-status |
qbt |
клиент qBittorrent WebUI API (сессия, добавление, опрос, удаление) | download-tracking |
recognize |
пред-парс имени, вызов LLM, разбор плана, модель уверенности | recognition |
llm |
провайдер LLM за интерфейсом (дискриминатор [llm].type) |
recognition |
metadata |
интерфейс метабаз + TMDB/TVDB/TVMaze (опц.) | metadata-match |
naming |
единая логика целевых имён и отображаемого имени раздачи | file-layout, ingest |
layout |
санитизация путей, хардлинкер, copy-fallback, undo, владение путём | file-layout, state-reconciliation |
store |
SQLite: загрузки, распознавания, подсказки, кандидаты, ссылки | identity |
ident |
генерация и нормализация ULID | identity |
httpapi |
REST + веб-UI на htmx (server-rendered партиалы) | web-ui, review |
tgbot |
Telegram: приём, парсер сообщений торрент-бота, карточки, пинги | notifications, review |
jellyfin |
триггер пересканирования медиатеки (опц.) | file-layout |
config |
загрузка и валидация TOML на старте | — |
logging, logctx |
slog-настройка и протяжка корреляции через контекст | identity |
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.
Эксплуатация
-
Где работает, что рядом, кто перезапускает: домашний медиа-сервер 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/logging.md |
| Настройки | один TOML-файл, валидируется на старте; образец config.example.toml — источник истины по полям |
Деплой
Работает в docker в одной среде с qBittorrent и Jellyfin — см. ADR-2026-07-24-local-image-build.
Сборка: статический бинарь (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.