docs: add FRONTEND-GUIDE.md and WIREFRAME.md for frontend/mockup development
Some checks failed
CI / backend (push) Failing after 23s

- FRONTEND-GUIDE.md: comprehensive API reference with all 16 endpoints,
  data models, user flows, auth model, error handling, pagination,
  provider system, example curl/JSON, playback rendering guide
- WIREFRAME.md: page descriptions for all 6 screens, component specs,
  navigation flow, mobile-first layout, color scheme, typography

Both in Russian for mockup development workflow.
This commit is contained in:
Kisskin-Mister 2026-07-10 15:10:29 +03:00
parent 8d8076bd3f
commit 87560d6f90
2 changed files with 1580 additions and 0 deletions

910
docs/FRONTEND-GUIDE.md Normal file
View file

@ -0,0 +1,910 @@
# 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: <your-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` | *обязательный* | Поисковый запрос (1200 символов) |
| `providers` | `string` | `local,youtube_official,soundcloud_official` | Через запятую |
| `limit` | `int` | `20` | 150 |
| `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 файла | — | `<audio src="...">` |
| `embed` | — | ✅ `youtube.com/embed/...` | `<iframe src="...">` |
| `official_stream` | ✅ временный URL | — | `<audio src="...">` (с учётом expires) |
| `unavailable` | — | — | "Невоспроизводимо" |
```bash
curl http://localhost:8080/v1/playback/local:seed-1
```
Пример ответа для YouTube:
```json
{
"track_id": "youtube_official:abc123",
"playback_type": "embed",
"provider_id": "youtube_official",
"stream_url": null,
"embed_url": "https://www.youtube.com/embed/abc123",
"expires_in_seconds": null,
"attribution": "YouTube embed playback; metadata not resolved without API key in this MVP.",
"capabilities": { "embed_playback": true, "raw_audio_stream": false, "..." : "..." },
"policy": { "cache_allowed": false, "download_allowed": false, "..." : "..." }
}
```
---
### 5.6 `POST /v1/favorites` — Добавить в избранное
**Auth**: `X-API-Key`
**Request body**:
```json
{
"track_id": "local:seed-1"
}
```
`track_id`: 3300 символов.
**Ответ 201**:
```json
{
"track_id": "local:seed-1",
"created_at": "2026-07-10T12:00:00+00:00"
}
```
**Идемпотентно**: повторный POST того же track_id → 200 с тем же объектом (не создаёт дубликат).
```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"}'
```
---
### 5.7 `GET /v1/favorites` — Список избранных
**Auth**: `X-API-Key`
**Query params**: `limit` (1100, default 50), `offset` (default 0)
**Ответ 200**:
```json
{
"limit": 50,
"offset": 0,
"total": 1,
"items": [
{ "track_id": "local:seed-1", "created_at": "2026-07-10T12:00:00+00:00" }
]
}
```
```bash
curl http://localhost:8080/v1/favorites -H 'X-API-Key: dev-local-key-change-me'
```
---
### 5.8 `DELETE /v1/favorites/{track_id}` — Удалить из избранного
**Auth**: `X-API-Key`
**Ответ**: `204 No Content` (идемпотентно — если не было в избранном, тоже 204)
```bash
curl -X DELETE http://localhost:8080/v1/favorites/local:seed-1 \
-H 'X-API-Key: dev-local-key-change-me'
```
---
### 5.9 `POST /v1/playlists` — Создать плейлист
**Auth**: `X-API-Key`
**Request body**:
```json
{
"name": "My Playlist",
"description": "Optional description"
}
```
`name`: 1120 символов. `description`: до 500 символов, опционально.
**Ответ 201**:
```json
{
"id": "uuid-...",
"name": "My Playlist",
"description": "Optional description",
"track_count": 0,
"created_at": "2026-07-10T12:00:00+00:00",
"updated_at": "2026-07-10T12:00:00+00:00",
"tracks": []
}
```
```bash
curl -X POST http://localhost:8080/v1/playlists \
-H 'X-API-Key: dev-local-key-change-me' \
-H 'Content-Type: application/json' \
-d '{"name": "Chill Vibes", "description": "Relaxing music"}'
```
---
### 5.10 `GET /v1/playlists` — Список плейлистов
**Auth**: `X-API-Key`
**Query params**: `limit` (1100, default 50), `offset` (default 0)
**Ответ 200**:
```json
{
"limit": 50,
"offset": 0,
"total": 1,
"items": [
{
"id": "uuid-...",
"name": "Chill Vibes",
"description": "Relaxing music",
"track_count": 3,
"created_at": "2026-07-10T12:00:00+00:00",
"updated_at": "2026-07-10T12:30:00+00:00"
}
]
}
```
```bash
curl http://localhost:8080/v1/playlists -H 'X-API-Key: dev-local-key-change-me'
```
---
### 5.11 `GET /v1/playlists/{playlist_id}` — Плейлист с треками
**Auth**: `X-API-Key`
**Ответ 200**:
```json
{
"id": "uuid-...",
"name": "Chill Vibes",
"description": "Relaxing music",
"track_count": 2,
"created_at": "2026-07-10T12:00:00+00:00",
"updated_at": "2026-07-10T12:30:00+00:00",
"tracks": [
{ "track_id": "local:seed-1", "position": 1, "added_at": "2026-07-10T12:10:00+00:00" },
{ "track_id": "local:seed-2", "position": 2, "added_at": "2026-07-10T12:20:00+00:00" }
]
}
```
**Ответ 404**: если плейлист не найден или принадлежит другому пользователю.
```bash
curl http://localhost:8080/v1/playlists/uuid-... -H 'X-API-Key: dev-local-key-change-me'
```
---
### 5.12 `POST /v1/playlists/{playlist_id}/tracks` — Добавить трек в плейлист
**Auth**: `X-API-Key`
**Request body**:
```json
{
"track_id": "local:seed-2"
}
```
**Ответ 201**: возвращает обновлённый `PlaylistRead` (с обновлённым списком треков).
**Идемпотентно**: если трек уже в плейлите, не создаёт дубликат.
```bash
curl -X POST http://localhost:8080/v1/playlists/uuid-.../tracks \
-H 'X-API-Key: dev-local-key-change-me' \
-H 'Content-Type: application/json' \
-d '{"track_id": "local:seed-2"}'
```
---
### 5.13 `POST /v1/jobs` — Создать фоновую задачу
**Auth**: `X-API-Key`
**Request body**:
```json
{
"type": "resolve",
"track_id": "youtube_official:abc123",
"payload": {}
}
```
`type`: `"resolve"` | `"metadata_refresh"` | `"local_ingest"`
**Ответ 202**:
```json
{
"id": "uuid-...",
"type": "resolve",
"status": "queued",
"track_id": "youtube_official:abc123",
"payload": {},
"result": null,
"error": null,
"created_at": "2026-07-10T12:00:00+00:00",
"updated_at": "2026-07-10T12:00:00+00:00"
}
```
> **MVP note**: задачи создаются и сохраняются со статусом `queued`, но не запускают реальных unsafe extractors.
```bash
curl -X POST http://localhost:8080/v1/jobs \
-H 'X-API-Key: dev-local-key-change-me' \
-H 'Content-Type: application/json' \
-d '{"type": "metadata_refresh", "track_id": "local:seed-1"}'
```
---
### 5.14 `GET /v1/jobs` — Список задач
**Auth**: `X-API-Key`
**Query params**: `limit` (1100, default 50), `offset` (default 0)
**Ответ 200**: `JobList` с массивом `JobRead`
```bash
curl http://localhost:8080/v1/jobs -H 'X-API-Key: dev-local-key-change-me'
```
---
### 5.15 `GET /v1/jobs/{job_id}` — Получить задачу
**Auth**: `X-API-Key`
**Ответ 200**: `JobRead`
**Ответ 404**: если задача не найдена
```bash
curl http://localhost:8080/v1/jobs/uuid-... -H 'X-API-Key: dev-local-key-change-me'
```
---
## 6. User Flows
### 6.1 Поиск → Прослушивание → Избранное → Плейлист
```
1. Пользователь вводит запрос в поисковую строку
2. GET /v1/search?q={query} → список треков
3. Клик на трек → GET /v1/playback/{track_id}
4. В зависимости от playback_type:
- local_stream → <audio src="{stream_url}">
- embed → <iframe src="{embed_url}">
- official_stream → <audio src="{stream_url}"> (с отслеживанием expires)
- unavailable → показать "Невоспроизводимо"
5. Кнопка ❤️ → POST /v1/favorites {track_id}
6. Кнопка "+ в плейлист" → POST /v1/playlists/{id}/tracks {track_id}
```
### 6.2 Просмотр библиотеки → Прослушивание
```
1. GET /v1/favorites → список избранных треков
2. Для каждого track_id → GET /v1/tracks/{track_id} (метаданные)
3. Клик → GET /v1/playback/{track_id} → воспроизведение
4. Аналогично для плейлистов: GET /v1/playlists → GET /v1/playlists/{id}
```
### 6.3 Скачивание/кэширование трека
```
1. Проверить policy.cache_allowed == true и policy.download_allowed == true
2. Только тогда показывать кнопку "Скачать"
3. GET /v1/playback/{track_id} → получить stream_url
4. Фронтенд: <a href="{stream_url}" download> или fetch + blob
5. (Опционально) POST /v1/jobs { type: "local_ingest", track_id, payload: { source_url } }
```
> ⚠️ Кнопки скачивания **скрыты** для YouTube и SoundCloud (policy.download_allowed == false).
### 6.4 Управление плейлистами
```
1. Создать: POST /v1/playlists { name, description? }
2. Список: GET /v1/playlists
3. Детали: GET /v1/playlists/{id} → список треков с позициями
4. Добавить трек: POST /v1/playlists/{id}/tracks { track_id }
5. (MVP: нет удаления трека из плейлиста, нет переупорядочивания)
```
---
## 7. Обработка ошибок
### Формат ошибки
Все ошибки возвращают JSON в едином формате:
```json
{
"error": {
"code": "unauthorized",
"message": "Invalid or missing API key",
"details": null
}
}
```
### Коды ошибок
| HTTP статус | code | Когда |
|---|---|---|
| `401` | `unauthorized` | Невалидный или отсутствующий X-API-Key |
| `404` | `http_error` | Трек/плейлист/задача не найдена |
| `422` | `validation_error` | Невалидные параметры (Pydantic validation) |
### Для фронтенда
- Показывать toast/banner с `error.message` пользователю.
- При 401 — редирект на экран ввода API key.
- При 422 — подсветить невалидное поле.
---
## 8. Пагинация
Все list-эндпоинты используют единую схему:
**Запрос**:
- `limit` — количество элементов (default зависит от эндпоинта)
- `offset` — смещение
**Ответ**:
```json
{
"limit": 20,
"offset": 0,
"total": 150,
"items": [...]
}
```
**Для фронта**: `total` показывает общее количество; для "Load More" увеличивать `offset += limit`. Для постраничной навигации: `page = offset / limit + 1`, `totalPages = ceil(total / limit)`.
**Эндпоинты с пагинацией**:
- `GET /v1/search` (limit 150)
- `GET /v1/favorites` (limit 1100)
- `GET /v1/playlists` (limit 1100)
- `GET /v1/jobs` (limit 1100)
---
## 9. Real-time и playback
### Текущий MVP
В MVP **нет** WebSocket или SSE. Playback статус отслеживается на стороне фронтенда:
- `<audio>` / `<iframe>` элементы + JS events (`play`, `pause`, `ended`, `timeupdate`).
- Для jobs: поллинг `GET /v1/jobs/{job_id}` с интервалом 25 секунд.
### Планируется
- WebSocket/SSE для real-time job status updates.
- Server-sent playback events для multi-device sync.
---
## 10. CORS и конфигурация
### CORS
Сервер настроен на:
- Origins: `http://localhost:5173`, `http://localhost:3000` (Vite dev, CRA)
- Methods: `GET`, `POST`, `DELETE`
- Headers: `X-API-Key`, `Content-Type`
- Credentials: отключены
### ENV-переменные (для справки)
| Переменная | По умолчанию | Описание |
|---|---|---|
| `APP_API_KEYS` | `dev-local-key-change-me` | API ключи через запятую |
| `APP_CORS_ORIGINS` | `http://localhost:5173,...` | Разрешённые origins |
| `APP_DATABASE_URL` | `sqlite:///./data/music-orchestrator.db` | БД |
| `APP_YOUTUBE_API_KEY` | пусто | YouTube Data API key |
| `APP_SOUNDCLOUD_CLIENT_ID` | пусто | SoundCloud client_id |
| `APP_ENABLE_RISKY_EXTRACTORS` | `false` | Включить risky extractors |
---
## Приложение: Quick Reference — все эндпоинты
| # | Method | Path | Auth | Описание |
|---|---|---|---|---|
| 1 | GET | `/health` | — | Статус сервиса |
| 2 | GET | `/v1/providers` | — | Список провайдеров |
| 3 | GET | `/v1/search?q=...` | — | Поиск треков |
| 4 | GET | `/v1/tracks/{track_id}` | — | Трек по ID |
| 5 | GET | `/v1/playback/{track_id}` | — | Инструкции воспроизведения |
| 6 | POST | `/v1/favorites` | ✅ | Добавить в избранное |
| 7 | GET | `/v1/favorites` | ✅ | Список избранных |
| 8 | DELETE | `/v1/favorites/{track_id}` | ✅ | Удалить из избранного |
| 9 | POST | `/v1/playlists` | ✅ | Создать плейлист |
| 10 | GET | `/v1/playlists` | ✅ | Список плейлистов |
| 11 | GET | `/v1/playlists/{playlist_id}` | ✅ | Плейлист с треками |
| 12 | POST | `/v1/playlists/{playlist_id}/tracks` | ✅ | Добавить трек в плейлист |
| 13 | POST | `/v1/jobs` | ✅ | Создать задачу |
| 14 | GET | `/v1/jobs` | ✅ | Список задач |
| 15 | GET | `/v1/jobs/{job_id}` | ✅ | Задача по ID |
| 16 | GET | `/openapi.json` | — | OpenAPI схема |

670
docs/WIREFRAME.md Normal file
View file

@ -0,0 +1,670 @@
# Music Orchestrator — Вайрфреймы и описание UI
## Содержание
1. [Общая архитектура UI](#1-общая-архитектура-ui)
2. [Навигация](#2-навигация)
3. [Страница: Home (Главная)](#3-страница-home-главная)
4. [Страница: Search (Поиск)](#4-страница-search-поиск)
5. [Страница: Library (Библиотека)](#5-страница-library-библиотека)
6. [Страница: Playlists (Плейлисты)](#6-страница-playlists-плейлисты)
7. [Страница: Playlist Detail (Детали плейлиста)](#7-страница-playlist-detail)
8. [Страница: Player (Плеер)](#8-страница-player-плеер)
9. [Страница: Settings (Настройки)](#9-страница-settings-настройки)
10. [Компоненты](#10-компоненты)
11. [Mobile-first layout](#11-mobile-first-layout)
12. [Цветовая схема и стили](#12-цветовая-схема-и-стили)
---
## 1. Общая архитектура UI
### Layout (мобильный, portrait)
```
┌─────────────────────────────┐
│ Header Bar │ ← logo, search icon, settings
├─────────────────────────────┤
│ │
│ Content Area │ ← текущая страница
│ (scrollable) │
│ │
│ │
├─────────────────────────────┤
│ Mini Player (sticky) │ ← artwork, title, play/pause
├─────────────────────────────┤
│ Bottom Navigation │ ← Home, Search, Library, Playlists
└─────────────────────────────┘
```
### Layout (desktop, ≥768px)
```
┌────────┬────────────────────────────────────────┐
│ │ Header Bar (search) │
│ Side ├────────────────────────────────────────┤
│ bar │ │
│ │ Content Area │
│ Nav │ (scrollable) │
│ │ │
│ ├────────────────────────────────────────┤
│ │ Mini Player (fixed bottom) │
└────────┴────────────────────────────────────────┘
```
---
## 2. Навигация
### Bottom nav / Sidebar nav
| Иконка | Label | Страница | Описание |
|---|---|---|---|
| 🏠 |Главная| Home | Рекомендации, недавно прослушанное |
| 🔍 |Поиск| Search | Поисковая строка + результаты |
| 📚 |Библиотека| Library | Избранное + все плейлисты |
| 🎵 |Плейлисты| Playlists | Управление плейлистами |
| ⚙️ |Настройки| Settings | API key, провайдеры, theme |
### Навигационные потоки
```
Home ──→ Search (по клику на search bar)
──→ Track detail ──→ Player
──→ Playlist detail
Search ──→ Track detail ──→ Player
──→ Add to favorites / playlist (bottom sheet)
Library ──→ Favorites list ──→ Player
──→ Playlist detail ──→ Player
Playlists ──→ Create playlist
──→ Playlist detail ──→ Player
Settings ──→ API key input
──→ Provider status
```
---
## 3. Страница: Home (Главная)
### Описание
Стартовый экран. Показывает приветствие, быстрый доступ к поиску, недавно прослушанным трекам и рекомендациям.
### Компоненты
1. **Hero / Search CTA** — большая карточка "Найди любую музыку" с полем ввода → переход на Search.
2. **Recently Played** (горизонтальный scroll)
- Карточки: обложка (64×64), название, исполнитель
- Клик → воспроизведение
- Если нет истории → скрыть секцию
3. **Your Favorites** (горизонтальный scroll)
- Первые 510 избранных треков
- "Показать все" → Library → Favorites
4. **Your Playlists** (горизонтальный scroll)
- Карточки: название, количество треков
- Клик → Playlist detail
5. **Provider Status** (info-блок)
- Список подключённых провайдеров с бейджами
- Green dot = configured, grey = not configured
### Wireframe
```
┌─────────────────────────────────────┐
│ 🎵 Music Orchestrator ⚙️ │
├─────────────────────────────────────┤
│ │
│ ┌─────────────────────────────┐ │
│ │ 🔍 Найди любую музыку... │ │
│ └─────────────────────────────┘ │
│ │
│ Недавно прослушанное → │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ 🎵 │ │ 🎵 │ │ 🎵 │ │
│ │ Song │ │ Song │ │ Song │ │
│ └──────┘ └──────┘ └──────┘ │
│ │
│ Избранное → │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ ❤️ │ │ ❤️ │ │ ❤️ │ │
│ │ Song │ │ Song │ │ Song │ │
│ └──────┘ └──────┘ └──────┘ │
│ │
├─────────────────────────────────────┤
│ [artwork] Song Title ▶ ⏭ │
├─────────────────────────────────────┤
│ 🏠 🔍 📚 🎵 ⚙️ │
└─────────────────────────────────────┘
```
---
## 4. Страница: Search (Поиск)
### Описание
Основной экран для поиска музыки. Показывает результаты из всех провайдеров.
### Компоненты
1. **Search Bar** (sticky top)
- Поле ввода с debounce 300мс
- Кнопка очистки (×)
- Иконка фильтра провайдеров (dropdown с чекбоксами)
2. **Provider Filter** (опциональный dropdown/chips)
- Чекбоксы: Local, YouTube, SoundCloud
- По умолчанию все включены
3. **Results List**
- Каждый трек — карточка:
- Обложка (48×48 или placeholder)
- Название, Исполнитель, Альбом
- Длительность (формат mm:ss)
- Provider badges (цветные иконки: 🟢 local, 🔴 YouTube, 🟡 SoundCloud)
- Risk badge (🟢 compliant, 🟡 constrained, 🔴 risky)
- Кнопки: ▶ Play, ❤️ Favorite, + Playlist
- Infinite scroll или "Загрузить ещё"
4. **Empty State** — "Введите запрос для поиска"
5. **No Results** — "Ничего не найдено по запросу ..."
### Wireframe
```
┌─────────────────────────────────────┐
│ 🔍 Enter song name... ✕ │
│ [Local ✓] [YouTube ✓] [SoundCloud ✓]│
├─────────────────────────────────────┤
│ │
│ ┌─────────────────────────────┐ │
│ │ 🎵 Demo Song │ │
│ │ Local Artist • 3:00 │ │
│ │ 🟢local 🟢compliant │ │
│ │ ▶ ❤️ + │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ 🎵 Another Track │ │
│ │ YouTube Artist • 4:22 │ │
│ │ 🔴youtube 🟢compliant │ │
│ │ ▶ ❤️ + │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ 🎵 SoundCloud Track │ │
│ │ SC Artist • 2:45 │ │
│ │ 🟡soundcloud 🟡constrained│ │
│ │ ⚠️ attribution required │ │
│ │ ▶ ❤️ + │ │
│ └─────────────────────────────┘ │
│ │
│ [Загрузить ещё] │
│ │
├─────────────────────────────────────┤
│ [artwork] Song Title ▶ ⏭ │
├─────────────────────────────────────┤
│ 🏠 🔍 📚 🎵 ⚙️ │
└─────────────────────────────────────┘
```
### Track Card — детальная структура
```
┌──────────────────────────────────────┐
│ ┌──────┐ Song Title │
│ │ │ Artist Name │
│ │ art │ Album • 3:45 │
│ │ work │ [🟢local] [🟢compliant] │
│ └──────┘ │
│ ▶ ❤️ ⋮ │
└──────────────────────────────────────┘
```
Кнопка ⋮ (more) → Bottom sheet:
- Добавить в плейлист
- Перейти к источнику
- Информация о треке
---
## 5. Страница: Library (Библиотека)
### Описание
Единый экран библиотеки: избранное + плейлисты. Вкладки или секции.
### Компоненты
1. **Tab Bar**: `Избранное` | `Плейлисты`
2. **Вкладка "Избранное"**
- Список треков (аналогичен search results, но без provider filter)
- Каждый трек: обложка, название, исполнитель, длительность, ▶, ⋮
- Кнопка ⋮ → "Удалить из избранного", "Добавить в плейлист"
- Empty state: "Нет избранных треков. Ищите музыку и добавляйте ❤️"
3. **Вкладка "Плейлисты"**
- Список плейлистов (карточки)
- Каждая: название, описание, количество треков
- FAB (floating action button): "+ Новый плейлист"
- Empty state: "Нет плейлистов. Создайте первый 🎵"
### Wireframe
```
┌─────────────────────────────────────┐
│ 📚 Библиотека │
├─────────────────────────────────────┤
│ [Избранное] [Плейлисты] │
├─────────────────────────────────────┤
│ │
│ ┌─────────────────────────────┐ │
│ │ 🎵 Favorite Song 1 │ │
│ │ Artist • 3:00 ▶ ⋮ │ │
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ 🎵 Favorite Song 2 │ │
│ │ Artist • 4:15 ▶ ⋮ │ │
│ └─────────────────────────────┘ │
│ │
│ ┌────┐ │
│ │ + │ ← FAB │
│ └────┘ │
├─────────────────────────────────────┤
│ [artwork] Song Title ▶ ⏭ │
├─────────────────────────────────────┤
│ 🏠 🔍 📚 🎵 ⚙️ │
└─────────────────────────────────────┘
```
---
## 6. Страница: Playlists (Плейлисты)
### Описание
Отдельный экран управления плейлистами (альтернативно может быть вкладкой в Library).
### Компоненты
1. **Playlist Cards** (grid или list)
- Каждая: название, описание (обрезать до 2 строк), "X треков", дата создания
- Клик → Playlist Detail
2. **Create Playlist Button** (FAB или header button)
- Открывает модальное окно:
- Название (обязательно, 1120 символов)
- Описание (опционально, до 500 символов)
- Кнопки: Отмена, Создать
---
## 7. Страница: Playlist Detail
### Описание
Детальный вид плейлиста с треками.
### Компоненты
1. **Playlist Header**
- Название, описание
- Количество треков, общая длительность (вычислить из track.duration_seconds)
- Кнопка ▶ Все (воспроизвести весь плейлист)
2. **Track List**
- Нумерованный список треков
- Каждый: #, обложка, название, исполнитель, длительность, ▶, ⋮
- Порядок по `position`
3. **Empty State**: "Плейлист пуст. Добавьте треки из поиска."
### Wireframe
```
┌─────────────────────────────────────┐
│ ← Chill Vibes │
│ Relaxing music │
│ 12 треков • 45 мин [▶ Все] │
├─────────────────────────────────────┤
│ │
│ 1 🎵 Song One 3:00 ⋮ │
│ 2 🎵 Song Two 4:15 ⋮ │
│ 3 🎵 Song Three 2:30 ⋮ │
│ 4 🎵 Song Four 5:00 ⋮ │
│ ... │
│ │
├─────────────────────────────────────┤
│ [artwork] Song Title ▶ ⏭ │
├─────────────────────────────────────┤
│ 🏠 🔍 📚 🎵 ⚙️ │
└─────────────────────────────────────┘
```
---
## 8. Страница: Player (Плеер)
### Описание
Полноэкранный плеер. Открывается по свайпу вверх от Mini Player или клику на него.
### Компоненты
1. **Artwork** — большое изображение обложки (по центру, адаптивное)
2. **Track Info** — Название (bold), Исполнитель, Альбом
3. **Provider Badge** — иконка провайдера + risk level
4. **Progress Bar**
- Полоса прогресса (seekable для local_stream)
- Текущее время / Общая длительность
5. **Playback Controls**
- ⏮ Previous (для плейлиста)
- ▶/⏸ Play/Pause
- ⏭ Next (для плейлиста)
- 🔀 Shuffle (опционально)
- 🔁 Repeat (опционально)
6. **Action Buttons**
- ❤️ Favorite toggle
- Add to playlist (bottom sheet)
- ⬇️ Download (только если policy.download_allowed)
7. **Attribution** (если policy.requires_attribution)
- Показать attribution text
### Playback Rendering
В зависимости от `playback_type`:
| playback_type | Что рендерить |
|---|---|
| `local_stream` | `<audio src="{stream_url}">` — стандартный HTML5 audio |
| `embed` | `<iframe src="{embed_url}">` — YouTube embed player |
| `official_stream` | `<audio src="{stream_url}">`с отслеживанием `expires_in_seconds` |
| `unavailable` | Блок с сообщением "Невоспроизводимо" |
### Mini Player (sticky bottom, виден на всех страницах)
```
┌─────────────────────────────────────┐
│ [🎵 art] Song Title ▶ ⏭ │
│ Artist Name │
│ ════════════════░░░░░░░░░░░░░░░░░ │ ← progress bar
└─────────────────────────────────────┘
```
### Full Player (по свайпу вверх)
```
┌─────────────────────────────────────┐
│ ─── (drag handle) ─── │
│ │
│ ┌───────────────────┐ │
│ │ │ │
│ │ │ │
│ │ ARTWORK │ │
│ │ (square) │ │
│ │ │ │
│ │ │ │
│ └───────────────────┘ │
│ │
│ Song Title │
│ Artist Name │
│ 🟢 local • compliant │
│ │
│ 1:23 ━━━━━━━━━━░━━━━━━━ 3:00 │
│ │
│ ⏮ ▶/⏸ ⏭ │
│ 🔀 🔁 │
│ │
│ ❤️ ⬇️ │
│ │
│ Source: Local library │
│ │
└─────────────────────────────────────┘
```
---
## 9. Страница: Settings (Настройки)
### Описание
Экран настроек: API key, провайдеры, тема.
### Компоненты
1. **API Key** (секция)
- Поле ввода API key (password type, с toggle visibility)
- Кнопка "Проверить" → вызывает `GET /health` или `GET /v1/providers` с ключом
- Статус: ✅ Подключено / ❌ Ошибка
2. **Провайдеры** (секция)
- Список провайдеров из `GET /v1/providers`
- Каждый: название, иконка, status badge
- 🟢 Configured & enabled
- 🟡 Enabled but not configured (нужны credentials)
- ⚪ Disabled
- Capabilities summary (icons: stream ✓, cache ✗, embed ✓, download ✗)
3. **Тема** (секция)
- Toggle: Светлая / Тёмная / Системная
4. **О приложении** (секция)
- Версия, ссылки на документацию, OpenAPI schema
---
## 10. Компоненты
### 10.1 Track Card
Универсальная карточка трека, используется в Search, Library, Playlist Detail.
```
Props:
- track: TrackRead (или FavoriteRead + загрузка трека)
- showProviderBadge: bool (default true)
- showRiskBadge: bool (default true)
- compact: bool (default false, для mini player context)
Actions:
- onPlay: (trackId) → void
- onFavorite: (trackId) → void
- onAddToPlaylist: (trackId) → void
- onMore: (trackId) → void (bottom sheet)
```
### 10.2 Playlist Card
```
Props:
- playlist: PlaylistSummary
Displays:
- name
- description (truncated)
- track_count
- created_at (relative: "2 дня назад")
```
### 10.3 Provider Badge
```
Props:
- providerId: string ("local" | "youtube_official" | "soundcloud_official")
Rendering:
- local → 🟢 "Local"
- youtube_official → 🔴 "YouTube"
- soundcloud_official → 🟡 "SoundCloud"
```
### 10.4 Risk Badge
```
Props:
- riskLevel: RiskLevel
Rendering:
- compliant → 🟢 "Compliant"
- constrained → 🟡 "Constrained"
- risky → 🔴 "Risky"
```
### 10.5 Bottom Sheet (Add to Playlist)
```
Content:
- Список существующих плейлистов (клик → добавить)
- Кнопка "Создать новый плейлист" → inline form
- Кнопка "Закрыть"
```
### 10.6 Toast / Notification
```
Types:
- success: "Добавлено в избранное ❤️"
- error: "Ошибка: {message}"
- info: "Трек недоступен для скачивания"
```
### 10.7 Search Input
```
Props:
- value: string
- onChange: (value) → void
- onClear: () → void
- debounce: 300ms
- placeholder: "Найди любую музыку..."
```
### 10.8 Empty State
```
Props:
- icon: string (emoji)
- title: string
- subtitle: string
- action?: { label: string, onClick: () → void }
```
---
## 11. Mobile-first layout
### Breakpoints
| Breakpoint | Layout |
|---|---|
| < 768px | Single column, bottom nav, mini player |
| 7681023px | Sidebar nav, 2-column grid для карточек |
| ≥ 1024px | Sidebar nav, 3-column grid, expanded player |
### Размеры и отступы
| Элемент | Размер |
|---|---|
| Header bar height | 56px |
| Bottom nav height | 64px |
| Mini player height | 72px |
| Content padding | 16px |
| Card gap | 12px |
| Artwork (search result) | 48×48 px |
| Artwork (player) | min(80vw, 400px) |
### Touch targets
- Минимальный touch target: 44×44 px
- Кнопки воспроизведения: 48×48 px
- Отступ между кнопками: минимум 8px
### Скролл поведение
- Content area: `overflow-y: auto`, `-webkit-overflow-scrolling: touch`
- Mini player: `position: sticky; bottom: 0` (или fixed)
- Bottom nav: `position: fixed; bottom: 0`
- Mini player показывает progress bar как тонкую полоску сверху
### Swipe gestures
- Свайп вверх от Mini Player → Full Player
- Свайп вниз от Full Player → Mini Player
- Свайп влево по треку в списке → action buttons (delete, add to playlist)
---
## 12. Цветовая схема и стили
### Тёмная тема (primary)
| Элемент | Цвет |
|---|---|
| Background | `#121212` |
| Surface | `#1E1E1E` |
| Card | `#282828` |
| Primary | `#1DB954` (Spotify-green) |
| Text primary | `#FFFFFF` |
| Text secondary | `#B3B3B3` |
| Accent | `#1DB954` |
| Error | `#E53935` |
| Warning | `#FFA726` |
| Success | `#66BB6A` |
### Светлая тема
| Элемент | Цвет |
|---|---|
| Background | `#FAFAFA` |
| Surface | `#FFFFFF` |
| Card | `#F5F5F5` |
| Primary | `#1DB954` |
| Text primary | `#212121` |
| Text secondary | `#757575` |
### Типографика
| Элемент | Размер | Weight |
|---|---|---|
| H1 (page title) | 28px | 700 |
| H2 (section title) | 22px | 600 |
| H3 (card title) | 18px | 600 |
| Body | 16px | 400 |
| Caption | 14px | 400 |
| Small (badges) | 12px | 500 |
### Иконки
Рекомендуемый набор: Material Icons или Phosphor Icons.
| Действие | Иконка |
|---|---|
| Play | ▶️ (play) |
| Pause | ⏸ (pause) |
| Next | ⏭ (skip_next) |
| Previous | ⏮ (skip_previous) |
| Favorite | ❤️ (heart) / 🤍 (heart_outline) |
| Add to playlist | (playlist_add) |
| Download | ⬇️ (download) |
| More | ⋮ (more_vert) |
| Search | 🔍 (search) |
| Settings | ⚙️ (settings) |
| Home | 🏠 (home) |
| Library | 📚 (library_music) |
| Playlist | 🎵 (queue_music) |