Files
pipu-music/docs/architecture.md
T
mrleo1nid 0a05bfe2cd
ci / module boundaries / boundaries (push) Successful in 7s
ci / api / api (push) Successful in 1m9s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m8s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m23s
ci / db / db (push) Successful in 2m29s
ci / ts / ts (packages + web) (push) Successful in 2m55s
feat(social): add friends, private chats, inbox and music sharing
2026-10-23 03:19:29 +03:00

155 lines
14 KiB
Markdown

# Архитектура
## 1. Контекст
- До ~100 пользователей, библиотека от 11k треков (рассчитываем на рост до 100k без смены архитектуры).
- Всё разворачивается на одном сервере через Docker Compose.
- Музыка лежит на NFS-хранилищах, которые подключает администратор из админки. Для всех сервисов доступ к музыке —
**только чтение**; единственный писатель — `storage-agent`, который раскладывает одобренные треки из очереди на добавление.
- Клиенты: веб (SPA), десктоп (Tauri) и Android-приложение.
## 2. Компоненты
```
┌──────────────┐ ┌────────────────┐
Web SPA ─────┐ │ │ │ │
Desktop ─────┼──► │ 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), коллекция, состояние плеера, события прослушиваний, админка, фоновые задачи: сканер внутри процесса, upload-воркеры со статусами в PostgreSQL. |
| `storage-agent` | Go | Единственный привилегированный контейнер: монтирует / размонтирует NFS-шары в `/library/<source_id>`, проверяет доступность, **записывает одобренные загрузки** в принимающие хранилища (только создание новых файлов). Принимает команды только от `api` по внутренней сети. |
| `recommender` | Python | Онлайн-сервис рекомендаций: генерация кандидатов, ранжирование, пачки для «Моей волны», обработка обратной связи в реальном времени; ночное обучение моделей (ALS, LightGBM). |
| `analyzer` | Python | Воркер аудиоанализа: Essentia — BPM, энергия, танцевальность, настроение, эмбеддинги. Тот же образ, что `recommender`, другая команда запуска. |
| `postgres` | — | Основное хранилище, pgvector для эмбеддингов, состояния upload/analysis jobs. |
| `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` запускает `Scanner.Start` в процессе → обход файлов, чтение тегов (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
│ ├── desktop/ # Tauri 2: то же 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/ library/ search/ genius/
│ │ │ ├── playback/ wave/ uploads/ appupdate/ system/
│ │ │ ├── social/ # друзья, личные сообщения, центр уведомлений
│ │ │ └── platform/ # db, redis, config, logging, http, jobs
│ │ └── migrations/ # goose SQL-миграции; SQL-запросы — в адаптерах internal/
│ └── recommender/ # Python (uv)
│ ├── pipu_rec/
│ │ ├── api/ # FastAPI
│ │ ├── analysis/ # признаки и эмбеддинги (numpy)
│ │ ├── candidates/ ranking/ wave/ training/
│ │ └── storage/ # доступ к Postgres / Redis
│ └── tests/
├── api/openapi.yaml # единый контракт публичного API
├── deploy/
│ ├── compose.yaml
│ ├── compose.dev.yaml
│ ├── nginx/
│ └── .env.example
├── docs/
├── turbo.json pnpm-workspace.yaml package.json
└── README.md
```
## 5. Архитектурный стиль
- **Модульный монолит** в `api`: каждый модуль в `internal/<module>` — `handler` (HTTP) → `service`
(операции приложения) → `store` (ручной SQL через pgx). Узкие порты отделяют изменяемые операции
от адаптеров; миграция существующих модулей постепенная. Read-проекции могут делать JOIN между областями.
- **Contract-first**: `api/openapi.yaml` — источник истины. Go-сервер генерирует типы и интерфейс
хендлеров (`oapi-codegen`, strict server), TS-клиент генерируется в `packages/api-client`.
Сгенерированный код совпадает с контрактом.
- **Внутренний API** `api ↔ recommender` и `api ↔ storage-agent` — HTTP/JSON во внутренней docker-сети,
реализован Go/Python DTO и обработчиками; отдельных `internal-*.yaml` сейчас нет.
- **Идемпотентность**: события и мутирующие запросы клиентов принимают `Idempotency-Key` / `event_id` (UUIDv7),
что важно для офлайн-синхронизации мобильного.
- **Деградация**: если `recommender` недоступен, `api` отдаёт волну по упрощённому fallback-алгоритму
(похожие по жанрам + лайки + случайное), приложение продолжает работать.
- **Наблюдаемость**: структурированные JSON-логи (`slog`, `structlog`), `/healthz` и `/readyz` у каждого сервиса,
метрики Prometheus `/metrics` (опционально подключаются Grafana / Prometheus).
## 6. Реализация и планы
Точки изменения после выделения обязанностей:
- `apps/web/src/player/player.ts` — фасад воспроизведения; `engine.ts`, `analysis.ts`, `media.ts`, `persistence.ts` — браузерные адаптеры. Типы состояния — `types.ts`.
- `packages/core/src/wave.ts` — общая политика refill/feedback и поколения сессий. Web и Android сохраняют собственные очереди.
- `apps/web/src/features/uploads` — API, queries, экран очереди, диалоги и настройки. Страница очереди только собирает feature.
- `services/api/internal/library`: `sources.go`, `templates.go`, `scans.go`, `schedule.go` — отдельные сценарии; `Service` сохраняет фасад для HTTP и сборки приложения.
- `services/api/internal/uploads`: `placement.go` планирует размещение через узкие порты, `persistence.go` владеет SQL-операциями и транзакциями.
- `services/api/internal/playback/personalizer.go` персонализирует каталог независимо от HTTP; `events.go` принимает события в транзакции.
`pnpm check:boundaries` проверяет новые границы без сборки приложений. Его отдельный CI workflow
запускается при изменении соответствующих областей; read-проекции и composition root не запрещены.
River и sqlc не используются. Сканер и расписание живут в процессе API; upload workers читают
состояния PostgreSQL и пробуждаются каналами/таймерами. Анализатор использует `analysis_jobs`.
Добавление устойчивой очереди или генератора SQL — отдельное архитектурное решение при появлении
соответствующей потребности, не обязательная часть текущего рефакторинга.