ci / api / api (push) Successful in 50s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m0s
ci / db / db (push) Successful in 58s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m27s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m39s
ci / ts / ts (packages + web) (push) Successful in 2m40s
4070 lines
126 KiB
YAML
4070 lines
126 KiB
YAML
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/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/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:
|
||
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
|
||
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"
|