ci / module boundaries / boundaries (push) Successful in 10s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m30s
ci / api / api (push) Successful in 58s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m1s
ci / db / db (push) Failing after 25s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m36s
ci / ts / ts (packages + web) (push) Failing after 6m3s
164 lines
15 KiB
Markdown
164 lines
15 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`.
|
|
После обработки пачки отдельная очередь PostgreSQL сопоставляет её с изданиями MusicBrainz.
|
|
Несколько альбомов получают постоянные `upload_release_groups` внутри исходной пачки: отдельные задачи
|
|
и обложки, FK из `upload_files`. `uploads` группирует по идентификаторам, альбомным тегам и папкам;
|
|
применение метаданных, инвалидирование аренды и чтение обложки ограничены выбранной группой.
|
|
По явному запросу ревьюера `internal/acoustid` распознаёт локальные Chromaprint-отпечатки;
|
|
`uploads` хранит результаты отдельно от тегов и использует recording ID при сопоставлении изданий.
|
|
Адаптер `internal/musicbrainz` отвечает за HTTP, лимит запросов и Cover Art Archive; `uploads` — за
|
|
комплектность, сравнение с библиотекой и применение выбранных администратором изменений одной транзакцией.
|
|
Сбой внешнего каталога не блокирует обработку, одобрение или импорт.
|
|
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 — отдельное архитектурное решение при появлении
|
|
соответствующей потребности, не обязательная часть текущего рефакторинга.
|