# Архитектура ## 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/ 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), коллекция, состояние плеера, события прослушиваний, админка, фоновые задачи: сканер внутри процесса, upload-воркеры со статусами в PostgreSQL. | | `storage-agent` | Go | Единственный привилегированный контейнер: монтирует / размонтирует NFS-шары в `/library/`, проверяет доступность, **записывает одобренные загрузки** в принимающие хранилища (только создание новых файлов). Принимает команды только от `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//` (или `/_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/` — `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 — отдельное архитектурное решение при появлении соответствующей потребности, не обязательная часть текущего рефакторинга.