Files
pipu-music/docs/uploads.md
T
mrleo1nidandClaude Opus 5.5 4d2c6ce22f
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m30s
ci / ts / ts (packages + web) (push) Successful in 2m11s
fix(web): treat AIFF and WavPack as lossless in the upload format hint
The web check only accepted FLAC and WAV, so AIFF files were flagged
"needs attention" although the server already counts them as lossless.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 10:35:43 +03:00

23 KiB
Raw Blame History

Предложение треков (загрузка с модерацией)

Модераторы (и админы) предлагают новые треки. Файлы попадают в очередь на добавление с общим лимитом размера, дубли отбрасываются сразу, аудитор загрузок одобряет или отклоняет, одобренные файлы раскладываются в хранилище по той же структуре папок, что принята в этом хранилище.

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. Просим присылать lossless — FLAC, WAV или AIFF (WavPack тоже считается lossless); MP3 и остальные lossy — крайний случай: форма предупреждает об этом, а очередь на добавление помечает такой файл «обратить внимание». Проверяется не расширение, а содержимое (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).
  • Хранилище по умолчанию — в настройках; при одобрении админ может выбрать другое.

Шаблон раскладки

Регэкспы умеют только разбирать путь, поэтому для записи у хранилища есть шаблон пути (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}. Агент:
    • создаёт каталоги;
    • создаёт файл сразу под итоговым именем (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 (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-каталога с БД: осиротевшие файлы удаляются.