# REST API Источник истины — `api/openapi.yaml` (OpenAPI 3.0.3 — лучше всего поддерживается oapi-codegen). Этот документ — обзор и соглашения. ## Соглашения - Базовый путь: `/api/v1`. Формат — JSON, `snake_case`. - Аутентификация: `Authorization: Bearer ` (кроме `/auth/login`, `/auth/refresh`, `/healthz`, `/stream/*` и `/covers/*` — у них подписанные ссылки). - Пагинация — курсорная: `?limit=50&cursor=` → `{ "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 | | POST | `/admin/users/{id}/reset-recommendations` → 204: рекомендации с нуля (профиль, статистика, показы волны) | 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`, ядро tus 1.0.0 — ADR-015) | | GET | `/uploads/mine?status=&cursor=` | moderator, admin | Мои предложения (`status` — через запятую) | | 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 · PATCH | `/admin/settings/uploads` | admin | Лимиты очереди и хранилище по умолчанию | | 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`) и тем, что сервисы не публикуют порты наружу.