feat(uploads): match release groups with MusicBrainz and AcoustID
ci / module boundaries / boundaries (push) Successful in 10s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m30s
ci / api / api (push) Successful in 58s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m1s
ci / db / db (push) Failing after 25s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m36s
ci / ts / ts (packages + web) (push) Failing after 6m3s

This commit is contained in:
mrleo1nid committed 2026-10-29 20:44:59 +03:00
1 parent 1e3a149aef
commit 39bf7d4b70
45 files changed
+4792 -417

No files matched your search

+9
View File
@@ -71,6 +71,15 @@
известные дубли отбрасываются без загрузки.
2. `POST /uploads` резервирует квоту очереди → загрузка по tus в staging (`/srv/pipu/uploads`).
3. Задача `process_upload_file`: ffprobe, хэш, Chromaprint → дубль (удаление, квота освобождена) или `pending_review`.
После обработки пачки отдельная очередь PostgreSQL сопоставляет её с изданиями MusicBrainz.
Несколько альбомов получают постоянные `upload_release_groups` внутри исходной пачки: отдельные задачи
и обложки, FK из `upload_files`. `uploads` группирует по идентификаторам, альбомным тегам и папкам;
применение метаданных, инвалидирование аренды и чтение обложки ограничены выбранной группой.
По явному запросу ревьюера `internal/acoustid` распознаёт локальные Chromaprint-отпечатки;
`uploads` хранит результаты отдельно от тегов и использует recording ID при сопоставлении изданий.
Адаптер `internal/musicbrainz` отвечает за HTTP, лимит запросов и Cover Art Archive; `uploads` — за
комплектность, сравнение с библиотекой и применение выбранных администратором изменений одной транзакцией.
Сбой внешнего каталога не блокирует обработку, одобрение или импорт.
4. Админ правит метаданные и одобряет → `import_upload_file`: целевой путь по `write_template` хранилища →
`storage-agent` записывает файл → `scan_path` → трек в каталоге, поиске и очереди анализа.
+11
View File
@@ -140,9 +140,20 @@ CREATE TABLE upload_batches ( -- одна «пачка» загруз
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE upload_release_groups (
id uuid PRIMARY KEY,
batch_id uuid NOT NULL REFERENCES upload_batches(id) ON DELETE CASCADE,
grouping_key text NOT NULL, -- ключ первичной группировки, после подтверждения тегов не меняется
artist text NOT NULL DEFAULT '',
album text NOT NULL DEFAULT '',
cover_path text, -- обложка только этого релиза
UNIQUE (batch_id, grouping_key)
);
CREATE TABLE upload_files (
id uuid PRIMARY KEY,
batch_id uuid NOT NULL REFERENCES upload_batches(id) ON DELETE CASCADE,
release_group_id uuid REFERENCES upload_release_groups(id) ON DELETE SET NULL,
author_id uuid NOT NULL REFERENCES users(id),
status upload_status NOT NULL,
original_name text NOT NULL,
+2
View File
@@ -79,6 +79,8 @@
| `GITEA_URL`, `GITEA_REPO`, `GITEA_TOKEN` | пусто — из `git remote` (`install.sh`, `update.sh`); токен — для закрытого репозитория | Самообновление Android и страница `/download` |
| `PIPU_FILE_UID`, `PIPU_FILE_GID` | `1000` | Владелец файлов, которые агент записывает в принимающие хранилища |
| `UPLOADS_DIR` | `/uploads` | Staging очереди на добавление (лимит очереди задаётся в админке) |
| `MUSICBRAINZ_ENABLED` | `true` | Фоновое сопоставление пачек с изданиями MusicBrainz и предложение обложек Cover Art Archive; нужен исходящий HTTPS |
| `ACOUSTID_API_KEY` | пусто | Секретный ключ приложения AcoustID для распознавания пачек по звуку; без него доступен только поиск по тегам |
| `LOG_LEVEL` | `info` | |
## TLS
+85
View File
@@ -180,6 +180,91 @@
- Если админ изменил метаданные — они **записываются в теги файла** при импорте (`ffmpeg -map 0 -c copy -metadata …`
без перекодирования; для FLAC/MP3/M4A/Ogg), чтобы теги и путь не противоречили друг другу.
### Сопоставление релиза и внешние обложки
После обработки всех файлов пачки API ставит её в отдельную фоновую очередь. MusicBrainz ищет
конкретные издания по сохранённому MusicBrainz Release ID либо исполнителю альбома и названию.
Внешнему сервису передаются только поисковые метаданные и идентификаторы, аудиофайлы не отправляются.
Для смешанной пачки или отсутствующих тегов показывается предложение уточнить поиск.
В заголовке пачки видно состояние поиска. Если найдено несколько изданий, администратор сначала
выбирает конкретное. Проверка сопоставляет дорожки по идентификаторам, названиям, исполнителям,
длительностям и нумерации дисков. «Полный состав» означает все дорожки выбранного издания с уверенными
совпадениями и без несопоставленных файлов. Один файл не считается синглом без типа Single в каталоге.
Несколько форматов одной песни не увеличивают число совпавших дорожек. Предположительные и неоднозначные
совпадения отмечаются отдельно. Для отсутствующих дорожек отдельно показаны совпадения с доступной
библиотекой по тегам и длительности; они не делают загруженную пачку полной.
Поиск сам не меняет теги, путь или обложку. В диалоге можно отдельно применить метаданные к совпавшим
редактируемым файлам и/или обложку. Обложка загружается из 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` (повторяемая, идемпотентная):