Files

211 lines
7.2 KiB
Markdown
Raw Permalink 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.
# media-mirror — API координатора зеркал
Сервис конвертации видео в «зеркала» для AR-очков: одно видео (480p, H.264, без звука) + N аудиодорожек (Vorbis/ogg) отдельными файлами. Воркеры на GPU-машинах молотят очередь из Postgres, готовые файлы лежат в S3 (SeaweedFS).
## Базовый URL
| Окружение | URL |
|-----------|-----|
| Прод (из интернета) | `https://mirror.binom.pw` |
| Прод (внутри сети) | `http://mirror-api-media-mirror-api:8080` |
## Авторизация
Все эндпоинты `/api/**` требуют заголовок:
```
X-API-Key: <ключ>
```
Без ключа или с неверным — `401`. `/actuator/**` открыт (health-пробы).
## Модель задачи (Job)
| Поле | Тип | Описание |
|------|-----|----------|
| `id` | uuid | ID задачи |
| `itemId` | string | ID элемента из Jellyfin |
| `status` | string | `new` → `processing` → `done` / `failed` / `cancelled` |
| `progress` | int | 0..100 |
| `videoKey` | string | S3-ключ видео (после done) |
| `audioKeys` | array | список аудиодорожек (после done) |
## Эндпоинты
### 1. Создать задачу на конвертацию
```
POST /api/mirror
```
**Запрос:**
```json
{
"itemId": "d57a20a9909921f0bd133c3bbae6f9db",
"sourceUrl": "", // опционально; пусто = воркер сам построит из itemId
"sourceType": "jellyfin" // по умолчанию jellyfin
}
```
**Ответы:**
- `201 Created` — задача создана
- `200 OK` — задача с таким itemId уже существует (не дублируется), возвращается существующая
```json
{ "id": "4a063de2-f5e9-40b7-b395-e9c17dd23301", "status": "new" }
```
- `400` — itemId пустой / битый JSON
- `401` — нет/неверный X-API-Key
### 2. Статус задачи по ID
```
GET /api/mirror/{id}
```
```json
{
"id": "4a063de2-f5e9-40b7-b395-e9c17dd23301",
"itemId": "d57a20a9909921f0bd133c3bbae6f9db",
"status": "processing",
"progress": 45
}
```
`404` — задачи нет.
### 3. Статус + файлы зеркала по itemId (главный для клиента!)
```
GET /api/mirror/by-item/{itemId}
```
**Ответ, если готово:**
```json
{
"itemId": "d57a20a9909921f0bd133c3bbae6f9db",
"status": "done",
"files": {
"video": {
"key": "mirror/d57a20a9909921f0bd133c3bbae6f9db/video.mkv",
"url": "https://s3.binom.pw/media/mirror/d57a20a9909921f0bd133c3bbae6f9db/video.mkv"
},
"audios": [
{
"index": 0,
"key": "mirror/d57a20a9909921f0bd133c3bbae6f9db/audio-0.ogg",
"title": "Dub [Reanimedia]",
"language": "rus",
"url": "https://s3.binom.pw/media/mirror/d57a20a9909921f0bd133c3bbae6f9db/audio-0.ogg"
},
{
"index": 1,
"key": "mirror/d57a20a9909921f0bd133c3bbae6f9db/audio-1.ogg",
"title": "Original",
"language": "eng",
"url": "https://s3.binom.pw/media/mirror/d57a20a9909921f0bd133c3bbae6f9db/audio-1.ogg"
}
]
}
}
```
**Ответ, если ещё не готово:**
```json
{ "itemId": "...", "status": "processing", "files": null }
```
**Ответ, если задачи нет вообще:**
`404` — клиент понимает: «ещё не конвертировано, можно заказать».
### 4. Список задач (фильтр по статусу)
```
GET /api/mirror?status=done&limit=50
```
`status` — опционально (`new`/`processing`/`done`/`failed`/`cancelled`), `limit` — опционально (дефолт 50).
**Ответ:**
```json
[
{ "id": "...", "itemId": "...", "status": "done", "progress": 100 }
]
```
### 5. Удалить / отменить задачу
```
DELETE /api/mirror/{id}
```
- задача `new`/`processing` → `cancelled` (воркер пропустит)
- задача `done` → файлы удаляются из S3 + запись удаляется
- повторный DELETE → `404`
### 6. Сканировать библиотеку Jellyfin (догонялка)
```
POST /api/mirror/scan
```
Обходит Jellyfin (Movies + Episodes), создаёт задачи на **только недостающее**:
- есть активная задача (`new`/`processing`/`done`) → **skip**
- нет задачи / последняя `failed`/`cancelled` → **create**
Идемпотентен — можно дёргать сколько угодно, дублей не будет. Используется, когда webhook'и Jellyfin потерялись.
**Ответ:**
```json
{ "found": 1387, "created": 12, "skipped": 1375 }
```
`502` — Jellyfin недоступен.
## Формат зеркала (что получит клиент)
| Файл | Кодек | Контейнер | Разрешение | Описание |
|------|-------|-----------|------------|----------|
| `video.mkv` | H.264 (NVENC) | mkv | 480p (высота ≤480) | **без звука**, без субтитров/метаданных |
| `audio-N.ogg` | Vorbis | ogg | 2ch | **каждая** дорожка исходника отдельным файлом |
Сколько аудио — столько дорожек было в исходнике (все без исключения). Мета каждой дорожки (`title`, `language`) — в ответе `by-item`, берётся из ffprobe исходника.
## Скачивание файлов (S3)
Клиент качает файлы **напрямую из S3** (SeaweedFS), не через координатор:
- S3 endpoint: `https://s3.binom.pw`
- bucket: `media`
- путь: `mirror/{itemId}/video.mkv`, `mirror/{itemId}/audio-{index}.ogg`
**Креды клиента (зашиты в приложение):**
```
accessKey: MirrorClient01QwertyKey
secretKey: MirrorClient01SecretKeyValueForGlassesOnly
```
Права клиентского ключа: **только чтение** `media/mirror/*` — писать/удалять/листать нельзя. Подписывать запросы по S3 SigV4 (AWS SDK, region `us-east-1`). Поддержка Range-запросов есть (видео можно качать/играть по кускам).
## Пример полного цикла клиента
```text
1. GET https://mirror.binom.pw/api/mirror/by-item/{itemId} [X-API-Key]
→ 404 → 2а
→ files → 4
2а. POST /api/mirror {"itemId": "..."} [X-API-Key]
2б. GET by-item, пока status != done (поллинг раз в 5-10с)
3. status == done → берём files.video.url + files.audios[].url
4. Качаем по url с mirror-client кредами (SigV4) → играем
(видео — на очки, выбранную аудио-дорожку — на телефон)
```
## Health
```
GET /actuator/health
```
Открыт без ключа. `{"status": "UP"}` — сервис жив.