ci / module boundaries / boundaries (push) Successful in 7s
ci / db / db (push) Successful in 2m33s
ci / api / api (push) Successful in 1m10s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 3m45s
ci / ts / ts (packages + web) (push) Successful in 6m29s
353 lines
42 KiB
Markdown
353 lines
42 KiB
Markdown
# Предложение треков (загрузка с модерацией)
|
||
|
||
Модераторы (и админы) предлагают новые треки. Файлы попадают в **очередь на добавление** с общим лимитом размера,
|
||
дубли отбрасываются сразу, аудитор загрузок одобряет или отклоняет, одобренные файлы раскладываются в хранилище
|
||
**по той же структуре папок, что принята в этом хранилище**.
|
||
|
||
## 1. Роли
|
||
|
||
| Действие | user | moderator | admin |
|
||
|---|:-:|:-:|:-:|
|
||
| Загрузить треки в очередь | — | ✅ | ✅ |
|
||
| Видеть свои предложения и их статус, отозвать до рассмотрения | — | ✅ | ✅ |
|
||
| Настраивать лимит очереди, целевое хранилище и шаблон раскладки | — | — | ✅ |
|
||
|
||
Видеть всю очередь, слушать, править метаданные, одобрять и отклонять может только тот, у кого есть право
|
||
**аудита загрузок** (`users.reviews_uploads`, в API — `reviews_uploads` в `Me` / `AdminUser`). Это не роль: право
|
||
выдаёт админ, оно может быть у нескольких людей сразу, у модератора или даже у обычного пользователя, а у админа
|
||
его может не быть. Без него админ по-прежнему настраивает лимиты и хранилища, но очередь не видит.
|
||
|
||
Загрузки админа проходят ту же проверку на дубли; в очередь они тоже попадают, но админ может одобрить их сразу.
|
||
|
||
## 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 Worker через `js-sha256`, без WebAssembly и загрузки всего файла в память, и отправляет в предпроверке. Сравнение с `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`. Просим присылать lossless — **FLAC, WAV или AIFF** (WavPack тоже считается lossless); MP3 и остальные lossy — крайний случай: форма предупреждает об этом, а очередь на добавление помечает такой файл «обратить внимание». Проверяется не расширение, а содержимое (`ffprobe`):
|
||
должен быть ровно один аудиопоток, длительность 5 с – 3 ч. Иначе `invalid` с кодом причины.
|
||
- Картинки `cover.jpg`, `cover.png` (также `folder`/`front`, JPEG/PNG/WebP) загружаются как обложка альбома,
|
||
а не пропускаются как не-аудио. Если отдельной картинки нет, берётся встроенная обложка из первого трека,
|
||
в котором она найдена. На каждую папку альбома создаётся своя пачка с обложкой, поэтому разные альбомы
|
||
не смешиваются. Обложка до 8 МБ сохраняется вместе с резервом пачки и встраивается при импорте
|
||
в поддерживаемые контейнеры FLAC/MP3/M4A/Ogg/Opus без перекодирования аудио.
|
||
Обложки больше 8 МБ клиент автоматически уменьшает до JPEG с длинной стороной не более 2048 пикселей
|
||
перед base64-кодированием; это относится и к отдельным файлам, и к встроенной обложке, и к правке пачки.
|
||
Меньшие картинки остаются без изменений. Код HTTP 413 от прокси показывается как превышение размера запроса.
|
||
- `artist.jpg` (также `artist.jpeg`, `artist.png`, `artist.webp`, регистр не важен) в папке исполнителя
|
||
передаётся отдельно от обложки. Ближайшая такая картинка наследуется вложенными папками альбомов;
|
||
соседние исполнители не смешиваются. Например `Исполнитель/artist.jpg` и две папки альбомов
|
||
дают две отдельные пачки с одной фотографией исполнителя и своими обложками.
|
||
После одобрения и импорта трека фотография сохраняется в кеш WebP и ставится исполнителю альбома,
|
||
только если у него нет ни локальной аватарки, ни изображения Genius. Гостям она не назначается.
|
||
Существующая аватарка сохраняется, в том числе при одновременном импорте нескольких альбомов.
|
||
До одобрения каталог не меняется. Лимит фотографии — 8 МБ после уменьшения, как у обложки;
|
||
nginx принимает до 32 МБ, чтобы вместить обе картинки в base64. Временная фотография удаляется
|
||
после завершения всех файлов пачки, а готовая аватарка остаётся в кеше.
|
||
|
||
## 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}` |
|
||
|
||
**Как получить шаблон:**
|
||
- **Автоподбор**: система сравнивает пути и метаданные до 200 существующих файлов хранилища, строит варианты
|
||
раскладки и выбирает вариант с наибольшим числом точных совпадений. Сохранённые регэкспы также используются
|
||
как кандидаты, но не обязательны. В интерфейсе показаны число совпадений и примеры реальных путей.
|
||
Если шаблон ещё не задан, предложение заполняется автоматически; администратор может изменить его и сохранить.
|
||
- **Проверка обратимости** (обязательна при сохранении): шаблон применяется к 50 существующим трекам хранилища,
|
||
полученные пути прогоняются через регэкспы этого хранилища — поля должны распознаться обратно без изменений,
|
||
а в ≥ 90% случаев путь должен совпасть с реальным путём файла. Иначе — предупреждение с примерами расхождений.
|
||
Так гарантируется, что новые файлы лежат так же, как старые, и сканер разберёт их правильно.
|
||
|
||
### Очистка имён
|
||
- Запрещённые символы `/ \ : * ? " < > |` и управляющие → `_`; точки и пробелы в конце сегмента удаляются.
|
||
- Длина сегмента ≤ 200 байт UTF-8, полный путь ≤ 1000 байт (обрезка по границе символа с сохранением расширения).
|
||
- Unicode NFC. Регистр не меняется.
|
||
- Существующая папка исполнителя / альбома переиспользуется, если её имя совпадает с нормализованным значением
|
||
(`Кино` ↔ `КИНО`) — берётся **уже существующее написание**, чтобы не плодить соседние папки.
|
||
|
||
### Конфликты
|
||
- Если файл по целевому пути уже существует — добавляется суффикс ` (2)`, а в задаче — предупреждение.
|
||
- Путь проверяется на выход за корень хранилища (`..`, абсолютные пути) — ошибка.
|
||
|
||
## 8. Рассмотрение (аудит загрузок)
|
||
|
||
Экран «Очередь на добавление»:
|
||
- Группировка по пачке загрузки / по альбому; фильтры: статус, автор, `possible_duplicate`.
|
||
Пачки по умолчанию свёрнуты; кнопка в заголовке раскрывает файлы, выбор всей пачки доступен и без раскрытия.
|
||
В заголовке указано наличие общей обложки пачки: «Есть обложка» / «Без обложки».
|
||
Там же показаны количества файлов по форматам и диапазоны битрейта, частоты и разрядности;
|
||
одинаковые значения не повторяются, отсутствующие измерения не учитываются как нули.
|
||
- Для каждого файла: прослушивание (стрим из staging), метаданные с возможностью правки, обложка, качество
|
||
(формат / битрейт / sample rate), флаги (возможный дубль — со ссылкой на похожий трек), **итоговый путь в хранилище**
|
||
с названием хранилища и путём относительно его корня. Полный адрес назначения (локальный путь API
|
||
или `server:/export/path` для NFS) доступен при наведении на путь. Для ожидающих рассмотрения, одобренных и ошибочных импортов
|
||
предпросмотр рассчитывается при чтении очереди по текущему шаблону — тем же планировщиком, что и импорт.
|
||
Сохранение шаблона обновляет кеш очереди. Уже импортированные файлы сохраняют фактический путь.
|
||
Если рассчитать путь нельзя, показаны причина и действие для исправления вместо старого пути.
|
||
Отсутствующий год отмечается предупреждением и не блокирует одобрение или импорт:
|
||
`{year}` опускается вместе со скобками или разделителем, необязательный фрагмент с годом опускается.
|
||
В теги неизвестный год не записывается.
|
||
- Прослушивание staging выдаёт сам API с поддержкой Range: приватные файлы tus (`0640`, каталоги `0750`)
|
||
недоступны пользователю nginx. Выдача не зависит от `X_ACCEL`; права существующих файлов менять не нужно.
|
||
Аудио и его метаданные загружаются только после Play (`preload="none"`); раскрытие пачки не запускает запросы к файлам.
|
||
При сбое показана ошибка (включая HTTP-статус, если доступен)
|
||
и кнопка повторной попытки с обновлением подписанных ссылок.
|
||
Одновременно играет только один предпросмотр: запуск другого ставит предыдущий на паузу с сохранением позиции.
|
||
Сворачивание пачки или уход со страницы также останавливает её прослушивание.
|
||
- Действия: одобрить / отклонить (с причиной — видна автору), массовые действия для выбранных, правка «на весь альбом»
|
||
(исполнитель, альбом, год, обложка одной операцией).
|
||
- Если админ изменил метаданные — они **записываются в теги файла** при импорте (`ffmpeg -map 0 -c copy -metadata …`
|
||
без перекодирования; для FLAC/MP3/M4A/Ogg), чтобы теги и путь не противоречили друг другу.
|
||
|
||
### Сопоставление релиза и внешние обложки
|
||
|
||
После обработки всех файлов пачки API ставит её в отдельную фоновую очередь. MusicBrainz ищет
|
||
конкретные издания по сохранённому MusicBrainz Release ID либо исполнителю альбома и названию.
|
||
Внешнему сервису передаются только поисковые метаданные и идентификаторы, аудиофайлы не отправляются.
|
||
Для смешанной пачки или отсутствующих тегов показывается предложение уточнить поиск.
|
||
|
||
В заголовке пачки видно состояние поиска. Если найдено несколько изданий, администратор сначала
|
||
выбирает конкретное. Проверка сопоставляет дорожки по идентификаторам, названиям, исполнителям,
|
||
длительностям и нумерации дисков. «Полный состав» означает все дорожки выбранного издания с уверенными
|
||
совпадениями и без несопоставленных файлов. Один файл не считается синглом без типа Single в каталоге.
|
||
Несколько форматов одной песни не увеличивают число совпавших дорожек. Предположительные и неоднозначные
|
||
совпадения отмечаются отдельно. Для отсутствующих дорожек отдельно показаны совпадения с доступной
|
||
библиотекой по тегам и длительности; они не делают загруженную пачку полной.
|
||
|
||
Число файлов в пачке и число сопоставленных дорожек — разные показатели. «Файл не сопоставлен»
|
||
не доказывает отсутствие дорожки: она может быть среди файлов с отличающимися тегами. Для
|
||
предположительного совпадения допускается разная запись состава исполнителей (запятая, `feat.`, `;`)
|
||
и пометки `feat.` / `Prod.` в конце названия при совпадении номера, диска и длительности с допуском
|
||
2 секунды. Пометки версий (`live`, `remix`, `instrumental`) сохраняются. Такие совпадения требуют
|
||
проверки и сами по себе не подтверждают «Полный состав». При изменении правил сопоставления
|
||
сохранённые результаты поиска пересчитываются при открытии пачки.
|
||
|
||
Поиск сам не меняет теги, путь или обложку. В диалоге можно отдельно применить метаданные к совпавшим
|
||
редактируемым файлам и/или обложку. Обложка загружается из Cover Art Archive только после запроса
|
||
предпросмотра. При отсутствии обложки конкретного издания возможна обложка группы изданий с явной пометкой.
|
||
Замена уже загруженной обложки требует отдельного выбора. Применение фиксируется одной транзакцией
|
||
и отвергает изменившуюся пачку (409); статусы одобрения и квота не меняются. MusicBrainz ID записываются
|
||
в теги при импорте вместе с остальными подтверждёнными метаданными.
|
||
|
||
`MUSICBRAINZ_ENABLED=true` включает поиск (можно отключить в конфигурации сервера). Нужен исходящий HTTPS
|
||
к MusicBrainz, Cover Art Archive и archive.org для обложек. Ключ API для MusicBrainz не требуется;
|
||
запросы ограничены одним за 1,1 секунды, результат кешируется на 7 дней, просроченный кеш удаляется.
|
||
Сбой провайдера не мешает загрузке или импорту: повтор через 10 минут, не более трёх автоматических попыток,
|
||
после чего доступен ручной поиск. Очередь и кеш сохраняются в PostgreSQL, незавершённая задача
|
||
после перезапуска повторяется по истечении пятиминутной аренды.
|
||
|
||
Источники: [MusicBrainz API](https://musicbrainz.org/doc/MusicBrainz_API),
|
||
[Cover Art Archive API](https://musicbrainz.org/doc/Cover_Art_Archive/API).
|
||
|
||
### Несколько релизов в одной пачке
|
||
|
||
После завершения обработки всей пачки треки получают постоянные группы релизов внутри исходной пачки.
|
||
Группировка использует MusicBrainz Release ID, затем album artist + album + год. Единственный известный
|
||
Release ID подхватывается треками того же альбома без идентификатора; разные известные издания разделяются.
|
||
При неполных тегах используется исходная папка альбома. Подпапки CD1/CD2, Disc 1/2 и «Диск 1/2»
|
||
относятся к общей родительской папке. Album artist сохраняет сборники с разными исполнителями треков;
|
||
без него помогает общая папка. Файлы без альбомных тегов и без папок остаются одной группой для ручной проверки.
|
||
|
||
У групп отдельные фоновые задачи, аренды, результаты MusicBrainz/AcoustID, выбранные издания и обложки.
|
||
После первичного разделения подтверждение тегов не меняет принадлежность файлов и не объединяет группы.
|
||
Правка метаданных или отзыв файла сбрасывает только поиск его группы. Ошибка одной группы не блокирует
|
||
поиск соседних. В очереди сохраняется исходная пачка с общими действиями и вложенными заголовками релизов;
|
||
выбор строк релиза действует на загруженные строки страницы. В диалоге сначала выбирается релиз внутри пачки,
|
||
затем его издание. Проверка состава учитывает все файлы группы, включая другие страницы очереди.
|
||
|
||
HTTP: `group_id` в запросах поиска/применения и query-параметр `group_id` при чтении отчёта/обложки.
|
||
Для единственной группы параметр можно опустить; в смешанной пачке сервер требует явного выбора и проверяет
|
||
принадлежность группы пачке. Применение блокирует и проверяет только файлы выбранной группы и её задачу.
|
||
Квота и статусы файлов не меняются. При импорте используется обложка своей группы; staging-очистка сохраняет
|
||
общую обложку, пока она нужна хотя бы одной активной группе.
|
||
|
||
В одиночном релизе сохраняется исходная обложка пачки. В смешанной пачке одну общую обложку нельзя надёжно
|
||
приписать всем альбомам, поэтому группы начинают без неё; исходная обложка остаётся в пачке.
|
||
Обложку из каталога можно применить отдельно к нужному релизу. Явное изменение обложки «На всю пачку»
|
||
по-прежнему меняет обложки всех её групп.
|
||
|
||
### Распознавание по звуку
|
||
|
||
Кнопка «Распознать по звуку» запускает отдельный запрос для пачки до 50 редактируемых треков.
|
||
Нужен ключ **приложения** AcoustID: зарегистрируйте приложение на [AcoustID](https://acoustid.org/webservice),
|
||
задайте секрет `ACOUSTID_API_KEY` в конфигурации сервера и примените изменения. Пользовательский ключ
|
||
для отправки записей не нужен. При пустом ключе кнопка недоступна, поиск по тегам продолжает работать.
|
||
|
||
`fpcalc` локально вычисляет сжатый Chromaprint первых 120 секунд. В AcoustID отправляются только
|
||
отпечаток и полная длительность из ffprobe; аудиофайл, теги, имя файла и ключи пользователя не передаются.
|
||
Автоматического распознавания всех загрузок и отправки отпечатков в базу AcoustID нет.
|
||
Ключ хранится как секрет; POST не помещает его и отпечаток в URL, редиректы запрещены.
|
||
|
||
Результат используется при оценке не ниже 0,90 и отрыве от следующей **другой** записи не менее 0,05.
|
||
Для каждого файла показаны распознавание, неоднозначность, отсутствие совпадений или недоступный отпечаток.
|
||
Слабые результаты не исправляют теги. Уверенное распознавание имеет приоритет над ошибочными тегами;
|
||
для подтверждения полного состава нужна также совместимая длительность дорожки (±5 секунд).
|
||
Повтор одной записи на разных позициях остаётся неоднозначным.
|
||
|
||
MusicBrainz ищет издания по первым восьми уникальным распознанным записям (до 100 изданий на запись),
|
||
выбирает до пяти кандидатов по числу совпадений и сопоставляет с ними все файлы. Это ограниченный набор
|
||
кандидатов, не гарантия нахождения нужного издания. Отпечаток не определяет конкретный мастеринг или издание;
|
||
выбор и применение метаданных/обложки остаются за администратором. Поиск имеет общий таймаут 4 минуты,
|
||
лимит AcoustID — один запрос за 350 мс, кеш — 7 дней. Нужен исходящий HTTPS к `api.acoustid.org`.
|
||
Условия бесплатного сервиса AcoustID допускают некоммерческое использование; для коммерческого нужен
|
||
отдельный договор с провайдером (см. документацию AcoustID).
|
||
|
||
## 9. Импорт (`importing`)
|
||
|
||
Задача River `import_upload_file` (повторяемая, идемпотентная):
|
||
1. Пересчитать целевой путь с финальными метаданными.
|
||
2. Запросить у `storage-agent` запись: `POST /internal/files` `{source_id, rel_path, staging_path}`.
|
||
Агент:
|
||
- создаёт каталоги;
|
||
- создаёт файл сразу под итоговым именем (`O_EXCL`, существующий не перезаписывается), копирует, `fsync`;
|
||
- сверяет SHA-256 копии со staging; при ошибке удаляет свой файл и созданные пустые каталоги —
|
||
на музыкальный диск не попадает ничего, кроме треков.
|
||
3. Запустить точечный скан файла (`scan_path`) → трек появляется в каталоге и поиске, ставится в аудиоанализ.
|
||
В `media_files` сохраняется `upload_id` — история происхождения.
|
||
4. Удалить файл из staging, освободить квоту, статус `imported`, запись в `audit_log`.
|
||
|
||
При ошибке (NFS недоступен, нет места) — `import_failed` с сообщением, квота не освобождается, файл остаётся в staging;
|
||
админ может повторить или отклонить.
|
||
|
||
## 10. Уведомления
|
||
- В «Моих предложениях» файлы сгруппированы по пачкам. Можно выбрать отдельные файлы, загруженные строки
|
||
страницы или всю собственную пачку (с загрузкой всех её страниц) и отозвать выбранные до решения администратора.
|
||
Успешные отзывы освобождают квоту; при частичной ошибке показана причина, неудавшиеся файлы остаются выбранными.
|
||
- Автор видит статусы в «Мои предложения» (раздел админки модератора), бейдж с числом решений с последнего визита.
|
||
- Аудитор загрузок видит бейдж «N на рассмотрении» в сайдбаре.
|
||
|
||
## Реализация (`services/api/internal/uploads`)
|
||
|
||
- Таблицы — миграция `00008_uploads.sql`; смещение tus — `upload_files.received_bytes`, отпечаток Chromaprint файла
|
||
очереди — `upload_files.fingerprint` (дубли внутри очереди ловятся так же, как с библиотекой).
|
||
- Квота: `Store.CreateBatch` резервирует под `SELECT … FOR UPDATE` строки `upload_quota`; любая смена статуса идёт через
|
||
`transition` — место освобождается в той же транзакции, если новый статус его не занимает (таблица раздела 2).
|
||
- Приём — ядро tus: `HEAD` / `PATCH /api/v1/uploads/files/{id}`, только автор и только в `uploading`.
|
||
Принят целиком → `processing`.
|
||
- Обработка (`worker.go`, по одному файлу): размер → `ffprobe` (5 с – 3 ч) → SHA-256 (сверка с заявленным) →
|
||
дубль по SHA-256 → Chromaprint против библиотеки и очереди (длительность ±5 с, `library.Similarity` ≥ 0.75) →
|
||
метаданные (теги → путь из папки по регэкспам целевого хранилища → имя файла) → совпадение метаданных
|
||
(`possible_duplicate`) → путь по `write_template` с переиспользованием существующих папок → `pending_review`.
|
||
Если путь не строится (нет хранилища / шаблона / поля), файл всё равно ждёт рассмотрения, причина — в `status_reason`.
|
||
- Хранилище: выбранное админом → `default_source_id` из настроек → единственное с `accepts_uploads`.
|
||
- Импорт: правленые метаданные → копия с тегами (`ffmpeg -map 0 -c copy -metadata …`, FLAC/MP3/M4A/Ogg/Opus) →
|
||
`storage-agent` `POST /internal/files` → `Scanner.ScanFile` (точечный скан) → `imported`, связь `media_files.upload_file_id`.
|
||
Повторяемо: если по пути уже лежит файл с тем же хэшем, запись пропускается. Прерванный импорт при старте
|
||
возвращается в `approved`.
|
||
- Очистка: раз в 10 минут `uploading` старше 24 ч → `expired`; при старте — удаление файлов staging без записи в БД.
|
||
- Настройки: `GET` / `PATCH /admin/settings/uploads` (ключ `uploads.settings` в `app_settings`).
|
||
- Веб (`apps/web/src/features/uploads`, `pages/uploads`, `pages/admin/UploadQueuePage.tsx`):
|
||
- «Предложить треки» (`/uploads/new`): drag-and-drop файлов и папок (FileSystemEntry API, относительные пути),
|
||
выбор файлов / папки; SHA-256 потоком в Web Worker (`js-sha256`); теги читает `music-metadata` и отдаёт их
|
||
в предпроверку; `POST /uploads`; `tus-js-client` с готовым `uploadUrl`, куски по 8 МБ, 4 файла параллельно,
|
||
401 → обновление токена и повтор. Загрузчик — синглтон модуля: переходы по приложению не прерывают загрузку.
|
||
Незавершённые tus-ссылки пишутся в `localStorage`; после перезагрузки пользователь выбирает те же файлы,
|
||
клиент сверяет SHA-256 и продолжает с принятого смещения.
|
||
- «Мои предложения» (`/uploads/mine`): статусы, причины отказа, отзыв до решения, квота. Бейдж — число решений
|
||
новее `upload_visits`; открытие страницы вызывает `POST /uploads/seen`.
|
||
- «Очередь на добавление» (`/admin/uploads`, admin): вкладки по статусам, фильтр возможных дублей, группировка
|
||
по пачкам, правка метаданных и хранилища, правка исполнителя / альбома / года / обложки на всю пачку,
|
||
прослушивание staging по подписанной ссылке, флаг улучшения качества, массовое одобрение / отклонение с причиной,
|
||
повтор импорта. Настройки очереди — лимиты, хранилище и `allow_quality_upgrade`.
|
||
- Сверка квоты: `used_bytes` обновляется в транзакции смены статуса и дополнительно пересчитывается по
|
||
`SUM(size_bytes)` занимающих статусов при старте `api` и в цикле очистки (раз в 10 минут).
|
||
|
||
## 11. Очистка
|
||
- `uploading` старше 24 ч → `expired`, файлы удаляются.
|
||
- Записи завершённых загрузок (`imported`, `rejected`, …) хранятся 180 дней для истории, без файлов.
|
||
- При старте `api` — сверка staging-каталога с БД: осиротевшие файлы удаляются.
|