openapi: 3.0.3 info: title: Pipu Music API version: 0.1.0 description: | Публичный REST API Pipu Music. Источник истины для сервера (oapi-codegen) и клиентов (openapi-typescript). Соглашения — docs/api.md. servers: - url: / tags: - name: system - name: auth - name: me - name: admin - name: library - name: catalog - name: search - name: playback - name: collection - name: wave - name: app security: - bearerAuth: [] paths: /api/v1/me/social/recommendations: put: operationId: setFriendsRecommendations tags: [me] requestBody: required: true content: application/json: schema: type: object required: [enabled] properties: enabled: {type: boolean} responses: '204': {description: Saved} default: {$ref: '#/components/responses/Problem'} /api/v1/me/social/listening: get: operationId: getListeningState tags: [me] responses: '200': description: Listening preferences, current room and pending requests content: application/json: schema: {$ref: '#/components/schemas/ListeningState'} default: {$ref: '#/components/responses/Problem'} /api/v1/me/social/listening/preferences: put: operationId: setListeningPreferences tags: [me] requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/ListeningPreferences'} responses: '204': {description: Saved} default: {$ref: '#/components/responses/Problem'} /api/v1/me/social/listening/actions: post: operationId: listeningAction tags: [me] description: Join requests target the host; invites target a guest and use the sender's own stream. Accept/decline/cancel target a request UUID. Leave/end target the room UUID. Automatic acceptance always respects the receiving user's admission policy. No control transfer. requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/ListeningAction'} responses: '204': {description: Applied} default: {$ref: '#/components/responses/Problem'} /api/v1/me/social/listening/state: put: operationId: publishListeningState tags: [me] description: Only the room owner's source device may publish. Monotonic sequence prevents stale updates; room expires after 30 seconds without host heartbeat. requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/ListeningRoomInput'} responses: '204': {description: Applied} default: {$ref: '#/components/responses/Problem'} /api/v1/me/social/activity: put: operationId: publishListeningActivity tags: [me] description: Ephemeral playback lease, expires after 90 seconds. Ignored when visibility is nobody. Sequence is monotonic per device; paused updates clear only that device. No offline replay. requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/ListeningActivityInput'} responses: '204': {description: Applied} default: {$ref: '#/components/responses/Problem'} /api/v1/me/social/activity/visibility: put: operationId: setActivityVisibility tags: [me] description: Applies to all devices. Nobody clears all current activity immediately. requestBody: required: true content: application/json: schema: type: object required: [visibility] properties: visibility: {$ref: '#/components/schemas/ActivityVisibility'} responses: '204': {description: Saved} default: {$ref: '#/components/responses/Problem'} /api/v1/social/activity/{userId}/playlist: get: operationId: getActivityPlaylist tags: [me] description: Read-only current playlist subject to live visibility, friendship, blocking and expiry. Does not create or reveal a permanent share token. parameters: - in: path name: userId required: true schema: {type: string, format: uuid} responses: '200': description: Read-only playlist content: application/json: schema: {$ref: '#/components/schemas/Playlist'} default: {$ref: '#/components/responses/Problem'} /api/v1/me/social: get: operationId: getSocial tags: [me] description: Friends, incoming/outgoing requests, own blocks and up to 100 notifications, unread first. No private account fields. responses: '200': description: Social inbox content: application/json: schema: {$ref: '#/components/schemas/SocialInbox'} default: {$ref: '#/components/responses/Problem'} /api/v1/me/social/people: get: operationId: searchPeople tags: [me] parameters: - in: query name: q required: true schema: {type: string, minLength: 2, maxLength: 80} responses: '200': description: Up to 30 people, excluding blocked accounts and either direction of personal blocking content: application/json: schema: type: array items: {$ref: '#/components/schemas/SocialPerson'} default: {$ref: '#/components/responses/Problem'} /api/v1/me/social/actions: post: operationId: socialAction tags: [me] description: Request is idempotent; accept is recipient-only. Remove cancels, declines or unfriends. Block removes friendship. Unblock never restores it. Read chat acknowledges only messages through up_to_seq. Read notification uses notification ID as target_id. requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/SocialAction'} responses: '204': {description: Applied} default: {$ref: '#/components/responses/Problem'} /api/v1/me/social/chats/{peerId}: parameters: - in: path name: peerId required: true schema: {type: string, format: uuid} get: operationId: getDirectMessages tags: [me] description: Last 50 messages in ascending sequence order. Before loads older history. Requires current friendship, never marks messages read. parameters: - in: query name: before schema: {type: integer, format: int64, minimum: 1} - in: query name: q schema: {type: string, maxLength: 100} responses: '200': description: Message page content: application/json: schema: type: array items: {$ref: '#/components/schemas/DirectMessage'} default: {$ref: '#/components/responses/Problem'} post: operationId: sendDirectMessage tags: [me] description: Friends only. Client-generated UUID makes retries idempotent; changed payload for the same ID returns 409. Playlist attachment is its active share UUID; track attachment is a catalog UUID. No arbitrary URLs. requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/SendDirectMessage'} responses: '200': description: Stored message content: application/json: schema: {$ref: '#/components/schemas/DirectMessage'} default: {$ref: '#/components/responses/Problem'} /api/v1/me/social/chats/{peerId}/meta: parameters: - in: path name: peerId required: true schema: {type: string, format: uuid} get: operationId: getChatMeta tags: [me] responses: '200': description: Ephemeral typing indicator, friends only content: application/json: schema: type: object required: [typing] properties: typing: {type: boolean} default: {$ref: '#/components/responses/Problem'} put: operationId: setChatTyping tags: [me] requestBody: required: true content: application/json: schema: type: object required: [typing] properties: typing: {type: boolean} responses: '204': {description: Applied, lease expires after 6 seconds} default: {$ref: '#/components/responses/Problem'} /api/v1/me/social/chats/{peerId}/messages/{messageId}: patch: operationId: updateChatMessage tags: [me] description: Friends only. Edit/delete are sender-only. Delete removes content for both participants; react toggles one allowed emoji. Reply targets must belong to the same conversation. parameters: - in: path name: peerId required: true schema: {type: string, format: uuid} - in: path name: messageId required: true schema: {type: string, format: uuid} requestBody: required: true content: application/json: schema: type: object required: [action] properties: action: {type: string, enum: [edit, delete, react]} text: {type: string, maxLength: 4000} emoji: {type: string, enum: ['❤️', '👍', '🔥', '😂', '🎵', '👏']} responses: '204': {description: Applied} default: {$ref: '#/components/responses/Problem'} /api/v1/admin/server/updates: get: operationId: getServerUpdates tags: [admin] parameters: - in: query name: source_id schema: {type: string} - in: query name: channel schema: {type: string, enum: [latest, version, main], default: latest} - in: query name: version schema: {type: string} - in: query name: fresh schema: {type: boolean, default: false} responses: "200": description: Server management result content: application/json: schema: $ref: "#/components/schemas/ServerUpdates" default: $ref: "#/components/responses/Problem" post: operationId: startServerUpdate tags: [admin] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ServerUpdateRequest" responses: "202": description: Server management result content: application/json: schema: $ref: "#/components/schemas/ServerJob" default: $ref: "#/components/responses/Problem" /api/v1/admin/server/update-sources: put: operationId: saveServerUpdateSources tags: [admin] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ServerSources" responses: "200": description: Server management result content: application/json: schema: $ref: "#/components/schemas/ServerSources" default: $ref: "#/components/responses/Problem" /api/v1/admin/server/config: get: operationId: getServerConfig tags: [admin] responses: "200": description: Server management result content: application/json: schema: $ref: "#/components/schemas/ServerConfig" default: $ref: "#/components/responses/Problem" patch: operationId: saveServerConfig tags: [admin] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ServerConfigPatch" responses: "200": description: Server management result content: application/json: schema: $ref: "#/components/schemas/ServerConfig" default: $ref: "#/components/responses/Problem" /api/v1/admin/server/config/apply: post: operationId: applyServerConfig tags: [admin] responses: "202": description: Server management result content: application/json: schema: $ref: "#/components/schemas/ServerJob" default: $ref: "#/components/responses/Problem" /api/v1/admin/genius: get: operationId: getGeniusStats tags: [admin] responses: "200": description: Persistent enrichment statistics (admin only) content: application/json: schema: $ref: "#/components/schemas/GeniusStats" post: operationId: controlGenius tags: [admin] requestBody: required: true content: application/json: schema: type: object required: [action] properties: action: type: string enum: [start, pause, retry] responses: "200": description: Updated statistics content: application/json: schema: $ref: "#/components/schemas/GeniusStats" /api/v1/tracks/{id}/genius: parameters: - $ref: "#/components/parameters/EntityId" get: operationId: getTrackGenius tags: [catalog] responses: "200": description: Cached lyrics and annotations; never makes an upstream request content: application/json: schema: $ref: "#/components/schemas/GeniusContent" /healthz: get: operationId: getHealthz tags: [system] security: [] summary: Живость процесса responses: "200": description: Процесс жив content: application/json: schema: $ref: "#/components/schemas/HealthStatus" /readyz: get: operationId: getReadyz tags: [system] security: [] summary: Готовность обслуживать запросы (зависимости доступны) responses: "200": description: Все зависимости доступны content: application/json: schema: $ref: "#/components/schemas/ReadyStatus" "503": description: Одна или несколько зависимостей недоступны content: application/json: schema: $ref: "#/components/schemas/ReadyStatus" /api/v1/server-info: get: operationId: getServerInfo tags: [system] security: [] summary: Сведения о сервере для клиентов description: | Вызывается без аутентификации: веб и Android проверяют версию сервера, совместимость клиента и поддерживаемые языки (экран подключения). responses: "200": description: Сведения о сервере content: application/json: schema: $ref: "#/components/schemas/ServerInfo" default: $ref: "#/components/responses/Problem" /api/v1/auth/login: post: operationId: login tags: [auth] security: [] summary: Вход по логину и паролю description: | Веб (`client_type: web`) получает refresh-токен в cookie `pipu_rt` (`HttpOnly; Secure; SameSite=Strict; Path=/api/v1/auth`), Android — в поле `refresh_token`. Неверный логин или пароль, а также временно заблокированный перебором вход — всегда `401 auth.invalid_credentials`. Превышен лимит попыток с адреса — `429 auth.too_many_attempts`. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/LoginRequest" responses: "200": description: Успешный вход content: application/json: schema: $ref: "#/components/schemas/AuthTokens" default: $ref: "#/components/responses/Problem" /api/v1/auth/refresh: post: operationId: refreshTokens tags: [auth] security: [] summary: Ротация refresh-токена description: | Refresh-токен берётся из cookie `pipu_rt` (веб) или из тела (Android). Старый токен становится использованным; его повторное предъявление отзывает всю семью токенов. parameters: - name: pipu_rt in: cookie required: false schema: type: string requestBody: required: false content: application/json: schema: $ref: "#/components/schemas/RefreshRequest" responses: "200": description: Новая пара токенов content: application/json: schema: $ref: "#/components/schemas/AuthTokens" default: $ref: "#/components/responses/Problem" /api/v1/auth/logout: post: operationId: logout tags: [auth] summary: Выход — отзыв текущей сессии responses: "204": description: Сессия завершена, cookie `pipu_rt` очищена default: $ref: "#/components/responses/Problem" /api/v1/auth/logout-all: post: operationId: logoutAll tags: [auth] summary: Выход на всех устройствах responses: "204": description: Все сессии пользователя завершены default: $ref: "#/components/responses/Problem" /api/v1/me: get: operationId: getMe tags: [me] summary: Текущий пользователь responses: "200": description: Профиль content: application/json: schema: $ref: "#/components/schemas/Me" default: $ref: "#/components/responses/Problem" patch: operationId: updateMe tags: [me] summary: Изменить имя, язык, настройки интерфейса description: Поля `ui_prefs` сливаются с сохранёнными; не переданные поля не меняются. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateMeRequest" responses: "200": description: Обновлённый профиль content: application/json: schema: $ref: "#/components/schemas/Me" default: $ref: "#/components/responses/Problem" /api/v1/me/password: post: operationId: changePassword tags: [me] summary: Сменить пароль description: | `current_password` обязателен, кроме случая `must_change_password` (вход по временному паролю). Все сессии отзываются; текущему устройству выдаётся новая пара токенов. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ChangePasswordRequest" responses: "200": description: Пароль изменён, новая пара токенов content: application/json: schema: $ref: "#/components/schemas/AuthTokens" default: $ref: "#/components/responses/Problem" /api/v1/me/bootstrap-credentials: post: operationId: setBootstrapCredentials tags: [me] summary: Первый запуск — новые логин и пароль администратора description: | Только при `must_change_credentials`. Пароль должен отличаться от текущего. Все прочие сессии отзываются, выдаётся новая пара токенов. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/BootstrapCredentialsRequest" responses: "200": description: Учётные данные заменены, новая пара токенов content: application/json: schema: $ref: "#/components/schemas/AuthTokens" default: $ref: "#/components/responses/Problem" /api/v1/me/sessions: get: operationId: listMySessions tags: [me] summary: Активные сессии (устройства) responses: "200": description: Список сессий, новые сверху content: application/json: schema: $ref: "#/components/schemas/SessionList" default: $ref: "#/components/responses/Problem" /api/v1/me/sessions/{id}: delete: operationId: revokeMySession tags: [me] summary: Завершить сессию на устройстве parameters: - name: id in: path required: true schema: type: string format: uuid responses: "204": description: Сессия завершена default: $ref: "#/components/responses/Problem" /api/v1/admin/users: get: operationId: listUsers tags: [admin] summary: Пользователи (admin, moderator) description: Сортировка по логину. `q` ищет по логину и имени без учёта регистра. parameters: - name: q in: query schema: type: string maxLength: 64 - name: role in: query schema: $ref: "#/components/schemas/Role" - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Cursor" responses: "200": description: Страница пользователей content: application/json: schema: $ref: "#/components/schemas/AdminUserPage" default: $ref: "#/components/responses/Problem" post: operationId: createUser tags: [admin] summary: Создать пользователя description: | Пароль генерирует сервер и возвращает **один раз**; при первом входе его нужно сменить. Модератор создаёт только `user`, админ — любые роли. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateUserRequest" responses: "201": description: Пользователь создан content: application/json: schema: $ref: "#/components/schemas/CreatedUser" default: $ref: "#/components/responses/Problem" /api/v1/admin/sources/{id}/cover-stats: get: operationId: getSourceCoverStats tags: [admin] summary: Покрытие аудиофайлов локальными обложками (admin) parameters: - $ref: "#/components/parameters/SourceId" responses: "200": description: Доступные аудиофайлы без исключённых папок; внешние обложки не учитываются content: application/json: schema: $ref: "#/components/schemas/SourceCoverStats" default: $ref: "#/components/responses/Problem" /api/v1/admin/users/export: get: operationId: exportUsers tags: [admin] summary: Экспорт учётных записей с ролями и хэшами паролей (admin) description: Только учётные записи и права доступа, без рекомендаций, истории, лайков и сессий. Ответ не кэшируется. responses: "200": description: Файл переноса пользователей content: application/json: schema: $ref: "#/components/schemas/UserArchive" default: $ref: "#/components/responses/Problem" /api/v1/admin/users/import: post: operationId: importUsers tags: [admin] summary: Импорт учётных записей с сохранением паролей (admin) description: | Атомарное добавление пользователей из экспорта, до 10000 записей и 16 MiB. Существующие логины пропускаются без изменений. Новые записи получают новые ID. Рекомендации, история, лайки и сессии не импортируются. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UserArchive" responses: "200": description: Результат импорта content: application/json: schema: $ref: "#/components/schemas/UserImportResult" default: $ref: "#/components/responses/Problem" /api/v1/admin/users/{id}: parameters: - $ref: "#/components/parameters/UserId" patch: operationId: updateUser tags: [admin] summary: Изменить имя, роль, блокировку description: | Роль меняет только админ. Смена роли и блокировка мгновенно отзывают токены пользователя. Последнего активного админа нельзя понизить или заблокировать — `409 users.last_admin`. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateUserRequest" responses: "200": description: Обновлённый пользователь content: application/json: schema: $ref: "#/components/schemas/AdminUser" default: $ref: "#/components/responses/Problem" delete: operationId: deleteUser tags: [admin] summary: Удалить пользователя (admin) responses: "204": description: Удалён default: $ref: "#/components/responses/Problem" /api/v1/admin/users/{id}/reset-password: parameters: - $ref: "#/components/parameters/UserId" post: operationId: resetUserPassword tags: [admin] summary: Сбросить пароль description: Новый временный пароль возвращается один раз; все сессии пользователя завершаются. responses: "200": description: Временный пароль content: application/json: schema: $ref: "#/components/schemas/TemporaryPassword" default: $ref: "#/components/responses/Problem" /api/v1/admin/users/{id}/reset-recommendations: parameters: - $ref: "#/components/parameters/UserId" post: operationId: resetUserRecommendations tags: [admin] summary: Сбросить рекомендации description: | Рекомендации начинаются с нуля: стираются профиль вкуса (онбординг, CF-вектор, бандит), статистика прослушиваний и показы волны; онбординг покажется снова. Лайки, дизлайки и история остаются. responses: "204": description: Сброшено default: $ref: "#/components/responses/Problem" /api/v1/uploads/quota: get: operationId: getUploadQuota tags: [uploads] summary: Квота очереди на добавление (moderator, admin) responses: "200": description: Лимиты и занятое место content: application/json: schema: $ref: "#/components/schemas/UploadQuota" default: $ref: "#/components/responses/Problem" /api/v1/uploads/precheck: post: operationId: precheckUploads tags: [uploads] summary: Предпроверка дублей до загрузки (moderator, admin) description: Ничего не резервирует. SHA-256 и теги клиент считает сам (docs/uploads.md#4). requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UploadPrecheckRequest" responses: "200": description: Итог по каждому файлу content: application/json: schema: $ref: "#/components/schemas/UploadPrecheckResult" default: $ref: "#/components/responses/Problem" /api/v1/uploads: post: operationId: createUploadBatch tags: [uploads] summary: Создать пачку и зарезервировать место (moderator, admin) description: | Для принятых файлов — `tus_url` (tus 1.0.0, только HEAD / PATCH, куски до 64 МБ). Не поместившиеся — `uploads.queue_full` / `uploads.user_quota_full`, дубли — `uploads.duplicate`, без места в квоте. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UploadBatchRequest" responses: "201": description: Пачка создана content: application/json: schema: $ref: "#/components/schemas/UploadBatchCreated" default: $ref: "#/components/responses/Problem" /api/v1/uploads/mine: get: operationId: listMyUploads tags: [uploads] summary: Мои предложения (moderator, admin) parameters: - $ref: "#/components/parameters/UploadStatusFilter" - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Cursor" responses: "200": description: Страница предложений content: application/json: schema: $ref: "#/components/schemas/UploadFilePage" default: $ref: "#/components/responses/Problem" /api/v1/uploads/unseen: get: operationId: unseenUploadDecisions tags: [uploads] summary: Сколько решений по своим предложениям с прошлого визита (moderator, admin) responses: "200": description: Число content: application/json: schema: type: object required: [count] properties: count: type: integer minimum: 0 default: $ref: "#/components/responses/Problem" /api/v1/uploads/seen: post: operationId: markUploadsSeen tags: [uploads] summary: Отметить свои предложения просмотренными responses: "204": description: Отмечено default: $ref: "#/components/responses/Problem" /api/v1/uploads/files/{id}: parameters: - $ref: "#/components/parameters/EntityId" delete: operationId: withdrawUpload tags: [uploads] summary: Отозвать своё предложение до решения description: HEAD / PATCH по этому же адресу — приём файла по tus (вне контракта OpenAPI). responses: "204": description: Отозвано, место освобождено default: $ref: "#/components/responses/Problem" /api/v1/admin/uploads: get: operationId: listUploads tags: [uploads] summary: Очередь на добавление (admin) parameters: - $ref: "#/components/parameters/UploadStatusFilter" - name: author in: query schema: type: string format: uuid - name: batch in: query schema: type: string format: uuid - name: possible_duplicate in: query schema: type: boolean - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Cursor" responses: "200": description: Страница очереди content: application/json: schema: $ref: "#/components/schemas/UploadFilePage" default: $ref: "#/components/responses/Problem" /api/v1/admin/uploads/files/{id}: parameters: - $ref: "#/components/parameters/EntityId" patch: operationId: updateUpload tags: [uploads] summary: Править метаданные и хранилище (admin) description: Путь в хранилище пересчитывается; правленые теги записываются в файл при импорте. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UploadPatch" responses: "200": description: Файл content: application/json: schema: $ref: "#/components/schemas/UploadFile" default: $ref: "#/components/responses/Problem" /api/v1/admin/uploads/files/{id}/retry: parameters: - $ref: "#/components/parameters/EntityId" post: operationId: retryUploadImport tags: [uploads] summary: Повторить импорт после import_failed (admin) responses: "200": description: Файл снова одобрен content: application/json: schema: $ref: "#/components/schemas/UploadFile" default: $ref: "#/components/responses/Problem" /api/v1/admin/uploads/files/{id}/stream: get: operationId: streamUpload tags: [uploads] security: [] summary: Прослушать файл из staging (подпись, как у стрима трека) description: Ссылку даёт `stream_url` файла очереди. Range поддерживается. parameters: - $ref: "#/components/parameters/EntityId" - name: exp in: query required: true schema: type: integer format: int64 - name: sig in: query required: true schema: type: string responses: "200": description: Аудио из staging content: audio/*: schema: type: string format: binary default: $ref: "#/components/responses/Problem" /api/v1/admin/uploads/batches/{id}: parameters: - $ref: "#/components/parameters/EntityId" patch: operationId: updateUploadBatch tags: [uploads] summary: Правка метаданных и обложки на всю пачку (admin) description: Исполнитель, альбом и год применяются к файлам в pending_review и import_failed. Обложка встраивается при импорте. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UploadBatchPatch" responses: "200": description: Обновлённые файлы пачки content: application/json: schema: type: object required: [items] properties: items: type: array items: $ref: "#/components/schemas/UploadFile" default: $ref: "#/components/responses/Problem" /api/v1/admin/uploads/review: post: operationId: reviewUploads tags: [uploads] summary: Одобрить / отклонить файлы (admin) requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UploadReviewRequest" responses: "200": description: Итог по каждому файлу content: application/json: schema: $ref: "#/components/schemas/UploadReviewResult" default: $ref: "#/components/responses/Problem" /api/v1/admin/settings/uploads: get: operationId: getUploadSettings tags: [uploads] summary: Настройки очереди (admin) responses: "200": description: Настройки content: application/json: schema: $ref: "#/components/schemas/UploadSettings" default: $ref: "#/components/responses/Problem" patch: operationId: updateUploadSettings tags: [uploads] summary: Изменить настройки очереди (admin) description: Уменьшение лимита ниже занятого ничего не удаляет — новые загрузки ждут, пока очередь освободится. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UploadSettingsPatch" responses: "200": description: Настройки content: application/json: schema: $ref: "#/components/schemas/UploadSettings" default: $ref: "#/components/responses/Problem" /api/v1/admin/audit: get: operationId: listAudit tags: [admin] summary: Журнал действий (admin), новые сверху parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Cursor" responses: "200": description: Страница журнала content: application/json: schema: $ref: "#/components/schemas/AuditPage" default: $ref: "#/components/responses/Problem" /api/v1/admin/sources: get: operationId: listSources tags: [library] summary: Хранилища (admin) responses: "200": description: Список хранилищ content: application/json: schema: $ref: "#/components/schemas/SourceList" default: $ref: "#/components/responses/Problem" post: operationId: createSource tags: [library] summary: Подключить хранилище (admin) description: | `local` — подпапка в каталоге LIBRARY_HOST сервера; `nfs` — монтирует storage-agent. Хранилище сохраняется, даже если подключиться не удалось: статус и сообщение — в ответе. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SourceInput" responses: "201": description: Хранилище создано content: application/json: schema: $ref: "#/components/schemas/Source" default: $ref: "#/components/responses/Problem" /api/v1/admin/sources/{id}: parameters: - $ref: "#/components/parameters/SourceId" get: operationId: getSource tags: [library] summary: Хранилище (admin) responses: "200": description: Хранилище content: application/json: schema: $ref: "#/components/schemas/Source" default: $ref: "#/components/responses/Problem" patch: operationId: updateSource tags: [library] summary: Изменить хранилище (admin) description: Изменение адреса NFS или папки переподключает хранилище. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SourcePatch" responses: "200": description: Хранилище content: application/json: schema: $ref: "#/components/schemas/Source" default: $ref: "#/components/responses/Problem" delete: operationId: deleteSource tags: [library] summary: Отключить и удалить хранилище (admin) description: Треки, файлы которых были только в этом хранилище, удаляются из каталога. responses: "204": description: Удалено default: $ref: "#/components/responses/Problem" /api/v1/admin/sources/{id}/test: parameters: - $ref: "#/components/parameters/SourceId" post: operationId: testSource tags: [library] summary: Проверить подключение (admin) responses: "200": description: Результат проверки content: application/json: schema: $ref: "#/components/schemas/SourceTestResult" default: $ref: "#/components/responses/Problem" /api/v1/admin/sources/{id}/patterns: parameters: - $ref: "#/components/parameters/SourceId" get: operationId: getSourcePatterns tags: [library] summary: Шаблоны путей (admin) responses: "200": description: Шаблоны по порядку content: application/json: schema: $ref: "#/components/schemas/PatternList" default: $ref: "#/components/responses/Problem" put: operationId: putSourcePatterns tags: [library] summary: Заменить шаблоны путей (admin) description: Ошибка компиляции — `422 validation.pattern_invalid` с `params.index` и `params.reason`. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PatternList" responses: "200": description: Сохранённые шаблоны content: application/json: schema: $ref: "#/components/schemas/PatternList" default: $ref: "#/components/responses/Problem" /api/v1/admin/sources/{id}/patterns/preview: parameters: - $ref: "#/components/parameters/SourceId" post: operationId: previewSourcePatterns tags: [library] summary: Предпросмотр разбора путей без сохранения (admin) requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PatternPreviewRequest" responses: "200": description: Разбор выборки файлов content: application/json: schema: $ref: "#/components/schemas/PatternPreview" default: $ref: "#/components/responses/Problem" /api/v1/admin/sources/{id}/write-template/suggest: parameters: - $ref: "#/components/parameters/SourceId" post: operationId: suggestWriteTemplate tags: [library] summary: Подобрать шаблон пути по существующей структуре (admin) description: | Выражение хранилища, совпавшее с наибольшим числом путей выборки, превращается в шаблон. Не удалось (нет файлов или выражения не переводятся) — `422 library.template_unavailable`. responses: "200": description: Предложенный шаблон content: application/json: schema: $ref: "#/components/schemas/WriteTemplateSuggestion" default: $ref: "#/components/responses/Problem" /api/v1/admin/sources/{id}/write-template/check: parameters: - $ref: "#/components/parameters/SourceId" post: operationId: checkWriteTemplate tags: [library] summary: Проверить шаблон на обратимость (admin) description: Шаблон применяется к 50 трекам хранилища, пути разбираются его выражениями обратно. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/WriteTemplateCheckRequest" responses: "200": description: Итог проверки content: application/json: schema: $ref: "#/components/schemas/WriteTemplateCheck" default: $ref: "#/components/responses/Problem" /api/v1/admin/sources/{id}/scan: parameters: - $ref: "#/components/parameters/SourceId" post: operationId: scanSource tags: [library] summary: Запустить скан (admin) description: Скан идёт в фоне; прогресс — в `GET /admin/library/status`. Уже идёт — `409 library.scan_running`. requestBody: required: false content: application/json: schema: $ref: "#/components/schemas/ScanRequest" responses: "202": description: Скан запущен content: application/json: schema: $ref: "#/components/schemas/ScanProgress" default: $ref: "#/components/responses/Problem" /api/v1/admin/library/status: get: operationId: getLibraryStatus tags: [library] summary: Состояние библиотеки (admin, moderator) responses: "200": description: Счётчики и сканы content: application/json: schema: $ref: "#/components/schemas/LibraryStatus" default: $ref: "#/components/responses/Problem" /api/v1/admin/library/errors: get: operationId: listLibraryErrors tags: [library] summary: Файлы, которые не удалось прочитать (admin) parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Cursor" responses: "200": description: Страница ошибок content: application/json: schema: $ref: "#/components/schemas/LibraryErrorPage" default: $ref: "#/components/responses/Problem" /api/v1/admin/library/analysis/errors: get: operationId: listAnalysisErrors tags: [library] summary: Неудавшиеся задачи аудиоанализа с путями файлов (admin) parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Cursor" responses: "200": description: Страница ошибок аудиоанализа, упорядоченная по ID трека content: application/json: schema: $ref: "#/components/schemas/AnalysisErrorPage" default: $ref: "#/components/responses/Problem" /api/v1/admin/library/host-dirs: get: operationId: listHostDirs tags: [library] summary: Подпапки каталога LIBRARY_HOST — выбор папки для local-хранилища (admin) parameters: - name: path in: query description: Путь относительно корня, пусто — корень schema: type: string responses: "200": description: Подпапки content: application/json: schema: $ref: "#/components/schemas/HostDirList" default: $ref: "#/components/responses/Problem" /api/v1/artists/{id}: parameters: - $ref: "#/components/parameters/EntityId" get: operationId: getArtist tags: [catalog] summary: Исполнитель responses: "200": description: Исполнитель content: application/json: schema: $ref: "#/components/schemas/Artist" default: $ref: "#/components/responses/Problem" /api/v1/artists/{id}/albums: parameters: - $ref: "#/components/parameters/EntityId" get: operationId: listArtistAlbums tags: [catalog] summary: Альбомы исполнителя (свои и сборники с его треками) responses: "200": description: Альбомы исполнителя (свои и сборники с его треками) content: application/json: schema: $ref: "#/components/schemas/AlbumSummaryList" default: $ref: "#/components/responses/Problem" /api/v1/artists/{id}/top-tracks: parameters: - $ref: "#/components/parameters/EntityId" get: operationId: listArtistTopTracks tags: [catalog] summary: Популярные треки исполнителя (по прослушиваниям всех пользователей) parameters: - name: limit in: query schema: type: integer minimum: 1 maximum: 50 default: 10 responses: "200": description: Треки, самые слушаемые первыми content: application/json: schema: $ref: "#/components/schemas/TrackList" default: $ref: "#/components/responses/Problem" /api/v1/albums/{id}: parameters: - $ref: "#/components/parameters/EntityId" get: operationId: getAlbum tags: [catalog] summary: Альбом с треками responses: "200": description: Альбом с треками content: application/json: schema: $ref: "#/components/schemas/Album" default: $ref: "#/components/responses/Problem" /api/v1/tracks/{id}: parameters: - $ref: "#/components/parameters/EntityId" get: operationId: getTrack tags: [catalog] summary: Трек responses: "200": description: Трек content: application/json: schema: $ref: "#/components/schemas/Track" default: $ref: "#/components/responses/Problem" /api/v1/search/suggest: get: operationId: searchSuggest tags: [search] summary: Выпадающий список поиска — до 3 исполнителей, 3 альбомов, 5 треков description: | Если по запросу ничего не найдено, сервер повторяет его в другой раскладке клавиатуры («ghbdtn» → «привет») и возвращает исправленный запрос в `corrected_query`. parameters: - name: q in: query required: true schema: type: string maxLength: 200 responses: "200": description: Подсказки content: application/json: schema: $ref: "#/components/schemas/SearchResults" default: $ref: "#/components/responses/Problem" /api/v1/search: get: operationId: search tags: [search] summary: Страница поиска description: | `type=all` — по несколько результатов каждого вида без пагинации; конкретный тип — страницы с `next_cursor`. parameters: - name: q in: query required: true schema: type: string maxLength: 200 - name: type in: query schema: type: string enum: [all, artists, albums, tracks] default: all - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Cursor" responses: "200": description: Результаты content: application/json: schema: $ref: "#/components/schemas/SearchResults" default: $ref: "#/components/responses/Problem" /api/v1/admin/library/analysis/retry: post: operationId: retryAnalysis tags: [library] summary: Вернуть в очередь треки, которые не удалось проанализировать (admin) responses: "200": description: Сколько задач снова в очереди content: application/json: schema: type: object required: [requeued] properties: requeued: type: integer default: $ref: "#/components/responses/Problem" /api/v1/admin/search/reindex: post: operationId: reindexSearch tags: [search] summary: Пересобрать поисковые индексы (admin) responses: "202": description: Переиндексация запущена default: $ref: "#/components/responses/Problem" /api/v1/covers/{id}: get: operationId: getCover tags: [catalog] security: [] summary: Обложка альбома (WebP) description: | Ссылку целиком даёт `cover_url` альбома (подписана, без срока: обложка с данным `v` неизменна); клиент добавляет `size`. Файл отдаёт nginx через X-Accel-Redirect. parameters: - $ref: "#/components/parameters/EntityId" - name: v in: query required: true schema: type: string - name: sig in: query required: true schema: type: string - name: size in: query schema: type: integer enum: [64, 300, 1200] default: 300 responses: "200": description: Картинка content: image/webp: schema: type: string format: binary default: $ref: "#/components/responses/Problem" /api/v1/stream/{id}: get: operationId: streamTrack tags: [playback] security: [] summary: Аудио трека (поддерживает Range) description: | Ссылку целиком даёт `stream_url` трека (подпись пользователя и срок 12 ч); клиент может добавить `q`. Файл отдаёт nginx через X-Accel-Redirect: оригинал из хранилища или транскод в Opus из кэша. parameters: - $ref: "#/components/parameters/EntityId" - name: u in: query required: true schema: type: string format: uuid - name: exp in: query required: true schema: type: integer format: int64 - name: sig in: query required: true schema: type: string - name: q in: query schema: $ref: "#/components/schemas/StreamQuality" responses: "200": description: Аудио content: audio/*: schema: type: string format: binary default: $ref: "#/components/responses/Problem" /api/v1/tracks/batch: post: operationId: getTracksBatch tags: [catalog] summary: Треки по списку id (восстановление очереди) requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/TrackIds" responses: "200": description: Треки в порядке запроса; удалённые пропущены content: application/json: schema: $ref: "#/components/schemas/TrackList" default: $ref: "#/components/responses/Problem" /api/v1/events: post: operationId: postEvents tags: [playback] summary: Пачка событий прослушивания (до 500) description: Повторно присланные события (тот же id) не учитываются дважды. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/EventBatch" responses: "200": description: Принято content: application/json: schema: $ref: "#/components/schemas/EventBatchResult" default: $ref: "#/components/responses/Problem" /api/v1/me/playback-state: get: operationId: getPlaybackState tags: [playback] summary: Последнее состояние плеера (трек, позиция, очередь) responses: "200": description: Состояние; пусто — плеер ещё не запускался content: application/json: schema: $ref: "#/components/schemas/PlaybackState" default: $ref: "#/components/responses/Problem" put: operationId: putPlaybackState tags: [playback] summary: Сохранить состояние плеера (last-write-wins) requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PlaybackStateInput" responses: "200": description: Сохранённое состояние content: application/json: schema: $ref: "#/components/schemas/PlaybackState" default: $ref: "#/components/responses/Problem" /api/v1/me/history: get: operationId: getHistory tags: [collection] summary: История прослушиваний, новые сверху parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Cursor" responses: "200": description: Страница истории content: application/json: schema: $ref: "#/components/schemas/HistoryPage" default: $ref: "#/components/responses/Problem" delete: operationId: clearHistory tags: [collection] summary: Очистить историю (события остаются для статистики) responses: "204": description: Очищено default: $ref: "#/components/responses/Problem" /api/v1/me/likes: get: operationId: getLikes tags: [collection] summary: Лайки, новые сверху parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Cursor" responses: "200": description: Страница content: application/json: schema: $ref: "#/components/schemas/ReactedTrackPage" default: $ref: "#/components/responses/Problem" /api/v1/me/dislikes: get: operationId: getDislikes tags: [collection] summary: Дизлайки, новые сверху parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Cursor" responses: "200": description: Страница content: application/json: schema: $ref: "#/components/schemas/ReactedTrackPage" default: $ref: "#/components/responses/Problem" /api/v1/me/reactions/tracks/{id}: parameters: - $ref: "#/components/parameters/EntityId" put: operationId: setTrackReaction tags: [collection] summary: Лайк или дизлайк трека requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ReactionInput" responses: "204": description: Сохранено default: $ref: "#/components/responses/Problem" delete: operationId: clearTrackReaction tags: [collection] summary: Снять реакцию responses: "204": description: Снято default: $ref: "#/components/responses/Problem" /api/v1/onboarding: get: operationId: getOnboarding tags: [wave] summary: Анкета рекомендаций — варианты, сохранённые ответы и необходимость заполнения description: | `required` — пользователь не проходил и не пропускал анкету либо администратор сбросил рекомендации. `choice` содержит ответы с текущей приблизительной возрастной группой; исходная группа и год хранятся на сервере. responses: "200": description: Варианты и состояние content: application/json: schema: $ref: "#/components/schemas/Onboarding" default: $ref: "#/components/responses/Problem" /api/v1/me/onboarding: put: operationId: saveOnboarding tags: [wave] summary: Сохранить выбор онбординга (или пропуск — пустые списки) requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/OnboardingChoice" responses: "204": description: Сохранено default: $ref: "#/components/responses/Problem" /api/v1/wave/options: get: operationId: getWaveOptions tags: [wave] summary: Занятия и настроения (ключи для i18n и иконки) responses: "200": description: Варианты content: application/json: schema: $ref: "#/components/schemas/WaveOptions" default: $ref: "#/components/responses/Problem" /api/v1/wave/sessions: post: operationId: startWave tags: [wave] summary: Начать «Мою волну» requestBody: required: false content: application/json: schema: $ref: "#/components/schemas/WaveStart" responses: "201": description: Сессия и первая пачка content: application/json: schema: $ref: "#/components/schemas/WaveBatch" default: $ref: "#/components/responses/Problem" /api/v1/wave/sessions/{id}: parameters: - $ref: "#/components/parameters/EntityId" patch: operationId: updateWave tags: [wave] summary: Сменить занятие / настроение на лету — пачка пересобирается requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/WaveContext" responses: "200": description: Новая пачка content: application/json: schema: $ref: "#/components/schemas/WaveBatch" default: $ref: "#/components/responses/Problem" delete: operationId: endWave tags: [wave] summary: Завершить сессию волны responses: "204": description: Завершена default: $ref: "#/components/responses/Problem" /api/v1/wave/sessions/{id}/next: parameters: - $ref: "#/components/parameters/EntityId" post: operationId: nextWave tags: [wave] summary: Следующая пачка с учётом реакций на выданные треки requestBody: required: false content: application/json: schema: $ref: "#/components/schemas/WaveNext" responses: "200": description: Пачка content: application/json: schema: $ref: "#/components/schemas/WaveBatch" default: $ref: "#/components/responses/Problem" /api/v1/app/clients: get: operationId: getAppClients tags: [app] security: [] # публично: страница /download без входа summary: Клиенты последних релизов (Android и десктоп) description: | Тот же релиз Gitea, что и для Android: APK, установщик Windows, .deb, DMG. У каждой платформы `older` — предыдущие сборки (до 8 релизов, сколько есть с файлами). Нет настроек Gitea или файлов клиентов — `404 app.update_unavailable`. responses: "200": description: Доступные сборки content: application/json: schema: $ref: "#/components/schemas/AppClients" default: $ref: "#/components/responses/Problem" /api/v1/app/download/{client}/{version}: get: operationId: downloadAppClient tags: [app] security: [] summary: Файл клиента (кэшируется на сервере, поддерживает Range) parameters: - name: client in: path required: true schema: $ref: "#/components/schemas/AppClient" - name: version in: path required: true schema: type: string pattern: '^\d+\.\d+\.\d+(-rc\.\d+)?$' responses: "200": description: Установщик или APK content: application/octet-stream: schema: type: string format: binary default: $ref: "#/components/responses/Problem" /api/v1/app/android/latest: get: operationId: getAndroidLatest tags: [app] security: [] # публично: страница /download раздаёт приложение без входа summary: Последняя версия Android-приложения (из релизов Gitea через сервер) description: | Сервер сам опрашивает Gitea своим токеном и кэширует ответ на 10 минут, поэтому приложению не нужен доступ к Gitea. Нет настроек Gitea или релиза с APK — `404 app.update_unavailable`. responses: "200": description: Последний релиз content: application/json: schema: $ref: "#/components/schemas/AndroidRelease" default: $ref: "#/components/responses/Problem" /api/v1/app/android/download/{version}: get: operationId: downloadAndroidApk tags: [app] security: [] summary: APK версии (кэшируется на сервере, поддерживает Range для докачки) parameters: - name: version in: path required: true schema: type: string pattern: '^\d+\.\d+\.\d+(-rc\.\d+)?$' responses: "200": description: APK content: application/vnd.android.package-archive: schema: type: string format: binary default: $ref: "#/components/responses/Problem" /api/v1/me/playlists: get: tags: [collection] operationId: listPlaylists responses: default: $ref: '#/components/responses/Problem' '200': description: Success content: application/json: schema: $ref: '#/components/schemas/PlaylistList' post: tags: [collection] operationId: createPlaylist requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PlaylistInput' responses: default: $ref: '#/components/responses/Problem' '201': description: Success content: application/json: schema: $ref: '#/components/schemas/Playlist' /api/v1/me/playlists/{id}: parameters: - in: path name: id required: true schema: { type: string, format: uuid } get: tags: [collection] operationId: getPlaylist responses: default: $ref: '#/components/responses/Problem' '200': description: Success content: application/json: schema: $ref: '#/components/schemas/Playlist' put: tags: [collection] operationId: updatePlaylist requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PlaylistUpdate' responses: default: $ref: '#/components/responses/Problem' '200': description: Success content: application/json: schema: $ref: '#/components/schemas/Playlist' delete: tags: [collection] operationId: deletePlaylist responses: default: $ref: '#/components/responses/Problem' '204': description: Deleted /api/v1/me/playlists/{id}/share: parameters: - in: path name: id required: true schema: { type: string, format: uuid } post: tags: [collection] operationId: sharePlaylist summary: Создать или вернуть ссылку только для чтения (только владелец) responses: default: $ref: '#/components/responses/Problem' '200': description: Идентификатор ссылки для /shared/playlist/{shareId}; для открытия требуется вход content: application/json: schema: $ref: '#/components/schemas/PlaylistShare' delete: tags: [collection] operationId: revokePlaylistShare summary: Отозвать ссылку (только владелец); повторная публикация создаёт новую ссылку responses: default: $ref: '#/components/responses/Problem' '204': description: Доступ по прежней ссылке отключён /api/v1/shared/playlists/{shareId}: parameters: - in: path name: shareId required: true schema: { type: string, format: uuid } get: tags: [collection] operationId: getSharedPlaylist summary: Прочитать плейлист по действующей ссылке (требуется вход) responses: default: $ref: '#/components/responses/Problem' '200': description: Плейлист с реакциями и ссылками на стрим для текущего слушателя content: application/json: schema: $ref: '#/components/schemas/Playlist' components: parameters: SourceId: name: id in: path required: true schema: type: string format: uuid EntityId: name: id in: path required: true schema: type: string format: uuid Limit: name: limit in: query schema: type: integer minimum: 1 maximum: 200 default: 50 Cursor: name: cursor in: query description: Непрозрачный курсор из `next_cursor` предыдущей страницы schema: type: string UploadStatusFilter: name: status in: query description: Статусы через запятую style: form explode: false schema: type: array items: $ref: "#/components/schemas/UploadStatus" UserId: name: id in: path required: true schema: type: string format: uuid securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT responses: Problem: description: Ошибка (RFC 9457) content: application/problem+json: schema: $ref: "#/components/schemas/Problem" schemas: ListeningPreferences: type: object required: [requests, auto_accept, autoplay] properties: requests: {$ref: '#/components/schemas/ActivityVisibility'} auto_accept: {$ref: '#/components/schemas/ActivityVisibility'} autoplay: {type: boolean, default: true} ListeningAction: type: object required: [id, action, target_id] properties: id: {type: string, format: uuid} action: {type: string, enum: [join, invite, accept, decline, cancel, leave, end]} target_id: {type: string, format: uuid} device_id: {type: string, format: uuid} track_id: {type: string, format: uuid} position_ms: {type: integer, minimum: 0} playing: {type: boolean} ListeningRoomInput: type: object required: [room_id, device_id, sequence, playing, position_ms] properties: room_id: {type: string, format: uuid} device_id: {type: string, format: uuid} sequence: {type: integer, format: int64, minimum: 1} track_id: {type: string, format: uuid} playing: {type: boolean} position_ms: {type: integer, minimum: 0} ListeningRoom: type: object required: [id, owner_id, owner_name, device_id, track_id, position_ms, playing, updated_at, participants] properties: id: {type: string, format: uuid} owner_id: {type: string, format: uuid} owner_name: {type: string} device_id: {type: string, format: uuid} track_id: {type: string, format: uuid, nullable: true} position_ms: {type: integer} playing: {type: boolean} updated_at: {type: string, format: date-time} participants: type: array items: type: object required: [id, name] properties: id: {type: string, format: uuid} name: {type: string} ListeningRequest: type: object required: [id, sender_id, sender_name, recipient_id, owner_id, kind, expires_at] properties: id: {type: string, format: uuid} sender_id: {type: string, format: uuid} sender_name: {type: string} recipient_id: {type: string, format: uuid} owner_id: {type: string, format: uuid} kind: {type: string, enum: [join, invite]} expires_at: {type: string, format: date-time} ListeningState: type: object required: [preferences, room, requests, server_time] properties: preferences: {$ref: '#/components/schemas/ListeningPreferences'} room: nullable: true allOf: - {$ref: '#/components/schemas/ListeningRoom'} requests: type: array items: {$ref: '#/components/schemas/ListeningRequest'} server_time: {type: string, format: date-time} ActivityVisibility: type: string enum: [nobody, friends, everyone] default: nobody ListeningActivityInput: type: object required: [device_id, sequence, playing, context] properties: device_id: {type: string, format: uuid} sequence: {type: integer, format: int64, minimum: 1} playing: {type: boolean} position_ms: {type: integer, minimum: 0, default: 0} track_id: {type: string, format: uuid} context: {type: string, enum: [other, wave, playlist]} playlist_id: {type: string, format: uuid} ListeningActivity: type: object required: [track_id, track_title, artist, context] properties: track_id: {type: string, format: uuid} track_title: {type: string} artist: {type: string} context: {type: string, enum: [other, wave, playlist]} playlist_id: {type: string, format: uuid} playlist_name: {type: string} SocialPerson: type: object required: [id, username, display_name, relationship, unread] properties: id: {type: string, format: uuid} username: {type: string} display_name: {type: string} relationship: {type: string, enum: [none, incoming, outgoing, friend, blocked]} unread: {type: integer} activity: {$ref: '#/components/schemas/ListeningActivity'} SocialNotification: type: object required: [id, actor_id, actor_name, kind, created_at, read] properties: id: {type: string, format: uuid} actor_id: {type: string, format: uuid} actor_name: {type: string} kind: {type: string, enum: [friend_request, friend_accepted, message, music, listening_request, listening_invite, listening_accepted]} created_at: {type: string, format: date-time} read: {type: boolean} SocialInbox: type: object required: [people, notifications, unread, activity_visibility, friends_recommendations] properties: friends_recommendations: {type: boolean, default: true} activity_visibility: {$ref: '#/components/schemas/ActivityVisibility'} people: type: array items: {$ref: '#/components/schemas/SocialPerson'} notifications: type: array items: {$ref: '#/components/schemas/SocialNotification'} unread: {type: integer} SocialAction: type: object required: [action, target_id] properties: action: {type: string, enum: [request, accept, remove, block, unblock, read_chat, read_notification]} target_id: {type: string, format: uuid} up_to_seq: {type: integer, format: int64, minimum: 1} SendDirectMessage: type: object required: [id, text, attachment_kind] properties: id: {type: string, format: uuid} text: {type: string, maxLength: 4000} attachment_kind: {type: string, enum: [none, track, playlist]} attachment_id: {type: string, format: uuid} reply_to: {type: string, format: uuid} DirectMessage: type: object required: [id, seq, sender_id, text, attachment_kind, created_at, read] properties: id: {type: string, format: uuid} seq: {type: integer, format: int64} sender_id: {type: string, format: uuid} text: {type: string} attachment_kind: {type: string, enum: [none, track, playlist]} attachment_id: {type: string, format: uuid} created_at: {type: string, format: date-time} read: {type: boolean} reply_to: {type: string, format: uuid} reply_preview: {type: string} edited_at: {type: string, format: date-time} deleted: {type: boolean} reactions: type: array items: $ref: '#/components/schemas/MessageReaction' MessageReaction: type: object required: [emoji, count, mine] properties: emoji: {type: string} count: {type: integer} mine: {type: boolean} ServerSource: type: object required: [id, name, url] properties: id: {type: string, maxLength: 64} name: {type: string, maxLength: 100} url: {type: string, maxLength: 2048} ServerSources: type: object required: [sources] properties: sources: type: array maxItems: 20 items: {$ref: "#/components/schemas/ServerSource"} ServerJob: type: object required: [id, kind, state, started_at, finished_at, target, log] properties: id: {type: string} kind: {type: string, enum: [update, config]} state: {type: string, enum: [running, succeeded, failed]} started_at: {type: string} finished_at: {type: string} target: {type: string} log: {type: string} ServerUpdates: type: object required: [sources, versions, current_version, job, source_error, current_commit, target_version, target_commit, update_state] properties: sources: type: array items: {$ref: "#/components/schemas/ServerSource"} versions: type: array items: {type: string} current_version: {type: string} current_commit: {type: string} target_version: {type: string} target_commit: {type: string} update_state: {type: string, enum: [available, current, ahead, unknown, unavailable, select_version]} source_error: {type: string} job: allOf: [{$ref: "#/components/schemas/ServerJob"}] nullable: true ServerUpdateRequest: type: object required: [source_id, channel] properties: source_id: {type: string} channel: {type: string, enum: [latest, version, main]} version: {type: string} expected_commit: {type: string, pattern: '^[a-f0-9]{40,64}$'} ServerConfigEntry: type: object required: [key, group, type, secret, readonly, effective, env, local, default_value, source] properties: key: {type: string} group: {type: string} type: {type: string} secret: {type: boolean} readonly: {type: boolean} effective: {type: string} env: {type: string, nullable: true} local: {type: string, nullable: true} default_value: {type: string} source: {type: string, enum: [env, local, default]} ServerConfig: type: object required: [entries, revision, restart_required] properties: revision: {type: string} restart_required: {type: boolean} entries: type: array items: {$ref: "#/components/schemas/ServerConfigEntry"} ServerConfigPatch: type: object required: [revision, values] properties: revision: {type: string} values: type: object additionalProperties: {type: string, nullable: true} PlaylistShare: type: object required: [share_id] properties: share_id: { type: string, format: uuid } PlaylistInput: type: object required: [name, description, track_ids] properties: name: { type: string, minLength: 1, maxLength: 120 } description: { type: string, maxLength: 2000 } track_ids: type: array maxItems: 2000 uniqueItems: true items: { type: string, format: uuid } PlaylistUpdate: allOf: - $ref: '#/components/schemas/PlaylistInput' - type: object required: [revision] properties: revision: { type: integer, minimum: 1 } PlaylistSummary: type: object required: [id, name, description, revision, track_count, duration_ms, created_at, updated_at] properties: id: { type: string, format: uuid } name: { type: string } description: { type: string } revision: { type: integer } track_count: { type: integer } duration_ms: { type: integer, format: int64 } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } Playlist: allOf: - $ref: '#/components/schemas/PlaylistSummary' - type: object required: [tracks] properties: share_id: type: string format: uuid nullable: true readOnly: true description: Действующая ссылка; возвращается только владельцу через /me/playlists tracks: type: array items: { $ref: '#/components/schemas/Track' } PlaylistList: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/PlaylistSummary' } Onboarding: type: object required: [required, artists, genres] properties: required: type: boolean choice: $ref: "#/components/schemas/OnboardingChoice" artists: type: array items: type: object required: [id, name, track_count, cover_url, cover_colors] properties: id: type: string format: uuid name: type: string track_count: type: integer cover_url: type: string nullable: true description: Обложка самого большого альбома исполнителя cover_colors: type: array items: type: string genres: type: array items: type: object required: [name, track_count] properties: name: type: string track_count: type: integer OnboardingChoice: type: object required: [artist_ids, genres] properties: gender: type: string nullable: true enum: [male, female, other] age_group: type: string nullable: true enum: [under_13, "13_19", "20_29", "30_39", "40_49", "50_59", 60_plus] description: Текущая приблизительная возрастная группа. Сервер хранит год выбора и учитывает прошедшие годы. age_group_year: type: integer readOnly: true description: Год последнего выбора возрастной группы. artist_ids: type: array maxItems: 50 items: type: string format: uuid genres: type: array maxItems: 30 items: type: string maxLength: 100 UserRef: type: object required: [id, username] properties: id: type: string format: uuid username: type: string UploadStatus: type: string enum: [uploading, processing, pending_review, approved, importing, imported, import_failed, duplicate, invalid, rejected, withdrawn, expired] UploadQuota: type: object required: [limit_bytes, used_bytes, per_user_limit_bytes, user_used_bytes, max_file_bytes] properties: limit_bytes: type: integer format: int64 used_bytes: type: integer format: int64 per_user_limit_bytes: type: integer format: int64 nullable: true user_used_bytes: type: integer format: int64 max_file_bytes: type: integer format: int64 UploadPrecheckRequest: type: object required: [files] properties: files: type: array maxItems: 500 items: type: object required: [client_id, size, sha256] properties: client_id: type: string maxLength: 100 size: type: integer format: int64 sha256: type: string artist: type: string title: type: string duration_ms: type: integer album: type: string maxLength: 500 UploadPrecheckResult: type: object required: [files] properties: files: type: array items: type: object required: [client_id, result, track_id, possible_duplicate] properties: client_id: type: string result: type: string enum: [accepted, duplicate, too_large] track_id: type: string format: uuid nullable: true description: С каким треком совпал (дубль или похожие метаданные) possible_duplicate: type: boolean description: Совпали только метаданные — файл примут, админ решит UploadBatchRequest: type: object required: [files] properties: comment: type: string maxLength: 1000 files: type: array minItems: 1 maxItems: 500 items: type: object required: [client_id, name, size, sha256] properties: client_id: type: string maxLength: 100 name: type: string maxLength: 255 rel_path: type: string maxLength: 1000 description: Относительный путь в перетащенной папке size: type: integer format: int64 sha256: type: string UploadBatchCreated: type: object required: [batch_id, files] properties: batch_id: type: string format: uuid files: type: array items: type: object required: [client_id, upload_file_id, tus_url, rejected, track_id] properties: client_id: type: string upload_file_id: type: string format: uuid nullable: true tus_url: type: string nullable: true rejected: type: string nullable: true description: Код отказа — uploads.queue_full, uploads.user_quota_full, uploads.too_large, uploads.duplicate, uploads.unsupported_format track_id: type: string format: uuid nullable: true UploadMeta: type: object required: [artist, albumartist, album, title, genre, year, track, disc] properties: artist: type: string maxLength: 500 albumartist: type: string maxLength: 500 album: type: string maxLength: 500 title: type: string maxLength: 500 genre: type: string maxLength: 100 year: type: integer nullable: true track: type: integer nullable: true disc: type: integer nullable: true UploadFile: type: object required: [id, batch_id, batch_comment, author, status, original_name, client_rel_path, size_bytes, received_bytes, format, codec, bitrate, sample_rate, bit_depth, duration_ms, tags, metadata, metadata_edited, matched_artist_id, matched_album_id, duplicate_of_track_id, duplicate_level, possible_duplicate, quality_upgrade, target_source_id, target_rel_path, status_reason, reviewed_at, created_at, updated_at, tus_url, stream_url] properties: id: type: string format: uuid batch_id: type: string format: uuid batch_comment: type: string nullable: true author: $ref: "#/components/schemas/UserRef" status: $ref: "#/components/schemas/UploadStatus" original_name: type: string client_rel_path: type: string nullable: true size_bytes: type: integer format: int64 received_bytes: type: integer format: int64 format: type: string nullable: true codec: type: string nullable: true bitrate: type: integer nullable: true sample_rate: type: integer nullable: true bit_depth: type: integer nullable: true duration_ms: type: integer nullable: true tags: type: object additionalProperties: type: string metadata: nullable: true allOf: - $ref: "#/components/schemas/UploadMeta" metadata_edited: type: boolean matched_artist_id: type: string format: uuid nullable: true description: Найден в каталоге — файл добавится к существующему исполнителю matched_album_id: type: string format: uuid nullable: true description: Найден в каталоге — «будет добавлено к существующему альбому» duplicate_of_track_id: type: string format: uuid nullable: true duplicate_level: type: string nullable: true enum: [sha256, metadata, fingerprint, null] possible_duplicate: type: boolean quality_upgrade: type: boolean description: Тот же трек, но лучшего качества — ждёт решения, если включён allow_quality_upgrade target_source_id: type: string format: uuid nullable: true target_rel_path: type: string nullable: true description: Путь в хранилище по write_template (null — не построился, причина в status_reason) status_reason: type: string nullable: true reviewed_at: type: string format: date-time nullable: true created_at: type: string format: date-time updated_at: type: string format: date-time tus_url: type: string nullable: true description: Только автору и пока файл в статусе uploading stream_url: type: string nullable: true description: Подписанная ссылка прослушивания из staging; null, если файла уже нет UploadFilePage: type: object required: [items, next_cursor] properties: items: type: array items: $ref: "#/components/schemas/UploadFile" next_cursor: type: string nullable: true UploadPatch: type: object properties: metadata: $ref: "#/components/schemas/UploadMeta" target_source_id: type: string format: uuid UploadBatchPatch: type: object description: Пустые поля не меняют файлы. cover_base64 null снимает обложку пачки. properties: artist: type: string maxLength: 500 album: type: string maxLength: 500 year: type: integer nullable: true cover_base64: type: string nullable: true description: JPEG, PNG или WebP, не больше 8 МБ после декодирования UploadReviewRequest: type: object required: [file_ids, decision] properties: file_ids: type: array minItems: 1 maxItems: 500 items: type: string format: uuid decision: type: string enum: [approve, reject] reason: type: string maxLength: 1000 description: Причина отклонения — её видит автор UploadReviewResult: type: object required: [items] properties: items: type: array items: type: object required: [id, ok, error] properties: id: type: string format: uuid ok: type: boolean error: nullable: true allOf: - $ref: "#/components/schemas/Problem" UploadSettings: type: object required: [queue_limit_bytes, per_user_limit_bytes, max_file_bytes, default_source_id, allow_quality_upgrade] properties: queue_limit_bytes: type: integer format: int64 per_user_limit_bytes: type: integer format: int64 nullable: true max_file_bytes: type: integer format: int64 default_source_id: type: string format: uuid nullable: true allow_quality_upgrade: type: boolean description: Тот же трек в лучшем качестве идёт на рассмотрение, а не в дубли UploadSettingsPatch: type: object description: Лимит на пользователя и хранилище по умолчанию передаются всегда (null — выключить / не задано). required: [per_user_limit_bytes, default_source_id] properties: queue_limit_bytes: type: integer format: int64 minimum: 0 per_user_limit_bytes: type: integer format: int64 nullable: true description: null — выключить лимит на пользователя max_file_bytes: type: integer format: int64 minimum: 1 default_source_id: type: string format: uuid nullable: true description: null — без хранилища по умолчанию allow_quality_upgrade: type: boolean HealthStatus: type: object required: [status] properties: status: type: string enum: [ok] ReadyStatus: type: object required: [status, checks] properties: status: type: string enum: [ok, degraded] checks: type: object description: Результат проверки каждой зависимости additionalProperties: type: string description: "`ok` или `fail` (причина — только в логе сервиса)" ServerInfo: type: object required: [name, version, api_version, supported_locales, min_android_version_code, bootstrap_required] properties: name: type: string example: pipu-music version: type: string description: Версия сервера (SemVer) example: 0.1.0 api_version: type: string example: v1 supported_locales: type: array items: type: string example: [ru, en] min_android_version_code: type: integer description: Минимальный versionCode Android-приложения, которое поддерживает сервер example: 10099 bootstrap_required: type: boolean description: Первый вход администратора ещё не завершён — клиент может показать подсказку Role: type: string enum: [admin, moderator, user] ClientType: type: string enum: [web, android, desktop] LoginRequest: type: object required: [username, password, client_type] properties: username: type: string password: type: string client_type: $ref: "#/components/schemas/ClientType" device_name: type: string maxLength: 100 description: Например «Chrome / Windows», «Pixel 8» RefreshRequest: type: object properties: refresh_token: type: string description: Android и desktop; веб передаёт токен в cookie AuthTokens: type: object required: [access_token, token_type, expires_in, user] properties: access_token: type: string token_type: type: string enum: [Bearer] expires_in: type: integer description: Срок жизни access-токена, секунды refresh_token: type: string description: "Для `client_type: android` и `client_type: desktop`" user: $ref: "#/components/schemas/Me" UiPrefs: type: object properties: theme: type: string enum: [dark, light, system] wave_variant: type: string enum: [orb, liquid] web_player: type: string enum: [bar, island] search_position: type: string enum: [center, left, right] description: Где в топбаре веба поиск (по умолчанию center) search_collapse: type: boolean description: Сворачивать поиск в кнопку с лупой без фокуса (по умолчанию false) home_activities: type: boolean description: Показывать на главной веба выбор занятия (по умолчанию true) home_moods: type: boolean description: Показывать на главной веба выбор настроения (по умолчанию true) mobile_player: type: string enum: [cover, wave] ambient: type: boolean crossfade_s: type: integer minimum: 0 maximum: 6 normalize: type: boolean Me: type: object required: [id, username, display_name, role, reviews_uploads, locale, ui_prefs, must_change_password, must_change_credentials, created_at] properties: id: type: string format: uuid username: type: string display_name: type: string nullable: true role: $ref: "#/components/schemas/Role" manages_updates: type: boolean description: Управление обновлениями сервера. У admin всегда; другим выдаёт только admin reviews_uploads: type: boolean description: Может рассматривать очередь на добавление. Не следует из роли и может быть у нескольких людей сразу locale: type: string ui_prefs: $ref: "#/components/schemas/UiPrefs" must_change_password: type: boolean must_change_credentials: type: boolean created_at: type: string format: date-time UpdateMeRequest: type: object properties: display_name: type: string maxLength: 64 description: Пустая строка — сбросить имя locale: type: string ui_prefs: $ref: "#/components/schemas/UiPrefs" ChangePasswordRequest: type: object required: [new_password] properties: current_password: type: string new_password: type: string BootstrapCredentialsRequest: type: object required: [new_username, new_password] properties: new_username: type: string new_password: type: string SourceCoverStats: type: object required: [files_total, files_with_cover, albums_pending] properties: files_total: type: integer format: int64 files_with_cover: type: integer format: int64 description: Файлы, альбом которых имеет готовую локальную обложку (встроенную или из папки) albums_pending: type: integer format: int64 UserArchive: type: object additionalProperties: false required: [version, users] properties: version: type: integer enum: [1] users: type: array maxItems: 10000 items: $ref: "#/components/schemas/UserArchiveEntry" UserArchiveEntry: type: object additionalProperties: false required: [username, display_name, role, password_hash, is_blocked, must_change_password, must_change_credentials, reviews_uploads, manages_updates] properties: username: type: string display_name: type: string nullable: true maxLength: 64 role: $ref: "#/components/schemas/Role" password_hash: type: string maxLength: 512 description: Argon2id PHC. Секретные данные; файл нужно хранить в защищённом месте. is_blocked: type: boolean must_change_password: type: boolean must_change_credentials: type: boolean reviews_uploads: type: boolean manages_updates: type: boolean UserImportResult: type: object required: [created, skipped] properties: created: type: integer skipped: type: array items: type: string AdminUser: type: object required: [id, username, display_name, role, reviews_uploads, locale, is_blocked, must_change_password, must_change_credentials, created_at, last_login_at] properties: id: type: string format: uuid username: type: string display_name: type: string nullable: true role: $ref: "#/components/schemas/Role" manages_updates: type: boolean description: Управление обновлениями сервера. У admin всегда; другим выдаёт только admin reviews_uploads: type: boolean description: Может рассматривать очередь на добавление. Не следует из роли и может быть у нескольких людей сразу locale: type: string is_blocked: type: boolean must_change_password: type: boolean must_change_credentials: type: boolean created_at: type: string format: date-time last_login_at: type: string format: date-time nullable: true AdminUserPage: type: object required: [items, next_cursor] properties: items: type: array items: $ref: "#/components/schemas/AdminUser" next_cursor: type: string nullable: true CreateUserRequest: type: object required: [username, role] properties: username: type: string display_name: type: string maxLength: 64 role: $ref: "#/components/schemas/Role" manages_updates: type: boolean description: Управление обновлениями сервера. У admin всегда; другим выдаёт только admin reviews_uploads: type: boolean description: Сразу выдать аудит загрузок. Может задать только админ CreatedUser: type: object required: [user, temporary_password] properties: user: $ref: "#/components/schemas/AdminUser" temporary_password: type: string description: Показывается один раз UpdateUserRequest: type: object properties: username: type: string description: Новый логин. Может менять только администратор, в том числе у себя display_name: type: string maxLength: 64 description: Пустая строка — сбросить имя role: $ref: "#/components/schemas/Role" is_blocked: type: boolean manages_updates: type: boolean description: Управление обновлениями сервера. У admin всегда; другим выдаёт только admin reviews_uploads: type: boolean description: Выдать или забрать аудит загрузок. Может менять только админ, в том числе у себя TemporaryPassword: type: object required: [temporary_password] properties: temporary_password: type: string AuditActor: type: object required: [id, username] properties: id: type: string format: uuid username: type: string AuditEntry: type: object required: [id, actor, action, target_type, target_id, payload, created_at] properties: id: type: string format: uuid actor: description: null — система или удалённый пользователь nullable: true allOf: - $ref: "#/components/schemas/AuditActor" action: type: string example: user.create target_type: type: string nullable: true target_id: type: string format: uuid nullable: true payload: type: object nullable: true additionalProperties: true created_at: type: string format: date-time AuditPage: type: object required: [items, next_cursor] properties: items: type: array items: $ref: "#/components/schemas/AuditEntry" next_cursor: type: string nullable: true SourceType: type: string enum: [nfs, local] SourceStatus: type: string enum: [pending, mounted, error, disabled] TagPriority: type: string enum: [tags_first, path_first] Source: type: object required: [id, name, type, nfs_server, nfs_export, nfs_options, local_path, tag_priority, scan_interval_minutes, accepts_uploads, write_template, status, status_message, last_scan_at, created_at, scan] properties: excluded_dirs: $ref: "#/components/schemas/ExcludedDirs" id: type: string format: uuid name: type: string type: $ref: "#/components/schemas/SourceType" nfs_server: type: string nullable: true nfs_export: type: string nullable: true nfs_options: type: string local_path: type: string nullable: true description: Подпапка относительно LIBRARY_HOST, пусто — весь каталог tag_priority: $ref: "#/components/schemas/TagPriority" scan_interval_minutes: type: integer nullable: true description: Автоматический скан, null — только вручную accepts_uploads: type: boolean description: Принимает импорт из очереди на добавление (NFS монтируется агентом ещё и на запись) write_template: type: string nullable: true description: Шаблон пути новых файлов (docs/uploads.md#7), null — не задан status: $ref: "#/components/schemas/SourceStatus" status_message: type: string nullable: true last_scan_at: type: string format: date-time nullable: true created_at: type: string format: date-time scan: nullable: true allOf: - $ref: "#/components/schemas/ScanProgress" SourceList: type: object required: [items] properties: items: type: array items: $ref: "#/components/schemas/Source" SourceInput: type: object required: [name, type] properties: excluded_dirs: $ref: "#/components/schemas/ExcludedDirs" name: type: string maxLength: 100 type: $ref: "#/components/schemas/SourceType" nfs_server: type: string nfs_export: type: string nfs_options: type: string local_path: type: string tag_priority: $ref: "#/components/schemas/TagPriority" scan_interval_minutes: type: integer minimum: 5 nullable: true accepts_uploads: type: boolean write_template: type: string maxLength: 500 SourcePatch: type: object properties: excluded_dirs: $ref: "#/components/schemas/ExcludedDirs" name: type: string maxLength: 100 nfs_server: type: string nfs_export: type: string nfs_options: type: string local_path: type: string tag_priority: $ref: "#/components/schemas/TagPriority" scan_interval_minutes: type: integer description: 0 — отключить автоматический скан enabled: type: boolean description: false — отключить хранилище (треки становятся недоступны), true — подключить снова accepts_uploads: type: boolean description: Смена переподключает NFS-хранилище (rw-точка агента) write_template: type: string maxLength: 500 description: | Шаблон пути новых файлов; пусто — убрать. Неверный синтаксис — `422 validation.template_invalid` с `params.reason`. Проверка обратимости не обязательна для сохранения — её показывает `write-template/check`. ExcludedDirs: type: array maxItems: 100 description: Literal folder paths relative to the storage root, including all descendants. Case sensitive; no glob matching. Empty list clears exclusions. Existing library records are retained, but their scan and analysis are skipped. Changes require no running scan. items: type: string minLength: 1 maxLength: 1000 SourceTestResult: type: object required: [ok, message, audio_files] properties: ok: type: boolean message: type: string audio_files: type: integer description: Сколько аудиофайлов найдено (проверка останавливается на 1000) write_ok: type: boolean nullable: true description: Удалось создать и удалить `.pipu-write-test` (только для accepts_uploads; иначе null) write_message: type: string nullable: true WriteTemplateSuggestion: type: object required: [template] properties: template: type: string WriteTemplateCheckRequest: type: object required: [template] properties: template: type: string maxLength: 500 WriteTemplateMismatch: type: object required: [rel_path, rendered, fields, error] properties: rel_path: type: string description: Реальный путь файла rendered: type: string nullable: true description: Путь по шаблону, null — не построился fields: type: array items: type: string description: Поля, которые регэкспы хранилища распознали не так; `pattern` — не совпало ни одно выражение error: type: string nullable: true WriteTemplateCheck: type: object required: [ok, total, round_trip, same_path, mismatches] properties: ok: type: boolean description: Все пути разобраны обратно без изменений и ≥ 90% совпали с реальными total: type: integer round_trip: type: integer same_path: type: integer mismatches: type: array maxItems: 10 items: $ref: "#/components/schemas/WriteTemplateMismatch" PathPattern: type: object required: [regex, is_enabled] properties: regex: type: string maxLength: 1000 is_enabled: type: boolean PatternList: type: object required: [patterns] properties: patterns: type: array maxItems: 50 items: $ref: "#/components/schemas/PathPattern" PatternPreviewRequest: type: object required: [patterns] properties: patterns: type: array maxItems: 50 items: $ref: "#/components/schemas/PathPattern" sample: type: integer minimum: 1 maximum: 200 default: 50 TrackMeta: type: object required: [artist, album_artist, album, title, genres, year, track, disc] properties: artist: type: string album_artist: type: string album: type: string title: type: string genres: type: array items: type: string year: type: integer nullable: true track: type: integer nullable: true disc: type: integer nullable: true PreviewItem: type: object required: [rel_path, pattern_index, skipped, path_fields, tags, result, error] properties: rel_path: type: string pattern_index: type: integer nullable: true description: Номер совпавшего шаблона, null — ни один skipped: type: boolean path_fields: type: object additionalProperties: type: string tags: type: object additionalProperties: type: string result: $ref: "#/components/schemas/TrackMeta" error: type: string nullable: true PatternPreview: type: object required: [items, unmatched] properties: items: type: array items: $ref: "#/components/schemas/PreviewItem" unmatched: type: integer ScanRequest: type: object properties: full: type: boolean description: Перечитать все файлы, а не только новые и изменённые ScanProgress: type: object required: [running, full, started_at, finished_at, found, to_process, processed, added, updated, moved, missing, errors, error] properties: phase: type: string enum: [walking, preparing, processing, finalizing] description: Current scan stage; progress percentage is available only during processing. walked_dirs: type: integer description: Directories visited so far, including the source root. current_dir: type: string description: Current directory relative to the source root, using forward slashes. running: type: boolean full: type: boolean started_at: type: string format: date-time finished_at: type: string format: date-time nullable: true found: type: integer to_process: type: integer processed: type: integer added: type: integer updated: type: integer moved: type: integer missing: type: integer errors: type: integer error: type: string nullable: true LibraryStatus: type: object required: [artists, albums, tracks, files, missing_files, file_errors, sources, analysis, wave] properties: analysis: $ref: "#/components/schemas/AnalysisStatus" wave: $ref: "#/components/schemas/WaveMetrics" artists: type: integer albums: type: integer tracks: type: integer files: type: integer missing_files: type: integer file_errors: type: integer sources: type: array items: $ref: "#/components/schemas/SourceStatusItem" SourceStatusItem: type: object required: [id, name, status, status_message, last_scan_at, scan] properties: id: type: string format: uuid name: type: string status: $ref: "#/components/schemas/SourceStatus" status_message: type: string nullable: true last_scan_at: type: string format: date-time nullable: true scan: nullable: true allOf: - $ref: "#/components/schemas/ScanProgress" LibraryError: type: object required: [file_id, source_id, rel_path, error] properties: file_id: type: string format: uuid source_id: type: string format: uuid rel_path: type: string error: type: string LibraryErrorPage: type: object required: [items, next_cursor] properties: items: type: array items: $ref: "#/components/schemas/LibraryError" next_cursor: type: string nullable: true AnalysisError: type: object required: [track_id, title, source_id, source_name, rel_path, error] properties: track_id: type: string format: uuid title: type: string source_id: type: string format: uuid nullable: true source_name: type: string nullable: true rel_path: type: string nullable: true description: Path relative to the source root; primary file if available, otherwise a known file of this track. error: type: string AnalysisErrorPage: type: object required: [items, next_cursor] properties: items: type: array items: $ref: "#/components/schemas/AnalysisError" next_cursor: type: string nullable: true HostDirList: type: object required: [path, dirs] properties: path: type: string dirs: type: array items: type: string ArtistRef: type: object required: [id, name] properties: id: type: string format: uuid name: type: string AlbumRef: type: object required: [id, title, year, cover_url, cover_colors] properties: id: type: string format: uuid title: type: string year: type: integer nullable: true cover_url: type: string nullable: true cover_colors: type: array items: type: string Track: type: object required: [id, title, duration_ms, artists, album, genres, disc_no, track_no, available, stream_url, reaction, features] properties: id: type: string format: uuid title: type: string duration_ms: type: integer artists: type: array items: $ref: "#/components/schemas/ArtistRef" album: $ref: "#/components/schemas/AlbumRef" genres: type: array items: type: string disc_no: type: integer nullable: true track_no: type: integer nullable: true available: type: boolean description: Есть доступный файл (хранилище подключено, файл на месте) stream_url: type: string nullable: true description: Подписанная ссылка на аудио (12 ч); null — трек недоступен reaction: nullable: true allOf: - $ref: "#/components/schemas/Reaction" features: nullable: true description: Аудиопризнаки (после анализа) — для анимации волны и выравнивания громкости allOf: - $ref: "#/components/schemas/TrackFeatures" GeniusStats: type: object required: [configured, paused, running, total, pending, matched, lyrics, annotations, images, unmatched, errors, last_error, cooldown_until] properties: configured: { type: boolean } paused: { type: boolean } running: { type: boolean } total: { type: integer } pending: { type: integer } matched: { type: integer } lyrics: { type: integer } annotations: { type: integer } images: { type: integer } unmatched: { type: integer } errors: { type: integer } last_error: { type: string } cooldown_until: { type: string, format: date-time } GeniusContent: type: object required: [status, lyrics, source_url, song_image_url, artist_image_url, description, annotations] properties: status: { type: string } lyrics: { type: string } source_url: { type: string } song_image_url: { type: string } artist_image_url: { type: string } description: { type: string } annotations: type: array items: $ref: "#/components/schemas/GeniusAnnotation" GeniusAnnotation: type: object required: [fragment, text, url, verified, authors] properties: fragment: { type: string } text: { type: string } url: { type: string } verified: { type: boolean } authors: { type: string } Artist: type: object required: [id, name, album_count, track_count] properties: image_url: { type: string } id: type: string format: uuid name: type: string album_count: type: integer track_count: type: integer AlbumSummary: type: object required: [id, title, year, cover_url, cover_colors, artist, track_count] properties: id: type: string format: uuid title: type: string year: type: integer nullable: true cover_url: type: string nullable: true cover_colors: type: array items: type: string artist: $ref: "#/components/schemas/ArtistRef" track_count: type: integer AlbumSummaryList: type: object required: [items] properties: items: type: array items: $ref: "#/components/schemas/AlbumSummary" Album: type: object required: [id, title, year, cover_url, cover_colors, artist, duration_ms, tracks] properties: id: type: string format: uuid title: type: string year: type: integer nullable: true cover_url: type: string nullable: true cover_colors: type: array items: type: string artist: $ref: "#/components/schemas/ArtistRef" duration_ms: type: integer tracks: type: array items: $ref: "#/components/schemas/Track" SearchResults: type: object required: [artists, albums, tracks, totals, corrected_query, next_cursor] properties: artists: type: array items: $ref: "#/components/schemas/ArtistRef" albums: type: array items: $ref: "#/components/schemas/AlbumSummary" tracks: type: array items: $ref: "#/components/schemas/Track" totals: type: object required: [artists, albums, tracks] description: Оценка общего числа совпадений properties: artists: type: integer albums: type: integer tracks: type: integer corrected_query: type: string nullable: true description: Запрос в другой раскладке, если по исходному ничего не нашлось next_cursor: type: string nullable: true TrackFeatures: type: object required: [bpm, energy, loudness_lufs] properties: bpm: type: number nullable: true energy: type: number nullable: true loudness_lufs: type: number nullable: true StreamQuality: type: string enum: [auto, original, high, medium, low] Reaction: type: string enum: [like, dislike] TrackIds: type: object required: [ids] properties: ids: type: array maxItems: 500 items: type: string format: uuid TrackList: type: object required: [items] properties: items: type: array items: $ref: "#/components/schemas/Track" PlayEvent: type: object required: [id, type, track_id, occurred_at] properties: id: type: string format: uuid description: UUIDv7 от клиента — ключ идемпотентности type: type: string enum: [play_start, play_end, skip, seek, pause, resume, like, unlike, dislike, undislike] track_id: type: string format: uuid occurred_at: type: string format: date-time position_ms: type: integer played_ms: type: integer from_ms: type: integer context_type: type: string enum: [wave, album, artist, search, collection, history, queue] context_id: type: string maxLength: 100 wave_session_id: type: string format: uuid offline: type: boolean EventBatch: type: object required: [events] properties: events: type: array maxItems: 500 items: $ref: "#/components/schemas/PlayEvent" EventBatchResult: type: object required: [accepted, duplicates, rejected] properties: accepted: type: integer duplicates: type: integer rejected: type: integer description: Слишком старые (> 30 дней) или с неизвестным треком PlaybackContext: type: object properties: type: type: string id: type: string label: type: string wave_session_id: type: string format: uuid PlaybackStateInput: type: object required: [track_id, position_ms, queue, queue_index] properties: track_id: type: string format: uuid nullable: true position_ms: type: integer minimum: 0 queue: type: array maxItems: 500 items: type: string format: uuid queue_index: type: integer minimum: 0 context: $ref: "#/components/schemas/PlaybackContext" shuffle: type: boolean repeat_mode: type: string enum: ["off", one, all] volume: type: number minimum: 0 maximum: 1 device_id: type: string maxLength: 100 PlaybackState: type: object required: [track_id, position_ms, queue, queue_index, context, shuffle, repeat_mode, volume, device_id, updated_at] properties: track_id: type: string format: uuid nullable: true position_ms: type: integer queue: type: array items: type: string format: uuid queue_index: type: integer context: nullable: true allOf: - $ref: "#/components/schemas/PlaybackContext" shuffle: type: boolean repeat_mode: type: string enum: ["off", one, all] volume: type: number device_id: type: string nullable: true updated_at: type: string format: date-time nullable: true HistoryItem: type: object required: [id, played_at, track] properties: id: type: string format: uuid played_at: type: string format: date-time track: $ref: "#/components/schemas/Track" HistoryPage: type: object required: [items, next_cursor] properties: items: type: array items: $ref: "#/components/schemas/HistoryItem" next_cursor: type: string nullable: true ReactedTrack: type: object required: [created_at, track] properties: created_at: type: string format: date-time track: $ref: "#/components/schemas/Track" ReactedTrackPage: type: object required: [items, total, next_cursor] properties: items: type: array items: $ref: "#/components/schemas/ReactedTrack" total: type: integer next_cursor: type: string nullable: true ReactionInput: type: object required: [reaction] properties: reaction: $ref: "#/components/schemas/Reaction" WaveOption: type: object required: [key, icon] properties: key: type: string example: running icon: type: string description: Имя иконки Material Symbols WaveOptions: type: object required: [activities, moods] properties: activities: type: array items: $ref: "#/components/schemas/WaveOption" moods: type: array items: $ref: "#/components/schemas/WaveOption" WaveContext: type: object properties: activity: type: string nullable: true mood: type: string nullable: true WaveStart: type: object properties: activity: type: string nullable: true mood: type: string nullable: true seed: type: object required: [type, id] properties: type: type: string enum: [track, album, artist] id: type: string format: uuid WaveFeedback: type: object required: [track_id, outcome] properties: track_id: type: string format: uuid outcome: type: string enum: [complete, skip, skip_early, like, dislike] WaveNext: type: object properties: feedback: type: array maxItems: 50 items: $ref: "#/components/schemas/WaveFeedback" WaveBatch: type: object required: [session_id, activity, mood, tracks, fallback] properties: session_id: type: string format: uuid activity: type: string nullable: true mood: type: string nullable: true tracks: type: array items: $ref: "#/components/schemas/Track" fallback: type: boolean description: Рекомендатель недоступен — пачка собрана упрощённо (лайки, любимые исполнители) AnalysisStatus: type: object required: [queued, running, done, failed, analyzer_ver, top_error] properties: queued: type: integer running: type: integer done: type: integer failed: type: integer analyzer_ver: type: string nullable: true top_error: type: string nullable: true description: Самая частая ошибка среди неудавшихся — подсказка, что чинить WaveMetrics: type: object required: [served, listen_through_rate, early_skip_rate, like_rate, sessions, by_model] description: Метрики волны за 7 дней properties: served: type: integer listen_through_rate: type: number nullable: true early_skip_rate: type: number nullable: true like_rate: type: number nullable: true description: Лайков на 100 треков sessions: type: integer by_model: type: array description: Те же метрики по версии модели показа (v1 / ALS / ранжировщик) — сравнение онлайн items: $ref: '#/components/schemas/WaveModelMetrics' WaveModelMetrics: type: object required: [model_ver, served, listen_through_rate, early_skip_rate, like_rate] properties: model_ver: type: string served: type: integer listen_through_rate: type: number nullable: true early_skip_rate: type: number nullable: true like_rate: type: number nullable: true AppClient: type: string enum: [android, windows, linux, macos] AppClientBuild: type: object required: [version, filename, size, download_url] properties: version: type: string filename: type: string size: type: integer format: int64 sha256: type: string download_url: type: string AppClientFile: type: object required: [client, filename, size, download_url] properties: client: $ref: "#/components/schemas/AppClient" version: type: string filename: type: string size: type: integer format: int64 sha256: type: string download_url: type: string older: type: array items: $ref: "#/components/schemas/AppClientBuild" AppClients: type: object required: [version, version_code, notes_md, clients] properties: version: type: string version_code: type: integer notes_md: type: string clients: type: array items: $ref: "#/components/schemas/AppClientFile" AndroidRelease: type: object required: [version, version_code, notes_md, size, sha256, download_url, min_supported] properties: version: type: string example: 0.6.1 version_code: type: integer notes_md: type: string description: Что нового (Markdown из релиза) size: type: integer format: int64 sha256: type: string download_url: type: string min_supported: type: integer description: Версии ниже обязаны обновиться Session: type: object required: [id, device_name, client_type, created_at, last_used_at, current] properties: id: type: string format: uuid device_name: type: string nullable: true client_type: $ref: "#/components/schemas/ClientType" created_at: type: string format: date-time description: Время входа last_used_at: type: string format: date-time current: type: boolean description: Сессия, из которой сделан запрос SessionList: type: object required: [items] properties: items: type: array items: $ref: "#/components/schemas/Session" FieldError: type: object required: [field, code] properties: field: type: string code: type: string Problem: type: object required: [status, code] properties: type: type: string default: about:blank status: type: integer code: type: string description: Машинный код ошибки для i18n, например `auth.invalid_credentials` detail: type: string description: Пояснение для логов (не для показа пользователю) params: type: object additionalProperties: true errors: type: array description: Ошибки отдельных полей запроса items: $ref: "#/components/schemas/FieldError"