Files
pipu-music/docs/uploads.md
T
mrleo1nidandClaude Opus 5.5 9eb1c25147 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>
2026-10-19 23:21:31 +03:00

17 KiB
Raw Blame History

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

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

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).
  • Хранилище по умолчанию — в настройках; при одобрении админ может выбрать другое.

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

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