BOOK-LIBRARY: импорт, метаданные и каталог

У импорта в BOOK-LIBRARY есть незаметная, но очень важная роль: он превращает разрозненный файл книги в запись, которую потом можно найти, понять и спокойно открыть в каталоге. Между «загрузили файл» и «видим аккуратную карточку» проходит несколько проверок и решений: что это за книга, кто автор, куда её положить, какая обложка подходит и не лежит ли она уже в библиотеке.

В раннем варианте акцент был на самой загрузке файла или ZIP-архива и потоковом NDJSON-прогрессе. Сейчас модель стала устойчивее: долгие операции оформлены как persistent jobs, их состояние хранится на backend, а админка показывает прогресс через polling.

Для реальной коллекции это важное изменение. Импорт может зависеть от распаковки, внешнего поиска, извлечения метаданных, LLM, обложек, дублей и категорий. Такой процесс не должен ломаться только потому, что вкладка обновилась или соединение оборвалось: книга должна либо попасть в каталог понятной карточкой, либо оставить после себя внятную причину ошибки.

Одиночный импорт

Одиночная загрузка остаётся базовым сценарием: администратор выбирает файл, при необходимости вводит название, автора, описание, категорию и обложку. Если часть данных не заполнена, backend пытается подготовить черновые метаданные автоматически, чтобы карточка не начиналась с пустого экрана и имени файла вместо книги.

Теперь это не просто синхронная обработка формы. Долгий импорт можно запускать как durable job: backend сохраняет задачу, обновляет статус, фиксирует ошибки и даёт админке возможность следить за выполнением. Пользователь видит не подвисшую кнопку, а понятное состояние операции: книга ещё в очереди, обрабатывается, завершилась или требует внимания.

Внутри импорта остаются привычные шаги:

  • принять и проверить файл;
  • извлечь метаданные, если формат это позволяет;
  • подготовить черновые title, author и description;
  • сопоставить категорию с деревом;
  • найти или заменить обложку;
  • сохранить запись в SQLite и файл в uploads;
  • показать результат или причину ошибки.

Batch ZIP без привязки к одному запросу

Пакетный импорт нужен, когда библиотека растёт не по одной книге, а целыми папками и архивами. Раньше его удобно было описывать через NDJSON stream: backend распаковывает архив и пишет прогресс в ответ. Для первого рабочего варианта это нормально, но у такого подхода есть слабое место — он держится на одном HTTP-соединении.

Актуальная модель лучше подходит для долгой операции:

  1. администратор загружает ZIP;
  2. backend создаёт import job;
  3. задача проходит по файлам внутри архива;
  4. для каждого файла фиксируется результат;
  5. админка получает статус и события через polling;
  6. ошибки можно разобрать после завершения, а не ловить в потерянном потоке.

Такой подход спокойнее для больших архивов. Он позволяет показать queued/running/failed/completed состояние, сохранить отчёт и не смешивать транспортный формат ответа с жизненным циклом импорта. Для администратора это превращает импорт из «надеюсь, вкладка доживёт» в наблюдаемую задачу, к которой можно вернуться.

Дубли и спорные совпадения

При импорте книг быстро появляется проблема дублей. Один и тот же текст может лежать в разных форматах, с другим именем файла, неполным автором или слегка отличающимся названием. Для читателя это выглядит как шум в выдаче, а для администратора — как каталог, который приходится чистить вручную. Поэтому импорт не должен бездумно создавать новую запись на каждый файл.

В проекте нужен и частично уже заложен более осторожный сценарий:

  • сравнивать title, author, формат и признаки файла;
  • показывать администратору возможный конфликт;
  • пропускать очевидные дубли;
  • сохранять отчёт о том, почему книга не была добавлена;
  • давать возможность ручного решения для спорных случаев.

Это не самая эффектная часть каталога, но именно она отличает рабочую библиотеку от папки с автоматическим скриптом: система не просто складывает файлы, а помогает сохранить порядок.

Дерево категорий и category matcher

Категории стали деревом, и именно они помогают читателю не потеряться в коллекции. Поэтому импорт больше не может относиться к категории как к одной строке из плоского списка. Если LLM или внешний источник предлагает «История», «История России» или «Научпоп / История», backend должен сопоставить это с уже существующей структурой, а не плодить рядом почти одинаковые полки.

Category matcher нужен, чтобы:

  • не создавать лишние почти одинаковые ветки;
  • выбирать существующую категорию, если совпадение достаточно уверенное;
  • оставлять спорные варианты для ручной проверки;
  • учитывать parent/child структуру;
  • сохранять каталог читаемым после массового импорта.

Автоматическая классификация здесь полезна только как черновик. Последнее слово лучше оставить администратору, особенно если библиотека ведётся как curated-каталог: машина помогает разложить книги, но человек отвечает за смысловую навигацию.

Anna’s Archive и Flibusta

Внешний поиск вынесен в React-админку через общий SearchPage. Он объединяет Anna’s Archive и Flibusta: администратор ищет книгу, смотрит найденные варианты и запускает импорт выбранного результата.

Практический смысл этого слоя простой: меньше ручной перекладки между сайтами, файлами и формами. Но внешние источники нельзя воспринимать как безошибочную базу данных. У найденной книги может быть странное описание, не та обложка, неполные метаданные или несколько похожих вариантов. Поэтому поиск здесь не заменяет проверку, а приносит материал для осознанного выбора.

Поэтому внешний импорт должен вести себя как управляемая операция:

  • показать источник и найденные candidates;
  • дать сравнить варианты;
  • запустить импорт как job;
  • сохранить ошибку, если источник недоступен;
  • после импорта позволить поправить карточку.

Интеллектуальная замена обложек

Обложка сильно влияет на качество каталога: по ней читатель быстрее узнаёт книгу, а карточка перестаёт быть просто строкой с названием. Но автоматическая замена без просмотра часто портит карточки. Поэтому сценарий с обложками лучше строить не вокруг одной найденной картинки, а вокруг candidates.

Актуальная логика выглядит так:

  • система ищет варианты обложек;
  • администратор видит candidates;
  • можно открыть preview;
  • выбранная обложка заменяет текущую;
  • итоговый файл приводится к формату, удобному для витрины.

Такой подход медленнее, чем полностью автоматический импорт, зато он снижает количество странных карточек. Для библиотеки это важнее скорости: аккуратная обложка помогает доверять каталогу так же, как корректное название и автор.

LLM-провайдеры как настройки, а не константы

LLM используется в нескольких местах: для черновых метаданных, категорий и большого pipeline переводов. Поэтому один захардкоженный provider плохо подходит проекту: импорт должен переживать разные режимы работы, а не зависеть от одной константы в коде.

Провайдеры стали runtime-сущностью в SQLite. Их можно настраивать из админки: включать, менять priority, задавать fallback и использовать разные endpoint без правки кода. Это особенно важно для долгих задач, где часть запросов может упасть или уйти к другому провайдеру.

При этом LLM не считается источником истины. Она помогает подготовить черновик, но карточки, категории, обложки и переводы должны оставаться проверяемыми. В этой архитектуре интеллект полезен не как автопилот без руля, а как помощник, который ускоряет скучную подготовку данных.

Как карточка попадает в публичный каталог

После успешного импорта backend сохраняет запись в SQLite, кладёт файл книги в uploads и связывает книгу с категорией. В этот момент загруженный файл становится частью библиотеки: у него появляется место в дереве, обложка, описание и технические признаки, по которым его можно показать читателю. Публичная витрина получает уже подготовленную карточку через public API.

Для страницы категории API отдаёт книги постранично. Вместе со списком возвращаются признаки пагинации: limit, offset, hasMore, total. Это важно для больших категорий и для Quartz-интеграции, где не всегда нужно забирать весь каталог как живой API-запрос.

Публично показываются только данные, пригодные для читателя: карточка, обложка, описание, формат, опубликованные переводы. Черновые задачи, ошибки импорта, candidates и непроверенные artifacts остаются в админке, потому что витрина должна быть спокойной и понятной, а не похожей на журнал сборки.

Что стоит держать под контролем

У слоя импорта есть несколько постоянных рисков. Они не видны читателю напрямую, но именно от них зависит, будет ли каталог выглядеть надёжным после десятков и сотен добавлений:

  • ZIP-пути и временные директории;
  • проверка расширений, MIME и реального содержимого;
  • очистка временных файлов после ошибок;
  • поведение внешних источников и лимиты запросов;
  • ложные дубли и пропущенные совпадения;
  • случайное разрастание дерева категорий;
  • качество LLM-описаний и переводов;
  • безопасность скачивания внешних обложек.

Часть этих вопросов уже закрыта архитектурно через jobs, polling, sessions, CSRF/CORS и разделение public/admin API. Остальное лучше развивать постепенно: отчёты, dry-run, проверка целостности, ручное подтверждение спорных действий и хорошие журналы операций. Тогда импорт остаётся не разовой кнопкой «загрузить», а контролируемым мостом между хаотичной коллекцией файлов и каталогом, которым удобно пользоваться.