Add an install step for mounting the music disk via fstab and wire it into the stack: LIBRARY_HOST in .env points at the host folder, compose mounts it as /library-host (read-only for api, nginx and analyzer; writable only for storage-agent), nginx serves it through X-Accel, and install.sh validates the path and records it in .env. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
13 KiB
Библиотека: хранилища, шаблоны путей, сканер, поиск
Хранилища (sources)
Источник — корень, из которого берутся файлы. Управляет только admin.
| Тип | Как подключается |
|---|---|
nfs |
Админ указывает server, export, опции. storage-agent выполняет mount -t nfs -o <opts> server:export /library/<id>. |
local |
Папка на диске сервера, заранее смонтированная на хосте (например, диск /mnt/music через fstab) и указанная в LIBRARY_HOST. В контейнерах видна как /library-host; админ выбирает подпапку. |
Монтирование (storage-agent)
- Общий каталог хоста
/srv/pipu/libraryмонтируется:- в
storage-agent—bind,propagation: rshared; - в
api,nginx,analyzer—bind,read_only,propagation: rslave.
- в
- Опции NFS по умолчанию:
ro,nfsvers=4.1,soft,timeo=150,retrans=3,noatime.roпринудительно добавляется для всех источников, кроме отмеченныхaccepts_uploads(см. ниже). - Валидация ввода:
server— hostname/IP,export— абсолютный путь без.., опции — только из whitelist (nfsvers, port, timeo, retrans, soft, hard, rsize, wsize, noatime, nolock, sec). Никаких произвольных строк в команду. POST /admin/sources/{id}/test: агент монтирует во временную точку, проверяет чтение листинга и одного файла, возвращает число найденных аудиофайлов в первых N каталогах.- Health-check: агент раз в минуту делает
statс таймаутом; при сбое —status=error, сообщение в админке; треки источника помечаются недоступными (не выдаются в волну, в UI серые), но не удаляются. - При рестарте агента — перемонтирование всех
status != disabledисточников.
Запись в хранилище
Единственный сценарий записи — импорт одобренных треков из очереди на добавление.
- Источник с
accepts_uploads = trueагент монтирует дважды:/library/<id>—ro, как обычно (его видятapi,nginx,analyzer);/library-rw/<id>—rw, виден только самому агенту (каталог без propagation наружу).
- Пишет только
storage-agentпо командеapi(POST /internal/files): создание каталогов, запись во временный файл,fsync, сверка SHA-256, атомарныйrename. Перезапись существующих файлов и удаление запрещены на уровне кода агента. - Права на NFS-сервере: экспорт должен разрешать запись для UID/GID агента (
PIPU_FILE_UID/PIPU_FILE_GID, задаются в.envпод владельца музыки на файловом сервере); новые файлы —0644, каталоги —0755. POST /admin/sources/{id}/testдля такого источника дополнительно проверяет запись: создаёт и удаляет.pipu-write-testв корне.- У источника задаётся
write_template— шаблон пути для новых файлов, с автоподбором по существующей структуре и проверкой обратимости через регэкспы источника (подробно — uploads.md).
Шаблоны путей (регэкспы)
Для каждого источника — упорядоченный список регулярных выражений (синтаксис RE2 / Go regexp)
с именованными группами. Применяются к пути относительно корня источника, с / как разделителем.
Первое совпавшее выражение побеждает.
Поддерживаемые группы:
| Группа | Поле |
|---|---|
artist |
Исполнитель трека |
albumartist |
Исполнитель альбома |
album |
Альбом |
year |
Год (4 цифры) |
disc |
Номер диска |
track |
Номер трека |
title |
Название |
genre |
Жанр |
skip |
Если совпала — файл игнорируется (например, ^(?P<skip>_incoming)/) |
Примеры:
# Исполнитель/2019 - Альбом/01 - Название.flac
^(?P<artist>[^/]+)/(?P<year>\d{4}) - (?P<album>[^/]+)/(?:(?P<disc>\d)-)?(?P<track>\d{1,3}) - (?P<title>[^/]+)\.\w+$
# Жанр/Исполнитель/Альбом (2019)/03. Название.mp3
^(?P<genre>[^/]+)/(?P<artist>[^/]+)/(?P<album>.+?) \((?P<year>\d{4})\)/(?P<track>\d+)\. (?P<title>[^/]+)\.\w+$
# Сборники: Compilations/Альбом/01 - Исполнитель - Название.mp3
^Compilations/(?P<album>[^/]+)/(?P<track>\d+) - (?P<artist>.+?) - (?P<title>[^/]+)\.\w+$
Слияние с тегами
tag_priority источника:
tags_first(по умолчанию): значение из тега, если тег пуст — из шаблона;path_first: значение из шаблона, если группа не совпала — из тега.
Если нет ни тега, ни шаблона: title = имя файла без расширения и номера, artist = «Unknown artist»,
album = имя родительской папки.
Нормализация: обрезка пробелов, _ → пробел, Unicode NFC; для ключей сопоставления (name_norm) —
lower-case, ё → е, удаление пунктуации, схлопывание пробелов. Множественные исполнители разбираются по
разделителям ;, /, feat., ft., & (последний — только если такого составного имени нет в библиотеке).
Предпросмотр
POST /admin/sources/{id}/patterns/preview берёт случайную выборку (по умолчанию 50) путей из
media_files (или из листинга, если скана ещё не было) и возвращает для каждого: какое выражение совпало,
извлечённые поля, значения из тегов, итоговый результат после слияния. UI показывает таблицу с подсветкой
групп и счётчик «не совпало ни одно выражение». Регэксп валидируется при вводе (компиляция на сервере,
ошибка с позицией). Ограничение длины выражения — 1000 символов (RE2 линейный, ReDoS невозможен).
Сканер
Задачи River в api:
| Задача | Когда |
|---|---|
scan_source(full=false) |
По расписанию источника, вручную, после изменения шаблонов (с full=true) |
scan_source(full=true) |
Перечитать теги / шаблоны у всех файлов |
scan_path(source_id, rel_path) |
Точечный скан одного файла после импорта из очереди |
extract_cover(album_id) |
Новый альбом |
fingerprint_file(file_id) |
Новый / изменённый файл: полный SHA-256 + Chromaprint (fpcalc) — для проверки дублей при загрузке; низкий приоритет |
reindex_search |
Вручную / после крупного скана |
Алгоритм инкрементального скана:
- Обход дерева (
filepath.WalkDir), фильтр по расширениямflac mp3 m4a aac ogg opus wav aiff wv ape. Параллелизм — 8 воркеров (NFS любит параллельные запросы, но не сотни). - Для каждого файла: если
(rel_path, size, mtime)не изменились иfull=false— пропуск. - Иначе —
ffprobe(длительность, кодек, битрейт) + чтение тегов, применение шаблонов,content_hash. - Сопоставление: по
content_hashищем «пропавший» файл — если нашли, это переименование / перенос: обновляем путь,track_idсохраняется (лайки и история не теряются). - Upsert исполнителей / альбомов / треков; дубли (одинаковые artist+album+title+duration ±2 с) сводятся к
одному треку,
primary_file_id— файл лучшего качества (lossless > битрейт). - Файлы, не встреченные при полном обходе, получают
missing_since; через 30 дней без появления трек удаляется (лайки / история сохраняются в событиях, в UI — «трек недоступен»). - Новые треки →
analysis_jobs, батчевое обновление Meilisearch (по 1000 документов). - Прогресс публикуется в Redis (
scan:<source_id>), админка опрашиваетGET /admin/library/statusраз в 2 с (или SSE/admin/library/events).
Ожидаемое время первого скана 11k файлов по NFS — 5–15 минут (bottleneck — чтение тегов).
Обложки
Источники по приоритету: встроенная в файл → cover.jpg|folder.jpg|front.* в папке альбома → плейсхолдер,
сгенерированный из цвета. Конвертируются в WebP 64/300/1200 px в /srv/pipu/cache/covers/, вычисляется
палитра из 3 цветов (k-means по пикселям; cover_colors) — для ambient-фона, цветов кнопки волны и плейсхолдеров.
Поиск
Meilisearch, индексы:
| Индекс | Поля поиска (по убыванию веса) | Фильтры / сортировка |
|---|---|---|
artists |
name, name_translit, aliases |
popularity |
albums |
title, artist_name, title_translit |
year, popularity |
tracks |
title, artist_names, album_title, title_translit |
genre, year, popularity, available |
*_translit— транслитерация (кириллица ↔ латиница), плюс поле с исправлением раскладки (ghbdtn→привет): при пустом результате клиентский запрос повторяется в альтернативной раскладке на сторонеapi.- Ранжирование: стандартные правила Meilisearch +
popularity:desc(глобальные засчитанные прослушивания за 90 дней). /search/suggestиспользуетmulti-search(один запрос — три индекса), отдаёт ≤ 3/3/5. Клиент делает debounce 150 мс, отменяет предыдущий запрос (AbortController).- Персонализация (опционально, этап 2): треки, которые пользователь лайкал / часто слушал, поднимаются выше
в выдаче
suggest(re-rank первых 20 на сторонеapi). - Синхронизация:
apiпосле изменения каталога ставит документы в батч-очередь; полная переиндексация — задачаreindex_search(индекс собирается во временном индексе и подменяется черезswap-indexes, без даунтайма).