Files
pipu-music/docs/decisions.md
T
mrleo1nidandClaude Opus 5.5 9eb1c25147 docs: add product idea, design prototypes and development documentation
- IDEA.md: product vision from the user's perspective (input for design)
- Architecture, ADRs, data model, REST API overview
- Auth and roles, library storage (NFS, path regexes, scanner, search),
  playback, recommendation system ("My Wave"), track upload queue
- Web, Android (offline, self-update), i18n, design system
- Testing strategy, Gitea Actions CI/CD, deployment, conventions, roadmap
- Clickable web/Android design prototypes and wave button variants
- .claude/launch.json to serve the prototypes locally

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 23:21:31 +03:00

11 KiB
Raw 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.