Files
pipu-music/docs/architecture.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

139 lines
11 KiB
Markdown

# Архитектура
## 1. Контекст
- До ~100 пользователей, библиотека от 11k треков (рассчитываем на рост до 100k без смены архитектуры).
- Всё разворачивается на одном сервере через Docker Compose.
- Музыка лежит на NFS-хранилищах, которые подключает администратор из админки. Для всех сервисов доступ к музыке —
**только чтение**; единственный писатель — `storage-agent`, который раскладывает одобренные треки из очереди на добавление.
- Клиенты: веб (SPA) и Android-приложение.
## 2. Компоненты
```
┌──────────────┐ ┌────────────────┐
Web SPA ─────┐ │ │ │ │
├──► │ nginx │────►│ api │──► PostgreSQL (+pgvector)
Android ─────┘ │ (TLS, SPA, │ │ (Go) │──► Redis
│ X-Accel) │ │ │──► Meilisearch
└──────┬───────┘ └───┬────────┬───┘
│ read-only │ HTTP │ HTTP (internal)
▼ ▼ ▼
/library/<source> recommender storage-agent
/cache/transcode (Python) (Go, CAP_SYS_ADMIN)
▲ │ │ mount -t nfs
│ ▼ ▼
│ analyzer /library/<source>
└────────── (Python, (mount propagation
Essentia) rshared → остальным)
```
| Сервис | Язык | Ответственность |
|---|---|---|
| `nginx` | — | TLS, раздача SPA, reverse proxy на `api`, отдача аудиофайлов и обложек через `X-Accel-Redirect` (internal locations), gzip/brotli, кэш статики. |
| `api` | Go | Весь публичный REST API: auth, пользователи, каталог, поиск (прокси к Meilisearch), коллекция, состояние плеера, события прослушиваний, админка, фоновые задачи (сканер, транскодинг) через очередь River (на Postgres). |
| `storage-agent` | Go | Единственный привилегированный контейнер: монтирует / размонтирует NFS-шары в `/library/<source_id>`, проверяет доступность, **записывает одобренные загрузки** в принимающие хранилища (только создание новых файлов). Принимает команды только от `api` по внутренней сети. |
| `recommender` | Python | Онлайн-сервис рекомендаций: генерация кандидатов, ранжирование, пачки для «Моей волны», обработка обратной связи в реальном времени; ночное обучение моделей (ALS, LightGBM). |
| `analyzer` | Python | Воркер аудиоанализа: Essentia — BPM, энергия, танцевальность, настроение, эмбеддинги. Тот же образ, что `recommender`, другая команда запуска. |
| `postgres` | — | Основное хранилище, pgvector для эмбеддингов, очередь задач River. |
| `redis` | — | Rate limiting, кэш, Redis Streams для событий в `recommender`, сессионные векторы волны. |
| `meilisearch` | — | Полнотекстовый поиск с опечатками и автокомплитом. |
## 3. Ключевые потоки
### 3.1 Воспроизведение
1. Клиент получает трек (DTO содержит `stream_url` — подписанную ссылку со сроком жизни).
2. `GET /api/v1/stream/{trackId}?q=high&exp=…&sig=…` → nginx → `api`.
3. `api` проверяет подпись / срок / пользователя, при необходимости готовит транскод в кэше,
отвечает `X-Accel-Redirect: /_internal/library/<source>/<path>` (или `/_internal/cache/...`).
4. nginx сам отдаёт файл с поддержкой `Range` (перемотка), `sendfile`, без участия Go.
Подробно — [playback.md](playback.md).
### 3.2 События и обучение
1. Клиент шлёт события (`play_start`, `progress`, `play_end`, `skip`, `like`, …) пачками в `POST /api/v1/events`.
2. `api` валидирует, пишет в `play_events` (источник истины) и публикует в Redis Stream `events`.
3. `recommender` читает стрим: обновляет сессионный вектор волны и вектор вкуса пользователя онлайн.
4. Ночью `recommender` переобучает ALS и LightGBM по `play_events` и пересчитывает агрегаты.
Подробно — [recommendations.md](recommendations.md).
### 3.3 Сканирование библиотеки
1. Админ добавляет хранилище → `api` → `storage-agent` монтирует NFS.
2. `api` ставит задачу `scan_source` в River → обход файлов, чтение тегов (Go, `dhowden/tag` + `ffprobe`),
применение шаблонов путей, upsert в БД, индексирование в Meilisearch.
3. Для новых / изменённых треков создаётся задача анализа (таблица `analysis_jobs`) → `analyzer`.
Подробно — [library-and-storage.md](library-and-storage.md).
### 3.4 Предложение треков
1. Модератор перетаскивает файлы → клиент считает SHA-256 и читает теги → `POST /uploads/precheck`:
известные дубли отбрасываются без загрузки.
2. `POST /uploads` резервирует квоту очереди → загрузка по tus в staging (`/srv/pipu/uploads`).
3. Задача `process_upload_file`: ffprobe, хэш, Chromaprint → дубль (удаление, квота освобождена) или `pending_review`.
4. Админ правит метаданные и одобряет → `import_upload_file`: целевой путь по `write_template` хранилища →
`storage-agent` записывает файл → `scan_path` → трек в каталоге, поиске и очереди анализа.
Подробно — [uploads.md](uploads.md).
## 4. Структура репозитория
```
pipu-music/
├── apps/
│ ├── web/ # React + Vite SPA
│ └── mobile/ # React Native (Expo) Android
├── packages/ # общий TypeScript-код (pnpm workspaces)
│ ├── api-client/ # сгенерированный из OpenAPI клиент + типы
│ ├── core/ # бизнес-логика клиентов: очередь плеера, события, сторы
│ ├── i18n/ # словари ru/en и настройка i18next
│ └── design-tokens/ # цвета, типографика, отступы → CSS vars и TS
├── services/
│ ├── api/ # Go
│ │ ├── cmd/api/ # main сервиса
│ │ ├── cmd/storage-agent/ # main агента хранилищ
│ │ ├── internal/
│ │ │ ├── auth/ users/ library/ catalog/ search/
│ │ │ ├── playback/ collection/ events/ wave/ admin/ uploads/
│ │ │ └── platform/ # db, redis, config, logging, http, jobs
│ │ ├── migrations/ # goose SQL-миграции
│ │ └── queries/ # sqlc SQL
│ └── recommender/ # Python (uv)
│ ├── pipu_rec/
│ │ ├── api/ # FastAPI
│ │ ├── analysis/ # Essentia
│ │ ├── candidates/ ranking/ wave/ training/
│ │ └── storage/ # доступ к Postgres / Redis
│ └── tests/
├── api/openapi.yaml # единый контракт публичного API
├── deploy/
│ ├── compose.yaml
│ ├── compose.dev.yaml
│ ├── nginx/
│ └── .env.example
├── tests/
│ ├── e2e/ # Playwright
│ └── fixtures/music/ # генерируемые тестовые аудиофайлы
├── .gitea/workflows/ # ci.yml, release.yml
├── docs/
├── turbo.json pnpm-workspace.yaml package.json
└── README.md
```
## 5. Архитектурный стиль
- **Модульный монолит** в `api`: каждый модуль в `internal/<module>` — `handler` (HTTP) → `service`
(бизнес-логика, интерфейсы) → `repository` (sqlc). Модули общаются через интерфейсы сервисов,
не лезут в чужие таблицы напрямую.
- **Contract-first**: `api/openapi.yaml` — источник истины. Go-сервер генерирует типы и интерфейс
хендлеров (`oapi-codegen`, strict server), TS-клиент генерируется в `packages/api-client`.
CI падает, если сгенерированный код не совпадает с контрактом.
- **Внутренний API** `api ↔ recommender` и `api ↔ storage-agent` — HTTP/JSON во внутренней docker-сети,
описан отдельными `internal-*.yaml` спеками.
- **Идемпотентность**: события и мутирующие запросы клиентов принимают `Idempotency-Key` / `event_id` (UUIDv7),
что важно для офлайн-синхронизации мобильного.
- **Деградация**: если `recommender` недоступен, `api` отдаёт волну по упрощённому fallback-алгоритму
(похожие по жанрам + лайки + случайное), приложение продолжает работать.
- **Наблюдаемость**: структурированные JSON-логи (`slog`, `structlog`), `/healthz` и `/readyz` у каждого сервиса,
метрики Prometheus `/metrics` (опционально подключаются Grafana / Prometheus).