ci / ts / ts (packages + web) (push) Canceled after 1m0s
ci / api / api (push) Successful in 46s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 56s
ci / db / db (push) Successful in 1m32s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m27s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m31s
Search hits omit album covers and artist images. The worker now reads the full song and artist, stores the description, and leaves Genius image URLs unchanged on mobile. Co-authored-by: Cursor <cursoragent@cursor.com>
3696 lines
113 KiB
YAML
3696 lines
113 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/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/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` — показать онбординг: пользователь его не проходил и не пропускал, а своей истории почти нет.
|
||
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, AppImage/deb, DMG.
|
||
Нет настроек 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
|
||
|
||
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:
|
||
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:
|
||
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
|
||
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:
|
||
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"
|
||
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"
|
||
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"
|
||
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
|
||
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:
|
||
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:
|
||
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:
|
||
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`.
|
||
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:
|
||
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
|
||
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]
|
||
AppClientFile:
|
||
type: object
|
||
required: [client, filename, size, download_url]
|
||
properties:
|
||
client:
|
||
$ref: "#/components/schemas/AppClient"
|
||
filename:
|
||
type: string
|
||
size:
|
||
type: integer
|
||
format: int64
|
||
sha256:
|
||
type: string
|
||
download_url:
|
||
type: string
|
||
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"
|