Files
pipu-music/api/openapi.yaml
T
mrleo1nidandClaude Opus 5.5 6a67c27f50
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 43s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m12s
ci / api / api (push) Successful in 35s
ci / ts / ts (packages + web) (push) Successful in 1m20s
feat(api): Android self-update endpoints proxied from Gitea releases (M5)
GET /api/v1/app/android/latest reads the latest Gitea release with the
server token (cached for 10 minutes); the download endpoint caches the
APK under /updates, verifies its SHA-256 and serves it with Range.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 12:52:41 +03:00

2380 lines
69 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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:
/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"
/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/android/latest:
get:
operationId: getAndroidLatest
tags: [app]
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]
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"
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, 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"
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
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]
properties:
queued:
type: integer
running:
type: integer
done:
type: integer
failed:
type: integer
analyzer_ver:
type: string
nullable: true
WaveMetrics:
type: object
required: [served, listen_through_rate, early_skip_rate, like_rate, sessions]
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
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"