Files
jellybit/docs/architecture.md
T
av c5d62d76ee docs: канон поднят с версии 7 до 12
- каталог задач переехал в tasks/ в корне, спринт упразднён — приоритет
  теперь порядок строк в BACKLOG.md, четыре задачи набора вернулись в беклог
- гейт: путь docs.py переведён на av-dev-docs вместо снесённого av-dev-pm,
  добавлены шаги tasks.py check и openspec.py check
- относительные ссылки внутри задач и ссылки из docs/ на задачи починены
2026-08-09 19:09:53 +03:00

18 KiB
Raw 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 — единственный способ качать: источник отдаём ему, сами по пользовательскому URL не ходим (SSRF исключён). Какие виды источника принимаются и как разбираются — ingest; способ добавления по source_typedownload-tracking; откуда берутся пути файлов — file-layout.
  • LLM — OpenAI-совместимый Chat Completions за интерфейсом ([llm].type); контракт вызова, формат вывода и разбор ответа — recognition.
  • Метабазы — TMDB, TVDB, TVMaze (последняя без ключа, только сериалы).
  • Jellyfin — HTTP-триггер пересканирования медиатеки, опционален; когда он дёргается и по каким переходам — file-layout.
  • 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 — период в database.md → «Настройки с числовым значением» — и периодическая сверка) плюс редкие события по запросу человека. Объём — единицы загрузок в день, десятки одновременно; ориентир масштаба и его аудит — задача в беклоге.

Единые точки проекта

Материал для вопроса «не появился ли второй способ делать то, что уже делается».

Что Где
Время store.Now() — единственный источник меток времени в данных, всегда UTC; формат хранения — RFC 3339. Вторая санкционированная точка wall-clock — timestamp-часть ULID в ident.NewID (исключение ^internal/(ident|store)/ в .golangci.yml). Отдельно от меток в данных стоят замеры длительности: cmd/jellybit исключён из forbidigo целиком (правило ^cmd/), плюс точечные //nolint:forbidigo в internal/logging/ext.go и internal/httpapi/httpapi.go
Идентификаторы 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.

Открытые вопросы

Места, где устройство знаемо тонкое: не дефекты, а принятые пока пробелы. Здесь только адрес и одна фраза — что именно не сделано; работа под каждым живёт задачей в tasks/BACKLOG.md. Список нужен ревью: правка, попавшая в такую область, стоит дороже, чем выглядит.

Область Чего нет сегодня Задача
Масштаб ориентир 100/1000 загрузок не зафиксирован, узкие места SQLite, воркера и поллинга не измерены scale-100-downloads
Ретеншен терминальные задачи и сырые ответы LLM копятся вечно, авточистки нет db-retention-cleanup
Бекап бекапить /data требуется, а стратегия и ротация не описаны sqlite-backup
Наблюдаемость healthcheck проверяет только сам сервис; метрик и алертинга нет, отказ виден по застрявшей задаче deep-healthcheck-dependencies
Идентичность раздачи split v1/v2-хеши не связаны, паре xt из магнета доверяем infohash-identity-integrity
Расход внешних лимитов кэша ответов метабаз нет, повтор распознавания бьёт провайдера заново metadata-cache
История переходов хранится только текущее состояние, «как сюда попали» восстанавливается по логам download-transition-history
Предел ответа LLM лимита на размер ответа нет — единственный недоверенный канал без предела; задачей пока не заведено