Files
pipu-music/docs/api.md
T
mrleo1nidandClaude Opus 5.5 ebf296542c
ci / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 3m55s
ci / api (push) Successful in 51s
ci / mobile (Metro-бандл Android) (push) Canceled after 0s
release / images (services/api/Dockerfile, storage-agent, storage-agent) (push) Canceled after 0s
release / images (services/recommender/Dockerfile, recommender, ) (push) Canceled after 0s
ci / recommender (push) Successful in 1m11s
ci / ts (packages + web) (push) Canceled after 3s
release / verify (push) Successful in 1m16s
release / images (apps/web/Dockerfile, web, ) (push) Failing after 2m40s
release / android + release (push) Skipped
release / images (services/api/Dockerfile, api, api) (push) Canceled after 1s
feat: M0 monorepo skeleton with services, clients, deploy and CI
- api/openapi.yaml: contract with /healthz, /readyz, /api/v1/server-info
- services/api (Go): api with oapi-codegen strict server, goose migrations,
  readiness checks for Postgres/Redis; storage-agent stub with internal token
- services/recommender (Python, uv): FastAPI health/readiness/internal status,
  `pipu-rec serve` and `pipu-rec analyze` entrypoints
- packages: generated api-client, core (server URL + server check),
  i18n (ru/en + dictionary check), design-tokens (CSS vars + TS from tokens.json)
- apps/web: Vite + React shell showing server status, language switch
- apps/mobile: Expo (SDK 53) Android app with server connect screen
- deploy: Dockerfiles, compose (prod + dev), nginx template, install/update scripts
- .gitea/workflows: ci.yml and tag-based release.yml (images, signed APK, release)
- scripts/version.mjs: version and Android versionCode from git tag
- tests/fixtures/music/generate.sh: synthetic test library via ffmpeg
- docs: versionCode formula, OpenAPI 3.0.3, dev instructions in README

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

165 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# REST API
Источник истины — `api/openapi.yaml` (OpenAPI 3.0.3 — лучше всего поддерживается oapi-codegen). Этот документ — обзор и соглашения.
## Соглашения
- Базовый путь: `/api/v1`. Формат — JSON, `snake_case`.
- Аутентификация: `Authorization: Bearer <access_jwt>` (кроме `/auth/login`, `/auth/refresh`, `/healthz`,
`/stream/*` и `/covers/*` — у них подписанные ссылки).
- Пагинация — курсорная: `?limit=50&cursor=<opaque>` → `{ "items": [...], "next_cursor": "..." | null }`.
- Ошибки — `application/problem+json` (RFC 9457):
```json
{ "type": "about:blank", "status": 422, "code": "validation.password_too_short",
"detail": "password must be at least 8 characters", "params": { "min": 8 },
"errors": [{ "field": "new_password", "code": "validation.password_too_short" }] }
```
Клиент показывает перевод `code` (с подстановкой `params`), `detail` — только для логов.
- Мутирующие запросы с клиента могут нести `Idempotency-Key` (UUID); повтор в течение 24 ч вернёт тот же ответ.
- Время — ISO 8601 UTC. Длительности — миллисекунды (`*_ms`).
- Rate limit: `429` с `Retry-After`. `/auth/login` — 5 попыток / мин на IP+логин.
## Основные DTO
```jsonc
// TrackDto
{ "id": "…", "title": "…", "duration_ms": 215000,
"artists": [{ "id": "…", "name": "…" }],
"album": { "id": "…", "title": "…", "year": 2019, "cover_url": "/api/v1/covers/…?sig=…",
"cover_colors": ["#F2A23A", "#1C3B6E", "#35D3FF"] },
"genres": ["indie rock"],
"reaction": "like" | "dislike" | null,
"stream_url": "/api/v1/stream/…?q=auto&exp=…&sig=…",
"features": { "bpm": 128, "energy": 0.72 } // для анимации волны, опционально
}
```
## Эндпоинты
### Служебные
| Метод | Путь | Описание |
|---|---|---|
| GET | `/healthz`, `/readyz` | Живость / готовность |
| GET | `/api/v1/server-info` | Версия сервера, мин. версия клиента, поддерживаемые локали, `bootstrap_required` |
### Аутентификация — [auth-and-roles.md](auth-and-roles.md)
| Метод | Путь | Кто | Описание |
|---|---|---|---|
| POST | `/auth/login` | все | `{username, password, client_type, device_name}` → `{access_token, expires_in, refresh_token?, user}`; веб получает refresh в cookie |
| POST | `/auth/refresh` | все | Ротация refresh → новая пара |
| POST | `/auth/logout` | auth | Отзыв текущей семьи токенов |
| POST | `/auth/logout-all` | auth | Отзыв всех сессий пользователя |
| GET | `/me` | auth | Текущий пользователь, флаги `must_change_*` |
| PATCH | `/me` | auth | `display_name`, `locale`, `ui_prefs` (тема, вид волны и плеера, ambient, кроссфейд, нормализация) |
| POST | `/me/password` | auth | `{current_password, new_password}`; отзывает прочие сессии |
| POST | `/me/bootstrap-credentials` | admin c `must_change_credentials` | `{new_username, new_password}` |
| GET | `/me/sessions` · DELETE `/me/sessions/{id}` | auth | Активные устройства |
### Каталог
| Метод | Путь | Описание |
|---|---|---|
| GET | `/artists/{id}` | Исполнитель + счётчики |
| GET | `/artists/{id}/albums` | Альбомы |
| GET | `/artists/{id}/top-tracks` | Популярные (по прослушиваниям всех пользователей) |
| GET | `/albums/{id}` | Альбом с треками |
| GET | `/tracks/{id}` | Трек |
| POST | `/tracks/batch` | `{ids:[]}` → треки (для восстановления очереди) |
| GET | `/covers/{albumId}?size=64|300|1200&sig=` | Обложка (WebP, X-Accel) |
### Поиск — [library-and-storage.md](library-and-storage.md#поиск)
| Метод | Путь | Описание |
|---|---|---|
| GET | `/search/suggest?q=` | Выпадающий список: до 3 исполнителей, 3 альбомов, 5 треков; < 50 мс |
| GET | `/search?q=&type=all|artists|albums|tracks&limit=&cursor=` | Полная страница |
| POST | `/search/click` | `{q, entity_type, entity_id}` — сигнал для ранжирования / рекомендаций |
### Воспроизведение — [playback.md](playback.md)
| Метод | Путь | Описание |
|---|---|---|
| GET | `/stream/{trackId}?q=auto|original|high|medium|low&exp&sig` | Аудио (X-Accel, Range) |
| GET | `/tracks/{id}/download-url?q=` | Подписанная ссылка для офлайн-загрузки |
| GET | `/me/playback-state` | Последний трек, позиция, очередь |
| PUT | `/me/playback-state` | Сохранить (клиент шлёт раз в 10 с и на паузе / закрытии; `If-Unmodified-Since` для конфликтов) |
| POST | `/events` | Пачка событий `{events:[{id, type, track_id, occurred_at, ...}]}` до 500 шт. → `{accepted, duplicates}` |
### Коллекция
| Метод | Путь | Описание |
|---|---|---|
| GET | `/me/history?cursor=` | История (сгруппирована по дням на клиенте) |
| DELETE | `/me/history` | Очистить историю (события остаются для статистики, помечаются скрытыми) |
| GET | `/me/likes?cursor=` · `/me/dislikes?cursor=` | Списки |
| PUT | `/me/reactions/tracks/{id}` | `{reaction: "like"|"dislike"}` |
| DELETE | `/me/reactions/tracks/{id}` | Снять реакцию |
| PUT/DELETE | `/me/reactions/artists/{id}` | Реакция на исполнителя |
### Моя волна — [recommendations.md](recommendations.md)
| Метод | Путь | Описание |
|---|---|---|
| GET | `/wave/options` | Доступные занятия / настроения (ключи для i18n, иконки) |
| POST | `/wave/sessions` | `{activity?, mood?, seed?: {type, id}}` → `{session_id, tracks:[5 TrackDto]}` |
| POST | `/wave/sessions/{id}/next` | `{feedback:[{track_id, outcome}]}` → следующая пачка |
| PATCH | `/wave/sessions/{id}` | Сменить занятие / настроение на лету |
| DELETE | `/wave/sessions/{id}` | Завершить |
### Мобильное обновление — [mobile.md](mobile.md#самообновление)
| Метод | Путь | Описание |
|---|---|---|
| GET | `/app/android/latest` | `{version, version_code, notes_md, size, sha256, download_url, min_supported}` |
| GET | `/app/android/download/{version}` | APK (прокси / кэш релиза Gitea) |
### Админка — права см. [auth-and-roles.md](auth-and-roles.md#матрица-прав)
| Метод | Путь | Кто |
|---|---|---|
| GET | `/admin/users?q=&role=&cursor=` | admin, moderator |
| POST | `/admin/users` `{username, display_name, role}` → `{user, temporary_password}` (пароль генерирует сервер, `must_change_password = true`) | moderator: только `role=user`; admin: любые |
| PATCH | `/admin/users/{id}` `{display_name?, role?, is_blocked?}` | role — только admin |
| POST | `/admin/users/{id}/reset-password` → `{temporary_password}` | moderator — только для user |
| DELETE | `/admin/users/{id}` | admin |
| GET/POST | `/admin/sources` | admin |
| GET/PATCH/DELETE | `/admin/sources/{id}` | admin |
| POST | `/admin/sources/{id}/test` | admin — проверить монтирование и доступ |
| PUT | `/admin/sources/{id}/patterns` `{patterns:[{regex, is_enabled}]}` | admin |
| POST | `/admin/sources/{id}/patterns/preview` `{patterns, sample?:50}` | admin — разбор реальных путей без сохранения |
| POST | `/admin/sources/{id}/scan` `{full?: bool}` | admin |
| GET | `/admin/library/status` | admin, moderator — счётчики, прогресс скана / анализа, ошибки |
| GET | `/admin/library/errors?cursor=` | admin |
| POST | `/admin/search/reindex` | admin |
| GET | `/admin/recommender/status` | admin — версии моделей, метрики |
| GET | `/admin/settings` · PATCH `/admin/settings` | admin — лимиты очереди, хранилище по умолчанию и пр. |
| POST | `/admin/sources/{id}/write-template/suggest` → `{template, matched_ratio}` | admin — автоподбор шаблона раскладки |
| POST | `/admin/sources/{id}/write-template/verify` `{template}` → `{ok, roundtrip_ratio, mismatches[]}` | admin — проверка обратимости |
### Предложение треков — [uploads.md](uploads.md)
| Метод | Путь | Кто | Описание |
|---|---|---|---|
| GET | `/uploads/quota` | moderator, admin | `{limit_bytes, used_bytes, per_user_limit_bytes, user_used_bytes, max_file_bytes}` |
| POST | `/uploads/precheck` | moderator, admin | `{files:[{client_id, size, sha256, tags:{artist,title,album,duration_ms}}]}` → для каждого `accepted` / `duplicate` (+ `track`) / `too_large`; ничего не резервирует |
| POST | `/uploads` | moderator, admin | Создать пачку `{comment, files:[{client_id, name, rel_path, size, sha256}]}` → резерв квоты; `{batch_id, files:[{client_id, upload_file_id, tus_url} | {client_id, rejected: code}]}`; при нехватке места — `uploads.queue_full` для не поместившихся |
| tus | `/uploads/files/{upload_file_id}` | автор | Resumable-загрузка (`HEAD`/`PATCH`) |
| GET | `/uploads/mine?status=&cursor=` | moderator, admin | Мои предложения |
| DELETE | `/uploads/files/{id}` | автор | Отозвать (`withdrawn`), только до решения |
| GET | `/admin/uploads?status=&author=&possible_duplicate=&cursor=` | admin | Очередь, сгруппированная по пачкам |
| GET | `/admin/uploads/files/{id}/stream?sig=` | admin | Прослушивание из staging |
| PATCH | `/admin/uploads/files/{id}` | admin | Правка `metadata`, `target_source_id`; ответ содержит пересчитанный `target_rel_path` |
| PATCH | `/admin/uploads/batches/{id}` | admin | Правка общих полей для всех файлов пачки (исполнитель, альбом, год, обложка) |
| POST | `/admin/uploads/review` | admin | `{file_ids:[], decision: "approve"|"reject", reason?}` — массовое решение |
| POST | `/admin/uploads/files/{id}/retry` | admin | Повторить импорт после `import_failed` |
| GET | `/admin/audit?cursor=` | admin |
## Внутренние API
`api → recommender` (`internal-recommender.yaml`):
- `POST /internal/wave/batch` `{user_id, session_id, activity, mood, seed, exclude_ids, feedback, n}` → `{tracks:[{id, source, score}], model_ver}`
- `GET /internal/similar/tracks/{id}`, `GET /internal/similar/artists/{id}`
- `GET /internal/status`
`api → storage-agent` (`internal-storage.yaml`):
- `POST /internal/mounts` `{source_id, type, server, export, options}` → `{status, message}`
- `DELETE /internal/mounts/{source_id}`
- `GET /internal/mounts` — состояние всех точек
- `POST /internal/files` `{source_id, rel_path, staging_path, sha256}` — безопасная запись файла в хранилище
(tmp → fsync → проверка хэша → atomic rename); только для `accepts_uploads` источников
- `GET /internal/files/exists?source_id=&rel_path=` — проверка конфликта пути
Внутренние вызовы защищены общим секретом (`INTERNAL_TOKEN`) и тем, что сервисы не публикуют порты наружу.