docs: add product idea, design prototypes and development documentation
- IDEA.md: product vision from the user's perspective (input for design)
- Architecture, ADRs, data model, REST API overview
- Auth and roles, library storage (NFS, path regexes, scanner, search),
playback, recommendation system ("My Wave"), track upload queue
- Web, Android (offline, self-update), i18n, design system
- Testing strategy, Gitea Actions CI/CD, deployment, conventions, roadmap
- Clickable web/Android design prototypes and wave button variants
- .claude/launch.json to serve the prototypes locally
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
36e99fe960
commit
9eb1c25147
30 files changed
+7538
No files matched your search
+181
@@ -0,0 +1,181 @@
|
||||
# Предложение треков (загрузка с модерацией)
|
||||
|
||||
Модераторы (и админы) предлагают новые треки. Файлы попадают в **очередь на добавление** с общим лимитом размера,
|
||||
дубли отбрасываются сразу, админ одобряет или отклоняет, одобренные файлы раскладываются в хранилище
|
||||
**по той же структуре папок, что принята в этом хранилище**.
|
||||
|
||||
## 1. Роли
|
||||
|
||||
| Действие | user | moderator | admin |
|
||||
|---|:-:|:-:|:-:|
|
||||
| Загрузить треки в очередь | — | ✅ | ✅ |
|
||||
| Видеть свои предложения и их статус, отозвать до рассмотрения | — | ✅ | ✅ |
|
||||
| Видеть всю очередь | — | — | ✅ |
|
||||
| Править метаданные, слушать, одобрять / отклонять | — | — | ✅ |
|
||||
| Настраивать лимит очереди, целевое хранилище и шаблон раскладки | — | — | ✅ |
|
||||
|
||||
Загрузки админа проходят ту же проверку на дубли; в очередь они тоже попадают, но админ может одобрить их сразу.
|
||||
|
||||
## 2. Жизненный цикл загрузки
|
||||
|
||||
```
|
||||
предпроверка (хэш + теги на клиенте)
|
||||
│ дубль → отказ сразу, файл не загружается, квота не тратится
|
||||
▼
|
||||
uploading ──► processing ──► pending_review ──► approved ──► importing ──► imported
|
||||
│ │ │ │
|
||||
│ ├─► duplicate ├─► rejected (причина) └─► import_failed
|
||||
│ └─► invalid └─► withdrawn (отозвал автор) (можно повторить)
|
||||
└─► expired (не докачан за 24 ч)
|
||||
```
|
||||
|
||||
| Статус | Занимает квоту | Файл в staging |
|
||||
|---|:-:|:-:|
|
||||
| `uploading` | ✅ (резерв по заявленному размеру) | частично |
|
||||
| `processing` | ✅ | ✅ |
|
||||
| `pending_review` | ✅ | ✅ |
|
||||
| `approved`, `importing` | ✅ (до окончания копирования) | ✅ |
|
||||
| `imported`, `import_failed`→после ретрая | — | удаляется |
|
||||
| `duplicate`, `invalid`, `rejected`, `withdrawn`, `expired` | — | удаляется сразу |
|
||||
|
||||
## 3. Квота очереди
|
||||
|
||||
- Глобальный лимит `uploads.queue_limit_bytes` (по умолчанию **10 ГБ**), меняет только админ в настройках админки.
|
||||
- Опционально — лимит на одного модератора `uploads.per_user_limit_bytes` (по умолчанию выключен) и максимальный
|
||||
размер одного файла `uploads.max_file_bytes` (по умолчанию 2 ГБ).
|
||||
- **Занято** = сумма `size_bytes` загрузок в статусах, отмеченных в таблице выше.
|
||||
Дубли и отклонённые не учитываются никогда.
|
||||
- Перед загрузкой клиент запрашивает резерв (`POST /uploads` со списком файлов и размерами). Сервер в транзакции
|
||||
(`SELECT … FOR UPDATE` строки счётчика) проверяет `занято + запрошено ≤ лимит` и резервирует место.
|
||||
Не помещающиеся файлы отклоняются с кодом `uploads.queue_full` (клиент показывает, сколько места осталось).
|
||||
- Снижение лимита ниже занятого не удаляет существующие загрузки — просто новые не принимаются, пока очередь не освободится.
|
||||
- Индикатор заполненности очереди виден модераторам и админу («6.2 из 10 ГБ»).
|
||||
|
||||
## 4. Проверка на дубли
|
||||
|
||||
Три уровня, от дешёвого к дорогому. Дубль на любом уровне = статус `duplicate`, файл удаляется,
|
||||
квота освобождается, автор видит, с каким существующим треком совпало (ссылка на трек).
|
||||
|
||||
| Уровень | Когда | Как |
|
||||
|---|---|---|
|
||||
| 1. Точная копия файла | **До загрузки** (на клиенте) и после | SHA-256 всего файла: клиент считает в браузере (Web Crypto, потоково в Web Worker) и отправляет в предпроверке. Сравнение с `media_files.sha256` и с другими загрузками в очереди. |
|
||||
| 2. Совпадение метаданных | **До загрузки** (теги читаются на клиенте библиотекой `music-metadata`) и после (на сервере по `ffprobe`) | Нормализованные `artist + title` (+ `album`, если есть) и длительность ±2 с против `tracks`. |
|
||||
| 3. Акустический отпечаток | После загрузки | Chromaprint (`fpcalc`), сравнение с `track_fingerprints` библиотеки и очереди. Ловит тот же трек в другом формате / битрейте / с другими тегами. |
|
||||
|
||||
Правила:
|
||||
- Совпадение уровня 1 или 3 — дубль безусловно.
|
||||
- Совпадение только уровня 2 (метаданные совпали, отпечаток — нет: ремастер, live, другая версия) — **не отбрасывается**,
|
||||
а помечается флагом `possible_duplicate` для решения админа.
|
||||
- Если загружаемый файл того же трека **лучшего качества** (lossless вместо MP3) — дубль всё равно отбрасывается автоматически,
|
||||
но в настройках можно включить режим `uploads.allow_quality_upgrade`: тогда такой файл идёт на рассмотрение
|
||||
с пометкой «замена на лучшее качество».
|
||||
- Дубли внутри одной пачки загрузки (два одинаковых файла) — второй отбрасывается.
|
||||
- Отпечатки для существующей библиотеки считает сканер (задача `fingerprint_track`, ~0.3 с на трек) —
|
||||
выполняется при первом скане и для новых файлов.
|
||||
|
||||
## 5. Загрузка
|
||||
|
||||
- Протокол **tus** (resumable upload): сервер — `tusd` как библиотека внутри `api` (`/api/v1/uploads/files/`),
|
||||
клиент — `tus-js-client`. Докачка после обрыва, чанки по 8 МБ.
|
||||
- Веб: drag-and-drop файлов и **папок** (сохраняются относительные пути — помогают угадать альбом),
|
||||
до 4 параллельных загрузок, прогресс по каждому файлу.
|
||||
- Мобильный клиент загрузку не поддерживает (первая версия).
|
||||
- Staging — локальный диск сервера `/srv/pipu/uploads/<upload_id>/<file_id>` (не NFS).
|
||||
- Разрешённые форматы: `flac mp3 m4a ogg opus wav aiff wv`. Проверяется не расширение, а содержимое (`ffprobe`):
|
||||
должен быть ровно один аудиопоток, длительность 5 с – 3 ч. Иначе `invalid` с кодом причины.
|
||||
- Допускается отдельная картинка обложки (`cover.jpg|png`) в той же пачке — привязывается к файлам той же папки.
|
||||
|
||||
## 6. Обработка (`processing`)
|
||||
|
||||
Задача River `process_upload_file`:
|
||||
1. `ffprobe` → формат, длительность, битрейт, теги.
|
||||
2. SHA-256, `content_hash`, `fpcalc` → проверки дублей (раздел 4).
|
||||
3. Метаданные-кандидаты: теги → если пусто, разбор относительного пути из браузера шаблонами целевого хранилища
|
||||
→ если пусто, имя файла.
|
||||
4. Нормализация и сопоставление с существующими исполнителями / альбомами каталога
|
||||
(чтобы «Кино» не превратилось во второго исполнителя «КИНО»): найденные id подставляются, админ видит
|
||||
«будет добавлено к существующему альбому» или «новый альбом».
|
||||
5. Расчёт **предварительного целевого пути** (раздел 7).
|
||||
6. Статус `pending_review`.
|
||||
|
||||
## 7. Раскладка в хранилище «как принято на сервере»
|
||||
|
||||
### Целевое хранилище
|
||||
- Админ отмечает одно или несколько хранилищ как **принимающие загрузки** (`accepts_uploads = true`).
|
||||
Только они монтируются на запись (см. [library-and-storage.md](library-and-storage.md#запись-в-хранилище)).
|
||||
- Хранилище по умолчанию — в настройках; при одобрении админ может выбрать другое.
|
||||
|
||||
### Шаблон раскладки
|
||||
Регэкспы умеют только *разбирать* путь, поэтому для *записи* у хранилища есть **шаблон пути** (`write_template`):
|
||||
|
||||
```
|
||||
{albumartist}/{year} - {album}/{disc:1-}{track:02} - {title}.{ext}
|
||||
```
|
||||
|
||||
| Плейсхолдер | Значение |
|
||||
|---|---|
|
||||
| `{artist}`, `{albumartist}` | Исполнитель трека / альбома (если нет — исполнитель трека) |
|
||||
| `{album}`, `{title}`, `{genre}` | |
|
||||
| `{year}` | 4 цифры |
|
||||
| `{track:02}` | Номер с дополнением нулями |
|
||||
| `{disc:1-}` | Номер диска с суффиксом `-`, выводится только если дисков > 1 |
|
||||
| `{ext}` | Расширение по реальному формату |
|
||||
| `[ ... ]` | Необязательный фрагмент: опускается целиком, если любой плейсхолдер внутри пуст, например `[{year} - ]{album}` |
|
||||
|
||||
**Как получить шаблон:**
|
||||
- **Автоподбор**: система берёт выборку существующих путей хранилища, определяет, какой регэксп совпадает чаще всего,
|
||||
и предлагает шаблон, порождающий такие же пути (группы регэкспа → плейсхолдеры, литералы между группами → текст шаблона).
|
||||
Админ подтверждает или правит.
|
||||
- **Проверка обратимости** (обязательна при сохранении): шаблон применяется к 50 существующим трекам хранилища,
|
||||
полученные пути прогоняются через регэкспы этого хранилища — поля должны распознаться обратно без изменений,
|
||||
а в ≥ 90% случаев путь должен совпасть с реальным путём файла. Иначе — предупреждение с примерами расхождений.
|
||||
Так гарантируется, что новые файлы лежат так же, как старые, и сканер разберёт их правильно.
|
||||
|
||||
### Очистка имён
|
||||
- Запрещённые символы `/ \ : * ? " < > |` и управляющие → `_`; точки и пробелы в конце сегмента удаляются.
|
||||
- Длина сегмента ≤ 200 байт UTF-8, полный путь ≤ 1000 байт (обрезка по границе символа с сохранением расширения).
|
||||
- Unicode NFC. Регистр не меняется.
|
||||
- Существующая папка исполнителя / альбома переиспользуется, если её имя совпадает с нормализованным значением
|
||||
(`Кино` ↔ `КИНО`) — берётся **уже существующее написание**, чтобы не плодить соседние папки.
|
||||
|
||||
### Конфликты
|
||||
- Если файл по целевому пути уже существует — добавляется суффикс ` (2)`, а в задаче — предупреждение.
|
||||
- Путь проверяется на выход за корень хранилища (`..`, абсолютные пути) — ошибка.
|
||||
|
||||
## 8. Рассмотрение (админ)
|
||||
|
||||
Экран «Очередь на добавление»:
|
||||
- Группировка по пачке загрузки / по альбому; фильтры: статус, автор, `possible_duplicate`.
|
||||
- Для каждого файла: прослушивание (стрим из staging), метаданные с возможностью правки, обложка, качество
|
||||
(формат / битрейт / sample rate), флаги (возможный дубль — со ссылкой на похожий трек), **итоговый путь в хранилище**
|
||||
(пересчитывается live при правке метаданных).
|
||||
- Действия: одобрить / отклонить (с причиной — видна автору), массовые действия для выбранных, правка «на весь альбом»
|
||||
(исполнитель, альбом, год, обложка одной операцией).
|
||||
- Если админ изменил метаданные — они **записываются в теги файла** при импорте (`ffmpeg -map 0 -c copy -metadata …`
|
||||
без перекодирования; для FLAC/MP3/M4A/Ogg), чтобы теги и путь не противоречили друг другу.
|
||||
|
||||
## 9. Импорт (`importing`)
|
||||
|
||||
Задача River `import_upload_file` (повторяемая, идемпотентная):
|
||||
1. Пересчитать целевой путь с финальными метаданными.
|
||||
2. Запросить у `storage-agent` запись: `POST /internal/files` `{source_id, rel_path, staging_path}`.
|
||||
Агент:
|
||||
- создаёт каталоги;
|
||||
- копирует во временный файл `.<name>.pipu-tmp` в целевой папке, `fsync`;
|
||||
- сверяет SHA-256 копии со staging;
|
||||
- атомарно переименовывает в итоговое имя.
|
||||
3. Запустить точечный скан файла (`scan_path`) → трек появляется в каталоге и поиске, ставится в аудиоанализ.
|
||||
В `media_files` сохраняется `upload_id` — история происхождения.
|
||||
4. Удалить файл из staging, освободить квоту, статус `imported`, запись в `audit_log`.
|
||||
|
||||
При ошибке (NFS недоступен, нет места) — `import_failed` с сообщением, квота не освобождается, файл остаётся в staging;
|
||||
админ может повторить или отклонить.
|
||||
|
||||
## 10. Уведомления
|
||||
- Автор видит статусы в «Мои предложения» (раздел админки модератора), бейдж с числом решений с последнего визита.
|
||||
- Админ видит бейдж «N на рассмотрении» в сайдбаре.
|
||||
|
||||
## 11. Очистка
|
||||
- `uploading` старше 24 ч → `expired`, файлы удаляются.
|
||||
- Записи завершённых загрузок (`imported`, `rejected`, …) хранятся 180 дней для истории, без файлов.
|
||||
- При старте `api` — сверка staging-каталога с БД: осиротевшие файлы удаляются.
|
||||
Reference in new issue
Block a user