- tracks carry a stream_url signed for the user for 12 h and the user's reaction; /stream checks the signature, expiry and that the user is not blocked, then hands the file to nginx by X-Accel-Redirect (or serves it with Range support when X_ACCEL=false) - original by default; formats browsers can't play and explicit high/medium/low go through Opus transcoding, never upwards: a lossy source under the target bitrate stays original; transcodes are cached per file and size, deduplicated in flight, limited by TRANSCODE_CONCURRENCY and trimmed hourly by LRU to the size limit - /events takes up to 500 events with client UUIDs, drops repeats and events older than 30 days, counts plays (≥ half or 4 min, or finished), early skips and history entries (≥ 30 s) - history with clearing, likes and dislikes with cursors and totals, track reactions, playback state with last-write-wins, tracks batch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2086 lines
61 KiB
YAML
2086 lines
61 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
|
||
security:
|
||
- bearerAuth: []
|
||
paths:
|
||
/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/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}/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/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/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"
|
||
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
|
||
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:
|
||
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]
|
||
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; веб передаёт токен в 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`"
|
||
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]
|
||
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, 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"
|
||
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, 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"
|
||
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"
|
||
CreatedUser:
|
||
type: object
|
||
required: [user, temporary_password]
|
||
properties:
|
||
user:
|
||
$ref: "#/components/schemas/AdminUser"
|
||
temporary_password:
|
||
type: string
|
||
description: Показывается один раз
|
||
UpdateUserRequest:
|
||
type: object
|
||
properties:
|
||
display_name:
|
||
type: string
|
||
maxLength: 64
|
||
description: Пустая строка — сбросить имя
|
||
role:
|
||
$ref: "#/components/schemas/Role"
|
||
is_blocked:
|
||
type: boolean
|
||
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, 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 — только вручную
|
||
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
|
||
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 — подключить снова
|
||
SourceTestResult:
|
||
type: object
|
||
required: [ok, message, audio_files]
|
||
properties:
|
||
ok:
|
||
type: boolean
|
||
message:
|
||
type: string
|
||
audio_files:
|
||
type: integer
|
||
description: Сколько аудиофайлов найдено (проверка останавливается на 1000)
|
||
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]
|
||
properties:
|
||
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]
|
||
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"
|
||
Artist:
|
||
type: object
|
||
required: [id, name, album_count, track_count]
|
||
properties:
|
||
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
|
||
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"
|
||
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"
|