- deploy/compose.yaml: every app service has `build:` (repo root context), local tags pipu-music-<service>:<version> - deploy/update.sh: git fetch → checkout tag (latest release by default, or vX.Y.Z / main) → docker compose build --pull → up -d → prune → /readyz; re-executes from a temp copy because checkout may replace the script - install.sh / .env.example: no REGISTRY, PIPU_VERSION set by update.sh, .env owned by the invoking user - release.yml: drop image publishing; release ships APK + sha256 only - ci.yml: new `docker` job builds all images without push - docs: install guide uses git clone, deployment/ci-cd updated, ADR-013 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 KiB
Архитектурные решения (ADR)
Формат: контекст → решение → последствия. Новое решение — новый раздел с номером; старые не переписываются,
а помечаются Заменено ADR-NNN.
ADR-001. Монорепозиторий
Контекст. Веб, мобильное, бэкенд, тесты и CI должны жить в одном репозитории; у веба и мобильного много общего (API-клиент, логика очереди и событий, словари i18n).
Решение. Монорепо: pnpm workspaces + Turborepo для TS-части; Go-модуль в services/api; Python-проект
(uv) в services/recommender. Единый api/openapi.yaml.
Последствия. Атомарные изменения контракта и клиентов в одном PR. CI использует фильтры путей, чтобы не гонять всё на каждый коммит.
ADR-002. Backend API на Go
Контекст. Нагрузка — IO (стриминг, NFS, БД), десятки одновременных слушателей; важна простота деплоя и низкое потребление памяти.
Решение. Go 1.23+: роутер chi, oapi-codegen (strict server), pgx + sqlc, миграции goose,
фоновые задачи River (очередь в Postgres), логирование log/slog, конфиг через env (caarlos0/env).
Последствия. Один статический бинарник на сервис, быстрые холодные старты, тайпсейф SQL. ML-код в Go не пишем — для этого отдельный сервис (ADR-003).
ADR-003. Рекомендации и аудиоанализ — отдельный Python-сервис
Контекст. Нужны: аудиоанализ (Essentia и её предобученные модели), коллаборативная фильтрация (implicit ALS), learning-to-rank (LightGBM). Экосистема есть только в Python.
Решение. services/recommender: FastAPI (онлайн-инференс), отдельный процесс analyzer (тот же образ),
обучение по расписанию (APScheduler внутри сервиса). Python 3.12, uv, numpy, implicit, lightgbm,
essentia-tensorflow, psycopg, redis.
Последствия. Два языка на бэкенде. Граница чёткая: Python не обслуживает пользователей напрямую,
только внутренние вызовы api. При падении сервиса работает fallback в api.
⚠️ Предобученные модели Essentia распространяются под CC BY-NC-SA 4.0 — допустимо для частного некоммерческого self-hosted использования. При коммерческом использовании нужно заменить модели.
ADR-004. Мобильное приложение — React Native (Expo), только Android
Контекст. Только Android, самообновление APK из релизов, фоновое воспроизведение, офлайн, анимированная кнопка волны. Рассматривались Flutter и React Native.
Решение. React Native + Expo SDK (prebuild / dev client, не Expo Go), expo-router,
react-native-track-player (ExoPlayer под капотом, MediaSession, уведомление, кэш), @shopify/react-native-skia
react-native-reanimatedдля волны,expo-sqliteдля офлайн-данных,expo-file-systemдля загрузок.
Почему не Flutter. Flutter не хуже по возможностям, но React Native даёт общий TypeScript-код с вебом: API-клиент, типы, логику очереди / событий / офлайн-синхронизации, словари i18n, дизайн-токены. Для команды из одного-двух человек это существенно сокращает объём работы.
Последствия. iOS не поддерживается (самообновление в обход стора там невозможно); при необходимости iOS добавляется позже через TestFlight без смены стека.
ADR-005. Раздача аудио через nginx X-Accel-Redirect
Контекст. Нужна перемотка (HTTP Range), высокая пропускная способность, минимум нагрузки на бэкенд.
Решение. Клиент обращается к api, api проверяет права и подпись ссылки и отвечает заголовком
X-Accel-Redirect на internal location nginx. Файл отдаёт nginx.
Последствия. Go не держит долгих соединений со стримами. Тома библиотеки и кэша транскодов
должны быть смонтированы и в api, и в nginx по одинаковым путям.
ADR-006. Подписанные ссылки на стрим
Контекст. <audio> в браузере и ExoPlayer не всегда умеют передавать заголовок Authorization.
Решение. В DTO трека отдаётся stream_url с параметрами exp и sig = HMAC-SHA256(user_id, track_id, quality, exp).
Срок жизни — 12 ч. Для офлайн-загрузки — отдельная ссылка со сроком 1 ч.
Последствия. Ссылки нельзя использовать после блокировки пользователя дольше срока жизни — при блокировке
дополнительно проверяется статус пользователя в api (кэш в Redis, 60 с).
ADR-007. Поиск — Meilisearch
Контекст. Мгновенный автокомплит по трём типам сущностей, опечатки, русский и английский, 11k–100k документов.
Решение. Meilisearch, три индекса (artists, albums, tracks), multi-search для выпадающего списка.
Postgres остаётся источником истины, индексы перестраиваемы.
Альтернатива. pg_trgm в Postgres — меньше компонентов, но хуже автокомплит и ранжирование. Оставлено как fallback.
ADR-008. Подключение NFS из админки через storage-agent
Контекст. Админ должен подключать хранилища из UI. Монтирование требует CAP_SYS_ADMIN,
давать его публичному api небезопасно.
Решение. Отдельный минимальный сервис storage-agent с CAP_SYS_ADMIN, без публичных портов.
Монтирует в /library/<source_id> внутри общего bind-тома с propagation: rshared;
api, nginx, analyzer видят точки монтирования через rslave. Поддерживаются типы
nfs (монтирует агент) и local (папка, уже смонтированная на хосте).
Последствия. Требуется Linux-хост (mount propagation) и nfs-common в образе агента.
При рестарте агент перемонтирует все активные источники из БД. Дополнено ADR-012 (запись).
ADR-009. Хранение векторов — pgvector
Контекст. Эмбеддинги треков (~11k–100k × 256 float) и векторы вкусов (~100). Нужен kNN.
Решение. pgvector, HNSW-индекс, косинусное расстояние. Отдельная векторная БД не нужна при таком объёме.
ADR-010. Аутентификация — свои JWT + ротируемые refresh-токены
Решение. Пароли — argon2id. Access JWT (EdDSA, 15 мин), refresh — непрозрачный токен (хэш в БД), 30 дней,
ротация при каждом использовании с детекцией повторного использования (отзыв всей «семьи»).
Веб хранит refresh в HttpOnly; Secure; SameSite=Strict cookie, мобильное — в expo-secure-store (Android Keystore).
Подробно — auth-and-roles.md.
ADR-011. Ошибки API — RFC 9457 + коды для i18n
Решение. Ответы об ошибках — application/problem+json с машинным полем code (auth.invalid_credentials,
validation.password_too_short, …). Клиенты переводят code через словари; сервер не возвращает локализованных текстов.
ADR-012. Очередь на добавление треков и запись в хранилище
Контекст. Модераторы предлагают новые треки; очередь ограничена по размеру (настраивает админ); дубли отсекаются; одобренные файлы должны лечь на файловый сервер в принятой там структуре папок. До этого библиотека была строго read-only.
Решение.
- Загрузка по протоколу tus (resumable) в локальный staging, квота — счётчик в Postgres, резервируется до загрузки.
- Дубли — три уровня: SHA-256 (предпроверка на клиенте), нормализованные метаданные, акустический отпечаток Chromaprint.
- Запись в хранилище — только
storage-agent, только в источники сaccepts_uploads, через отдельнуюrw-точку монтирования, невидимую другим контейнерам; только создание новых файлов (tmp → fsync → hash → rename). - Путь — по
write_templateхранилища; шаблон автоподбирается по существующей структуре и проверяется на обратимость регэкспами хранилища.
Последствия. Публичный api по-прежнему не может изменить библиотеку. Нужны права на запись в NFS-экспорт
для UID агента. Подробно — uploads.md.
ADR-013. Образы собираются на сервере, реестр не используется
Контекст. Публикация образов в Gitea Container Registry требует настройки реестра, токенов на push и pull,
docker login на сервере. Сервер один, и он сам может собрать образы.
Решение. На сервере клонируется репозиторий; deploy/update.sh [vX.Y.Z] переключается на тег и выполняет
docker compose build + up -d (build: у сервисов в deploy/compose.yaml). Релиз в Gitea содержит только APK.
CI на каждый PR собирает все образы без push, чтобы сборка на сервере не ломалась.
Последствия. Серверу нужен доступ к репозиторию и интернет на время сборки; первая сборка — 5–15 минут,
нужно ~5–10 ГБ под кэш сборки. Откат — update.sh на предыдущий тег (пересборка из кэша). Отпадают секреты
REGISTRY_USER / REGISTRY_TOKEN. Заменяет часть ADR-001 / ci-cd о публикации образов.