Files
pipu-music/docs/decisions.md
T
mrleo1nidandClaude Opus 5.5 ca106e8eb1
ci / api / api (push) Successful in 46s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m3s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m22s
ci / ts / ts (packages + web) (push) Successful in 1m38s
feat(api): upload queue with quota, tus intake, processing and import (M5.5)
- Migration 00008: upload_batches, upload_files, upload_quota.
- Quota reserved under a row lock when a batch is created; every status
  change goes through one transition that frees space in the same
  transaction. SHA-256 duplicates are rejected before any upload.
- tus 1.0.0 core (HEAD/PATCH) for the author only; offset is the real
  staging file size, data beyond the declared size is refused (ADR-015).
- Processing worker: ffprobe checks, SHA-256 verification, exact and
  Chromaprint duplicates against library and queue, metadata from tags
  or the dropped folder path, possible_duplicate on metadata match, and
  the target path from write_template reusing existing folder spelling.
- Import worker: edited tags written with ffmpeg stream copy, file
  written by storage-agent, point scan of the new file, imported status;
  idempotent and resumed after restart. Stale uploads expire after 24 h,
  orphan staging files are removed at startup.
- API: quota, precheck, batches, my uploads, withdraw, admin list,
  metadata edit, bulk review, retry and queue settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 17:23:05 +03:00

16 KiB
Raw Permalink Blame History

Архитектурные решения (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 о публикации образов.


ADR-014. Анализатор v1 — признаки на numpy, Essentia позже

Контекст. Нейросетевые модели Essentia (Discogs-EffNet, жанры, настроения) требуют essentia-tensorflow: тяжёлый образ, ограниченный набор платформ и версий Python. Волне v1 нужны звучание и базовые признаки уже сейчас.

Решение. Анализатор basic-1 (services/recommender/pipu_rec/analysis/features.py) считает признаки сам, на numpy поверх декодирования ffmpeg: 3 фрагмента по 30 с; эмбеддинг 256 = форма спектра, разброс, динамика и ритмичность по 64 mel-полосам (рекомендатель стандартизирует его по библиотеке); темп по автокорреляции онсетов; тональность по хромаграмме; громкость, энергия, танцевальность, акустичность, длина вступления. Valence — грубая оценка (лад + яркость); mood_*, arousal, жанры анализа не заполняются.

Последствия. Анализ работает на любом CPU без моделей. Контексты используют энергию, темп, танцевальность и valence; настроения «весёлое» / «грустное» пока приблизительны. Переход на Essentia — новая версия analyzer_ver: воркер сам ставит пересчёт всех треков.


ADR-015. Приём загрузок — ядро tus своими силами, без tusd

Контекст. ADR-012 предполагал tusd как библиотеку. Но файл очереди создаёт POST /uploads вместе с резервом квоты и проверкой дублей, а права (только автор, только uploading) проверяет api. От tus остаётся только ядро протокола: HEAD (сколько принято) и PATCH (следующий кусок с Upload-Offset).

Решение. Ядро tus 1.0.0 — в internal/uploads/tus.go (~200 строк): смещение — фактический размер файла в staging, один PATCH на файл одновременно, данные сверх заявленного размера не принимаются, куски до 64 МБ. Расширения creation / termination / checksum не нужны. Клиент — tus-js-client с готовым uploadUrl из ответа POST /uploads.

Последствия. Нет новой зависимости и отдельного хранилища состояния tus; квота и права — в одной транзакции с созданием файла. Целостность проверяется после приёма: SHA-256 файла сверяется с заявленным клиентом.