# 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"}` — сервис жив.