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

15 KiB
Raw Permalink Blame History

Архитектура

Обзор: как сложено и где что работает. Поведение системы здесь не описывается — его нормативный дом 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.