- Migration 00008: upload_batches, upload_files, upload_quota. - Quota reserved under a row lock when a batch is created; every status change goes through one transition that frees space in the same transaction. SHA-256 duplicates are rejected before any upload. - tus 1.0.0 core (HEAD/PATCH) for the author only; offset is the real staging file size, data beyond the declared size is refused (ADR-015). - Processing worker: ffprobe checks, SHA-256 verification, exact and Chromaprint duplicates against library and queue, metadata from tags or the dropped folder path, possible_duplicate on metadata match, and the target path from write_template reusing existing folder spelling. - Import worker: edited tags written with ffmpeg stream copy, file written by storage-agent, point scan of the new file, imported status; idempotent and resumed after restart. Stale uploads expire after 24 h, orphan staging files are removed at startup. - API: quota, precheck, batches, my uploads, withdraw, admin list, metadata edit, bulk review, retry and queue settings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
20 KiB
Предложение треков (загрузка с модерацией)
Модераторы (и админы) предлагают новые треки. Файлы попадают в очередь на добавление с общим лимитом размера, дубли отбрасываются сразу, админ одобряет или отклоняет, одобренные файлы раскладываются в хранилище по той же структуре папок, что принята в этом хранилище.
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:
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}. Агент:- создаёт каталоги;
- копирует во временный файл
.<name>.pipu-tmpв целевой папке,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 (ADR-015):
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). - Пока не сделано:
allow_quality_upgrade, правка «на всю пачку» (PATCH /admin/uploads/batches/{id}), прослушивание из staging, отдельная обложка пачки, ночная сверка счётчика квоты, экраны веба.
11. Очистка
uploadingстарше 24 ч →expired, файлы удаляются.- Записи завершённых загрузок (
imported,rejected, …) хранятся 180 дней для истории, без файлов. - При старте
api— сверка staging-каталога с БД: осиротевшие файлы удаляются.