diff --git a/README.md b/README.md index 5e20254..96f542a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,210 @@ -# media-mirror-api +# media-mirror — API координатора зеркал -Media mirror: конвертация в зеркала для очков \ No newline at end of file +Сервис конвертации видео в «зеркала» для 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"}` — сервис жив.