ci / api / api (push) Successful in 43s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m19s
ci / desktop / rust (desktop proxy) (push) Failing after 3m34s
ci / ts / ts (packages + web) (push) Failing after 2m49s
release / release (версия и заметки) (push) Successful in 5s
ci / db / db (push) Successful in 40s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m12s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m28s
release / android (APK в релиз) (push) Failing after 2m0s
EOF Co-authored-by: Cursor <cursoragent@cursor.com>
136 lines
11 KiB
Markdown
136 lines
11 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), коллекция, состояние плеера, события прослушиваний, админка, фоновые задачи (сканер, транскодинг) через очередь 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
|
|
│ ├── 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/ 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/ # признаки и эмбеддинги (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`
|
|
(бизнес-логика, интерфейсы) → `repository` (sqlc). Модули общаются через интерфейсы сервисов,
|
|
не лезут в чужие таблицы напрямую.
|
|
- **Contract-first**: `api/openapi.yaml` — источник истины. Go-сервер генерирует типы и интерфейс
|
|
хендлеров (`oapi-codegen`, strict server), TS-клиент генерируется в `packages/api-client`.
|
|
Сгенерированный код совпадает с контрактом.
|
|
- **Внутренний 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).
|