- Introduced a new warning for users uploading non-FLAC or WAV formats, indicating that these formats should only be used as a last resort. - Added functionality to highlight files needing format attention in the upload queue. - Updated the UI to provide clearer guidance on acceptable audio formats during uploads. - Improved documentation to reflect changes in upload requirements and user interface updates.
23 KiB
Предложение треков (загрузка с модерацией)
Модераторы (и админы) предлагают новые треки. Файлы попадают в очередь на добавление с общим лимитом размера, дубли отбрасываются сразу, аудитор загрузок одобряет или отклоняет, одобренные файлы раскладываются в хранилище по той же структуре папок, что принята в этом хранилище.
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 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. Просим присылать FLAC или WAV; MP3 и остальные — крайний случай: форма предупреждает об этом, а очередь на добавление помечает такой файл «обратить внимание». Проверяется не расширение, а содержимое (ffprobe): должен быть ровно один аудиопоток, длительность 5 с – 3 ч. Иначеinvalidс кодом причины. - Допускается отдельная картинка обложки (
cover.jpg|png) в той же пачке — привязывается к файлам той же папки.
6. Обработка (processing)
Задача River process_upload_file:
ffprobe→ формат, длительность, битрейт, теги.- SHA-256,
content_hash,fpcalc→ проверки дублей (раздел 4). - Метаданные-кандидаты: теги → если пусто, разбор относительного пути из браузера шаблонами целевого хранилища → если пусто, имя файла.
- Нормализация и сопоставление с существующими исполнителями / альбомами каталога (чтобы «Кино» не превратилось во второго исполнителя «КИНО»): найденные id подставляются, админ видит «будет добавлено к существующему альбому» или «новый альбом».
- Расчёт предварительного целевого пути (раздел 7).
- Статус
pending_review.
7. Раскладка в хранилище «как принято на сервере»
Целевое хранилище
- Админ отмечает одно или несколько хранилищ как принимающие загрузки (
accepts_uploads = true). Только они монтируются на запись (см. 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 (повторяемая, идемпотентная):
- Пересчитать целевой путь с финальными метаданными.
- Запросить у
storage-agentзапись:POST /internal/files{source_id, rel_path, staging_path}. Агент:- создаёт каталоги;
- создаёт файл сразу под итоговым именем (
O_EXCL, существующий не перезаписывается), копирует,fsync; - сверяет SHA-256 копии со staging; при ошибке удаляет свой файл и созданные пустые каталоги — на музыкальный диск не попадает ничего, кроме треков.
- Запустить точечный скан файла (
scan_path) → трек появляется в каталоге и поиске, ставится в аудиоанализ. Вmedia_filesсохраняетсяupload_id— история происхождения. - Удалить файл из 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-agentPOST /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 (hash-wasm); теги читает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-каталога с БД: осиротевшие файлы удаляются.