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

158 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Архитектурные решения (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](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](uploads.md).