testube — справочник для фронтенда
Всё, что нужно, чтобы написать клиент: адреса, формы запросов и ответов, правила доступа и грабли, на которые здесь легко наступить.
- Базовый адрес:
http://localhost:8080(порт изAPP__SERVER__PORT). - Формат — JSON, кодировка UTF-8. Тела запросов требуют
Content-Type: application/json. - CORS открыт полностью, так что клиент можно поднимать на любом порту.
Аутентификация
JWT в заголовке: Authorization: Bearer <access_token>. Access-токен живёт
expires_in секунд (по умолчанию час), после чего меняется на новый через
/auth/refresh — заново спрашивать пароль не нужно.
| Метод | Путь | Тело | Ответ |
|---|---|---|---|
| POST | /auth/register | {username, email, password} | 201 TokenResponse |
| POST | /auth/login | {email, password} | 200 TokenResponse |
| POST | /auth/refresh | {refresh_token} | 200 TokenResponse |
| POST | /auth/logout | {refresh_token} | 204 |
| POST | /auth/logout-all | — (токен) | 204 |
| GET | /auth/me | — | 200 User |
// TokenResponse
{
"access_token": "eyJhbGciOi...",
"refresh_token": "7TXspv4MCFitf_Z1MOvBNbF8xXlEsKE7UPJFKYkPaXs",
"token_type": "Bearer",
"expires_in": 3600,
"user": {
"id": "...",
"username": "neo",
"display_name": "Нео",
"email": "neo@example.com",
"avatar_url": null,
"created_at": "2026-09-06T10:00:00Z"
}
}
Refresh-токен одноразовый. Каждый обмен выдаёт новую пару и гасит
предъявленный токен; храните только последний. Если предъявить уже
потраченный, вся цепочка обновлений этой сессии гасится целиком и приходит
401 — это защита от повторного использования украденного токена, а не
ошибка клиента. Практический вывод: не обновляйте токен из двух вкладок
одновременно, иначе вторая уронит сессию обеим. Живёт refresh-токен 30 суток
(JWT__REFRESH_TTL_SECS).
/auth/logout гасит одну сессию и не требует access-токена: выйти нужно
уметь и с протухшим. /auth/logout-all гасит все сессии пользователя.
Ограничения: username — 3–32 символа латиницы, цифр, _ и -;
пароль — 8–128 символов. Email приводится к нижнему регистру.
username — это ещё и адрес канала (@username в ссылке), а display_name —
показываемое имя: его правят через PATCH /me/channel, и ссылки при этом
не ломаются.
/auth/login не уточняет, что именно неверно — email или пароль:
иначе эндпоинт превращается в оракул для перебора зарегистрированных адресов.
Показывайте пользователю общее «неверный email или пароль».
Идентификаторы
У видео короткий публичный идентификатор — 11 символов алфавита
A-Za-z0-9_-, как в ссылках YouTube (dQw4w9WgXcQ). Именно он приходит
в поле id и подставляется во все пути:
/videos/dQw4w9WgXcQ
/videos/dQw4w9WgXcQ/playback
/videos/dQw4w9WgXcQ/hls/master.m3u8
Внутренний UUID наружу не выходит вовсе. Идентификатор непрозрачен: не разбирайте его и не пытайтесь по нему что-то вычислить.
Остальные сущности — пользователи, каналы, комментарии, плейлисты,
уведомления — адресуются UUID. Канал вдобавок доступен по имени:
/channels/by-username/neo. Строка неверного формата в пути видео даёт 404,
а не 400: API не подсказывает, какие идентификаторы бывают.
Ошибки
Тело всегда одинаковое: {"error": "человекочитаемый текст"}.
| Код | Когда |
|---|---|
400 | не прошла валидация, битый курсор, подписка на себя |
401 | нет токена, токен просрочен или испорчен |
403 | доступ есть, но действие чужое (правка чужого видео) |
404 | нет объекта — или он есть, но скрыт (см. ниже) |
409 | состояние не позволяет: файл не загружен, видео не готово |
422 | тело не разобралось (не тот тип поля, неизвестный enum) |
429 | превышен лимит частоты, см. заголовок Retry-After |
500 | внутренняя ошибка; подробности только в логах сервера |
404 вместо 403 для приватного. Чужое приватное видео отвечает 404,
как будто его не существует. Это намеренно: иначе по кодам ответов можно
перебором узнать, какие идентификаторы заняты. Не показывайте пользователю
«нет доступа» там, где пришёл 404.
Ограничение частоты
Общий лимит — 20 запросов в секунду с всплеском до 40, на /auth/* строже:
10 в минуту. При отказе приходит 429 и Retry-After в секундах.
Клиенту стоит уважать Retry-After и не ретраить вслепую: лимит считается
по IP, и параллельные вкладки складываются в один счётчик.
Пагинация
Два разных механизма — не перепутайте.
Курсорная (/videos, /videos/my, /feed, /channels/{id}/videos,
/me/history, /notifications, комментарии). Параметры ?limit= (1–100,
по умолчанию 20) и ?cursor=.
{ "items": [/* ... */], "next_cursor": "dHwyMDI2LTA5LTA2VDEwOjAwOjAwWnw..." }
Следующая страница — тот же запрос с cursor=next_cursor. next_cursor: null
означает, что данные кончились. Курсор непрозрачен: не разбирайте его и не
конструируйте сами.
Внутри курсора спрятаны две разные формы пагинации. Хронологические
сортировки листаются по ключу «время + id», ранжированные (sort=trending,
sort=most_viewed, sort=top у комментариев) — по смещению: keyset-пагинация
требует стабильного полного порядка, а ранг не уникален и меняется вместе
с данными. Снаружи разницы нет, но курсор нельзя переносить между
сортировками: смена sort без сброса cursor даёт 400. Меняете
сортировку — начинайте список заново.
Offset-овая (/videos/search, /subscriptions, /playlists,
/playlists/{id}/items, /me/reports). Параметры ?limit= и ?offset=,
в ответе они же:
{ "items": [/* ... */], "limit": 20, "offset": 0 }
Модели
type VideoStatus = "uploading" | "processing" | "ready" | "failed";
type Visibility = "public" | "unlisted" | "private";
type Reaction = "like" | "dislike";
type FeedSort = "newest" | "oldest" | "trending" | "most_viewed";
type CommentSort = "newest" | "top";
interface ChannelSummary {
// всё, что нужно строке под заголовком
id: string; // UUID автора, он же id канала
username: string;
display_name: string;
avatar_url: string | null;
subscriber_count: number;
}
interface Video {
id: string; // 11 символов, короткая ссылка
channel: ChannelSummary; // канал едет вместе с видео, второй запрос не нужен
title: string;
description: string | null;
status: VideoStatus;
visibility: Visibility;
duration_secs: number | null; // появляется после транскодинга
thumbnail_url: string | null; // путь этого же API, не ссылка на S3
hls_url: string | null; // null, пока видео не транскодировано
view_count: number;
like_count: number;
dislike_count: number;
comment_count: number;
tags: string[]; // до 15 штук, нижний регистр
published_at: string | null; // когда стало видно; null — не публиковалось
created_at: string; // RFC 3339
updated_at: string;
}
interface Comment {
id: string;
video_id: string;
author_id: string;
author_username: string;
author_display_name: string;
author_avatar_url: string | null;
parent_id: string | null; // корень ветки; null — сам корень
body: string;
like_count: number;
reply_count: number;
liked_by_me: boolean | null; // null для анонима
edited_at: string | null; // непусто → показать «изменено»
created_at: string;
updated_at: string;
}
interface ChannelLink {
label: string;
display: string;
url: string;
}
interface Channel {
id: string;
username: string; // адрес канала: @username
display_name: string;
description: string; // короткая строка карточки
about: string | null; // длинный текст вкладки «о канале»
avatar_url: string | null;
banner_url: string | null;
country: string | null; // ISO 3166-1 alpha-2
links: ChannelLink[];
created_at: string;
subscriber_count: number;
video_count: number; // только публичные готовые видео
view_count: number; // суммарно по всем публичным видео канала
subscribed: boolean | null; // null для анонима
notifications_enabled: boolean | null; // колокольчик; null, если подписки нет
}
interface Playlist {
id: string;
owner_id: string;
owner_username: string;
owner_display_name: string;
owner_avatar_url: string | null;
title: string;
description: string | null;
visibility: Visibility;
video_count: number;
cover_video_id: string | null; // первое видео плейлиста — обложка карточки
created_at: string;
updated_at: string;
}
interface WatchProgress {
position_secs: number; // где остановились
watched_secs: number; // суммарно просмотрено
completed: boolean;
last_watched_at: string;
}
type NotificationKind = "new_video" | "comment_reply" | "video_comment";
interface Notification {
id: string;
kind: NotificationKind;
actor_id: string;
actor_username: string;
actor_display_name: string;
actor_avatar_url: string | null;
video_id: string | null; // короткий id, если событие про видео
video_title: string | null;
comment_id: string | null;
read: boolean;
created_at: string;
}
type ReportReason =
"spam" | "sexual" | "violence" | "hate" | "harassment" | "misinformation" | "copyright" | "other";
interface Report {
id: string;
video_id: string;
reason: ReportReason;
details: string | null;
status: "pending" | "reviewed" | "dismissed";
created_at: string;
}
Формулировку уведомления («X выложил Y») собирает клиент: она зависит от
языка, а API отдаёт только kind, автора и заголовок видео.
Жизненный цикл видео: uploading → (подтверждение загрузки) → processing →
(воркер) → ready, либо failed, если транскодинг не удался. Пока статус не
ready, hls_url и thumbnail_url пустые, а видео не попадает ни в ленту,
ни в поиск.
Видео
| Метод | Путь | Доступ | Описание |
|---|---|---|---|
| POST | /videos | токен | Создать запись, получить ссылку для загрузки |
| POST | /videos/{id}/complete | владелец | Подтвердить загрузку |
| GET | /videos | все | Лента: public + ready, курсор |
| GET | /videos/search?q= | все | Поиск, offset |
| GET | /videos/my | токен | Свои видео, все статусы, курсор |
| GET | /videos/{id} | по видимости | Карточка |
| PATCH | /videos/{id} | владелец | Правка метаданных |
| DELETE | /videos/{id} | владелец | Удалить запись и файлы |
| GET | /videos/{id}/playback | по видимости | Как воспроизводить |
| POST | /videos/{id}/view | все | Засчитать просмотр |
| GET | /videos/{id}/related | по видимости | Похожие видео (до 20) |
| GET | /videos/{id}/thumbnail | по видимости | Превью (картинка) |
| GET | /videos/{id}/hls/{path} | по видимости | Манифест и сегменты |
Лента: сортировка и категории
GET /videos?sort=trending&tag=rust&limit=20
sort — newest (по умолчанию), oldest, most_viewed, trending.
«В тренде» — просмотры, поделённые на возраст: без деления подборка навсегда
застревала бы на старых роликах с большим счётчиком.
tag фильтрует ленту по одному тегу — это и есть чипы категорий над ней.
Теги хранятся в нижнем регистре, так что фильтр регистронезависим только
если вы шлёте их уже нормализованными.
Смена sort требует сбросить cursor (см. «Пагинация»).
Просмотры
POST /videos/{id}/view возвращает {"view_count": 123}.
Зовите его один раз на начало просмотра, а не по таймеру. Для авторизованного зрителя повторный заход в пределах получаса не засчитывается — дедупликация идёт по истории просмотров. Анонимов дедуплицировать нечем, их просмотры считаются всегда. Ответ приходит и в том случае, когда просмотр не зачли: в нём текущее значение счётчика, которое и надо нарисовать.
Загрузка: три шага
// 1. Создаём запись. visibility по умолчанию private.
const created = await post("/videos", {
title: "Прогулка по городу",
description: null,
visibility: "public",
});
// { video: Video, upload_url: "https://s3...", expires_in: 900 }
// 2. Кладём файл прямо в хранилище. Этот запрос идёт мимо API.
await fetch(created.upload_url, { method: "PUT", body: file });
// 3. Подтверждаем: API сам проверит, что объект появился в хранилище.
await post(`/videos/${created.video.id}/complete`);
// Video со статусом processing
Замечания:
upload_urlживётexpires_inсекунд; просроченную ссылку заново не выдают — создавайте видео заново.- Шаг 2 не отправляйте с заголовком
Authorization: подпись уже в ссылке, лишние заголовки только мешают. - Если браузер ругается на CORS на шаге 2, это настройка хранилища
(
MINIO_API_CORS_ALLOW_ORIGIN), а не API. - Повторный
/completeна уже подтверждённом видео вернёт409— это защита от повторной постановки в очередь. /completeвернёт409и в том случае, если файла в хранилище нет: API верит хранилищу, а не клиенту.
Теги
tags задаются при создании и заменяются целиком при правке (PATCH с полем
tags). До 15 штук, каждый 1–30 символов. API обрезает пробелы, приводит
к нижнему регистру и убирает повторы, так что ["Rust", "rust", " Axum "]
превращается в ["rust", "axum"]. Теги участвуют в поиске с меньшим весом,
чем заголовок и описание, и по ним же фильтруется лента.
Правка
PATCH /videos/{id} принимает любое подмножество полей title,
description, visibility. Отсутствие поля и null — разные вещи:
{ "title": "Новый заголовок" } // описание не тронуто
{ "description": null } // описание очищено
Воспроизведение
GET /videos/{id}/playback возвращает один из двух вариантов:
{ "hls_url": "/videos/<id>/hls/master.m3u8" }
{ "source_url": "https://s3...", "expires_in": 3600 }
Первый — готовое видео. Второй отдаётся только владельцу и только пока
транскодинг не закончен, чтобы автор мог проверить загрузку сразу. Остальным
до готовности приходит 409.
Манифест и сегменты идут через API, а не по presigned-ссылкам: пути внутри манифеста относительные, и подписать их по отдельности плеер не умеет. Поэтому каждый запрос сегмента проверяет видимость видео — и требует токен, если видео не публичное.
Как подключать плеер
// hls.js предпочтительнее нативного HLS везде, где поддерживается.
// Проверять canPlayType первым нельзя: Chromium отвечает "maybe",
// хотя HLS не играет, и вы уйдёте на путь без токена.
if (Hls.isSupported()) {
const hls = new Hls({
xhrSetup: (xhr) => xhr.setRequestHeader("Authorization", `Bearer ${token}`),
});
hls.loadSource(playback.hls_url);
hls.attachMedia(video);
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = playback.hls_url; // Safari; приватные видео так не откроются
}
Уровни качества берите из события MANIFEST_PARSED (data.levels),
переключение — присваиванием hls.currentLevel (-1 — автовыбор).
Обработчик ERROR обязан считать попытки восстановления: и startLoad(),
и recoverMediaError() заново тянут сегменты, а на битом файле ошибка
повторяется мгновенно — без счётчика плеер уходит в бесконечный цикл
запросов к серверу.
Превью
thumbnail_url — это путь API (/videos/{id}/thumbnail), а не ссылка на
хранилище. Тег <img> не умеет отправлять Authorization, поэтому превью
приватных видео в разметке не отобразится. Варианты: показывать заглушку
или грузить через fetch с токеном в blob:-ссылку.
Реакции
| Метод | Путь | Тело | Ответ |
|---|---|---|---|
| PUT | /videos/{id}/reaction | {"reaction": "like"} | счётчики |
| DELETE | /videos/{id}/reaction | — | счётчики |
| GET | /videos/{id}/reaction | — | счётчики и свой голос |
{ "likes": 12, "dislikes": 1, "my_reaction": "like" }
Реакция у пользователя одна: PUT с другим значением переносит голос, а не
добавляет второй. Все три ручки требуют токен. Счётчики в карточке видео
(like_count, dislike_count) обновляются той же транзакцией, так что после
ответа можно просто перерисовать число из него.
Комментарии
| Метод | Путь | Доступ |
|---|---|---|
| GET | /videos/{id}/comments | по видимости, курсор |
| POST | /videos/{id}/comments | токен, тело {"body": "...", "parent_id": null} |
| GET | /comments/{id}/replies | по видимости, курсор |
| PATCH | /comments/{id} | только автор |
| DELETE | /comments/{id} | автор или владелец видео |
| PUT | /comments/{id}/like | токен |
| DELETE | /comments/{id}/like | токен |
Длина текста — 1–10 000 символов после обрезки пробелов. Владелец видео может
удалять чужие комментарии под своим роликом, но не править их. Правка
проставляет edited_at — по нему и рисуется метка «изменено»; updated_at
для этого не годится, его двигают и служебные пересчёты счётчиков.
Ответы одноуровневые. GET /videos/{id}/comments отдаёт только верхний
уровень с reply_count у каждого; ветка разворачивается отдельным запросом
/comments/{id}/replies (там старые сверху — ветку читают как разговор).
parent_id должен указывать на корневой комментарий этого же видео: ответ
на ответ — 400, чужой корень — 404. Удаление корня уносит ветку целиком.
?sort=top сортирует верхний уровень по лайкам вместо свежести. Лайк
идемпотентен: повторное нажатие не удваивает счётчик. liked_by_me в ответе
показывает собственный голос, для анонима он null.
Комментарий под чужим видео создаёт уведомление владельцу, ответ — автору ветки. Себе уведомления не приходят.
Каналы и подписки
| Метод | Путь | Доступ | Описание |
|---|---|---|---|
| GET | /channels/{id} | все | Карточка канала |
| GET | /channels/by-username/{username} | все | Она же по имени из ссылки |
| GET | /channels/{id}/videos | все | Публичные видео, курсор |
| PATCH | /me/channel | токен | Правка своего профиля |
| PUT | /channels/{id}/subscription | токен | Подписаться / колокольчик |
| DELETE | /channels/{id}/subscription | токен | Отписаться |
| GET | /subscriptions | токен | Мои подписки, offset |
| GET | /feed | токен | Лента подписок, курсор |
Идентификатор канала — это channel.id из карточки видео. Адресовать канал
можно и по имени: /channels/by-username/neo или /channels/by-username/@neo
— ведущая @ принимается, регистр не важен.
Подписка и отписка идемпотентны: повтор возвращает тот же 204. Подписка
на себя — 400, на несуществующий канал — 404. Тело PUT необязательно;
{"notifications_enabled": false} оставляет подписку, но выключает
уведомления о новых видео. Тем же запросом колокольчик и переключают.
Правка профиля
PATCH /me/channel
{
"display_name": "Нео",
"description": "про матрицу",
"about": "длинный текст вкладки «о канале»",
"avatar_url": "https://...",
"banner_url": null,
"country": "UZ",
"links": [{"label": "Сайт", "display": "example.com", "url": "https://example.com"}]
}
Все поля необязательны. Отсутствие поля означает «не трогать», явный null —
«очистить»; строка из одних пробелов равносильна null. links заменяется
целиком (до 10 штук). Адрес ссылки принимается только со схемой http://,
https:// или mailto: — он уходит в href на клиенте, и javascript:
там был бы XSS. username через этот запрос не меняется: он адресует канал.
Ответ — та же карточка канала, что и у GET /channels/{id}.
Поиск
GET /videos/search?q=прогулка+по+городу&limit=20&offset=0
Ищет по заголовку и описанию публичных готовых видео; заголовок весит больше.
Запрос разбирается как в поисковике: кавычки задают точную фразу, -слово
исключает. Пустой q — 400. Результаты отсортированы по релевантности,
затем по свежести.
История просмотров
| Метод | Путь | Доступ | Описание |
|---|---|---|---|
| PUT | /videos/{id}/progress | токен | Сохранить позицию |
| GET | /videos/{id}/progress | токен | Где остановились |
| GET | /me/history | токен | История, курсор |
| DELETE | /me/history/{id} | токен | Убрать одно видео |
| DELETE | /me/history | токен | Очистить целиком |
PUT /videos/dQw4w9WgXcQ/progress
{"position_secs": 42.5, "watched_delta_secs": 10, "completed": false}
watched_delta_secs — приращение с прошлого отчёта, а не «сколько всего»:
суммарного времени клиент не знает, смотреть могли и с другого устройства.
Отправляйте отчёт раз в 10 секунд воспроизведения, а не на каждый тик.
completed только взводится: досмотрев ролик и перемотав назад, зритель
не делает его недосмотренным. Порог «досмотрено» решает клиент — он знает
и длительность, и то, что для тридцатисекундного ролика и двухчасового фильма
это разный запас.
GET /videos/{id}/progress отвечает 404, если это видео ещё не смотрели:
«нечего возобновлять» — отсутствие записи, а не нулевой прогресс, который
пришлось бы отличать от настоящего нуля.
Строка /me/history — это карточка видео плюс поле progress:
{
"id": "dQw4w9WgXcQ",
"title": "...",
"channel": {},
"progress": {
"position_secs": 42.5,
"watched_secs": 60,
"completed": false,
"last_watched_at": "2026-09-07T10:00:00Z"
}
}
Удалённые и ставшие приватными чужие видео из истории выпадают сами.
Плейлисты
| Метод | Путь | Доступ | Описание |
|---|---|---|---|
| POST | /playlists | токен | Создать |
| GET | /playlists?owner_id= | по видимости | Библиотека, offset |
| GET | /playlists/{id} | по видимости | Карточка |
| PATCH | /playlists/{id} | владелец | Правка |
| DELETE | /playlists/{id} | владелец | Удалить |
| GET | /playlists/{id}/items | по видимости | Содержимое по порядку, offset |
| POST | /playlists/{id}/items | владелец | Добавить видео в конец |
| PUT | /playlists/{id}/items/{video_id} | владелец | Переставить |
| DELETE | /playlists/{id}/items/{video_id} | владелец | Убрать |
| GET | /videos/{id}/playlists | токен | В каких моих плейлистах уже есть |
GET /playlists без owner_id показывает свою библиотеку целиком; с чужим
owner_id — только публичные плейлисты этого канала. Без токена и без
owner_id — 401: чья библиотека, неизвестно.
Повторное добавление того же видео — 409, а не второй экземпляр. Чужое
приватное видео в плейлист не кладётся (404): это был бы обход приватности.
Наоборот тоже: приватные видео не отдаются из чужого публичного плейлиста.
Позиции плотные и считаются с нуля. PUT .../items/{video_id} с
{"position": 0} двигает видео в начало, сдвигая остальных; позиция за
пределами списка прижимается к краю, и ответ говорит, куда видео встало
на самом деле. Удаление подтягивает позиции за собой, дыр не остаётся.
cover_video_id — короткий id первого видео плейлиста; обложку карточки
рисуйте из /videos/{cover_video_id}/thumbnail.
GET /videos/{id}/playlists возвращает массив UUID — это галочки в меню
«сохранить в плейлист».
Уведомления
| Метод | Путь | Описание |
|---|---|---|
| GET | /notifications?unread=true | Ящик, курсор |
| GET | /notifications/unread-count | Цифра на колокольчике |
| POST | /notifications/{id}/read | Отметить прочитанным |
| POST | /notifications/read | Отметить прочитанным весь ящик |
| DELETE | /notifications/{id} | Убрать из ящика |
Все требуют токен. Уведомления создаются сами:
new_video— канал из подписок опубликовал видео. Рассылается подписчикам с включённым колокольчиком в момент, когда видео впервые становится видно (переход видимости изprivateпри статусеready);video_comment— под вашим видео появился комментарий;comment_reply— вам ответили в ветке.
Чужое уведомление отвечает 404 и на отметку, и на удаление.
Жалобы
| Метод | Путь | Доступ |
|---|---|---|
| POST | /videos/{id}/report | токен |
| GET | /me/reports | токен, offset |
POST /videos/dQw4w9WgXcQ/report
{"reason": "spam", "details": "сплошная реклама"}
reason — из перечисления ReportReason; неизвестное значение даёт 422.
Для other пояснение обязательно (400 без него), в остальных случаях —
необязательно, до 1000 символов. Повторная жалоба того же человека на то же
видео — 409: форму отправляют дважды случайно, и это не должно выглядеть
как две жалобы.
Разбор жалоб живёт вне API: status меняет модератор, клиент видит его
в /me/reports.
Служебное
| Метод | Путь | Описание |
|---|---|---|
| GET | /health | Жив ли процесс |
| GET | /health/ready | Доступны ли БД и хранилище (503, если нет) |
| GET | /metrics | Экспозиция Prometheus |
| GET | /docs | Этот документ страницей для браузера |
| GET | /docs.md | Он же исходником в Markdown, для инструментов |
Типовой сценарий
POST /auth/registerили/auth/login→ сохранить обе половины пары; по истеченииexpires_in—POST /auth/refresh.GET /videos?sort=trending&limit=20→ лента; листать поnext_cursor. Карточка канала уже внутри каждого элемента.GET /videos/{id}+GET /videos/{id}/playback→ карточка и плеер,POST /videos/{id}/viewодин раз на старте,GET /videos/{id}/progress→ предложить продолжить с сохранённой позиции.GET /videos/{id}/comments,GET /videos/{id}/reaction,GET /videos/{id}/related→ обсуждение и колонка «дальше».- Во время воспроизведения —
PUT /videos/{id}/progressраз в 10 секунд. - Загрузка:
POST /videos→PUT upload_url→POST /complete, дальше опрашиватьGET /videos/{id}, покаstatusне станетready(транскодинг идёт в фоне, уведомлений о его ходе нет).
Чего в API нет
Об этом стоит знать заранее, чтобы не искать:
- загрузки картинок.
avatar_urlиbanner_urlв профиле — это ссылки, которые вы записываете сами; хранилище API принимает только видео; - разбора жалоб. Модераторского интерфейса нет,
statusменяется вне API; - живых обновлений. Ни WebSocket, ни SSE: уведомления и статус транскодинга узнаются опросом;
- прогресса транскодинга в процентах — только
status; - смены пароля и email, восстановления доступа;
- истории и плейлистов для анонимов — и то и другое привязано к аккаунту.