Files
pipu-music/docs/architecture.md
T
mrleo1nidandClaude Opus 5.5 718125b719
ci / api / api (push) Successful in 54s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 54s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 53s
ci / recommender / recommender (push) Successful in 38s
ci / ts / ts (packages + web) (push) Successful in 37s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m47s
release / release (версия и заметки) (push) Successful in 7s
release / android (APK в релиз) (push) Canceled after 4m25s
ci: split CI by area with path filters and speed up releases
- ci.yml becomes ci-api, ci-contract, ci-recommender, ci-ts, ci-mobile and
  docker workflows, each filtered by paths; docs-only commits run nothing
- docker builds all four images in one job with plain docker build on the
  runner host, so the layer cache survives between runs
- release no longer re-runs the test suite (the tagged commit already passed
  CI on main); version and release notes are one job
- the android job mounts persistent Gradle and Android SDK caches from
  /opt/ci-cache (when the runner allows the volumes) and uses --build-cache
- RELEASE_TOKEN is optional: fall back to the built-in job token
- docs: CI layout and act_runner settings (capacity, cache, valid_volumes)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 03:55:45 +03:00

11 KiB

Архитектура

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/<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.

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.

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.

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.

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, docker.yml (по областям), release.yml
├── 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. 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).