# Music Orchestrator — Гайд для фронтенда (v0.1.0 MVP) ## Содержание 1. [Видение продукта](#1-видение-продукта) 2. [Аутентификация](#2-аутентификация) 3. [Модели данных](#3-модели-данных) 4. [Система провайдеров](#4-система-провайдеров) 5. [Полный справочник API](#5-полный-справочник-api) 6. [User Flows](#6-user-flows) 7. [Обработка ошибок](#7-обработка-ошибок) 8. [Пагинация](#8-пагинация) 9. [Real-time и playback](#9-real-time-и-playback) 10. [CORS и конфигурация](#10-cors-и-конфигурация) --- ## 1. Видение продукта **Music Orchestrator** — self-hosted сервис в стиле Spotify / YouTube Music, где **интернет = библиотека**. Пользователь ищет музыку через единый UI, а бэкенд оркестрирует несколько провайдеров (YouTube, SoundCloud, локальная библиотека/Navidrome), нормализует метаданные и отдаёт треки с учётом capabilities и policy каждого источника. ### Ключевые принципы - **Единый поиск**: один запрос → результаты из всех подключённых провайдеров, merged и deduplicated. - **Capabilities-first**: фронтенд **никогда не предполагает** возможности провайдера — всегда читает `capabilities` и `policy` из ответа. - **Provider badges**: каждый трек показывает, откуда он пришёл и какие ограничения есть. - **Playback-aware**: YouTube → embed iframe, Local → audio stream, SoundCloud → official stream URL. - **Безопасный по умолчанию**: cache/download кнопки показываются **только** если `policy.cache_allowed` / `policy.download_allowed` == `true`. ### MVP scope - Поиск по YouTube (metadata + embed), SoundCloud (metadata), Local (полный stream/cache). - Избранное и плейлисты (серверные, привязаны к API key). - Background jobs (resolve, metadata refresh, local ingest) — в MVP только хранят статус, не запускают unsafe extractors. - Single-user model (один API key = один user_id `local-user`). --- ## 2. Аутентификация Все **записывающие** (POST, DELETE) эндпоинты требуют заголовок `X-API-Key`. ``` X-API-Key: ``` - По умолчанию dev key: `dev-local-key-change-me` (настраивается через `APP_API_KEYS` env). - Можно указать несколько ключей через запятую в env. - При невалидном/отсутствующем ключе → `401 Unauthorized`. ### Пример ```bash curl -X POST http://localhost:8080/v1/favorites \ -H 'X-API-Key: dev-local-key-change-me' \ -H 'Content-Type: application/json' \ -d '{"track_id": "local:seed-1"}' ``` ### Для фронтенда - GET-эндпоинты (search, providers, tracks, playback, health) **не требуют** API key. - POST/DELETE (favorites, playlists, jobs) **требуют** API key. - Фронтенд должен хранить ключ в localStorage/secure storage и передавать в заголовке. --- ## 3. Модели данных ### 3.1 TrackRead — Трек (результат поиска / получения) | Поле | Тип | Описание | |---|---|---| | `id` | `string` | Уникальный ID трека, формат `{provider}:{provider_track_id}`, напр. `local:seed-1` | | `title` | `string` | Название трека | | `artist` | `string?` | Исполнитель | | `album` | `string?` | Альбом | | `duration_seconds` | `int?` | Длительность в секундах | | `artwork_url` | `string?` | URL обложки | | `canonical_key` | `string` | Нормализованный ключ для deduplication (lowercase `artist::title`) | | `providers` | `string[]` | Список ID провайдеров, у которых есть этот трек | | `provider_results` | `ProviderResult[]` | Детальные результаты от каждого провайдера | | `risk_level` | `RiskLevel` | Объединённый уровень риска | | `capabilities` | `Capabilities` | Объединённые capabilities | | `policy` | `Policy` | Объединённая policy | ### 3.2 ProviderResult — Результат от одного провайдера | Поле | Тип | Описание | |---|---|---| | `provider_id` | `string` | ID провайдера (`local`, `youtube_official`, `soundcloud_official`) | | `provider_track_id` | `string` | ID трека внутри провайдера | | `title` | `string` | Название | | `artist` | `string?` | Исполнитель | | `album` | `string?` | Альбом | | `duration_seconds` | `int?` | Длительность | | `artwork_url` | `string?` | Обложка | | `source_url` | `string?` | Прямая ссылка на источник | | `attribution` | `string?` | Текст attribution (обязателен для SoundCloud) | | `risk_level` | `RiskLevel` | Уровень риска провайдера | | `capabilities` | `Capabilities` | Возможности | | `policy` | `Policy` | Ограничения | ### 3.3 PlaybackRead — Инструкции воспроизведения | Поле | Тип | Описание | |---|---|---| | `track_id` | `string` | ID трека | | `playback_type` | `PlaybackType` | Тип воспроизведения | | `provider_id` | `string` | Какой провайдер используется | | `stream_url` | `string?` | URL для прямого аудио-стрима (local, official_stream) | | `embed_url` | `string?` | URL для iframe embed (YouTube) | | `expires_in_seconds` | `int?` | TTL стрим-URL (для official_stream) | | `attribution` | `string?` | Текст attribution | | `capabilities` | `Capabilities` | | | `policy` | `Policy` | | #### PlaybackType enum | Значение | Описание | Когда | |---|---|---| | `local_stream` | Прямой аудиофайл | Local/Navidrome | | `embed` | iframe embed | YouTube | | `official_stream` | Официальный стрим URL | SoundCloud | | `unavailable` | Невоспроизводимый | Нет прав/возможностей | ### 3.4 Capabilities (ProviderCapabilities) | Поле | Тип | Описание | |---|---|---| | `search_metadata` | `bool` | Может искать метаданные | | `raw_audio_stream` | `bool` | Может отдать raw audio | | `embed_playback` | `bool` | Поддерживает embed-воспроизведение | | `official_stream_url` | `bool` | Отдаёт official stream URL | | `persistent_cache` | `bool` | Разрешено кэшировать на диск | | `offline_playback` | `bool` | Поддерживает offline | | `server_favorites` | `bool` | Серверные избранные | | `server_playlists` | `bool` | Серверные плейлисты | | `multiuser_safe` | `bool` | Безопасно для multiuser | | `public_deployment_safe` | `bool` | Безопасно для публичного деплоя | ### 3.5 Policy | Поле | Тип | Описание | |---|---|---| | `compliant_mode` | `bool` | Работает в compliant режиме | | `cache_allowed` | `bool` | **Можно ли кэшировать** (→ показывать кнопку "Скачать") | | `download_allowed` | `bool` | **Можно ли скачивать** (→ показывать кнопку "Сохранить") | | `requires_attribution` | `bool` | Нужно показывать attribution | | `requires_external_credentials` | `bool` | Нужны внешние credentials | | `notes` | `string[]` | Пояснения/предупреждения | ### 3.6 RiskLevel enum | Значение | Описание | UI-цвет (рекомендация) | |---|---|---| | `compliant` | Полностью легально | 🟢 зелёный | | `constrained` | Ограничения (attribution, no cache) | 🟡 жёлтый | | `risky` | ToS risk, личное использование | 🔴 красный | ### 3.7 FavoriteRead | Поле | Тип | |---|---| | `track_id` | `string` | | `created_at` | `string` (ISO 8601) | ### 3.8 PlaylistSummary / PlaylistRead **PlaylistSummary** (для списков): | Поле | Тип | |---|---| | `id` | `string` (UUID) | | `name` | `string` | | `description` | `string?` | | `track_count` | `int` | | `created_at` | `string` | | `updated_at` | `string` | **PlaylistRead** (extends PlaylistSummary): | Поле | Тип | |---|---| | `tracks` | `PlaylistTrackRead[]` | **PlaylistTrackRead**: | Поле | Тип | |---|---| | `track_id` | `string` | | `position` | `int` | | `added_at` | `string` | ### 3.9 JobRead | Поле | Тип | Описание | |---|---|---| | `id` | `string` (UUID) | ID задачи | | `type` | `string` | Тип: `resolve`, `metadata_refresh`, `local_ingest` | | `status` | `JobStatus` | Статус | | `track_id` | `string?` | Связанный трек | | `payload` | `object` | Произвольные параметры | | `result` | `object?` | Результат | | `error` | `string?` | Текст ошибки | | `created_at` | `string` | | | `updated_at` | `string` | | #### JobStatus enum: `queued` → `running` → `succeeded` / `failed` / `blocked_by_policy` ### 3.10 ProviderRead | Поле | Тип | |---|---| | `id` | `string` | | `name` | `string` | | `kind` | `"local"` \| `"youtube"` \| `"soundcloud"` | | `enabled` | `bool` | | `configured` | `bool` | | `risky_enabled` | `bool` | | `risk_level` | `RiskLevel` | | `capabilities` | `Capabilities` | | `policy` | `Policy` | | `docs_url` | `string?` | ### 3.11 HealthRead | Поле | Тип | |---|---| | `status` | `"ok"` | | `mode` | `"compliant"` | | `risky_extractors_enabled` | `bool` | | `database` | `string` | --- ## 4. Система провайдеров ### Провайдеры в MVP | Провайдер | ID | kind | Что может | Playback | |---|---|---|---|---| | Local / Navidrome | `local` | `local` | Поиск, stream, cache, download, offline | `local_stream` (прямой URL к файлу) | | YouTube official | `youtube_official` | `youtube` | Metadata/search (нужен API key), embed playback | `embed` (iframe `youtube.com/embed/...`) | | SoundCloud official | `soundcloud_official` | `soundcloud` | Metadata/search (нужен client_id), official stream | `official_stream` (временный URL) | ### Ключевое правило для UI ``` Перед показом любой кнопки воспроизведения / скачивания / кэша: 1. Проверить capabilities нужного типа 2. Проверить policy.cache_allowed / policy.download_allowed 3. Показывать provider badge и risk_level на каждом треке ``` ### Track ID формат ``` {provider_id}:{provider_track_id} ``` Примеры: `local:seed-1`, `youtube_official:dQw4w9WgXcQ`, `soundcloud_official:12345` --- ## 5. Полный справочник API **Base URL**: `http://localhost:8080` **OpenAPI schema**: `GET /openapi.json` --- ### 5.1 `GET /health` — Проверка здоровья **Auth**: не нужен **Ответ 200**: ```json { "status": "ok", "mode": "compliant", "risky_extractors_enabled": false, "database": "sqlite" } ``` ```bash curl http://localhost:8080/health ``` --- ### 5.2 `GET /v1/providers` — Список провайдеров **Auth**: не нужен **Ответ 200**: ```json { "items": [ { "id": "local", "name": "Local / Navidrome-compatible library", "kind": "local", "enabled": true, "configured": true, "risky_enabled": false, "risk_level": "compliant", "capabilities": { "search_metadata": true, "raw_audio_stream": true, "embed_playback": false, "official_stream_url": false, "persistent_cache": true, "offline_playback": true, "server_favorites": true, "server_playlists": true, "multiuser_safe": true, "public_deployment_safe": true }, "policy": { "compliant_mode": true, "cache_allowed": true, "download_allowed": true, "requires_attribution": false, "requires_external_credentials": false, "notes": ["Local files only. ..."] }, "docs_url": "https://www.navidrome.org/docs/developers/subsonic-api/" }, { "id": "youtube_official", "name": "YouTube official metadata/embed", "kind": "youtube", "enabled": false, "configured": false, "risk_level": "compliant", "capabilities": { "raw_audio_stream": false, "embed_playback": true, "persistent_cache": false, "offline_playback": false, "public_deployment_safe": true }, "policy": { "cache_allowed": false, "download_allowed": false, "requires_external_credentials": true, "notes": ["Metadata/search requires YouTube Data API key. ..."] }, "docs_url": "https://developers.google.com/youtube/v3" } ] } ``` ```bash curl http://localhost:8080/v1/providers ``` --- ### 5.3 `GET /v1/search` — Поиск треков **Auth**: не нужен **Параметры запроса**: | Параметр | Тип | По умолчанию | Описание | |---|---|---|---| | `q` | `string` | *обязательный* | Поисковый запрос (1–200 символов) | | `providers` | `string` | `local,youtube_official,soundcloud_official` | Через запятую | | `limit` | `int` | `20` | 1–50 | | `offset` | `int` | `0` | ≥ 0 | **Ответ 200**: ```json { "query": "demo song", "limit": 20, "offset": 0, "total": 2, "items": [ { "id": "local:seed-1", "title": "Demo Song", "artist": "Local Artist", "album": "Home Library", "duration_seconds": 180, "artwork_url": null, "canonical_key": "local artist::demo song", "providers": ["local"], "provider_results": [ { "provider_id": "local", "provider_track_id": "seed-1", "title": "Demo Song", "artist": "Local Artist", "album": "Home Library", "duration_seconds": 180, "artwork_url": null, "source_url": "/media/demo-song.opus", "attribution": "Local library", "risk_level": "compliant", "capabilities": { "...": "..." }, "policy": { "...": "..." } } ], "risk_level": "compliant", "capabilities": { "raw_audio_stream": true, "persistent_cache": true, "cache_allowed": true, "download_allowed": true }, "policy": { "compliant_mode": true, "cache_allowed": true, "download_allowed": true, "requires_attribution": false, "requires_external_credentials": false, "notes": ["Local files only. ..."] } } ] } ``` ```bash curl 'http://localhost:8080/v1/search?q=demo%20song&limit=10' curl 'http://localhost:8080/v1/search?q=rock&providers=local&limit=5&offset=0' ``` --- ### 5.4 `GET /v1/tracks/{track_id}` — Получить трек по ID **Auth**: не нужен **Path params**: `track_id` — формат `{provider}:{id}`, напр. `local:seed-1` **Ответ 200**: объект `TrackRead` (см. §3.1) **Ответ 404**: если трек не найден ```bash curl http://localhost:8080/v1/tracks/local:seed-1 ``` --- ### 5.5 `GET /v1/playback/{track_id}` — Получить инструкции воспроизведения **Auth**: не нужен **Path params**: `track_id` **Ответ 200**: объект `PlaybackRead` (см. §3.3) В зависимости от провайдера вернётся один из типов: | playback_type | stream_url | embed_url | Пример UI | |---|---|---|---| | `local_stream` | ✅ прямой URL файла | — | `