# Архитектура ## 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/ recommender storage-agent /cache/transcode (Python) (Go, CAP_SYS_ADMIN) ▲ │ │ mount -t nfs │ ▼ ▼ │ analyzer /library/ └────────── (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/`, проверяет доступность, **записывает одобренные загрузки** в принимающие хранилища (только создание новых файлов). Принимает команды только от `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//` (или `/_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/` — `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).