- 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>
16 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 о публикации образов.
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 файла сверяется с заявленным клиентом.