calibre
Author: Kovid Goyal
Date: 2025-07-18T03:33:48+00:00
Description: Comprehensive documentation for calibre

Навигация

- модули
- вперёд |
- Старт »
- Руководство пользователя calibre

Руководство пользователя calibre
calibre — программа управления электронной библиотекой. В ней можно читать, конвертировать и каталогизировать книги в большинстве известных форматов. Она умеет взаимодействовать со многими устройствами чтения электронных книг, выходить в Интернет и получать метаданные для ваших книг. Можно загружать газеты и для удобства превращать их в электронные книги. Программа кроссплатформенная: работает в Linux, Windows и macOS.

Итак, вы запустили calibre, что дальше? Прежде чем сделать что-либо с вашими электронными книгами, программа должна о них узнать. Перетащите несколько файлов электронных книг в calibre или нажмите кнопку «Добавить книги» и найдите те, с которыми хотите работать. Добавленные книги появятся в главном окне, примерно так:

Когда вы всласть налюбуетесь списком добавленных книг, вам, возможно, захочется прочесть одну из них. Для этого нужно преобразовать книгу в формат, понятный устройству. При первом запуске calibre откроется Мастер начальной настройки и настроит программу для вашего устройства чтения. Преобразование делается легче лёгкого. Просто выберите книгу, которую хотите преобразовать, и щёлкните по кнопке «Преобразовать книги». Проигнорируйте пока все опции и нажмите «OK». В правом нижнем углу начнёт вращаться маленький значок. Как только вращение прекратилось, преобразованная книга готова. Нажмите кнопку «Просмотр», чтобы приступить к чтению.

Если вы хотите читать книгу на своём устройстве чтения, подключите его к компьютеру, подождите, пока calibre определит его (10—20 секунд), и нажмите кнопку «Отправить на устройство». Когда значок снова перестанет вращаться, отключите устройство и погружайтесь в чтение! Если вы не преобразовали книгу на предыдущем шаге, calibre автоматически преобразует её в формат, понятный вашему устройству.

Чтобы использовать программу на более продвинутом уровне, прочтите раздел «Графический интерфейс пользователя». Станьте ещё круче и многогранней, изучив раздел «Командная строка». Также небезынтересными могут оказаться ответы на «Часто задаваемые вопросы».

Если у вас ещё остались вопросы, или вы хотите обсудить calibre с другими пользователями или попросить помощи с конкретными вещами, есть :вебсайт:`форумы и другие справочные ресурсы <help>`

Разделы

- Графический интерфейс пользователя
- Добавление любимых новостных веб-сайтов
- Просмотрщик электронных книг
- Конвертирование книг
- Редактирование книг
- Контент-сервер calibre
- Сравнение книг
- Редактирование метаданных книги
- Часто задаваемые вопросы
- Руководства
- Схема URL-адресов calibre://
- Настройка calibre
- Командная строка
- Настройка среды разработки calibre

- Технические средства защиты авторских прав (DRM — digital rights management)

- Глоссарий

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Графический интерфейс пользователя

Графический интерфейс пользователя

Графический интерфейс пользователя (GUI) обеспечивает доступ ко всем функциям управления библиотекой и преобразования форматов электронных книг. Основная работа в calibre начинается с добавления в библиотеку книг с вашего жёсткого диска. Программа автоматически попытается прочитать метаданные из книг и добавить их во внутреннюю базу данных. Когда они окажутся в базе данных, с ними можно выполнять различные: ref:actions, включающие преобразование из одного формата в другой, отправку на устройство чтения, просмотр на компьютере и редактирование метаданных. Последнее, кроме прочего, позволяет изменять обложку, описание и теги. Обратите внимание, что calibre создаёт копии добавляемых файлов, а исходные файлы остаются нетронутыми.

Интерфейс разделён на различные секции:

- Действия

- Параметры

- Каталоги

- Поиск и сортировка

- Интерфейс поиска

- Сохранение поисковых строк

- Поиск по полному тексту всех книг

- Виртуальные библиотеки

- Временно пометить книги

- Предположение метаданных на основе имён файлов

- Описание книги

- Браузер тегов

- Сетка обложек

- Браузер обложек

- Добавление примечаний для авторов, серий и т.д.

- Быстрый просмотр

- Задания

- Сочетания клавиш

Действия

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

- Добавить книги

- Редактировать метаданные

- Преобразовать книги

- Просмотр

- Отправить на устройство

- Собрать новости

- Библиотека

- Устройство

- Сохранить на диск

- Подключить/поделиться

- Удалить книги

Добавить книги

Действие Добавить книги имеет семь вариантов, доступных по щелчку на нём правой кнопкой мыши.

1. Добавить книги из одной папки: открывает диалоговое окно выбора файлов и позволяет указать, какие книги в папке следует добавить. Это действие зависит от контекста, т. е. зависит от того, какой catalog вы выбрали. Если вы выбрали Библиотека, книги будут добавлены в библиотеку. Если вы выбрали устройство для чтения электронных книг, книги будут загружены на устройство и так далее.

2. Добавить книги из папок и подпапок: позволяет выбрать папку. Папка и все ее подпапки сканируются рекурсивно, и все найденные электронные книги добавляются в библиотеку. Вы можете выбрать, будет ли calibre добавлять все файлы, находящиеся в одной папке, к одной записи книги или к нескольким записям книги. calibre предполагает, что каждая папка содержит одну книгу. Предполагается, что все файлы электронных книг в папке являются одной и той же книгой в разных форматах. Это действие является обратным действию Сохранить на диск, то есть вы можете Сохранить на диск, удалять книги и повторно добавлять их в режиме отдельной книги для каждой папки без потери информации, за исключением дату (предполагается, что вы не изменили никаких настроек для действия «Сохранить на диск»).

3. Добавить несколько книг из архива (ZIP/RAR): позволяет добавить несколько электронных книг, хранящихся в выбранных файлах ZIP или RAR. Это удобный ярлык, который позволяет избежать необходимости распаковывать архив, а затем добавлять книги с помощью одного из вышеуказанных вариантов.

4. Добавить пустую книгу (запись о книге без форматов): позволяет создать пустую книгу. Полезно, чтобы вручную заполнить информацию о книге, которой, возможно, ещё нет в вашей коллекции.

5. Добавить по ISBN: позволяет добавить одну или несколько книг, введя их ISBN.

6. Добавить файлы к выбранным записям книг: позволяет добавить или обновить файлы, связанные с существующей книгой в библиотеке.

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

8. Добавить пустой файл к выбранным записям книг: позволяет добавить пустой файл указанного формата к выбранным записям книг.

Действие Добавить книги может читать метаданные из самых разных форматов электронных книг. Кроме того, оно пытается угадать метаданные из имени файла. См. Предположение метаданных на основе имён файлов, чтобы узнать, как это настроить.

Чтобы добавить дополнительный формат для существующей книги вы можете сделать любую из трёх вещей:

1. Перетащите файл на панель «Описание книги» в правой части главного окна

2. Щёлкните правой кнопкой мыши по значку Добавить книги и выберите Добавить файлы к выбранным записям книг.

3. Нажмите Добавить книги в верхней правой области диалогового окна Редактирование метаданных, доступного через действие Редактировать метаданные.

Редактировать метаданные

Действие Редактировать метаданные имеет четыре варианта, которые доступны по правому щелчку мышью на кнопке.

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

2. Редактировать метаданные массово: позволяет редактировать общие поля метаданных для большого количества книг одновременно. Работает со всеми книгами, выделенными в библиотеке.

3. Загрузить метаданные и обложки: загружает метаданные и обложки (если доступны) для выбранных в списке книг.

4. Объединить записи книг: позволяет объединить метаданные и форматы двух и более записей книг. Вы можете как удалить, так и сохранить записи, нажатые не первыми.

5. Управление файлами данных: управление дополнительными файлами данных, связанными с выбранными книгами.

Подробнее смотрите Редактирование метаданных книги.

Преобразовать книги

Электронные книги можно преобразовать из нескольких форматов в любой предпочтительный для вашего устройства чтения. Многие электронные книги, доступные для покупки, будут защищены технологией Digital Rights Management (DRM). Такие книги calibre конвертировать не будет. Из многих форматов DRM легко удалить, однако это может быть незаконно, и вам придётся самостоятельно найти инструменты, чтобы освободить свои книги, а затем использовать calibre для их преобразования.

Для большинства людей преобразование — дело одного щелчка мыши. Если вы хотите узнать больше о процессе преобразования, смотрите Конвертирование книг.

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

1. Индивидуальное преобразование: позволяет указать параметры для настройки преобразования каждой выбранной электронной книги.

2. Общее преобразование: позволяет однократно указать параметры для преобразования нескольких электронных книг сразу.

3. Создать каталог книг в вашей библиотеке calibre: позволяет создать полный список книг в вашей библиотеке, включая все метаданные, в нескольких форматах, таких как XML, CSV, BiBTeX, EPUB и MOBI. Каталог будет содержать все книги, которые в данный момент отображены в списке библиотеки. Это позволяет использовать возможности поиска, чтобы ограничить количество книг, подлежащих каталогизации. Кроме того, если вы выделите несколько книг с помощью мыши, в каталог будут добавлены только они. Если вы создаёте каталог в формате электронной книги, таком как EPUB, MOBI или AZW3, при следующем подключении вашей читалки он будет автоматически отправлен на устройство. Подробнее о работе каталогов читайте в Создание AZW3 • EPUB • MOBI каталогов.

Просмотр

Действие View отображает книгу в программе просмотра электронных книг. calibre имеет встроенную программу просмотра многих форматов электронных книг. Для других форматов используется приложение операционной системы по умолчанию. Вы можете настроить, какие форматы должны открываться во внутреннем средстве просмотра через Preferences-Interface-Behavior. Если книга имеет более одного формата, вы можете просмотреть определенный формат, щелкнув кнопку правой кнопкой мыши.

Отправить на устройство

Действие Отправить на устройство имеет восемь вариантов, которые доступны по правому щелчку мышью на кнопке.

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

2. Отправить на карту памяти A: выбранные книги переносятся на карту памяти A на устройстве чтения электронных книг.

3. Отправить на карту памяти B: выбранные книги переносятся на карту памяти B на устройстве чтения электронных книг.

4. Отправить определённый формат…: выделенные книги переносятся в выбранное место хранения на устройстве в указанном формате.

5. Извлечь устройство: отсоединяет устройство от calibre.

6. Установить стандартное действие отправки на устройство: позволяет задать, какая из настроек, с 1 по 5 выше или 7 ниже, будет стандартным действием при нажатии основной кнопки.

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

8. Извлечь заметки (экспериментально): переносит заметки, которые вы, возможно, сделали в электронной книге на своём устройстве, в метаданные комментариев к книге в библиотеке calibre.

Вы можете управлять именем файла и структурой папок файлов, отправленных на устройство, настроив шаблон в Параметры → Импорт-экспорт → Передача книг на устройства. См. также «Язык шаблонов calibre».

Собрать новости

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

Действие Собрать новости использует простые рецепты (10—15 строк кода) для каждого сайта новостей. Чтобы узнать, как создавать рецепты для ваших источников новостей, смотрите «Добавление любимых новостных веб-сайтов».

Действие Получить новости имеет три варианта, доступные по правому щелчку мышью на кнопке.

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

2. Добавить источник новостей: позволяет создать простой рецепт загрузки новостей с произвольного новостного сайта, к которому вы бы хотели обращаться. Создание рецепта - просто указание URL-адреса RSS-ленты новостей или написание кода на Python. Подробнее смотрите Добавление любимых новостных веб-сайтов.

3. Загрузить все запланированные новостные источники: заставляет calibre немедленно начать загрузку всех запланированных источников новостей.

Библиотека

Действие Библиотека позволяет создавать, переключаться, переименовывать или удалять библиотеки. calibre позволяет создать сколько угодно библиотек. Например, художественную, научно-популярную, на иностранных языках, библиотеку проекта или любую структуру, соответствующую вашим потребностям. Библиотеки — организационная структура высшего порядка в calibre. В каждой библиотеке свой набор книг, тегов, категорий и основное место хранения.

1. Переключить/создать библиотеку: позволяет: а) подключиться к уже существующей библиотеке calibre в другом месте, б) создать пустую библиотеку на новом месте или в) переместить текущую библиотеку на указанное новое место.

2. Быстрое переключение: позволяет переключаться между библиотеками, зарегистрированными или созданными в calibre.

3. Переименовать библиотеку: позволяет переименовать библиотеку.

4. Выбрать случайную книгу: выбирает для вас случайную книгу из библиотеки

5. Удалить библиотеку: позволяет отменить регистрацию библиотеки в calibre.

6. Экспорт/импорт всех данных calibre: позволяет либо экспортировать данные calibre для переноса на новый компьютер, либо импортировать экспортированные ранее данные.

7. <library name>: действия 7, 8 и т. д. дают возможность мгновенно переключаться между несколькими созданными или присоединёнными библиотеками. В этом списке только 5 наиболее часто используемых библиотек. Полный список см. в меню «Быстрое переключение».

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

Примечание

Метаданные о ваших электронных книгах, например название, автор и теги, хранятся в одном файле в папке библиотеки calibre под названием metadata.db. Если этот файл повреждён (очень редкое событие), вы можете потерять метаданные. К счастью, calibre автоматически резервирует метаданные для каждой отдельной книги в их папках в виде файла OPF. С помощью вышеописанного действия «Восстановить базу данных» из «Обслуживания библиотеки», вы можете настроить восстановление файла metadata.db из отдельных файлов OPF.

Вы можете копировать или перемещать книги между разными библиотеками (если у вас настроено более одной), нажав правой кнопкой мыши на книгу и выбрав действие Копировать в библиотеку.

Устройство

Действие Устройство позволяет просматривать книги в основной памяти или на карте памяти вашего устройства или извлечь устройство (отключить от calibre). Значок автоматически появляется на главной панели инструментов calibre при подключении поддерживаемого устройства. Нажав на него, вы увидите книги на вашем устройстве. Также можно перетащить книги из библиотеки calibre на этот значок, чтобы перенести их на устройство. И наоборот, вы можете перетащить книги с вашего устройства на значок библиотеки на панели инструментов, чтобы перенести их с вашего устройства в библиотеку calibre.

Сохранить на диск

Действие Сохранить на диск имеет пять вариантов, которые доступны по правому щелчку мышью на кнопке.

1. Сохранить на диск: сохраняет выбранные книги на диск, организованные в папки. Структура папок выглядит так:

Author_(sort)
Title
Book Files

Вы можете управлять именем файла и структурой папок файлов, сохранённых на диске, настроив шаблон в меню Параметры → Импорт-экспорт → Сохранение книг на диск. Также см. Язык шаблонов calibre.

2. Сохранить на диск в одной папке: сохраняет выбранные книги на диск в одной папке.

1 и 2 пункты сохраняют все доступные форматы и метаданные на диск для каждой выбранной книги. Метаданные хранятся в файле OPF. Сохранённые книги можно импортировать в библиотеку ещё раз без потери какой-либо информации с помощью действия Добавить книги.

3. Сохранить только *<your preferred>* формат на диск: сохраняет выбранные книги на диск в структуре папок, как показано в (1.), но только в предпочитаемом вами формате электронной книги. Вы можете установить предпочтительный формат в Preferences-Interface-Behavior-Preferred output format

4. Сохранить только *<your preferred>* формат на диск в одной папке: сохраняет выбранные книги на диск в одной папке, но только в предпочитаемом вами формате электронной книги. Вы можете установить предпочтительный формат в Preferences-Interface-Behavior-Preferred output format

5. Сохранить один формат на диск…: сохраняет выбранные книги на диск в структуре папок, как показано в (1.), но только в формате, который вы выбираете из всплывающего списка.

Подключить/поделиться

Действие Подключить/поделиться позволяет вручную подключиться к устройству или папке на компьютере, а также настроить доступ к вашей библиотеке calibre через веб-браузер или электронную почту.

Действие Подключить/поделиться имеет четыре варианта, которые доступны по правому щелчку мышью по кнопке.

1. Подключиться к папке: позволяет подключиться к любой папке на компьютере как к устройству и использовать в ней все возможности calibre для устройств. Полезно, если ваше устройство не поддерживается calibre, но доступно как диск USB.

2. Запустить контент-сервер: запускает встроенный веб-сервер calibre. Когда он запущен, ваша библиотека calibre доступна через веб-браузер из интернета (по вашему выбору). Вы можете настроить способ доступа к веб-серверу, установив настройки в меню Параметры → Общий доступ к файлам → Сетевой доступ

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

Удалить книги

Действие Удалить книги безвозвратно удаляет книги, поэтому используйте его с осторожностью. Оно зависит от контекста, т. е. от выбранного каталога. Если вы выбрали Библиотека, книги будут удалены из библиотеки. Если вы выбрали устройство для чтения электронных книг, книги будут удалены с устройства. Чтобы удалить только определённый формат для данной книги, используйте действие Редактировать метаданные. Удаление книг также имеет пять вариантов, к которым можно получить доступ правым щелчком по кнопке.

1. Удалить выбранные книги: позволяет вам безвозвратно удалить все книги, выделенные в списке книг.

2. Удалить файлы определённого формата из выбранных книг…: позволяет безвозвратно удалить файлы электронных книг указанного формата из выделенных в списке книг.

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

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

5. Удалить обложки из выбранных книг: позволяет безвозвратно удалить файлы обложек из выделенных в списке книг.

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

7. Восстановить недавно удаленные: позволяет отменить удаление книг или форматов.

Примечание

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

Параметры

Действие Параметры позволяет вам изменить различные аспекты работы calibre. Имеет четыре варианта, доступные по правому щелчку мышью по кнопке.

1. Параметры: позволяет изменить различные аспекты работы calibre. Нажатие кнопки также выполняет это действие.

2. Запустить мастер начальной настройки: позволяет запустить Мастер начальной настройки, который появлялся при первом запуске calibre.

3. Получить плагины для расширения calibre: открывает новое окно, которое показывает плагины для calibre. Эти плагины создаются сторонними разработчиками для расширения функциональности calibre.

4. Перезапустить в режиме отладки: позволяет включить отладочный режим, который помогает разработчикам calibre в решении проблем, с которыми вы столкнулись. Для большинства пользователей он должен оставаться выключенным, если только разработчиком не дано соответствующее указание.

Каталоги

Каталог — это коллекция книг. calibre может управлять двумя типами различных каталогов:

1. Библиотека: это коллекция книг, хранящаяся в библиотеке calibre на вашем компьютере.

2. Устройство: это коллекция книг, хранящихся на вашем устройстве для чтения электронных книг. Она становится доступной при подключении устройства к компьютеру.

Многие операции, такие как добавление книг, удаление, просмотр и т. д., зависят от контекста. Например, если вы нажмёте кнопку Просмотр, когда выбран каталог Устройство, calibre откроет файлы с устройства. Если у вас выбран каталог Библиотека, то будут открываться файлы из вашей библиотеки calibre.

Поиск и сортировка

Раздел «Поиск и сортировка» позволяет выполнять несколько эффективных действий с вашими коллекциями книг.

- Вы можете сортировать их по названию, автору, дате, оценке и т. д., нажимая на заголовки столбцов. Также можно сортировать по нескольким столбцам. Например, если вы нажмёте на столбец «Название» а затем на столбец «Автор», книги будут отсортированы по автору, а потом все записи с одинаковым автором будут отсортированы по названию.

- Вы можете искать определённую книгу или набор книг с помощью строки поиска. Подробнее об этом ниже.

- Вы можете быстро и удобно редактировать метаданные, выбрав запись, которую вы хотите изменить, в списке и нажав клавишу E.

- Вы можете выполнять Действия с наборами книг. Чтобы выделить несколько книг, нужно:

- удерживая клавишу Ctrl, нажимать на книги, которые хотите выбрать;

- удерживая клавишу Shift, нажать на первую и последнюю книги из ряда книг, который хотите выбрать.

- Вы можете настроить, какие поля будут отображаться, в диалоговом окне Параметры.

- Чтобы выполнить сложную подсортировку по нескольким столбцам, добавьте инструмент Сортировать по на панель инструментов через Настройки-Панели инструментов и меню.

Интерфейс поиска

Вы можете искать все метаданные, введя условия поиска в строку поиска. Например:

Asimov Foundation format:lrf

Это будет соответствовать всем книгам в вашей библиотеке, которые содержат в метаданных Asimov и Foundation и доступны в формате LRF. Ещё несколько примеров:

author:Asimov and not series:Foundation
title:"The Ring" or "This book is about a ring"
format:epub publisher:feedbooks.com

Виды поиска

Существует четыре вида поиска: contains, equality, regular expression (см. `регулярные выражения https://en.wikipedia.org/wiki/Regular_expression`_) и character variant. Выбор вида поиска - символом префикса.

„Contains“ поиски

Поиски без символа префикса - Содержит и по умолчанию нечувствительны к регистру. Элемент соответствует, если строка поиска появляется где-либо в указанных метаданных. Можно сделать поиск Содержит чувствительным к регистру, установив флажок Поиск с учетом регистра в Настройки/Поиск. Если отмечена опция поиска «Символы без ударения соответствуют символам с диакритическими знаками, а знаки препинания игнорируются», то символ будет соответствовать всем его вариантам (например, e соответствует é, è, ê и ë) и все знаки препинания, включая пробелы, игнорируются. Например, если установлен флажок «Символы без диакритических знаков соответствуют …», то для двух названий книг:

1. Big, Bothéred, and Bad

2. Big Bummer

то эти поиски находят:

- title:"er" совпадает с обоими („e“ совпадает с обоими „é“ и „e“).

- title:"g " соответствует обоим, т.к. пробелы игнорируются.

- title:"g," соответствует обоим, т.к. запятая игнорируется.

- title:"gb" соответствует обоим, т.к. „, „ игнорируются в книге 1, а пробелы игнорируются в книге 2.

- title:"g b" соответствует обоим, потому что запятая и пробел игнорируются.

- title:"db" соответствует #1, потому что пробел в „and Bad“ игнорируется.

- title:"," соответствует обоим (фактически соответствует всем книгам), потому что запятые игнорируются.

Если параметр Символы без диакритических знаков соответствуют … не отмечен, то варианты символов, знаки препинания и пробелы значимы.

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

„Equality“ поиски

Поиск равенства обозначается префиксом строки поиска со знаком равенства (=). Например, запрос tag:"=science" будет соответствовать science, но не science fiction или hard science. Варианты символов значимы: é не соответствует e.

Два варианта поиска равенства используются для иерархических элементов (например, A.B.C): иерархический поиск префикса и иерархический поиск компонентов. Первый, обозначенный точкой после равенства (=.), соответствует начальным частям иерархического элемента. Второй, обозначенный двумя точками после знака равенства (=..), соответствует внутреннему имени в иерархическом элементе. Примеры использования тега History.Military.WWII в качестве значения:

- tags:"=.History" : True. History префикс тега.

- tags:"=.History.Military" : True. History.Military префикс тега.

- tags:"=.History.Military.WWII" : True. History.Military.WWII префикс тега, хоть и неподходящий.

- tags:"=.Military" : False. Military не префикс тега.

- tags:"=.WWII" : False. WWII не префикс тега.

- tags:"=..History" : True. Иерархия содержит значение History.

- tags:"=..Military" : True. Иерархия содержит значение Military.

- tags:"=..WWII" : True. Иерархия содержит значение WWII.

- tags:"=..Military.WWII" : False. .. поиск ищет одиночные значения.

„Regular expression“ поиски

Поиск с использованием регулярных выражений обозначается префиксом строки поиска с помощью тильды (~). Можно использовать любое Python-совместимое регулярное выражение [https://docs.python.org/library/re.html]. Обратная косая черта, используемая для экранирования специальных символов в регулярных выражениях, должна быть удвоена, так как одиночная обратная косая черта будет удалена при синтаксическом анализе запроса. Например, чтобы сопоставить литеральную круглую скобку, вы должны ввести``\(.или, альтернативно, используйте `супер-кавычки` (см. ниже). Поиск по регулярным выражениям является поиском 'contains', если только выражение не привязано. Варианты символов значимы: ``~e не соответствует é.

„Character variant“ поиски

Поиск вариантов символов обозначается префиксом строки поиска с символом вставки (^). Этот поиск аналогичен поиску contains (выше), за исключением того, что:

- регистр букв всегда игнорируется.

- варианты символов всегда соответствуют друг другу.

- знаки препинания и пробелы всегда значимы.

Параметры поиска Символы без ударения соответствуют знакам с диакритическими знаками, а знаки препинания игнорируются и Поиск с учетом регистра игнорируются. Они не влияют на поведение этого поиска.

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

1. Big, Bothéred, and Bad

2. Big Bummer

то эти поиски варианты символов находят:

- title:"^er" соответствует обоим („e“ соответствует обоим „é“ и „e“)

- title:"^g" соответствует обоим

- title:"^g " соответствует #2, т.к. пробел значим

- title:"^g," соответствует #1, т.к. запятая значима

- title:"^gb" ничего не соответствует, т.к. пробел и запятая значимы

- title:"^g b" соответствует #2, т.к. запятая значима

- title:"^db" ничего не соответствует

- title:"^," соответствует #1 (а не всем книгам), т.к. запятая значима

Синтаксис поискового выражения

«Выражение поиска» представляет собой последовательность «терминов поиска», необязательно разделенных операторами and и or. Если два поисковых запроса встречаются без разделительного оператора, подразумевается и. Оператор and имеет приоритет над оператором or; например, выражение a or b and c такое же, как a or (b and c). Вы можете использовать круглые скобки для изменения приоритета; например (a or b) and c, чтобы or оценивалось перед and. Вы можете использовать оператор not для отрицания (инвертирования) результата вычисления поискового выражения. Примеры:

- not tag:foo находит все книги, не содержащие тега foo

- not (author:Asimov or author:Weber) находит все книги, написанные не Азимовым или Вебером.

В приведенных выше примерах показаны примеры «условий поиска». Базовый «термин поиска» — это последовательность символов, не содержащая пробелов, кавычек ("), обратной косой черты (\\) или скобок (( )). Ему может предшествовать спецификатор имени столбца: имя поиска столбца, за которым следует двоеточие (:), например, author:Asimov. Если поисковый запрос должен содержать пробел, то весь термин должен быть заключен в кавычки, например, title:"The Ring". Если поисковый запрос должен содержать кавычки, они должны быть экранированы обратной косой чертой. Например, чтобы найти серию под названием The «Ball» and The «Chain», используйте:

series:"The \"Ball\" and The \"Chain\"

Если вам нужна обратная косая черта, что часто случается при поиске c регулярными выражениями, используйте две из них (\\).

Иногда бывает трудно правильно подобрать все escape-последовательности, чтобы получить желаемый результат, особенно при поиске с регулярными выражениями и по шаблону. В этих случаях используйте супер-кавычки: """sequence of characters""". Символы в суперкавычках используются без изменений: обработка экранирования не выполняется.

Дополнительная информация

Для поиска строки, которая начинается с знака равенства, тильды или знака каретки; предварите строку обратной косой чертой (backslash).

Заключайте строки поиска в кавычки («), если они содержат скобки или пробелы. Например, для поиска по тегу Science Fiction нужно будет искать tag:"=science fiction". Если вы ищете tag:=science fiction, вы найдёте все книги с тегом science, содержащие слово fiction в любых метаданных.

Вы можете легко создавать расширенные поисковые запросы с помощью окна Расширенный поиск, открывающегося нажатием кнопки .

Доступные поля для поиска: tag, title, author, publisher, series, series_index, rating, cover, comments, format, identifiers, date, pubdate, search, size, vl и пользовательские столбцы. Если устройство подключено, при поиске в библиотеке calibre становится доступно поле ondevice. Чтобы найти имя для поиска (которое на самом деле называется lookup name) для пользовательского столбца, наведите мышь на заголовок столбца в библиотеке.

Dates

Синтаксис поиска по датам:

pubdate:>2000-1 Will find all books published after Jan, 2000
date:<=2000-1-3 Will find all books added to calibre before 3 Jan, 2000
pubdate:=2009 Will find all books published in 2009

Если дата неясна, для сравнения дат используется текущая локаль. Например, в локали мм/дд/гггг 2/1/2009 интерпретируется как 1 февраля 2009 года. В локали дд/мм/гггг та же дата интерпретируется как 2 января 2009 года. Доступны некоторые специальные строки дат. Строка today переводится в сегодняшнюю дату, какая бы она ни была. Строки yesterday и thismonth (или переведённый эквивалент на текущем языке) также работают. В добавление, строка daysago (также и переведённая) может использоваться для сравнения с датой, которая была несколько дней назад. Например:

date:>10daysago
date:<=45daysago

Во избежание потенциальных проблем с переведёнными строками при использовании не английской версии calibre, строки _today, _yesterday, _thismonth, and _daysago всегда доступны. Они не переводятся.

Поиск дат и числовых значений с реляционным сравнением

Даты и числовые поля поддерживают операторы сравнения: = (равно), > (больше), >= (больше или равно), < (меньше), <= (меньше или равно) и != (не равно). Поля оценки считаются числовыми. Например, поиск rating:>=3 найдёт все книги с оценкой 3 или выше.

Вы можете искать книги, которые имеют формат определённого размера, например:

- size:>1.1M найдёт книги размером более 1,1 МБ.

- size:<=1K найдет книги с размером меньше или равным 1 КБ

Вы можете искать по числу элементов в полях с несколькими значениями, такими как теги. Этот поиск начинается с символа #, потом используйте такой же синтаксис, как и для числовых полей. Например, чтобы найти книги с пятью и более тегами используйте tags:#>4. Для поиска всех книг с 10 тегами используйте tags:#=10.

Индексы серий

Индексы серии доступны для поиска. Для стандартной серии имя для поиска - series_index. Для пользовательски столбцов серии используйте имя поиска столбца с приставкой _index. Например, для поиска индексов для пользовательского столбца серии с названием #my_series вы могли бы использовать имя #my_series_index. Индексы серии - числа, так что вы можете использовать операторы отношений, описанные выше.

Сохранённые поиски

Специальное поле search используется для saved searches. Поэтому, если вы сохраните поиск с именем «My spouse’s books», вы можете ввести search:"My spouse's books" в строке поиска для повторного использования сохранённого поиска. Подробнее о сохранении запросов ниже.

Виртуальные библиотеки

Специальное поле vl используется для поиска книг в виртуальной библиотеке. Например, vl: Read найдет все книги в виртуальной библиотеке Read. Поиск vl: Read and vl: «Science Fiction» найдет все книги, которые находятся в виртуальных библиотеках Read и Science Fiction. Значение, следующее за vl:, должно быть именем виртуальной библиотеки. Если имя виртуальной библиотеки содержит пробелы, то окружайте его кавычками.

Имеет ли поле значение

Вы можете искать по наличию или отсутствию поля, используя специальные значения «true» и «false». Например:

- cover:false находит все книги без обложки

- series:true находит все книги из серии

- series:false находит все книги, не входящие в серию

- comments:false находит все книги с пустым комментарием

- formats:false находит все книги без файлов книг (пустые записи)

Yes/no пользовательские столбцы

Пользовательские столбцы да/нет доступны для поиска. Поиск по false, empty, или blank найдёт все книги с неопределёнными значениями в столбце. Поиск по true найдёт все книги, которые не имеют неопределённых значений в этом столбце. Поиск по yes или checked найдёт все книги с Yes в этом столбце. Поиск по no или unchecked найдёт все книги с No в этом столбце. Заметьте, что слова yes, no, blank, empty, checked and unchecked переводятся; вы можете использовать как эквивалент на текущем языке, так и английское слово. Слова true и false и специальные значения _yes, _no, и _empty не переводятся.

Идентификаторы

Идентификаторы (например, ISBN, DOI, LCCN и т. д.) также используют расширенный синтаксис. Идентификатор имеет форму type:value, как в isbn:123456789. Расширенный синтаксис позволяет вам независимо указывать тип и значение для поиска. И в части типа, и в значении запроса могут использоваться search kinds. Примеры:

- identifiers:true будет искать книги с любым идентификатором.

- identifiers:false будет искать книги без идентификатора.

- identifiers:123 будет искать книги с любым типом, имеющим значение содержащее 123.

- identifiers:=123456789 будет искать книги с любым типом, имеющим значение равное 123456789.

- identifiers:=isbn: и identifiers:isbn:true найдут книги с типом, равным ISBN с любым значением

- identifiers:=isbn:false будет искать книги без эквивалентного ISBN типа.

- identifiers:=isbn:123 будет искать книги с типом эквивалентным ISBN содержащим 123.

- identifiers:=isbn:=123456789 будет искать книги с типом, эквивалентным ISBN равным 123456789.

- identifiers:i:1 будет искать книги с типом содержащим i и имеющим значение содержащее 1.

Категории видимые в Браузере тегов

Поиск in_tag_browser:true находит все книги, находящихся в категориях (теги, авторы и т. д.), отображаемых в данный момент в Браузер тегов. Это полезно, если вы установили два параметра: Настройки → Оформление → Браузер тегов  → Скрыть пустые категории (столбцы) и Найти и показать все соответствующие элементы. С этими двумя настройками выполнение Найти в Просмотр тегов показывает только категории, содержащие элементы, соответствующие Найти. Затем поиск in_tag_browser:true дополнительно находит книги с этими категориями/элементами. Вы можете легко запустить этот поиск, нажав клавишу Ctrl+Alt+Shift+F или щелкнув кнопку настройки в Браузер тегов и выбрав пункт Показывать только книги с видимыми категориями .

Поиск по шаблонам

Вы можете искать, используя шаблон в Язык шаблонов calibre вместо поля метаданных. Для этого вы вводите шаблон, тип поиска и значение для поиска. Синтаксис:

template: (the template) #@#: (search type) : (the value)

template — это любой допустимый шаблон на языке шаблонов calibre. search type должен быть одним из t (текстовый поиск), d (поиск по дате), n (числовой поиск) или b (set/not set (логическое значение)). value может быть любым, которое вы хотите, и вы можете использовать search kinds, описанные выше для различных типов поиска. Вы должны обрамить кавычками всю строку поиска, если в ней есть пробелы.

Примеры:

- template:"program: connected_device_name('main')#@#:t:kindle" – true когда подключено устройство kindle.

- template:"program: select(formats_sizes(), 'EPUB')#@#:n:>1000000" – найти книги с файлами EPUB размером более 1 МБ.

- template:"program: select(formats_modtimes('iso'), 'EPUB')#@#:d:>10daysago" – найти книги с EPUB файлами новее чем 10 дней тому назад

- template:'''program: book_count('tags:^'' & $series & ''', 0) != 0#@#:n:1''' – находит все книги, содержащие серию имя в тегах. В этом примере используются суперкавычки, так как шаблон использует как одинарные ('), так и двойные кавычки (") при построении выражения поиска.

Вы можете легко создавать шаблоны поисковых запросов с помощью диалога Расширенный поиск, нажав кнопку . Можно протестировать шаблоны на определённых книгах, используя Тестер шаблонов calibre. Его можно добавить на панели инструментов или в меню через Параметры → Панели инструментов и меню. Ему также можно назначить сочетание клавиш через Параметры → Комбинации клавиш.

Диалог расширенного поиска

Диалоговое окно Расширенный поиск

Сохранение поисковых строк
calibre позволяет вам сохранить часто используемый поиск под специальным именем, а затем повторно использовать этот поиск одним щелчком мыши. Для этого создайте свой поиск, набрав его на панели поиска или с помощью браузера тегов. Затем введите имя, которое вы хотели бы дать поиску в поле «Сохраненные поиски» рядом с панелью поиска. Нажмите значок «плюс» рядом с сохраненным полем поиска, чтобы сохранить поиск.

Теперь вам доступен сохранённый поиск в браузере тегов в разделе «Сохранённые поиски». Один щелчок позволит с лёгкостью использовать любой произвольно сложный запрос без необходимости его пересоздавать.

Поиск по полному тексту всех книг
calibre может опционально индексировать полный текст книг в библиотеке, чтобы обеспечить мгновенный поиск слов внутри любой книги. Чтобы использовать эту функцию, нажмите кнопку FT у левого края строки поиска.

Затем включите индексирование для текущей библиотеки. После завершения индексации вы можете искать весь текст в полной библиотеке. Когда вы добавляете новые книги в библиотеку, они автоматически индексируются в фоновом режиме. Этот поиск позволяет быстро найти все книги, содержащие слово или комбинацию слов. Вы даже можете искать слова, которые встречаются рядом с другими словами, как показано в примерах во всплывающем окне поиска. Обратите внимание, что этот инструмент поиска найдет только одно вхождение поискового запроса в конкретной книге, а не список всех вхождений, для этого лучше всего искать внутри книги с помощью calibre E-book viewer.

Вы можете переиндексировать отдельную книгу, щёлкнув правой кнопкой мыши на панели сведений о книге в calibre и выбрав Переиндексировать эту книгу для полнотекстового поиска

Виртуальные библиотеки

Виртуальная библиотека - это способ притвориться, что в вашей библиотеке calibre всего несколько книг вместо полной коллекции. Это отличный способ разбить вашу большую коллекцию книг на более мелкие, управляемые куски. Чтобы узнать, как создавать и использовать виртуальные библиотеки, см. Учебное пособие: Виртуальные библиотеки.

Временно пометить книги

Вы можете временно отмечать произвольные наборы книг. Отмеченные книги будут отмечены булавкой, и их можно будет найти с помощью поиска marked:true. Чтобы отметить книгу, нажмите Ctrl+M или перейдите в Настройки-Панели инструментов и меню и добавьте кнопку Отметить книги на главную панель инструментов.

Вы можете пометить книги определенной текстовой меткой, щелкнув правой кнопкой мыши кнопку Отметить книги и выбрав Пометить книги текстовой меткой. Книги, отмеченные текстовыми метками, позже можно будет найти с помощью поиска marked:"=the-text-you-entered".

Предположение метаданных на основе имён файлов

Обычно calibre считывает метаданные из файла книги. Однако вместо этого программу можно настроить для чтения метаданных из имени файла через Параметры → Импорт-экспорт → Добавление книг → Считывать метаданные из содержимого файла.

Вы также можете управлять чтением метаданных из имени файла с помощью регулярных выражений (см. Всё об использования регулярных выражений в calibre). В разделе Добавление книг диалогового окна конфигурации вы можете указать регулярное выражение, которое calibre будет использовать, чтобы попытаться угадать метаданные из имен файлов электронных книг, которые вы добавляете в библиотеку. Регулярное выражение по умолчанию:

title - author

то есть предполагается, что все символы до первого «-» (дефиса) — название книги, а все последующие — автор. Например, имя файла:

Foundation and Earth - Isaac Asimov.txt

будет интерпретировано как название «Foundation and Earth» и автор «Isaac Asimov».

Совет

Если в имени файла нет дефиса, вышеуказанное регулярное выражение не сработает.

Описание книги

Панель «Описание книги» показывает обложку и все метаданные выделенной в данный момент книги. Её можно скрыть с помощью кнопки Макет в правом нижнем углу главного окна calibre. Имена авторов на панели описания книги кликабельны, и по умолчанию ведут на страницу автора в Википедии. Также их можно настроить, щёлкнув правой кнопкой мыши имя автора и выбрав «Управление этим автором».

Аналогично, если вы загружаете метаданные, панель описания книги автоматически покажет ссылки на веб-страницы Amazon, Worldcat и т. д., с которых они загружены.

Вы можете щёлкнуть правой кнопкой мыши по отдельным форматам на панели описания книги, чтобы удалить их, сравнить с оригинальными версиями, сохранить на диск, открыть с помощью внешней программы и т. д.

Вы можете изменить обложку книги, просто перетащив изображение на панель описания книги. Если вы хотите отредактировать обложку во внешней программе, просто щёлкните её правой кнопкой мыши и выберите Открыть с помощью….

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

Двойной щелчок на панели «Описание книги» откроет её в отдельном всплывающем окне.

Наконец, вы можете точно настроить, какая информация отображается на панели описания книги через Параметры → Интерфейс → Оформление → Описание книги.

Браузер тегов

Браузер тегов позволяет легко просматривать вашу коллекцию по автору/тегам/сериям/ и т. д. Если вы нажмёте любой элемент в браузере тегов, например, имя автора «Айзек Азимов», то список книг справа покажет только книги этого автора. Также можно нажимать на названия категорий. Например, нажатие «Серии» покажет вам все книги любых серий.

Первое нажатие на элемент ограничит список книг теми, которые содержат или соответствуют элементу. Продолжая вышеописанный пример, щёлкнув «Айзек Азимов», вы увидите книги этого автора. При повторном нажатии список изменится в зависимости от того, имеются ли дочерние элементы (см. подкатегории и иерархические элементы ниже). Вернёмся к примеру с Азимовым: повторное нажатие на «Айзек Азимов» ограничит список книгами, не принадлежащими Айзеку Азимову. Третье нажатие снимет ограничение, показав все книги. Если вы, удерживая клавиши Ctrl или Shift, нажимаете на несколько элементов, то создаются ограничения на основе нескольких элементов. Например, вы можете удерживать Ctrl и нажимать на теги «история» и «Европа» для поиска книг по истории Европы. Браузер тегов работает, создавая выражения поиска, которые автоматически вводятся в строку поиска. Наблюдать за тем, что генерирует браузер тегов — хороший способ научиться строить основные поисковые выражения.

Значки элементов в браузере тегов частично окрашены. Количество цвета зависит от средней оценки книг в категории. Так, например, если у книг Айзека Азимова в среднем четыре звезды, то его значок в браузере тегов будет окрашен на 4/5. Вы можете увидеть среднюю оценку, наведя указатель мыши на значок.

Элементы внешнего уровня в Tag browser „, такие как Авторы и Серии, называются категориями. Вы можете создать свои собственные категории, называемые guilabel :`Категории пользователей, это удобно для организации элементов. Например, можно использовать :guilabel: «Редактор категорий пользователей» (нажмите кнопку «guilabel:Configure» в левом нижнем углу :guilabel:«Браузер тегов» и выберите :guilabel:«Управление авторами, сериями, и т.д. -> Категории пользователей) для создания категории пользователей под названием «Любимые авторы», а затем поместите элементы для избранных в категорию. Категории пользователей могут иметь подкатегории. Например, категория пользователя Favorites.Authors` является подкатегорией ``Favorites. У вас также может быть Favorites.Series, и в этом случае под Favorites будут две подкатегории. Подкатегории можно создать, щелкнув правой кнопкой мыши по категории «Пользователь», выбрав :guilabel: «Добавить подкатегорию в …» и ввести имя подкатегории; Или с помощью :guilabel: «Редактор категорий пользователей», введя имена, подобные приведенному выше.

Вы можете искать категории пользователей так же, как и встроенные категории, нажав на них. Вы можете выполнить четыре разных запроса, нажав:

1. «всё соответствующее элементу в этой категории» отмечено одним зелёным знаком плюс.

2. «всё соответствующее элементу в этой категории или подкатегориях» отмечено двумя зелёными знаками минус.

3. «всё несоответствующее элементу в этой категории» показывается одним красным знаком минус.

4. «всё несоответсвующее элементу в категории или подкатегориях» показано двумя красными знаками минус.

Можно также создать иерархии внутри некоторых текстовых категорий, таких как теги, серии и пользовательские столбцы. Эти иерархии показываются с маленьким треугольником, позволяя скрыть подпункты. Чтобы использовать иерархии элементов в категории, вы должны сначала перейти к Настройки → Интерфейс → Look & feel и ввести названия категории в поле «Категории с иерархическими элементами». Как только это будет сделано, элементы в этой категории, которые содержат точки, будут показаны с помощью маленького треугольника. Например, предположим, что вы создали пользовательский столбец с именем «Жанр» и указали, что он содержит иерархические элементы. После этого такие элементы, как Mystery.Thriller и Mystery.English, будут отображаться как Mystery с небольшим треугольником рядом с ним. Нажатие на треугольник покажет триллер и английский в качестве подпунктов. Смотрите Управление подгруппами книг, например «жанр» для получения дополнительной информации.

Иерархические элементы (элементы с потомками) используют те же четыре «щелчка», что и категории пользователей. Элементы, у которых нет потомков, используют два запроса: «всё совпадающее» и «всё не совпадающее».

Вы можете перетаскивать элементы в браузере тегов в Пользовательские категории, чтобы добавить их в эту категорию. Если источник - категория пользователя, с удержанием клавиши shift при перетаскивании элемент будет перемещён в новую категорию. Вы также можете перетаскивать книги из списка книг на элементы в браузере тегов; Удаление книги по элементу - элемент автоматически применяется к перетащенным книгам. Например, перетаскивание книги на имя Исаака Азимова -> автор этой книги - Исаак Асимов. Перетаскивание её на тег History добавит тег History в теги книги.

Вы можете легко найти любой элемент в браузере тегов, нажав кнопку поиска в правом нижнем углу. Кроме того, вы можете щёлкнуть правой кнопкой мыши по любому элементу и выбрать одну из нескольких операций. В некоторых примерах можно скрыть его, переименовать или открыть диалоговое окно «Управление x», которое позволяет вам управлять элементами такого типа. Например, диалоговое окно Управление авторами позволяет вам переименовывать авторов и управлять сортировкой их имен.

Вы можете управлять сортировкой элементов в браузере тегов с помощью кнопки Настроить в его левом нижнем углу. Там можно выбрать сортировку по названию, средней оценке или популярности (популярность — это количество книг с тем или иным элементом в вашей библиотеке, например, популярность Айзека Азимова — это количество книг Айзека Азимова в вашей библиотеке).

Вы можете использовать свои собственные значки для категорий и значений в категориях. Чтобы изменить значок категории, щелкните правой кнопкой мыши по категории (элемент внешнего уровня) и выберите Изменить значок категории (название категории). Откроется диалоговое окно, в котором вы можете выбрать изображение для использования в качестве значка. Чтобы восстановить значок по умолчанию, выберите „Восстановить значок по умолчанию (название категории)`.

Чтобы выбрать значки для значений в категориях, щелкните правой кнопкой мыши значение, затем выберите `Управлять значком для (название значения)“. Вы увидите список вариантов:

`Выбрать значок для этого значения, но не для его дочерних элементов“. Откроется диалоговое окно, в котором вы выберете значок для значения. Дочерние элементы этого значения не унаследуют этот значок.

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

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

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

Использовать значок по умолчанию для этого значения. Эта опция предлагается, если у элемента есть значок. Она удаляет значок из значения и всех дочерних элементов, наследующих значок. Значок по умолчанию указан ниже.

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

Использовать/редактировать шаблон, чтобы выбрать значок значения по умолчанию. Этот параметр позволяет вам предоставить шаблон calibre, который возвращает имя файла значка, который будет использоваться в качестве значка по умолчанию. Шаблон может использовать две переменные:

- category: название категории для поиска, например authors, series, #mycolumn.

- value: значение элемента внутри категории.

- count: количество книг с этим значением. Если значение является частью иерархии, то количество включает дочерние элементы.

- avg_rating: средний рейтинг для книг с этим значением. Если значение является частью иерархии, то среднее значение включает дочерние элементы.

Метаданные книги, такие как название, недоступны. Функции базы данных шаблонов, такие как : ref: ff_book_count и :ref: ff_book_values, будут работать, но производительность может быть неприемлемой. Следующие шаблонные функции будут работать в графическом интерфейсе, но не будут работать на сервере контента: connected_device_name, connected_device_uuid, current_virtual_library_name, is_marked и :ref: ff_virtual_libraries.

В графическом интерфейсе шаблоны Python имеют полный доступ к базе данных calibre. На сервере контента шаблоны Python имеют доступ к новому API (см. Документация API для интерфейса базы данных [https://manual.calibre-ebook.com/db_api.html]), но не к старому API (LibraryDatabase).

Например, этот шаблон указывает, что любое значение в категории, на которую нажимают, начинающееся с History, будет иметь значок с именем flower.png:

program:
if substr($value, 0, 7) == 'History' then 'flower.png' fi

Если шаблон возвращает пустую строку (''), то будет использоваться значок категории. Если шаблон возвращает имя файла, которое не существует, то значок не отображается.

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

Значок выбирается с использованием следующей иерархии:

1. Значок, указанный для значения, если оно существует.

2. Значок, указанный для родительского узла, найденного путем перемещения вверх по дереву, если таковой существует.

3. Значок из шаблона, если шаблон существует и он возвращает непустую строку.

4. Значок категории по умолчанию, который существует всегда.

Значки для каждого пользователя, а не для каждой библиотеки, хранятся в папке конфигурации calibre. Значки для значений элементов хранятся во вложенной папке tb_icons. Значки, используемые шаблонами, находятся во вложенной папке template_icons из tb_icons.

Сетка обложек

Вы можете отображать сетку обложек книг, а не список книг, если предпочитаете просматривать свою коллекцию в виде обложек Опция Сетка обложек активируется нажатием кнопки Макет в нижнем правом углу основного окна. Вы можете настроить размеры обложки и фон Cover grid через Настройки →  Интерфейс →  Внешний вид →  Сетка обложек. Вы можете даже отображать любое указанное поле под обложками, например, в заголовке или авторах или в рейтинге, или в специальном столбце, который вы можете создать.

Браузер обложек

В дополнение к Сетке обложек, описанной выше, вы можете также отображать обложки в одной строке. Это активируется с помощью кнопки Макет в нижнем правом углу главного окна. В меню Параметры → Интерфейс → Оформление → Браузер обложек вы можете изменить количество отображаемых обложек и даже отобразить Браузер обложек в отдельном всплывающем окне.

Добавление примечаний для авторов, серий и т.д.

Вы можете добавить примечания для автора/серии/тега/издателя и т.д. в вашу библиотеку calibre. Для этого щелкните правой кнопкой мыши на имени автора в Браузере тегов слева или на панели Описание книги справа и выберите Создать примечание или Редактировать примечание.

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

Вы можете искать все примечания в вашей библиотеке с помощью инструмента «Просмотр примечаний», нажав Ctrl+Shift+N или добавив его на панель инструментов через Настройки → Панели инструментов и меню.

Быстрый просмотр

Иногда вы хотите выбрать книгу и быстро получить список книг с одинаковым значением в какой-либо категории (авторы, теги, издатель, серия и т.д.) в качестве текущей выбранной книги, но не изменяя текущего представления библиотеки. Вы можете сделать это с помощью Quickview. Quickview открывает либо второе окно, либо панель в списке книг, отображающую список интересующих книг. Предположим, что вы хотите просмотреть список всех книг с одним или несколькими авторами выбранной в данный момент книги. Нажмите интересующую вас ячейку автора и нажмите клавишу «Q» или щелкните значок Quickview в разделе Layout окна calibre. Окно или панель откроются со всеми авторами для этой книги слева, а все книги выбранного автора справа.

Некоторые примеры использования Быстрого просмотра: быстро посмотреть, что другие книги:

- имеют некоторые теги, применяемые к текущей выбранной книге,

- в той же серии, что и выбранная книга

- имеют те же значения в пользовательском столбце, как и текущая книга

- написаны одним из тех же авторов, что и текущая книга

- разделяемые значения в настраиваемом столбце

Существует два варианта отображения информации Quickview:

1. Он может открыть «отстыкован»: поверх окна calibre и останется открытым, пока вы явно не закроете его.

2. Он может «пристыкован»: как панель в разделе списка книг главного окна калибра.

Вы можете переместить это окно из закреплённого в незакреплённое по желанию, используя кнопку «Закрепить/Открепить»

Панель Quickview может быть оставлена открытой навсегда, и в этом случае она следует за движениями в списке книг. Например, если вы щелкнете в представлении библиотеки calibre в столбце категории (теги, серии, издателя, авторов и т.д.) для книги, содержимое окна Quickview изменится, чтобы показать вам в левой боковой панели значения в этой категории для выбранной книги (например, теги для этой книги). Первый элемент в этом списке будет выбран, а Quickview покажет вам справа все книги в вашей библиотеке, использующие это значение. Нажмите на другое значение в левой панели, чтобы увидеть книги с этим другим значением.

Дважды щелкните по книге в окне Quickview, чтобы выбрать эту книгу в представлении библиотеки. Это также изменит отображение элементов в окне QuickView (левая панель), чтобы отобразить элементы в недавно выбранной книге.

Shift- или Ctrl- дважды щёлкните книгу в окне быстрого просмотра, чтобы открыть диалог редактирования её метаданных в окне calibre. Отредактированная книга будет «быстро просмотрена» после закрытия диалога редактирования метаданных.

Вы можете увидеть, можно ли посмотреть столбец в Быстром просмотре, по подсказке, когда проводите мышью по заголовку столбца. Правым кликом мыши на заголовке столбца вы можете увидеть настройку «Быстрый просмотре» в показанном меню, таким образом будет ясно, в каких случаях выбор этой настройки эквивалентен нажатию „Q“ в текущей ячейке.

Опции (в Параметры → Оформление → Быстрый просмотр):

- Относиться (или нет) к текущей виртуальной библиотеке. Если этот флажок установлен, Quickview показывает только книги в текущей виртуальной библиотеке. По умолчанию: относиться к виртуальным библиотекам

- Изменяйте содержимое окна Quickview, когда столбец меняется в списке книг клавишами курсора. По умолчанию: не выполнять изменения, сделанные с помощью клавиш курсора

- Изменять столбец «quickview’ed», когда ячейка в окне Quickview дважды нажата. В противном случае книга будет изменена, но отображаемый столбец не будет. По умолчанию: изменить столбец

- Изменить столбец «quickview’ed» на текущий столбец, когда клавиша возврата нажата в панели Quickview. В противном случае книга будет изменена, но рассматриваемый столбец не будет. По умолчанию: изменить столбец

- Выберите, какие столбцы показывать в окне/на панели «Быстрый просмотр».

Задания

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

Сочетания клавиш

В calibre есть горячие клавиши для экономии времени и движений мышью. Эти сочетания активны при просмотре списка книг (когда вы не редактируете данные конкретной книги), и большинство из них влияет на выделенное вами наименование. Читалка электронных книг calibre имеет собственные горячие клавиши, которые можно настроить, нажав в ней кнопку Параметры.

Примечание

Примечание: если специально не оговорено, то для горячих клавиш calibre не требуется ключ-модификатор (Command, Option, Control и т. д.). Необходимо нажать только клавишу с буквой, например «E» для редактирования.

Горячие клавиши для главного окна calibre

Сочетание клавиш

Действие

F2 (Enter для macOS)

Редактировать метаданные текущей выбранной области в списке книг.

A

Добавить книги

Shift+A

Добавить форматы в выбранные книги

C

Конвертировать выбранные книги

D

Отправить на устройство

Del

Удалить выбранные книги

E

Редактировать метаданные выбранных книг

G

Приобрести книги

I

Показать описание книги

K

Редактировать оглавление

M

Объединить выбранные записи

Alt+M

Объединить выбранные записи, сохранив оригиналы

O

Открыть папку

P

Полировка книг

S

Сохранить на диск

T

Редактировать книгу

V

Просмотр

Shift+V

Посмотреть последнюю прочитанную книгу

Alt+V/Cmd+V для macOS

Просмотреть конкретный формат

Alt+Shift+J

Переключить список задач

Alt+Shift+B

Переключить браузер обложек

Alt+Shift+D

Переключить панель описания книги

Alt+Shift+T

Переключить браузер тегов

Alt+Shift+G

Переключить сетку обложек

Alt+A

Показать книги с тем же автором, что и у текущей

Alt+T

Показать книги с теми же тегами, что и текущая

Alt+P

Показать книги с таким же издательством, как у текущей

Alt+Shift+S

Показать книги из той же серии, что и текущая

/, Ctrl+F

Фокус на панель поиска

Shift+Ctrl+F

Открыть диалог расширенного поиска

Shift+Alt+F

Переключить строку поиска

Esc

Очистить текущий поиск

Shift+Esc

Фокус на список книг

Ctrl+Esc

Очистить виртуальную библиотеку

Alt+Esc

Очистить дополнительное ограничение

Ctrl+*

Создать временную Виртуальную библиотеку на основе текущего поиска

Ctrl+Right

Выбрать следующую вкладку Виртуальной библиотеки

Ctrl+Left

Выбрать предыдущую вкладку Виртуальной библиотеки

N или F3

Найти следующую книгу, которая соответствует текущим условиям поиска (работает только если в настройках поиска включена подсветка поиска)

Shift+N или Shift+F3

Найти предыдущую книгу, которая соответствует текущим условиям поиска (работает только если в настройках поиска включена подсветка поиска)

Ctrl+D

Загрузить метаданные и обложки

Ctrl+R

Перезапустить calibre

Ctrl+Shift+R

Перезапустить calibre в отладочном режиме

Shift+Ctrl+E

Добавить пустые книги в calibre

Ctrl+M

Переключить статус „помечено“ для выбранных книг

Ctrl+/ или Ctrl+Alt+F

Откройте всплывающее окно для поиска по полному тексту всех книг в библиотеке.

Q

Открыть окно Быстрого Просмотра для просмотра книг из связанных серий/тегов и т.д.

Shift+Q

Фокус на открытую панель быстрого просмотра

Shift+S

Выполнить поиск в панели быстрого просмотра

F5

Re-apply the current sort

Ctrl+Q

Выйти из calibre

X

Включить автоматическую прокрутку списка книг

Ctrl+Alt+Shift+F

Ограничить отображаемые книги только книгами, находящимися в категории, отображаемой в данный момент в Браузер тегов

B

Просмотреть аннотации (выделения и закладки), сделанные в просмотрщике calibre для всех книг в библиотеке

Ctrl+Shift+N

Просмотреть примечания, связанные с авторами/сериями/тегами/и т.д.

Alt+Shift+L

Переключить на широкий или узкий макет

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Добавление любимых новостных веб-сайтов

Добавление любимых новостных веб-сайтов
calibre обладает мощной, гибкой и простой в использовании платформой для загрузки новостей из Интернета и преобразования их в электронную книгу. Далее на примерах будет показано, как получать новости с различных веб-сайтов.

Чтобы понять, как использовать платформу, следуйте примерам в порядке, указанном ниже:

- Полностью автоматическая загрузка

- Блог calibre

- bbc.co.uk

- Настройка процесса получения

- Использование печатной версии bbc.co.uk

- Замена стилей статей

- Шинковка ломтиками и кубиками

- Пример из реальной жизни

- Советы по созданию новых рецептов

- Дальнейшее чтение

- Документация API

Полностью автоматическая загрузка

Если ваш источник новостей достаточно прост, calibre вполне может получить его полностью автоматически, все, что вам нужно сделать, это предоставить URL. calibre собирает всю информацию, необходимую для загрузки источника новостей, в recipe. Чтобы сообщить calibre об источнике новостей, вы должны создать для него recipe. Давайте посмотрим несколько примеров:

Блог calibre

Блог calibre - это блог постов, которые описывают многие полезные функции calibre простым и доступным способом для пользователей нового calibre. Чтобы загрузить этот блог в электронную книгу, мы полагаемся на RSS ленту блога:

http://blog.calibre-ebook.com/feeds/posts/default

Я получил URL-адрес RSS, посмотрев в разделе «Подписаться на» в нижней части страницы блога и выбрав Posts →  Atom. Чтобы заставить calibre загружать каналы и конвертировать их в электронную книгу, вы должны щелкнуть правой кнопкой мыши по кнопке Fetch news, а затем Добавить пользовательский источник новостей и затем по кнопке :guilabel:`«Новый рецепт». Должно открыться диалоговое окно, подобное показанному ниже.

Сначала введите Calibre Blog в поле Название рецепта. Это будет название электронной книги, которая будет создана из статей в вышеупомянутых каналах.

Следующие два поля (Самая старая статья и :guilabel:` Максимальное количество статей`) позволяют вам контролировать, сколько статей следует загружать из каждого канала, и они довольно понятны.

Чтобы добавить каналы в рецепт, введите заголовок канала и URL-адрес канала и нажмите кнопку Добавить канал. После добавления канала просто нажмите кнопку Сохранить, и все готово! Закройте диалог.

Чтобы проверить свой новый recipe, нажмите кнопку Получить новости и в подменю Пользовательские источники новостей нажмите Блог calibre. Через пару минут недавно загруженная электронная книга из сообщений блога появится в главном представлении библиотеки (если у вас подключена читалка, она будет помещена в читалку, а не в библиотеку). Выберите книгу и нажмите кнопку View, чтобы прочитать!

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

bbc.co.uk

Давайте попробуем следующие два фида из * BBC *:

1. Главная страница новостей: https://newsrss.bbc.co.uk/rss/newsonline_world_edition/front_page/rss.xml

2. Наука/Природа: https://newsrss.bbc.co.uk/rss/newsonline_world_edition/science/nature/rss.xml

Следуйте процедуре, описанной в Блог calibre выше, чтобы создать рецепт для BBC (используя фиды выше). Глядя на загруженную электронную книгу, мы видим, что calibre проделал достойную работу по извлечению только того контента, который вам интересен, с веб-страницы каждой статьи. Однако процесс извлечения не идеален. Иногда он оставляет нежелательный контент, такой как меню и навигационные средства, или удаляет контент, который должен был остаться, например заголовки статей. Чтобы обеспечить идеальное извлечение контента, нам нужно настроить процесс извлечения, как описано в следующем разделе.

Настройка процесса получения

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

Самая простая и часто наиболее производительная настройка заключается в использовании версии для печати онлайн-статей. Такая версия, как правило, имеет гораздо меньше ошибок и гораздо более гладко переводится в электронную книгу. Давайте попробуем использовать версию для печати статей из BBC.

Использование печатной версии bbc.co.uk

Первым шагом является просмотр электронной книги, которую мы скачали ранее bbc.co.uk. В конце каждой статьи в электронной книге есть небольшая реклама, в которой рассказывается, откуда была загружена статья. Скопируйте и вставьте этот URL в браузер. Теперь на веб-странице статьи найдите ссылку, которая указывает на «Версия для печати». Нажмите на неё, чтобы увидеть печатную версию статьи. Это выглядит намного аккуратнее! Теперь сравните два URL. Для меня они были такими:

URL статьи

https://news.bbc.co.uk/2/hi/science/nature/7312016.stm

Печатная версия URL

https://newsvote.bbc.co.uk/mpapps/pagetools/print/news.bbc.co.uk/2/hi/science/nature/7312016.stm

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

newsvote.bbc.co.uk/mpapps/pagetools/print/

Теперь в Advanced Mode диалога Custom news sources вы должны увидеть что-то вроде (не забудьте выбрать рецепт BBC перед переходом в расширенный режим):

Вы можете видеть, что поля из Basic mode были прямо переведены в код Python. Нам нужно добавить инструкции к этому рецепту, чтобы использовать печатную версию статей. Всё, что нужно, это добавить следующие две строки:

def print_version(self, url):
return url.replace('https://', 'https://newsvote.bbc.co.uk/mpapps/pagetools/print/')

Это Python, поэтому отступы важны. После того, как вы добавили строки, это должно выглядеть так:

Выше def print_version(self, url) определяет метод, который вызывается calibre для каждой статьи. url - это URL оригинальной статьи. print_version - это взять этот URL и заменить его новым URL, который указывает на печатную версию статьи. Чтобы узнать о Python [https://www.python.org], см. tutorial [https://docs.python.org/tutorial/].

Теперь нажмите кнопку Добавить/обновить рецепт, и ваши изменения будут сохранены. Повторно загрузите электронную книгу. У вас должна быть намного улучшенная электронная книга. Одна из проблем новой версии заключается в том, что шрифты на веб-странице версии для печати слишком малы. Это автоматически исправляется при преобразовании в электронную книгу, но даже после процесса исправления размер шрифта меню и панели навигации становится слишком большим по сравнению с текстом статьи. Чтобы исправить это, мы сделаем ещё несколько настроек в следующем разделе.

Замена стилей статей

В предыдущем разделе мы видели, что размер шрифта для статей из печатной версии BBC был слишком мал. На большинстве веб-сайтов, включая BBC, этот размер шрифта устанавливается с помощью CSS таблиц стилей. Мы можем отключить выборку таких таблиц стилей, добавив строку:

no_stylesheets = True

Рецепт теперь выглядит так:

Новая версия выглядит довольно хорошо. Если вы перфекционист, вам нужно прочитать следующий раздел, посвященный изменению загруженного контента.

Шинковка ломтиками и кубиками

Что касается управления загруженным содержимым, calibre обладает мощными и гибкими возможностями. Чтобы похвастаться парочкой, давайте снова посмотрим на нашего старого друга — рецепт BBC. Взглянув на исходный код (HTML) пары статей (версия для печати), мы увидим в них нижний колонтитул без какой-либо полезной информации, содержащейся в

<div class="footer">
...
</div>

Это может быть удалено путем добавления:

remove_tags = [dict(name='div', attrs={'class':'footer'})]

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

extra_css = '.headline {font-size: x-large;} \n .fact { padding-top: 10pt }'

Благодаря этим дополнениям наш рецепт стал «производственным качеством».

Это recipe исследует только верхушку айсберга, когда дело доходит до силы calibre. Чтобы изучить больше возможностей calibre, мы рассмотрим более сложный пример из реальной жизни в следующем разделе.

Пример из реальной жизни

Достаточно сложный пример из реальной жизни, который раскрывает большую часть API BasicNewsRecipe, это recipe для The New York Times
import string, re from calibre import strftime from calibre.web.feeds.recipes import BasicNewsRecipe from calibre.ebooks.BeautifulSoup import BeautifulSoup class NYTimes(BasicNewsRecipe):

title = 'The New York Times'
__author__ = 'Kovid Goyal'
description = 'Daily news from the New York Times'
timefmt = ' [%a, %d %b, %Y]'
needs_subscription = True remove_tags_before = dict(id='article')
remove_tags_after = dict(id='article')
remove_tags = [dict(attrs={'class':['articleTools', 'post-tools', 'side_tool', 'nextArticleLink clearfix']}), dict(id=['footer', 'toolsRight', 'articleInline', 'navigation', 'archive', 'side_search', 'blog_sidebar', 'side_tool', 'side_index']), dict(name=['script', 'noscript', 'style'])]
encoding = 'cp1252'
no_stylesheets = True extra_css = 'h1 {font: sans-serif large;}\n.byline {font:monospace;}'

def get_browser(self):
br = BasicNewsRecipe.get_browser(self)
if self.username is not None and self.password is not None:
br.open('https://www.nytimes.com/auth/login')
br.select_form(name='login')
br['USERID'] = self.username br['PASSWORD'] = self.password br.submit()
return br def parse_index(self):
soup = self.index_to_soup('https://www.nytimes.com/pages/todayspaper/index.html')

def feed_title(div):
return ''.join(div.findAll(text=True, recursive=False)).strip()

articles = {}
key = None ans = []
for div in soup.findAll(True, attrs={'class':['section-headline', 'story', 'story headline']}):

if ''.join(div['class']) == 'section-headline':
key = string.capwords(feed_title(div))
articles[key] = []
ans.append(key)

elif ''.join(div['class']) in ['story', 'story headline']:
a = div.find('a', href=True)
if not a:
continue url = re.sub(r'\?.*', '', a['href'])
url += '?pagewanted=all'
title = self.tag_to_string(a, use_alt=True).strip()
description = ''
pubdate = strftime('%a, %d %b')
summary = div.find(True, attrs={'class':'summary'})
if summary:
description = self.tag_to_string(summary, use_alt=False)

feed = key if key is not None else 'Uncategorized'
if feed not in articles:
articles[feed] = []
if not 'podcasts' in url:
articles[feed].append( dict(title=title, url=url, date=pubdate, description=description, content=''))
ans = self.sort_index_by(ans, {'The Front Page':-1, 'Dining In, Dining Out':1, 'Obituaries':2})
ans = [(key, articles[key]) for key in ans if key in articles]
return ans def preprocess_html(self, soup):
refresh = soup.find('meta', {'http-equiv':'refresh'})
if refresh is None:
return soup content = refresh.get('content').partition('=')[2]
raw = self.browser.open('https://www.nytimes.com'+content).read()
return BeautifulSoup(raw.decode('cp1252', 'replace'))

В этом мы видим несколько новых функций recipe. Во-первых, у нас есть:

timefmt = ' [%a, %d %b, %Y]'

Это устанавливает отображаемое время на первой странице созданной электронной книги в формате Day, Day_Number Month, Year. Смотрите timefmt.

Затем мы видим группу директив для очистки загруженного файла HTML:

remove_tags_before = dict(name='h1')
remove_tags_after = dict(id='footer')
remove_tags = ...

Они удаляют все до первого тега <h1> и все после первого тега с идентификатором `` footer``. Смотрите remove_tags, remove_tags_before, remove_tags_after.

Следующая интересная особенность:

needs_subscription = True
...
def get_browser(self):
...

needs_subscription = True сообщает calibre, что для получения доступа к содержимому этого рецепта требуется имя пользователя и пароль. Это заставляет calibre запрашивать имя пользователя и пароль всякий раз, когда вы пытаетесь использовать этот рецепт. Код в calibre.web.feeds.news.BasicNewsRecipe.get_browser() фактически выполняет вход на веб-сайт NYT. После входа в систему calibre будет использовать тот же, вошедший в систему, экземпляр браузера, чтобы получить весь контент. Смотрите mechanize [https://mechanize.readthedocs.io/en/latest/], чтобы понять код в get_browser.

Следующей новой функцией является метод calibre.web.feeds.news.BasicNewsRecipe.parse_index(). Его работа заключается в том, чтобы перейти на https://www.nytimes.com/pages/todayspaper/index.html и получить список статей, которые появляются в сегодняшнем документе. Хотя этот рецепт сложнее, чем просто использовать RSS, этот рецепт создает электронную книгу, которая очень точно соответствует дневнику. parse_index интенсивно использует BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] для анализа ежедневной веб-страницы газеты. Вы также можете использовать другие, более современные парсеры, если вам не нравится BeautifulSoup. calibre поставляется с lxml <https://lxml.de/> _ и html5lib [https://github.com/html5lib/html5lib-python], которые являются рекомендуемыми парсерами. Чтобы использовать их, замените вызов index_to_soup() следующим:

raw = self.index_to_soup(url, raw=True)
# For html5lib
import html5lib
root = html5lib.parse(raw, namespaceHTMLElements=False, treebuilder='lxml')
# For the lxml html 4 parser
from lxml import html
root = html.fromstring(raw)

Последняя новая функция - это метод calibre.web.feeds.news.BasicNewsRecipe.preprocess_html(). Его можно использовать для выполнения произвольных преобразований на каждой загруженной HTML-странице. Здесь он используется для обхода рекламы, которую nytimes показывает перед каждой статьей.

Советы по созданию новых рецептов

Лучший способ разработать новые рецепты - использовать интерфейс командной строки. Создайте рецепт, используя ваш любимый редактор Python, и сохраните его в файл, скажем myrecipe.recipe. Требуется расширение .recipe. Вы можете скачать контент, используя этот рецепт с командой:

ebook-convert myrecipe.recipe .epub --test -vv --debug-pipeline debug

Команда ebook-convert загрузит все веб-страницы и сохранит их в файл EPUB myrecipe.epub. Параметр -vv заставляет ebook-convert выдавать много информации о том, что он делает. Параметр ebook-convert-recipe-input --test позволяет загружать только пару статей максимум из двух каналов. Кроме того, ebook-convert поместит загруженный HTML-код в папку debug/input, где debug - это папка, указанная в параметре ebook-convert --debug-pipeline.

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

ebook-convert myrecipe.recipe myrecipe.epub
ebook-convert myrecipe.recipe myrecipe.mobi
...

Если вы удовлетворены своим рецептом и чувствуете, что существует достаточный спрос, чтобы оправдать его включение в набор встроенных рецептов, опубликуйте свой рецепт на форуме рецептов calibre [https://www.mobileread.com/forums/forumdisplay.php?f=228], чтобы поделиться им с другими пользователями.

Примечание

В macOS инструменты командной строки находятся внутри пакета calibre, например, если вы установили calibre в /Applications, то инструменты командной строки будут в /Applications/calibre.app/Contents/MacOS/.

См. также

ebook-convert

Интерфейс командной строки для всех конвертаций электронных книг.

Дальнейшее чтение

Чтобы узнать больше о написании расширенных рецептов с использованием некоторых средств, доступных в BasicNewsRecipe, вам следует обратиться к следующим источникам:

Документация API

Документация класса BasicNewsRecipe и всех его важных методов и полей.

BasicNewsRecipe [https://github.com/kovidgoyal/calibre/blob/master/src/calibre/web/feeds/news.py]

Исходный код BasicNewsRecipe

Встроенные рецепты [https://github.com/kovidgoyal/calibre/tree/master/recipes]

Исходный код для встроенных рецептов, которые идут с calibre

Форум рецептов calibre [https://www.mobileread.com/forums/forumdisplay.php?f=228]

Много знающих авторов рецепта calibre тусуются здесь.

Документация API

Документация API рецептовBasicNewsRecipe

BasicNewsRecipe.adeify_images()
BasicNewsRecipe.image_url_processor()
BasicNewsRecipe.print_version()
BasicNewsRecipe.tag_to_string()
BasicNewsRecipe.abort_article()
BasicNewsRecipe.abort_recipe_processing()
BasicNewsRecipe.add_toc_thumbnail()
BasicNewsRecipe.canonicalize_internal_url()
BasicNewsRecipe.cleanup()
BasicNewsRecipe.clone_browser()
BasicNewsRecipe.default_cover()
BasicNewsRecipe.download()
BasicNewsRecipe.extract_readable_article()
BasicNewsRecipe.get_article_url()
BasicNewsRecipe.get_browser()
BasicNewsRecipe.get_cover_url()
BasicNewsRecipe.get_extra_css()
BasicNewsRecipe.get_feeds()
BasicNewsRecipe.get_masthead_title()
BasicNewsRecipe.get_masthead_url()
BasicNewsRecipe.get_obfuscated_article()
BasicNewsRecipe.get_url_specific_delay()
BasicNewsRecipe.index_to_soup()
BasicNewsRecipe.is_link_wanted()
BasicNewsRecipe.parse_feeds()
BasicNewsRecipe.parse_index()
BasicNewsRecipe.populate_article_metadata()
BasicNewsRecipe.postprocess_book()
BasicNewsRecipe.postprocess_html()
BasicNewsRecipe.preprocess_html()
BasicNewsRecipe.preprocess_image()
BasicNewsRecipe.preprocess_raw_html()
BasicNewsRecipe.publication_date()
BasicNewsRecipe.skip_ad_pages()
BasicNewsRecipe.sort_index_by()
BasicNewsRecipe.articles_are_obfuscated
BasicNewsRecipe.auto_cleanup
BasicNewsRecipe.auto_cleanup_keep
BasicNewsRecipe.browser_type
BasicNewsRecipe.center_navbar
BasicNewsRecipe.compress_news_images

BasicNewsRecipe.compress_news_images_auto_size

BasicNewsRecipe.compress_news_images_max_size
BasicNewsRecipe.conversion_options
BasicNewsRecipe.cover_margins
BasicNewsRecipe.delay
BasicNewsRecipe.description
BasicNewsRecipe.encoding
BasicNewsRecipe.extra_css
BasicNewsRecipe.feeds
BasicNewsRecipe.filter_regexps
BasicNewsRecipe.handle_gzip
BasicNewsRecipe.ignore_duplicate_articles
BasicNewsRecipe.keep_only_tags
BasicNewsRecipe.language
BasicNewsRecipe.masthead_url
BasicNewsRecipe.match_regexps
BasicNewsRecipe.max_articles_per_feed
BasicNewsRecipe.needs_subscription
BasicNewsRecipe.no_stylesheets
BasicNewsRecipe.oldest_article
BasicNewsRecipe.preprocess_regexps
BasicNewsRecipe.publication_type
BasicNewsRecipe.recipe_disabled
BasicNewsRecipe.recipe_specific_options
BasicNewsRecipe.recursions
BasicNewsRecipe.remove_attributes
BasicNewsRecipe.remove_empty_feeds
BasicNewsRecipe.remove_javascript
BasicNewsRecipe.remove_tags
BasicNewsRecipe.remove_tags_after
BasicNewsRecipe.remove_tags_before
BasicNewsRecipe.requires_version
BasicNewsRecipe.resolve_internal_links
BasicNewsRecipe.reverse_article_order
BasicNewsRecipe.scale_news_images
BasicNewsRecipe.scale_news_images_to_device
BasicNewsRecipe.simultaneous_downloads
BasicNewsRecipe.summary_length
BasicNewsRecipe.template_css
BasicNewsRecipe.timefmt
BasicNewsRecipe.timeout
BasicNewsRecipe.title
BasicNewsRecipe.use_embedded_content

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Добавление любимых новостных веб-сайтов »
- Документация API рецептов

Документация API рецептов

API для написания рецептов определяется классом BasicNewsRecipe
class calibre.web.feeds.news.BasicNewsRecipe(options, log, progress_reporter)[исходный код]

Основной класс, содержащий логику, необходимую для всех рецептов. Переопределяя функциональные возможности этого класса, вы постепенно сможете создавать более настраиваемые/действенные рецепты. Вводный курс по созданию рецептов помещён в разделе «Добавление любимых новостных веб-сайтов».

classmethod adeify_images(soup)[исходный код]

Если ваш рецепт при преобразовании в EPUB имеет проблемы с изображениями при просмотре в Adobe Digital Editions, вызовите этот метод изнутри postprocess_html().

classmethod image_url_processor(baseurl, url)[исходный код]

Выполнить обработку URL-адресов изображений (возможно, удалить ограничения по размеру для динамически генерируемых изображений и т.д.) и вернуть обработанный URL-адрес. Вернуть None или пустую строку, чтобы пропустить получение изображения.

classmethod print_version(url)[исходный код]

Взять url, указывающий на веб-страницу с содержанием статьи, и вернуть URL, указывающий на печатную версию статьи. По умолчанию ничего не делает. Например:

def print_version(self, url):
return url + '?&pagewanted=print'

classmethod tag_to_string(tag, use_alt=True, normalize_whitespace=True)[исходный код]

Удобный метод для того, чтобы взять `BeautifulSoup https://www.crummy.com/software/BeautifulSoup/bs4/doc/`_: code:` Tag` и рекурсивно извлечь из него текст, включая любые разделы CDATA и атрибуты тегов alt. Вернуть возможно пустую строку Unicode.

use_alt: If True try to use the alt attribute for tags that don’t have any textual content
tag: BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/]
Tag

abort_article(msg=None)[исходный код]

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

abort_recipe_processing(msg)[исходный код]

Заставляет систему загрузки рецептов прервать загрузку этого рецепта, отображая простое сообщение обратной связи для пользователя.

add_toc_thumbnail(article, src)[исходный код]

Вызовите из populate_article_metadata с атрибутом src тега <img> из статьи, который подходит для использования в качестве эскиза, представляющего статью в Оглавлении. Используется ли миниатюра на самом деле, зависит от устройства (сейчас используется только в Kindle). Обратите внимание, что указанное изображение должно быть успешно загружено, иначе оно будет проигнорировано.

canonicalize_internal_url(url, is_link=True)[исходный код]

Вернуть набор канонических представлений url. Реализация по умолчанию использует только имя хоста сервера и путь URL-адреса, игнорируя любые параметры запроса, фрагменты и т.д. Канонические представления должны быть уникальными для всех URL-адресов для этого источника новостей. В противном случае внутренние ссылки могут быть разрешены неверно.

Параметры:

is_link – Имеет значение True, если URL-адрес поступает по внутренней ссылке в файле HTML. False, если URL-адрес является URL-адресом, используемым для загрузки статьи.

cleanup()[исходный код]

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

clone_browser(br)[исходный код]

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

Клонированные экземпляры браузера по умолчанию используют тот же потокобезопасный CookieJar, если вы не настроили обработку файлов cookie.

default_cover(cover_file)[исходный код]

Create a generic cover for recipes that don’t have a cover

download()[исходный код]

Загрузить и предварительно обработать все статьи из каналов в этом рецепте. Этот метод следует вызывать только один раз для конкретного экземпляра Recipe. Вызов его более одного раза приведет к неопределенному поведению. :return:Путь к index.html

extract_readable_article(html, url)[исходный код]

Извлечь основное содержание статьи из html, очистить и вернуть в виде кортежа (article_html, extract_title). На основе оригинального алгоритма читаемости от Arc90.

get_article_url(article)[исходный код]

Override in a subclass to customize extraction of the URL that points to the content for each article. Return the article URL. It is called with article, an object representing a parsed article from a feed. See feedparser [https://pythonhosted.org/feedparser/].
By default it looks for the original link (for feeds syndicated via a service like FeedBurner or Pheedo) and if found, returns that or else returns article.link [https://pythonhosted.org/feedparser/reference-entry-link.html].

get_browser(*args, **kwargs)[исходный код]

Вернуть экземпляр браузера, используемый для получения документов из Интернета. По умолчанию он возвращает экземпляр браузера mechanize [https://mechanize.readthedocs.io/en/latest/], который поддерживает файлы cookie, игнорирует robots.txt, обрабатывает обновления и имеет случайный пользовательский агент.

Чтобы настроить браузер, переопределите этот метод в вашем подклассе следующим образом:

def get_browser(self, *a, **kw):
br = super().get_browser(*a, **kw)
# Add some headers
br.addheaders += [
('My-Header', 'one'),
('My-Header2', 'two'),
]
# Set some cookies
br.set_cookie('name', 'value')
br.set_cookie('name2', 'value2', domain='.mydomain.com')
# Make a POST request with some data
br.open('https://someurl.com', {'username': 'def', 'password': 'pwd'}).read()
# Do a login via a simple web form (only supported with mechanize browsers)
if self.username is not None and self.password is not None:
br.open('https://www.nytimes.com/auth/login')
br.select_form(name='login')
br['USERID'] = self.username
br['PASSWORD'] = self.password
br.submit()
return br

get_cover_url()[исходный код]

Return a URL to the cover image for this issue or None.
By default it returns the value of the member self.cover_url which is normally None. If you want your recipe to download a cover for the e-book override this method in your subclass, or set the member variable self.cover_url before this method is called.

get_extra_css()[исходный код]

По умолчанию возвращает self.extra_css. Переопределите, если вы хотите программно сгенерировать файл extra_css.

get_feeds()[исходный код]

Вернуть список RSS каналов для получения в этом профиле. Каждый элемент списка д. б. двухэлементным кортежем формы (заголовок, URL). Если заголовок - None или пустая строка, используется заголовок из фида. Метод полезен, если рецепт требует вычисления списка каналов для загрузки. Если так, переопределите в своем подклассе.

get_masthead_title()[исходный код]

Переопределить в подклассе, чтобы использовать что-то другое, кроме названия рецепта

get_masthead_url()[исходный код]

Вернуть URL к изображению masthead для этой проблемы или None. По умолчанию возвращает значение члена self.masthead_url, которое обычно равно None. Если вы хотите, чтобы ваш рецепт загружал заголовок для электронной книги, переопределите этот метод в своем подклассе или установите переменную члена self.masthead_url перед вызовом этого метода. Изображения masthead используются в файлах Kindle MOBI.

get_obfuscated_article(url)[исходный код]

Если вы установите articles_are_obfuscated, этот метод вызывается для каждого URL-адреса статьи. Он должен вернуть путь к файлу в файловой системе, содержащему HTML-код статьи. Этот файл обрабатывается механизмом рекурсивной выборки HTML, поэтому он может содержать ссылки на страницы/изображения в Интернете. Альтернативно вы можете вернуть словарь вида: {„data“: <HTML data>, „url“: <the resolved URL of the article>}. Это позволяет избежать необходимости создавать временные файлы. Ключ url в словаре полезен, если эффективный URL-адрес статьи отличается от URL-адреса, переданного в этот метод, например, из-за перенаправлений. Его можно опустить, если URL-адрес не изменился.

Этот метод обычно полезен для сайтов, которые пытаются затруднить автоматический доступ к содержимому статьи.

get_url_specific_delay(url)[исходный код]

Возвращает задержку в секундах перед загрузкой этого URL. Если вы хотите программно определить задержку для указанного URL-адреса, переопределите этот метод в своём подклассе, возвращая self.delay по умолчанию для URL-адресов, на которые вы не хотите влиять.

Результат:

Число с плавающей запятой, задержка в секундах.

index_to_soup(url_or_raw, raw=False, as_tree=False, save_raw=None)[исходный код]

Удобный метод, который принимает URL-адрес на страницу индекса и возвращает его `BeautifulSoup https://www.crummy.com/software/BeautifulSoup/bs4/doc`__.

url_or_raw: либо URL, либо загруженная страница индекса в виде строки

is_link_wanted(url, tag)[исходный код]

Вернуть True, если по ссылке нужно перейти, или False в противном случае. По умолчанию вызывает NotImplementedError, что заставляет загрузчик игнорировать его.

Параметры:

- url – URL, по которому нужно следовать

- tag – Тег, из которого был получен URL

parse_feeds()[исходный код]

Создать список статей из списка каналов, возвращаемых BasicNewsRecipe.get_feeds(). Вернуть список Feed.

parse_index()[исходный код]

Этот метод д. б. реализован в рецептах для анализа веб-сайта, а не в каналах для создания списка статей. Типичное использование - источники новостей, с веб-страницей «Для печати» со всеми статьями. Если эта функция реализована, она будет использоваться вместо BasicNewsRecipe.parse_feeds().

It must return a list. Each element of the list must be a 2-element tuple of the form ('feed title', list of articles).

Каждый список статей должен содержать словари вида:

{
'title' : article title,
'url' : URL of print version,
'date' : The publication date of the article as a string,
'description' : A summary of the article
'content' : The full article (can be an empty string). Obsolete
do not use, instead save the content to a temporary
file and pass a file:///path/to/temp/file.html as
the URL.
}

Для примера см. Рецепт загрузки The Atlantic. Кроме того, вы можете добавить author для автора статьи.

Если вы хотите по какой-то причине прервать обработку, то чтобы calibre показал пользователю простое сообщение вместо ошибки, вызовите abort_recipe_processing().

populate_article_metadata(article, soup, first)[исходный код]

Вызывается при загрузке каждой HTML-страницы, принадлежащей статье. Предназначен для получения метаданных статьи, таких как автор/резюме/и т. д. из разобранного HTML (soup)

Параметры:

- article – Объект класса calibre.web.feeds.Article. Если вы измените сводку, не забудьте также изменить text_summary

- soup – Разобранный HTML, принадлежащий этой статье

- first – True, если проанализированный HTML - первая страница статьи.

postprocess_book(oeb, opts, log)[исходный код]

Запустить любую необходимую постобработку для проанализированной загруженной электронной книги.

Параметры:

- oeb – OEBBook объект

- opts – Опции конвертации

postprocess_html(soup, first_fetch)[исходный код]

Этот метод вызывается для источника каждого загруженного файла HTML после его анализа на наличие ссылок и изображений. Можно использовать для постобработки HTML. После обработки он должен вернуть soup.

Параметры:

- soup – A BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] instance containing the downloaded HTML.

- first_fetch – True если это первая страница статьи.

preprocess_html(soup)[исходный код]

Этот метод вызывается для источника каждого загруженного файла: term: HTML перед анализом на предмет ссылок и изображений. Он вызывается после очистки, remove_tags и т. д. Можно использовать для предварительной обработки HTML. После обработки он должен вернуть soup.

soup: экземпляр` BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`__, содержащий загруженный HTML.

preprocess_image(img_data, image_url)[исходный код]

Выполнить обработку загруженных данных изображения. Вызывается для сырых данных перед изменением размера. Должен возвращать обработанные сырые данные. Вернуть None, чтобы пропустить изображение.

preprocess_raw_html(raw_html, url)[исходный код]

Этот метод вызывается для источника каждого загруженного файла HTML перед его синтаксическим анализом в дерево объектов. raw_html - это Юникод-строка, представляющая необработанный HTML-код, загруженный из Интернета. url - это URL-адрес, откуда был загружен HTML-код.

Note that this method acts before preprocess_regexps.

Этот метод должен возвращать обработанный raw_html как Unicode-объект .

publication_date()[исходный код]

Используйте этот метод, чтобы установить дату публикации этого выпуска. По умолчанию на момент загрузки. Должен вернуть datetime.datetime объект.

skip_ad_pages(soup)[исходный код]

Этот метод вызывается для источника каждого загруженного файла HTML перед применением любого из атрибутов очистки, таких как remove_tags, keep_only_tags. Обратите внимание, что preprocess_regexps уже был применен. Это сделано для того, чтобы рецепт пропускал рекламные страницы. Если soup - рекламная страница, вернуть HTML реальной страницы. Иначе вернуть None.

soup: экземпляр` BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`__, содержащий загруженный HTML.

sort_index_by(index, weights)[исходный код]

Удобный метод сортировки заголовков в index по weights. index сортируется на месте. Возвращает index.

index: A list of titles.

weights: словарь, который отображает веса в названия. Если какие-либо названия в индексе не имеют веса, предполагается, что они имеют вес 0.

articles_are_obfuscated = False

Установите значение True и реализуйте get_obfuscated_article() для обработки веб-сайтов, пытающихся затруднить очистку содержимого.

auto_cleanup = False

Автоматически извлечь весь текст со страниц загруженных статей. Использует алгоритмы из проекта удобочитаемости. Установить значение в True чтобы не чистить загруженный HTML вручную (хотя ручная очистка всегда будет лучше).

auto_cleanup_keep = None

Укажите элементы, которые алгоритм автоматической очистки никогда не должен удалять. Синтаксис - это выражение XPath. Например:

auto_cleanup_keep = '//div[@id="article-image"]' will keep all divs with
id="article-image"
auto_cleanup_keep = '//*[@class="important"]' will keep all elements
with class="important"
auto_cleanup_keep = '//div[@id="article-image"]|//span[@class="important"]'
will keep all divs with id="article-image" and spans
with class="important"

browser_type = 'mechanize'

Имитируемый движок браузера для использования при загрузке с серверов. По умолчанию используется движок браузера Python mechanize, который поддерживает вход в систему. Однако, если вам не нужен вход в систему, рассмотрите возможность изменения этого параметра либо на „webengine“, который использует реальный браузер Chromium для выполнения сетевых запросов, либо на „qt“, который использует сетевой сервер Qt. И „webengine“, и „qt“ поддерживают HTTP/2, не поддерживаемый mechanize, и, следовательно, его сложнее идентифицировать для служб защиты от ботов.

center_navbar = True

If True the navigation bar is center aligned, otherwise it is left aligned

compress_news_images = False

Установите значение False, чтобы игнорировать все параметры масштабирования и сжатия и передавать изображения без изменений. Если True и другие параметры сжатия оставлены в значениях по умолчанию, изображения JPEG будут масштабированы, чтобы соответствовать размерам экрана, установленным выходным профилем, и сжиматься до размера не более (w * h)/16, где w x h - размеры масштабированного изображения.

compress_news_images_auto_size = 16

Коэффициент, используемый при автоматическом сжатии изображений JPEG. Если установлено значение «Нет», автоматическое сжатие отключено. В противном случае изображения будут уменьшены в размере до (w * h) / compress_news_images_auto_size байтов, если это возможно, за счет снижения уровня качества, где w x h - размеры изображения в пикселях. Минимальное качество JPEG будет 5/100, поэтому м.б. это ограничение не будет выполнено. Этот параметр можно переопределить с помощью параметра compress_news_images_max_size, обеспечивающего фиксированный максимальный размер изображений. Обратите внимание: если вы включите scale_news_images_to_device, изображение сначала будет масштабировано, а затем его качество будет снижено до тех пор, пока его размер не станет меньше, чем (w * h) / factor, где w и h теперь являются * масштабированными * размерами изображения. Другими словами, это сжатие происходит после масштабирования.

compress_news_images_max_size = None

Установить качество JPEG, чтобы изображения не превышали указанный размер (в килобайтах). Если установлено, этот параметр отменяет автоматическое сжатие через compress_news_images_auto_size. Минимальное качество JPEG будет 5/100, поэтому м.б. это ограничение не будет выполнено.

conversion_options = {}

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

conversion_options = {
'base_font_size' : 16,
'linearize_tables' : True,
}

cover_margins = (0, 0, '#ffffff')

По умолчанию изображение обложки, возвращаемое функцией get_cover_url(), будет использоваться в качестве обложки для журнала. Отмена этого параметра в вашем рецепте дает команду calibre преобразовать загруженную обложку в рамку, ширина и высота которой выражаются в процентах от загруженной обложки. cover_margins = (10, 15, „#ffffff“) дополняет обложку белым полем 10 пикселей слева и справа, 15 пикселей сверху и снизу. Названия цветов определены здесь [https://www.imagemagick.org/script/color.php]. Учтите, что белый цвет почему-то не всегда работает в Windows. Вместо этого используйте #ffffff

delay = 0

The default delay between consecutive downloads in seconds. The argument may be a floating point number to indicate a more precise time. See get_url_specific_delay() to implement per URL delays.

description = ''

A couple of lines that describe the content this recipe downloads.
This will be used primarily in a GUI that presents a list of recipes.

encoding = None

Указать переопределяемую кодировку для сайтов с неправильным указанием кодировки. Чаще всего указывается latin1 и используется cp1252. Если None, попробует определить кодировку. Объект вызывается с двумя аргументами: объектом рецепта и источником для декодирования. Он должен возвращать декодированный источник.

extra_css = None

Указать любую дополнительную :term: CSS, которая д. б. добавлена к загружаемым HTML файлам. Она будет вставлена в <style>`теги `непосредственно перед закрывающим `</head>`тегом, переопределив все :term:`CSS, за исключением той, которая объявлена с использованием стилевого атрибута в отдельных тегах HTML. Обратите внимание: если вы хотите программно сгенерировать extra_css, вместо этого переопределите метод get_extra_css (). Например:

extra_css = '.heading { font: serif x-large }'

feeds = None

List of feeds to download.
Can be either [url1, url2, ...] or [('title1', url1), ('title2', url2),...]

filter_regexps = []

Список регулярных выражений, определяющий, какие ссылки игнорировать. Если пустой - игнорируется. Используется, только если is_link_wanted не реализован. Например:

filter_regexps = [r'ads\.doubleclick\.net']

will remove all URLs that have ads.doubleclick.net in them.

Должен быть определен только один из BasicNewsRecipe.match_regexps или BasicNewsRecipe.filter_regexps.

handle_gzip = True

Установите значение False, если вы не хотите использовать gzip-сжатие передач в браузере mechanize. Внимание! Некоторые старые серверы могут захлебнуться при включенном gzip-сжатии.

ignore_duplicate_articles = None

Игнорировать дубликаты статей, которые присутствуют более чем в одном разделе. Дублирующаяся статья - это статья с таким же названием и/или URL. Чтобы игнорировать статьи с таким же названием, установите для этого параметра значение:

ignore_duplicate_articles = {'title'}

Для использования URL-адресов вместо этого, установите значение:

ignore_duplicate_articles = {'url'}

Для соответствия заголовку или URL, установите значение:

ignore_duplicate_articles = {'title', 'url'}

keep_only_tags = []

Оставить только указанные теги и их дочерние элементы. Формат указания тега см. BasicNewsRecipe.remove_tags. Если этот список не пуст, то <body> тег будет очищен и повторно заполнен тегами, соответствующими записям в этом списке. Например:

keep_only_tags = [dict(id=['content', 'heading'])]

сохранить только теги, у которых есть атрибут id «content» или «heading».

language = 'und'

Язык новостей. Должен быть двух- или трёхзначным кодом ISO-639.

masthead_url = None

По умолчанию calibre будет использовать изображение по умолчанию для шапки (только Kindle). Переопределите это в своем рецепте, чтобы указать URL-адрес для использования в качестве шапки.

match_regexps = []

List of regular expressions that determines which links to follow.
If empty, it is ignored. Used only if is_link_wanted is not implemented. For example:

match_regexps = [r'page=[0-9]+']

будет соответствовать всем URL-адресам, в которых есть page = some number.

Должен быть определен только один из BasicNewsRecipe.match_regexps или BasicNewsRecipe.filter_regexps.

max_articles_per_feed = 100

Максимальное количество статей для загрузки из каждого канала. Это в первую очередь полезно для каналов, в которых нет дат статьи. Для большинства каналов вы должны использовать BasicNewsRecipe.oldest_article

needs_subscription = False

Если True, графический интерфейс запросит у пользователя имя пользователя и пароль для использования при загрузке. Если установлено значение „optional“, использование имени пользователя и пароля становится необязательным.

no_stylesheets = False

Convenient flag to disable loading of stylesheets for websites that have overly complex stylesheets unsuitable for conversion to e-book formats.
If True stylesheets are not downloaded and processed

oldest_article = 7.0

Самая старая статья для загрузки из этого источника новостей. В днях.

preprocess_regexps = []

Список правил подстановки regexp для запуска в загруженном HTML. Каждый элемент списка должен быть двухэлементным кортежем. Первый элемент кортежа должен быть скомпилированным регулярным выражением, а второй - вызываемым, который принимает единственный соответствующий объект и возвращает строку для замены совпадения. Например:

preprocess_regexps = [
(re.compile(r'<!--Article ends here-->.*</body>', re.DOTALL|re.IGNORECASE),
lambda match: '</body>'),
]

удалит все от <!–Article ends here–>` до </body>.

publication_type = 'unknown'

Тип публикации Газета, журнал или блог. Если установлено значение Нет, метаданные типа публикации не будут записываться в файл opf.

recipe_disabled = None

Установите непустую строку, чтобы отключить этот рецепт. Строка будет использоваться как отключенное сообщение

recipe_specific_options = None

Укажите параметры, специфичные для этого рецепта. Они будут доступны пользователю для настройки на вкладке «Дополнительно» диалогового окна «Получение новостей» или в командной строке ebook-convert. Параметры указываются как имя параметра сопоставления словаря с метаданными об этом параметре. Например:

recipe_specific_options = {
'edition_date': {
'short': 'The issue date to download',
'long': 'Specify a date in the format YYYY-mm-dd to download the issue corresponding to that date',
'default': 'current',
}
}

При запуске рецепта self.recipe_specify_options будет именем параметра сопоставления dict со значением параметра, указанным пользователем. Если параметр не указан пользователем, он будет иметь значение, указанное в значении «по умолчанию». Если значение по умолчанию не указано, этот параметр вообще не будет присутствовать в словаре, если он не указан пользователем.

recursions = 0

Количество уровней ссылок для перехода на веб-страницах статей

remove_attributes = []

Список атрибутов, которые нужно удалить из всех тегов. Например:

remove_attributes = ['style', 'font']

remove_empty_feeds = False

Если True, пустые каналы удаляются из вывода. Эта опция не действует, если parse_index переопределен в подклассе. Он предназначен только для рецептов, которые возвращают список каналов с использованием feeds или get_feeds(). Он также используется, с опцией ignore_duplicate_articles.

remove_javascript = True

Convenient flag to strip all JavaScript tags from the downloaded HTML

remove_tags = []

Список удаляемых тегов. Указанные теги удаляются из загруженного HTML. Тег задается как словарь формы:

{
name : 'tag name', #e.g. 'div'
attrs : a dictionary, #e.g. {'class': 'advertisement'}
}

Все ключи необязательны. Полное объяснение критериев поиска см. В `Beautiful Soup https://www.crummy.com/software/BeautifulSoup/bs4/doc/#searching-the-tree`__ A common example:

remove_tags = [dict(name='div', class_='advert')]

Это приведет к удалению всех тегов <div class=»advert»> и всех их дочерних элементов из загруженного HTML.

remove_tags_after = None

Удалить все теги, появляющиеся после указанного тега. Формат указания тега см. В BasicNewsRecipe.remove_tags. Например:

remove_tags_after = [dict(id='content')]

удалит все теги после первого элемента с id=»content».

remove_tags_before = None

Удалить все теги, появляющиеся перед указанным тегом. Формат указания тега см. В BasicNewsRecipe.remove_tags. Например:

remove_tags_before = dict(id='content')

удалит все теги перед первым элементом с id=»content».

requires_version = (0, 6, 0)

Минимальная версия calibre, необходимая для использования этого рецепта

resolve_internal_links = False

Если установлено значение True, то ссылки в загруженных статьях, которые указывают на другие загруженные статьи, изменяются, чтобы указывать на загруженную копию статьи, а не на ее исходный веб-URL. При установке значения True, вам также может потребоваться реализовать canonicalize_internal_url() для работы со схемой URL-адресов вашего конкретного веб-сайта.

reverse_article_order = False

Измените порядок статей в каждом фиде

scale_news_images = None

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

scale_news_images_to_device = True

Rescale images to fit in the device screen dimensions set by the output profile.
Ignored if no output profile is set.

simultaneous_downloads = 5

Количество одновременных загрузок. Установите 1, если сервер придирчив. Автоматически уменьшается до 1, если BasicNewsRecipe.delay > 0

summary_length = 500

Максимальное количество символов в кратком описании

template_css = '\n            .article_date {\n                color: gray; font-family: monospace;\n            }\n\n            .article_description {\n                text-indent: 0pt;\n            }\n\n            a.article {\n                font-weight: bold; text-align:left;\n            }\n\n            a.feed {\n                font-weight: bold;\n            }\n\n            .calibre_navbar {\n                font-family:monospace;\n            }\n    '

The CSS that is used to style the templates, i.e., the navigation bars and the Tables of Contents. Rather than overriding this variable, you should use extra_css in your recipe to customize look and feel.

timefmt = ' [%a, %d %b %Y]'

Строка формата даты, показанной на первой странице. По умолчанию: Day_Name, Day_Number Month_Name Year

timeout = 120.0

Таймаут для загрузки файлов с сервера в секундах

title = 'Неизвестный новостной ресурс'

Название для электронной книги

use_embedded_content = None

Обычно попытка угадать, есть ли в фиде встроенные полные статьи, основывается на длине встроенного контента. Если «Нет», то используется угадывание по умолчанию. Если True - всегда предполагается, что у каналов есть встроенный контент, False - всегда предполагается, что у каналов нет встроенного контента.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Просмотрщик электронных книг

Просмотрщик электронных книг

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

- Запуск просмотрщика электронных книг

- Навигация по электронной книге

- Выделение текста

- Чтение вслух

- Поиск текста

- Переход по ссылкам только с клавиатуры

- Настройка внешнего вида по вашим предпочтениям

- Поиск в словаре

- Копирование текста и изображений

- Увеличение масштаба изображений

- Синхронизация с бумажным изданием текущей книги

- Горячие клавиши

- Неперетекающий контент

- Проектирование вашей книги для хорошего просмотра в просмотрщике электронных книг calibre

Запуск просмотрщика электронных книг

Чтобы просмотреть или прочитать любую из книг в вашей библиотеке calibre, выделите её и нажмите кнопку Просмотр, и она откроется в просмотрщике. Также можно запустить сразу сам просмотрщик из меню «Пуск» в Windows. В macOS можно прикрепить её к доку и запускать оттуда, а в Linux — воспользоваться кнопкой запуска в меню рабочего стола или выполнить команду ebook-viewer.

Навигация по электронной книге

«Перелистывать страницы» книги можно:

- нажатием на левое или правое поле страницы мышкой;

- нажатием клавиш пробел, page up, page down или клавиш со стрелками;

- коснувшись текста или проведя пальцем влево и вправо — для сенсорных экранов.

Доступ к управлению просмотрщиком осуществляется:

- правым щелчком по тексту;

- нажатием клавиш Esc или Menu;

- нажатием на верхнюю треть экрана — для сенсорных экранов.

У просмотрщика есть два режима, «страничный» и «потоковый». В страничном режиме текст отображается в виде страниц, как в бумажных книгах, в потоковом — показывается непрерывно, как в веб-браузере. Переключаться между ними можно через «Параметры» просмотрщика в Настройки раздел Макет страницы или сочетанием клавиш Ctrl+M.

Закладки

Если вы, прочтя половину книги, закроете просмотрщик, он запомнит то место, где вы остановились, и вернётся к нему, когда вы откроете книгу в следующий раз. Также в книге можно делать закладки с помощью кнопки Закладки в меню управления просмотрщиком или сочетанием клавиш Ctrl+B. Для книг в формате EPUB закладки сохраняются прямо в файле EPUB. Вы можете, добавив закладки, переслать файл друзьям. Когда они его откроют, то увидят и ваши закладки. Отключается эта возможность в разделе Разное в параметрах просмотрщика.

Оглавление

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

Навигация по местоположению

Электронные книги, в отличие от бумажных, не имеют понятия о страницах. Вы можете ссылаться на точные места в электронных книгах с помощью функции Перейти → Место в элементах управления просмотрщиком.

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

Это URL-адрес, который вы можете скопировать в буфер обмена и вставить в другие программы или документы. Щелчок по этому URL-адресу откроет книгу в просмотрщике электронных книг calibre в текущем местоположении.

Если вы нажмете на ссылки внутри электронной книги, чтобы перейти к различным частям книги, например к сноске, вы можете использовать кнопки Назад и Вперед в верхнем левом углу просмотрщика. Эти кнопки ведут себя так же, как кнопки в веб-браузере.

Режим отсылок

У calibre также есть очень удобный Режим отсылок. Вы можете включить его, нажав кнопку Режим отсылок в элементах управления просмотрщиком. Как только вы это сделаете, в начале каждого абзаца будет отображаться уникальный номер, состоящий из номеров разделов и абзацев.

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

Выделение текста

Когда вы выделяете текст в средстве просмотра, рядом с выбранным фрагментом появляется небольшая всплывающая панель. Вы можете нажать кнопку выделения на этой панели, чтобы создать выделение. Вы можете добавлять заметки и изменять цвет выделения. На сенсорном экране нажмите и удерживайте слово, чтобы выбрать его и отобразить всплывающую панель. Находясь в режиме выделения, вы можете изменить выбранный текст, используя удобные ручки выбора на сенсорном экране. Перетащите маркеры к верхнему или нижнему полю для прокрутки при выборе. Вы также можете удерживать клавишу Shift+click или right click, чтобы расширить выделение, что особенно полезно для многостраничного выделения.

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

Посмотреть все выделения в библиотеке calibre можно, щёлкнув правой кнопку Просмотр и выбрав Просмотреть заметки.

Наконец, если вы пользуетесь браузерной читалкой контент-сервера calibre, то можете синхронизировать с ней заметки основной читалки, перейдя там в Параметры → Разное и введя имя пользователя читалки контент-сервера для синхронизации. Воспользуйтесь специальным значением *, чтобы синхронизироваться с анонимными пользователями.

Чтение вслух

Просмотрщик может озвучивать текст книги. Чтобы использовать его, вы можете просто нажать кнопку Читать вслух в элементах управления средства просмотра. Слово, которое в данный момент читается, будет выделено. Речь синтезируется из текста с помощью служб вашей операционной системы для преобразования текста в речь или Piper [https://github.com/rhasspy/piper] (нейронный движок преобразования текста в речь). Вы можете изменить используемый голос, щелкнув по значку шестеренки на панели, которая отображается, когда опция Читать вслух активна.

Вы также можете прочитать вслух выделенные отрывки, добавив кнопку :guilabel: Читать вслух на полосу выбора в настройках просмотрщика в разделе :guilabel:` Поведения выделения`.

Примечание

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

Поиск текста

Просмотрщик имеет очень мощные возможности поиска. Нажмите клавишу Ctrl+F или откройте элементы управления просмотрщиком и щёлкните по поиску. Простейшая форма поиска — это просто поиск любого текста, который вы вводите в текстовое поле. Различные формы поиска выбираются в поле режима поиска под полем поиска. Доступные режимы:

1. Содержит - Самый простой режим по умолчанию. Текст, введенный в поле поиска, ищется где угодно. Все знаки препинания, ударения и пробелы игнорируются. Например, поиск: Pena будет соответствовать всем следующим: penal, pen a, pen.a и Peña. Если вы установите флажок С учетом регистра, акценты, пробелы и знаки препинания больше не будут игнорироваться.

2. Целые слова - Поиск целых слов. Так, например, при поиске pena будет найдено слово Peña, но не слово Penal. Как и в случае с поиском Содержит, указанным выше, акценты и знаки препинания игнорируются, если не установлен флажок С учетом регистра.

3. Соседние слова - Ищет целые слова, которые находятся рядом друг с другом. Так, например, поисковый запрос calibre cool будет соответствовать местам, где слова calibre и cool встречаются в пределах шестидесяти символов друг от друга. Чтобы изменить количество символов, добавьте новое число в конец списка слов. Например, calibre cool awesome 120 будет соответствовать местам, где три слова встречаются в пределах 120 символов друг от друга. Обратите внимание, что знаки препинания и диакритические знаки не игнорируются при таком поиске.

4. Regex - Интерпретирует искомый текст как регулярное выражение. Чтобы узнать больше об использовании регулярных выражений, см. учебник.

Переход по ссылкам только с клавиатуры

В просмотрщике есть :guilabel: Режим подсказок, позволяющий вам нажимать ссылки в тексте без использования мыши. Нажмите клавишу Alt + F, и все ссылки на текущем экране будут выделены цифрой или буквой над ними. Нажмите букву на клавиатуре, чтобы перейти по ссылке. Нажатие клавиши :kbd: Esc прервёт режим Подсказки без выбора какой-либо ссылки.

Если на экране более тридцати пяти ссылок, то некоторые из них будут иметь несколько букв. В этом случае введите первую и вторую или первую и нажмите Enter для активации. Вы также можете использовать клавишу Backspace, чтобы исправить ошибку при наборе текста.

Настройка внешнего вида по вашим предпочтениям

Размер шрифта можно менять на лету через Размер шрифта в меню управления просмотрщика, или сочетаниями клавиш Ctrl++ и Ctrl+-, или, зажав клавишу Ctrl, крутить колёсико мыши.

Цвет можно изменить в разделе Цвета в параметрах просмотрщика.

Количество страниц на экране, а также размер полей можно изменить в разделе Макет страницы в параметрах просмотрщика.

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

Более продвинутую настройку можно выполнить с помощью настроек Стили. Здесь вы можете указать фоновое изображение для отображения под текстом, а также таблицу стилей, которая будет применяться к каждой книге. Используя CSS, вы можете изменять стили абзаца, выравнивать текст и т. д. Примеры таблиц стилей от пользователей calibre, см. на форумах [https://www.mobileread.com/forums/showthread.php?t=51500.].

Поиск в словаре

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

Копирование текста и изображений

Вы можете выбрать текст и изображения, перетаскивая содержимое мышью, а затем щёлкнув правой кнопкой мыши и выбрав Копировать для копирования в буфер обмена. Скопированный материал может быть вставлен в другое приложение в виде простого текста и изображений.

Увеличение масштаба изображений

Вы можете увеличить изображение, чтобы посмотреть его в полном размере в отдельном окне, двойным щелчком или долгим касанием. Также можно щёлкнуть по нему правой кнопкой мыши и выбрать Рассмотреть изображение.

Синхронизация с бумажным изданием текущей книги

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

Горячие клавиши

Просмотрщик имеет обширные сочетания клавиш, как и остальная часть calibre. Их можно настроить в Настройки. Умолчальные сочетания клавиш ниже:

Сочетания клавиш для просмотрщика calibre

Сочетание клавиш

Действие

Home, Ctrl+ArrowUp, Ctrl+ArrowLeft

Прокрутить до начала текущего файла в многофайловой книге

Ctrl+Home

Прокрутить до начала книги

Ctrl+End

Прокрутить до конца книги

End, Ctrl+ArrowDown, Ctrl+ArrowRight

Прокрутить до конца текущего файла в многофайловой книге

ArrowUp

Прокрутить назад, плавно в потоковом режиме и на полный экран в страничном

ArrowDown

Прокрутить вперёд, плавно в потоковом режиме и на полный экран в страничном

ArrowLeft

Прокрутить левее: слегка — в потоковом режиме и на страницу — в страничном

ArrowRight

Прокрутить правее: слегка — в потоковом режиме и на страницу — в страничном

PageUp, Shift+Spacebar

Прокрутить на полный экран назад

PageDown, Spacebar

Прокрутить на полный экран вперёд

Ctrl+PageUp

Перейти к предыдущему разделу

Ctrl+PageDown

Перейти к следующему разделу

Alt+ArrowLeft

Назад

Alt+ArrowRight

Вперёд

Ctrl+T

Переключить оглавление

Ctrl+S

Чтение вслух

Alt+P

Быстро меняйте настройки, создавая и переключаясь на profiles

Alt+f

Переходить по ссылкам с клавиатуры

Ctrl+C

Копировать в буфер

Alt+C

Копировать текущее местоположение в буфер обмена

Ctrl+Shift+C

Скопировать текущее местоположение как calibre:// URL в буфер обмена

/, Ctrl+f, Cmd+f

Начать поиск

F3, Enter

Найти следующее

Shift+F3, Shift+Enter

Найти предыдущее

Ctrl+Plus, Meta+Plus

Увеличить размер шрифта

Ctrl+Minus, Meta+Minus

Уменьшить размер шрифта

Ctrl+0

Восстановить размер шрифта по умолчанию

Ctrl+]

Увеличить количество страниц на экране

Ctrl+[

Уменьшить количество страниц на экране

Ctrl+Alt+C

Автоматически определять количество страниц на экране

F11, Ctrl+Shift+F

Переключить полноэкранный режим

Ctrl+M

Переключиться между страничным и потоковым режимами

Ctrl+W

Переключить полосу прокрутки

Ctrl+X

Переключить режим отсылок

Ctrl+B

Показать/скрыть закладки

Ctrl+Alt+B

Новая закладка

Ctrl+N, Ctrl+E

Показать метаданные книги

Ctrl+Alt+F5, Ctrl+Alt+R

Перезагрузить книгу

Ctrl+Shift+ArrowRight

Изменить текущее выделение на слово вперёд

Ctrl+Shift+ArrowLeft

Изменить текущее выделение на слово назад

Shift+ArrowRight

Изменить текущий выбор вперед на символ

Shift+ArrowLeft

Изменить текущий выбор назад на символ

Shift+ArrowDown

Изменить текущий выбор на строку вперед

Shift+Home

Расширить текущий выбор до начала строки

Shift+End

Расширить текущий выбор до конца строки

Ctrl+A

Выделить всё

Shift+ArrowUp

Изменить текущий выбор назад на строку

Ctrl+Shift+ArrowDown

Изменить текущий выбор вперед на абзац

Ctrl+Shift+ArrowUp

Изменить текущий выбор назад на абзац

Esc, MenuKey

Показать элементы управления просмотрщиком электронных книг

Ctrl+Comma, Ctrl+Esc, Meta+Esc, Meta+Comma

Показать параметры просмотрщика электронных книг

Ctrl+G, ;, :

Перейти к определённому месту в книге

Ctrl+Spacebar

Переключить автопрокрутку

Alt+ArrowUp

Ускорить автопрокрутку

Alt+ArrowDown

Замедлить автопрокрутку

Ctrl+I

Показать/скрыть инспектор

Ctrl+L

Показать/скрыть панель поиска слов

Ctrl+Q (Cmd+Q on macOS)

Выйти

Ctrl+P

Печать книги в PDF

Ctrl+F11

Переключить панель инструментов

Ctrl+H

Переключить панель выделений

Ctrl+D

Редактировать эту книгу

Неперетекающий контент

Некоторые книги имеют очень широкий контент, который невозможно разбить по границам страницы. Например, таблицы или теги <pre>. В таких случаях вам следует переключить просмотрщик в режим потока, нажав Ctrl + M, чтобы прочитать это содержимое. В качестве альтернативы можно добавить следующий CSS-код в раздел Styles настройках просмотрщика, для разбивания строк текста в тегах <pre>:

code, pre { white-space: pre-wrap }

Проектирование вашей книги для хорошего просмотра в просмотрщике электронных книг calibre

Просмотрщик calibre установит класс is-calibre-viewer для корневого элемента. Поэтому можно написать правила CSS, применимые только к нему. Кроме того, просмотрщик установит следующие классы для элемента body:

body.calibre-viewer-dark-colors

Set when using a dark color scheme

body.calibre-viewer-light-colors

Set when using a light color scheme

body.calibre-viewer-paginated

Устанавливается в постраничном режиме

body.calibre-viewer-scrolling

Устанавливается в потоковом режиме (без разбивки на страницы)

body.calibre-footnote-container

Установить при отображении всплывающей сноски

Наконец, вы можете использовать цвета цветовой схемы calibre через CSS-переменные [https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties]. Просмотрщик calibre определяет следующие переменные: –calibre-viewer-background-color`, --calibre-viewer-foreground-color и необязательно --calibre-viewer-link-color в цветовых темах, определяющих цвет ссылки.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Конвертирование книг

Конвертирование книг

Calibre имеет очень простую в использовании систему конвертации. Обычно вы просто добавляете книгу в Calibre, нажимаете «конвертировать», и Calibre изо всех сил старается генерировать выходные данные, максимально приближенные к входным. Тем не менее, Calibre принимает очень большое количество входящих форматов, не все из которых подходят для конвертации в электронные книги. В случае таких входящих форматов или если вы просто хотите лучше контролировать систему конвертации, у Calibre есть много опций для точной настройки процесса конвертации. Однако обратите внимание, что система конвертации Calibre не заменяет полноценного редактора электронных книг. Чтобы редактировать электронные книги, я рекомендую сначала преобразовать их в EPUB или AZW3 с использованием Calibre, а затем с помощью функции Edit book, привести их в идеальную форму. Затем вы можете использовать отредактированную электронную книгу в качестве входящих данных для конвертации в другие форматы в Calibre.

Этот документ, в основном, будет ссылаться на настройки, находящиеся в диалоге конвертации, изображённом ниже. Все эти настройки также доступны через интерфейс командной строки для конвертации, задокументированный в ebook-convert. В calibre можно получить справку по любому отдельному параметру, удерживая над ним курсор мыши до появления подсказки с описанием.

Содержание

- Предисловие

- Оформление

- Настройка страницы

- Эвристическая обработка

- Поиск и замена

- Определение структуры

- Содержание

- Использование изображений в качестве заголовков глав при преобразовании входных документов HTML

- Использование атрибутов тега для предоставления текста для записей в оглавлении

- Как параметры устанавливаются/сохраняются для конвертации

- Формат-специфичные советы

Предисловие

Первое, что нужно понять о системе конвертации, это то, что она спроектирована как конвейер. Схематически это выглядит так:

Формат ввода сначала конвертируется в XHTML с помощью соответствующего плагина ввода. Этот HTML затем трансформируется. На последнем шаге обработанный XHTML преобразуется в указанный формат вывода соответствующим плагином вывода. Результаты преобразования могут сильно различаться в зависимости от формата ввода. Список лучших исходных форматов для конвертации доступен: ref:here<best-source-formats>.

Конвертации, которые действуют на вывод XHTML, - это место, где происходит вся работа. Существуют различные конвертации: вставка метаданных книги в виде страницы в начало книги, обнаружение заголовков глав и автосоздание оглавления, пропорциональная настройка размеров шрифта и т. д. Все конвертации действуют на вывод XHTML с помощью плагина ввода, а не на сам входной файл. При конвертации calibre-ом RTF-файла в EPUB, сначала он конвертируется во внутренний XHTML, к XHTML применяются различные конвертации, а затем плагин Вывод создаёт файл EPUB, автоматически генерируя для файла все метаданные, оглавление и т. д.

Вы можете увидеть этот процесс в действии, используя параметр отладки . Укажите путь к папке для вывода отладки. Во время конвертации calibre помещает XHTML, сгенерированный на различных этапах конвейера конвертации, в разные подпапки. Четыре подпапки:

Этапы конвейера конвертации

Папка

Описание

ввод

Содержит HTML-вывод плагина ввода. Для отладки плагинов ввода.

разобранный

Результат предварительной обработки и конвертации в XHTML вывода из плагина ввода. Для отладки детектирования структуры.

структура

После обнаружения структуры, но перед сведением CSS и преобразованием размеров шрифта. Для отладки конвертации размера шрифта и преобразований CSS.

обработано

Перед тем как книга передается в плагин вывода. Для отладки плагинов вывода.

Если вы хотите немного отредактировать входной документ до того, как calibre преобразовал его, лучше всего отредактировать файлы в подпапке input, затем заархивировать его и использовать ZIP-файл в качестве входного формата для последующих преобразований. Для этого используйте диалоговое окно Редактировать метаданные, чтобы добавить ZIP-файл в качестве формата для книги, а затем в верхнем левом углу диалогового окна конвертации выберите ZIP в качестве входного формата.

Этот документ будет посвящен главным образом различным преобразованиям, которые работают с промежуточным XHTML, и тому, как их контролировать. В конце приведены некоторые советы, специфичные для каждого формата ввода/вывода.

Оформление

Содержание

- Шрифты

- Текст

- Макет

- Стилизация

- Стили трансформации

- Трансформировать HTML

Эта группа параметров контролирует различные аспекты внешнего вида конвертированной электронной книги.

Шрифты

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

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

Обычно calibre автоматически выбирает основной размер шрифта, соответствующий выбранному вами выходному профилю (см .:ref:page-setup). Значение по умолчанию можно переопределить здесь.

Опция Font size key позволяет контролировать масштабирование размеров неосновных шрифтов. Алгоритм масштабирования шрифта работает с использованием ключа размера шрифта (просто список разделенных запятыми размеров шрифтов). Ключ размера шрифта сообщает calibre, на сколько «шагов» больше или меньше данного размера шрифта следует сравнивать с базовым размером шрифта. В документе должно быть ограниченное количество размеров шрифта. Один размер для основного текста, несколько размеров для разных уровней заголовков и несколько размеров для индексов super/sub и сносок. Ключ размера шрифта позволяет calibre разделить размеры шрифта во входных документах на отдельные «ячейки», соответствующие различным логическим размерам шрифта.

Давайте проиллюстрируем на примере. Предположим, исходный документ, который мы конвертируем, был создан кем-то с отличным зрением и имеет базовый размер шрифта 8pt. Это означает, что основная часть текста в документе имеет размер 8 пт, а заголовки несколько больше (скажем, 10 и 12 пт), а сноски несколько меньше - 6 пт. Теперь, если мы используем следующие настройки:

Base font size : 12pt
Font size key : 7, 8, 10, 12, 14, 16, 18, 20

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

New font size key : 7, 9, 12, 14, 18, 20, 22

Самые большие заголовки теперь станут 18pt, а сноски - 9pt. Вы можете поиграть с этими настройками, чтобы попытаться выяснить, что будет оптимальным для вас, используя мастер изменения размера шрифта, к которому можно перейти, нажав маленькую кнопку рядом с настройкой guilabel:Font size key.

Здесь также можно отключить изменение размера шрифта при преобразовании, если вы хотите сохранить размеры шрифта во входном документе.

Связанная настройка Высота строки. Высота строки управляет вертикальной высотой строк. По умолчанию (высота строки 0) никакие манипуляции с высотой строки не выполняются. Если вы укажете значение, отличное от значения по умолчанию, высота строк будет установлена во всех местоположениях, которые не указывают свои собственные высоты строк. Тем не менее, это что-то вроде тупого оружия и должно использоваться с осторожностью. Если вы хотите настроить высоту строки для какого-то раздела ввода, лучше использовать Extra CSS.

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

Текст

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

Вы также можете сказать calibre :guilabel: Smarten пунктуация, который заменит простые кавычки, тире и многоточия их типографически правильными альтернативами. Обратите внимание, что этот алгоритм не идеален, поэтому стоит проверить результаты. Также доступно обратное, а именно: guilabel:Unsmarted пунктуация.

Наконец, есть Кодировка входящих символов. В старых документах иногда не указывается кодировка символов. При конвертировании это может привести к повреждению неанглийских символов или специальных символов, таких как смарт-кавычки. Calibre пытается автоматически определить кодировку символов исходного документа, но это не всегда удается. Вы можете заставить его принять определенную кодировку символов с помощью этого параметра. cp1252 - это обычная кодировка документов, созданных с использованием программного обеспечения Windows. Вы также должны прочитать Как конвертировать мой файл, если он включает не английские символы или «умные» кавычки?, чтобы узнать больше о проблемах кодирования.

Макет

Обычно абзацы в XHTML рендерятся с пустой строкой между ними без начального отступа текста. Calibre может конролировать это несколькими вариантами. Убрать интервалы между абзацами принудительно убедиться, что у всех абзацев нет интервала между абзацами. Также устанавливается отступ текста на 1,5em (можно изменить), чтобы отметить начало каждого абзаца. :guilabel: Вставить пустую строку делает противоположное, гарантируя, что между каждой парой абзацев ровно одна пустая строка. Обе эти опции всеобъемлющие, убирают пробел или вставляют его для всех абзацев (технически <p> и <div> тегов). Это делается для того, чтобы вы могли просто установить параметр и быть уверенным, что он работает так, как объявлено, независимо от того, насколько грязен входной файл. Единственное исключение - когда входной файл использует жесткие разрывы строк для реализации межпараграфного интервала.

Если вы хотите удалить интервал между всеми абзацами, кроме нескольких избранных, не используйте эти параметры. Вместо этого добавьте следующий код CSS в Extra CSS:

p, div { margin: 0pt; border: 0pt; text-indent: 1.5em }
.spacious { margin-bottom: 1em; text-indent: 0pt; }

Затем в исходном документе отметьте абзацы, требующие интервала, с помощью class=»spacious». Если ваш входной документ не в формате HTML, используйте опцию Debug, описанную во введении, чтобы получить HTML (используйте подпапку input).

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

Стилизация

Опция Extra CSS позволяет вам указать произвольный CSS, который будет применяться ко всем HTML-файлам во входных данных. Этот CSS-код применяется с очень высоким приоритетом и поэтому должен перекрывать большую часть CSS-кода, присутствующего во входном документе. Вы можете использовать этот параметр для точной настройки презентации/макета вашего документа. Например, если вы хотите, чтобы все абзацы класса endnote были выровнены по правому краю, просто добавьте

.endnote { text-align: right }

или если вы хотите изменить отступ всех абзацев:

p { text-indent: 5mm; }

Extra CSS - очень мощный вариант, но вам нужно понять, как работает CSS, чтобы использовать его в полной мере. Вы можете использовать опцию конвейера отладки, описанную выше, чтобы увидеть, какой CSS присутствует во входном документе.

Более простой вариант - использовать Фильтровать информацию о стиле. Это позволяет удалить все свойства CSS указанных типов из документа. Например, вы можете использовать его, чтобы удалить все цвета или шрифты.

Стили трансформации

Это самое мощное средство для стилизации. Вы можете использовать его для определения правил, которые меняют стили в зависимости от условий. Например, вы можете использовать его, чтобы изменить все входения зелёного цвета на синий, или удалить все полужирные стили из текста или раскрасить все заголовки определённым цветом и т.д.

Трансформировать HTML

Подобно стилям трансформации, но позволяет вносить изменения в HTML-содержимое книги. Вы можете заменить один тег другим, добавить классы или другие атрибуты к тегам в зависимости от их содержимого и т. д.

Настройка страницы

Параметры Page setup предназначены для управления макетом экрана, например, полями и размерами экрана. Существуют варианты настройки полей страницы, которые будут использоваться плагином вывода, если выбранный формат вывода поддерживает поля страницы. Кроме того, вы должны выбрать Входной профиль и Выходной профиль. Оба набора профилей в основном имеют дело с тем, как интерпретировать измерения в документах ввода/вывода, размеры экрана и стандартные ключи изменения масштаба шрифта.

Если вы знаете, что файл, который вы конвертируете, предназначен для использования на определённом устройстве/программной платформе, выберите соответствующий профиль ввода, в противном случае просто выберите профиль ввода по умолчанию. Если вы знаете, что создаваемые вами файлы предназначены для определённого типа устройства, выберите соответствующий выходной профиль. В противном случае выберите один из общих выходных профилей. Если вы конвертируете в MOBI или AZW3, выбирайте один из выходных профилей Kindle. В противном случае для современных устройств для чтения электронных книг лучше всего выбрать выходной профиль Generic e-ink HD.

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

Эвристическая обработка

Эвристическая обработка предоставляет множество функций, которые можно использовать для обнаружения и исправления типичных проблем в плохо отформатированных входных документах. Используйте эти функции, если ваш входной документ страдает от плохого форматирования. Поскольку эти функции основаны на общих шаблонах, имейте в виду, что в некоторых случаях один из вариантов может привести к худшим результатам, поэтому используйте его с осторожностью. В качестве примера, некоторые из этих опций будут удалять все объекты без пробелов или могут включать ложно-положительные совпадения, относящиеся к функции.

Включить эвристическую обработку

Эта опция активирует Эвристическая обработка конвейера конвертирования. Должно быть включено для применения различных субфункций

Развернуть строки

Включение этой опции приведет к тому, что calibre попытается обнаружить и исправить жесткие разрывы строк, существующие в документе, используя знаки препинания и длину строки. Calibre сначала попытается определить, существуют ли жесткие разрывы строк, если они, по-видимому, не существуют, calibre не будет пытаться развернуть строки. Коэффициент развертывания строки может быть уменьшен, если вы хотите «заставить» calibre развернуть строки.

Коэффициент разворачивания строки

Эта опция контролирует алгоритм, используемый calibre для удаления жёстких разрывов строк. Например, если значение этого параметра равно 0,4, это означает, что calibre удалит жёсткие разрывы строк в конце строк, длина которых меньше, чем длина 40% всех строк в документе. Если в вашем документе есть только несколько разрывов строк, которые нуждаются в исправлении, то это значение следует уменьшить до значения между 0,1 и 0,2.

Обнаружение и разметка неформатированных заголовков и подзаголовков глав

Если ваш документ не имеет заголовков и заголовков глав, отформатированных иначе, чем остальная часть текста, calibre может использовать эту опцию, чтобы попытаться обнаружить их и окружить их тегами заголовков. <h2> теги используются для заголовков глав; <h3> теги используются для любых обнаруженных заголовков.

Эта функция не будет создавать TOC, но во многих случаях она приведет к тому, что настройки обнаружения глав по умолчанию для calibre будут правильно определять главы и создавать TOC. Настройте XPath в разделе «Определение структуры», если оглавление не создается автоматически. Если в документе нет других заголовков, тогда настройка «//h:h2» в разделе «Определение структуры» будет самым простым способом создания оглавления для документа.

Вставленные заголовки не отформатированы, чтобы применить форматирование, используйте параметр Extra CSS в настройках преобразования Look and Feel. Например, чтобы центрировать теги заголовков, используйте следующее:

h2, h3 { text-align: center }

Перенумеровать последовательности <h1> или <h2> теги

Некоторые издатели форматируют заголовки глав, используя несколько <h1> или <h2> тегов последовательно. Настройки конвертации по умолчанию приведут к тому, что такие заголовки будут разделены на две части. Эта опция будет перенумеровывать теги заголовков, чтобы предотвратить расщепление.

Удалить пустые строки между абзацами

Эта опция заставит calibre анализировать пустые строки, включенные в документ. Если каждый абзац перемежается пустой строкой, тогда calibre удалит все эти пустые абзацы. Последовательности из нескольких пустых строк будут считаться разрывами сцены и сохраняться как один абзац. Этот параметр отличается от параметра Удалить интервал между абзацами в разделе Look and Feel тем, что он фактически изменяет содержимое HTML, в то время как другой параметр изменяет стили документа. Эта опция также может удалить абзацы, которые были вставлены с помощью опции calibre Insert blank line.

Убедиться, что разрывы сцен последовательно отформатированы

С помощью этой опции calibre попытается обнаружить общие маркеры разрыва сцены и убедиться, что они выровнены по центру. «Мягкие» маркеры разрыва сцены, т. е. Разрывы сцены, определяемые только дополнительным пробелом, имеют стиль, гарантирующий, что они не будут отображаться вместе с разрывами страниц.

Replace scene breaks

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

В общем, вы должны избегать использования тегов HTML, calibre отбрасывает любые теги и использует предопределенную разметку. <hr /> теги, то есть горизонтальные линейки, и <img> теги являются исключениями. Горизонтальные линейки можно опционально указывать с помощью стилей, если вы решите добавить свой собственный стиль, обязательно включите параметр „width“, иначе информация о стиле будет отброшена. Теги изображений могут использоваться, но calibre не предоставляет возможность добавлять изображение во время преобразования, это необходимо сделать после, используя функцию «Редактировать книгу».

Пример тега изображения (поместите изображение в папку „Images“ внутри EPUB после преобразования):

<img style=»width:10%» src=»../Images/scenebreak.png» />

Пример горизонтальной линейки со стилями:

<hr style=»width:20%;padding-top: 1px;border-top: 2px ridge black;border-bottom: 2px groove black;»/>

Remove unnecessary hyphens

calibre будет анализировать весь контент со знаками переноса в документе, когда эта опция включена. Сам документ используется в качестве словаря для анализа. Это позволяет точно удалять дефисы для любых слов в документе на любом языке, наряду с вымышленными и неясными научными словами. Основной недостаток - слова, появляющиеся только один раз в документе, не будут изменены. Анализ происходит в два прохода, первый проход анализирует окончания строки. Строки развертываются только в том случае, если в документе есть слово с дефисом или без него. Во втором проходе анализируются все дефисные слова по всему документу, дефисы удаляются, если слово существует в другом месте документа без совпадения.

Выделять общие слова и шаблоны курсивом

Когда включено, calibre будет искать общие слова и образцы, которые обозначают курсив и выделять их курсивом. Примерами являются обычные текстовые соглашения, такие как ~слово~ или фразы, которые обычно должны быть выделены курсивом, например латинские фразы типа „etc.“ или „et cetera“.

Заменить существующие отступы CSS-отступами

В некоторых документах используется соглашение об определении отступов текста с использованием неразрывных пробелов. Когда эта опция включена, calibre будет пытаться обнаружить этот вид форматирования и преобразовать их в 3% текстовый отступ, используя CSS.

Поиск и замена

Эти параметры полезны главным образом для конвертирования документов PDF или преобразования OCR, хотя их также можно использовать для устранения многих специфических проблем документов. Например, некоторые преобразования могут оставлять верхние и нижние колонтитулы в тексте. Эти параметры используют регулярные выражения, чтобы попытаться обнаружить заголовки, нижние колонтитулы или другой произвольный текст и удалить или заменить их. Помните, что они работают на промежуточном XHTML, созданном конвейером конвертации. Существует мастер, который поможет вам настроить регулярные выражения для вашего документа. Нажмите волшебную палочку рядом с полем выражения и нажмите кнопку «Тест» после составления поискового выражения. Успешные совпадения будут выделены желтым цветом.

Поиск работает с использованием регулярных выражений Python. Весь подобранный текст просто удаляется из документа или заменяется с помощью шаблона замены. Шаблон замены является необязательным, если оставить его пустым, текст, соответствующий шаблону поиска, будет удален из документа. Вы можете узнать больше о регулярных выражениях и их синтаксисе по адресу Всё об использования регулярных выражений в calibre.

Определение структуры

Обнаружение структуры заставляет calibre, пытаться обнаружить структурные элементы во входном документе, когда они не определены должным образом. Например, главы, разрывы страниц, верхние и нижние колонтитулы и т. д. Как вы можете себе представить, этот процесс сильно варьируется от книги к книге. К счастью, у calibre есть очень мощные возможности для управления этим. С силой приходит сложность, но если вы потратите время на изучение сложности, вы найдете, что она того стоит.

Главы и разрывы страниц
calibre имеет два набора параметров для определение главы и вставка разрывов страниц. Иногда это может немного сбивать с толку, так как по умолчанию calibre вставляет разрывы страниц перед обнаруженными главами, а также местоположения, определяемые параметром разрывов страниц. Причиной этого является то, что часто есть места, где следует вставлять разрывы страниц, которые не являются границами глав. Кроме того, обнаруженные главы могут быть дополнительно вставлены в автоматически сгенерированное оглавление.

calibre использует XPath, мощный язык, позволяющий пользователю определять границы глав/разрывы страниц. Поначалу XPath может показаться немного сложным, к счастью, в руководстве пользователя есть Учебник по XPath. Помните, что определение структуры работает на промежуточном XHTML, созданном конвейером конвертации. Используйте опцию отладки, описанную в Предисловие, чтобы выяснить значение соответствующих настроек для вашей книги. Существует также кнопка для мастера XPath, которая поможет с генерацией простых выражений XPath.

По умолчанию calibre использует следующее выражение для определения глав:

//*[((name()='h1' or name()='h2') and re:test(., 'chapter|book|section|part\s+', 'i')) or @class = 'chapter']

Это выражение довольно сложное, потому что оно пытается обрабатывать несколько общих случаев одновременно. Это означает, что calibre будет предполагать, что главы начинаются с тегов <h1> или <h2>, в которых есть любое из слов (глава, книга, раздел или часть) chapter, book, section or part или которые имеют атрибут class=»chapter».

Связанная опция Метка главы, которая позволяет вам контролировать, что делает calibre при обнаружении главы. По умолчанию он вставляет разрыв страницы перед главой. Вы можете добавить вставленную линию вместо этого или в дополнение к разрыву страницы. Вы также можете этого не делать.

Настройка по умолчанию для обнаружения разрывов страниц:

//*[name()='h1' or name()='h2']

это означает, что calibre будет вставлять разрывы страниц перед каждым тегом <h1> и <h2> по умолчанию.

Примечание

Выражения по умолчанию могут меняться в зависимости от формата ввода, который вы конвертируете.

Разное

В этом разделе есть еще несколько опций.

Вставить метаданные как страницу в начале книги

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

Удалить первое изображение

Иногда исходный документ, который вы конвертируете, включает обложку как часть книги, а не как отдельную обложку. Если вы также укажете обложку calibre, то в конвертированной книге будет две обложки. Эта опция просто удалит первое изображение из исходного документа, тем самым гарантируя, что конвертированная книга имеет только одну обложку, ту, которая указана в calibre.

Содержание

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

Примечание

Использование этих опций может быть немного сложным, чтобы получилось абсолютно правильно. Если вы предпочитаете создавать/редактировать оглавление вручную, преобразуйте его в форматы EPUB или AZW3 и установите флажок в нижней части раздела «Оглавление» диалогового окна преобразования, которое гласит :guilabel: «Вручную настроить таблицу содержания после конвертации`. Запустится редактор таблицы содержания после конвертации. Он позволит вам создавать записи в оглавлении, просто щёлкнув место в книге, на которое будет указывать запись. Вы также можете использовать Редактор оглавления самостоятельно, без конвертации. Перейдите по адресу Настройки →  Интефейс →  Панели инструментов и добавьте ToC редактор на главную панель инструментов. Затем просто выберите книгу, которую вы хотите отредактировать, и нажмите кнопку ToC редактор.

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

Способ создания автоматически сгенерированного оглавления по умолчанию заключается в том, что calibre сначала попытается добавить любые обнаруженные главы в сгенерированное оглавление. Вы можете узнать, как настроить обнаружение глав в разделе Определение структуры выше. Если вы не хотите включать обнаруженные главы в сгенерированное оглавление, отметьте опцию Не добавлять обнаруженные главы.

Если было обнаружено меньше глав, чем число Порог главы, то calibre добавит любые гиперссылки, найденные во входном документе, в оглавление. Это часто хорошо работает, многие входные документы включают в себя содержание с гиперссылкой в самом начале. Опция Количество ссылок может использоваться для управления этим поведением. Если установлено в ноль, ссылки не добавляются. Если установлено значение больше нуля, добавляется самое большое количество ссылок.

calibre будет автоматически фильтровать дубликаты из сгенерированного содержания. Однако, если есть некоторые дополнительные нежелательные записи, вы можете отфильтровать их, используя опцию TOC Filter. Это регулярное выражение, которое будет соответствовать заголовку записей в сгенерированном оглавлении. Всякий раз, когда совпадение найдено, оно будет удалено. Например, чтобы удалить все записи заголовков «Next» или «Previous», используйте:

Next|Previous

Опции Уровень 1,2,3 TOC позволяют вам создавать сложные многоуровневые оглавления. Это выражения XPath, которые соответствуют тегам в промежуточном XHTML, созданном конвейером преобразования. Чтобы узнать, как получить доступ к этому XHTML, смотрите Предисловие. Также прочитайте Учебник по XPath, чтобы узнать, как создавать выражения XPath. Рядом с каждым параметром находится кнопка, которая запускает мастер, помогающий создавать основные выражения XPath. В следующем простом примере показано, как использовать эти параметры.

Предположим, у вас есть входной документ с XHTML, который выглядит следующим образом:

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Sample document</title>
</head>
<body>
<h1>Chapter 1</h1>
...
<h2>Section 1.1</h2>
...
<h2>Section 1.2</h2>
...
<h1>Chapter 2</h1>
...
<h2>Section 2.1</h2>
...
</body>
</html>

Затем мы устанавливаем параметры как:

Level 1 TOC : //h:h1
Level 2 TOC : //h:h2

Это приведет к автоматически сгенерированному двухуровневому оглавлению, которое выглядит следующим образом:

Chapter 1
Section 1.1
Section 1.2
Chapter 2
Section 2.1

Предупреждение

Не все выходные форматы поддерживают многоуровневое оглавление. Сначала вы должны попробовать с выходом EPUB. Если это работает, попробуйте ваш формат выбора.

Использование изображений в качестве заголовков глав при преобразовании входных документов HTML

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

<html>
<body>
<h2>Chapter 1</h2>
<p>chapter 1 text...</p>
<h2 title="Chapter 2"><img src="chapter2.jpg" /></h2>
<p>chapter 2 text...</p>
</body>
</html>

Установите для Уровень 1 TOC значение //h:h2. Затем, во второй главе, calibre получит заголовок из значения атрибута title тега <h2>, поскольку у тега нет текста.

Использование атрибутов тега для предоставления текста для записей в оглавлении

Если у вас особенно длинные заголовки глав и вам нужны сокращенные версии в оглавлении, вы можете использовать атрибут title для достижения этого, например:

<html>
<body>
<h2 title="Chapter 1">Chapter 1: Some very long title</h2>
<p>chapter 1 text...</p>
<h2 title="Chapter 2">Chapter 2: Some other very long title</h2>
<p>chapter 2 text...</p>
</body>
</html>

Установите для Level 1 TOC значение //h:h2/@title. Тогда calibre получит заголовок из значения атрибута title в тегах <h2> вместо использования текста внутри тега. Обратите внимание на завершающий /@title в выражении XPath, вы можете использовать эту форму, чтобы указать calibre, что нужно получить текст из любого понравившегося вам атрибута.

Как параметры устанавливаются/сохраняются для конвертации

Есть два места, где параметры конвертации могут быть установлены в calibre. Первый находится в Предпочтения-> Конверсия. Эти настройки являются настройками по умолчанию для параметров конвертации. Всякий раз, когда вы пытаетесь сконвертировать новую книгу, настройки, установленные здесь, будут использоваться по умолчанию.

Вы также можете изменить настройки в диалоге конвертации для каждой конвертации книги. Когда вы конвертируете книгу, calibre запоминает настройки, которые вы использовали для этой книги, так что если вы конвертируете её снова, сохраненные настройки для отдельной книги будут иметь приоритет над значениями по умолчанию, установленными в Настройки. Вы можете восстановить отдельные настройки по умолчанию, используя кнопку Восстановить настройки по умолчанию в диалоге конвертации отдельной книги. Вы можете удалить сохраненные настройки для группы книг, выбрав все книги и нажав кнопку Редактировать метаданные, чтобы открыть диалоговое окно массового редактирования метаданных, в нижней части диалогового окна есть опция для удаления настроек сохраненной конвертации.

Когда вы массово конвертируете набор книг, настройки берутся в следующем порядке (выигрывает последняя):

- Из значений по умолчанию, установленных в Настройки->Конвертация

- Из сохраненных настроек конвертации для каждой конвертируемой книги (если есть). Это можно отключить с помощью параметра в верхнем левом углу диалогового окна «Массовая конвертация».

- Из настроек, установленных в диалоговом окне Массовая конвертация

Обратите внимание, что окончательные настройки для каждой книги в Массовой конвертации будут сохранены и использованы повторно, если книга будет преобразована снова. Так как наивысший приоритет в массовой конвертации преобразовании отдается настройкам в диалоговом окне «Массовая конвертация», они переопределяют любые параметры, специфичные для книги. Таким образом, вы должны конвертировать только те книги, которые нуждаются в похожих настройках. Исключение составляют метаданные и специфичные для входного формата настройки. Поскольку в диалоговом окне «Массовая конвертация» нет настроек для этих двух категорий, они будут взяты из настроек книги (если они есть) или значений по умолчанию.

Примечание

Вы можете увидеть фактические настройки, использованные во время любой конвертации, щелкнув вращающийся значок в правом нижнем углу, а затем дважды щёлкнув на отдельном задании конвертации. Это вызовет журнал конвертаций, который будет содержать фактические используемые настройки в верхней части.

Формат-специфичные советы

Здесь вы найдете советы, касающиеся конвертации определенных форматов. Параметры, специфичные для конкретного формата, независимо от того, доступны ли вход или выход в диалоге преобразования в их собственном разделе, например, TXT input или` EPUB output`.

Конвертация документов Microsoft Word
calibre может автоматически конвертировать файлы .docx, созданные в Microsoft Word 2007 и новее. Просто добавьте файл в calibre и нажмите «Конвертировать».

Примечание

Существует :download_file:`demo .docx file <demos/demo.docx>, демонстрирующий возможности механизма конвертации calibre. Просто скачайте его и конвертируйте в EPUB или AZW3, чтобы узнать на что способен calibre.

calibre автоматически сгенерирует оглавление на основе заголовков, если вы пометите свои заголовки стилями «Заголовок 1», «Заголовок 2» и т. д. в Microsoft Word. Откройте выходную электронную книгу в просмотрщике электронных книг calibre и нажмите кнопку Оглавление, чтобы просмотреть сгенерированное оглавление.

Старые файлы .doc

Для старых файлов .doc вы можете сохранить документ в формате HTML с помощью Microsoft Word, а затем преобразовать полученный HTML-файл в calibre. При сохранении в формате HTML обязательно используйте параметр «Сохранить как веб-страницу, отфильтрованный», поскольку при этом будет получен чистый HTML, который будет хорошо конвертироваться. Обратите внимание, что Word создает грязный HTML, его преобразование может занять много времени, так что наберитесь терпения. Если у вас есть более новая версия Word, вы также можете напрямую сохранить файл в формате docx.

Другой альтернативой является использование бесплатного OpenOffice/LibreOffice. Откройте файл .doc в OpenOffice/LibreOffice и сохраните его в формате .docx, которыей может напрямую конвертироваться в calibre

Конвертировать TXT документы

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

Ввод TXT поддерживает ряд опций, позволяющих различать способы обнаружения абзацев.

Стиль абзаца: Авто

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

Стиль абзаца: Блок

Предположить, что одна или несколько пустых строк являются границей абзаца:

This is the first.

This is the
second paragraph.

Стиль абзаца: Одиночный

Предположить, что каждая строка - абзац:

This is the first.
This is the second.
This is the third.

Стиль абзаца: Печать

Предположить, что каждый абзац начинается с отступа (либо табуляция, либо 2+ пробелов). Абзацы заканчиваются, когда достигается следующая строка, начинающаяся с отступа:

This is the
first.
This is the second.

This is the
third.

Стиль абзаца: Неформатированный

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

Стиль форматирования: Авто

Попытаться определить тип используемой разметки форматирования. Если разметка не используется, будет применено эвристическое форматирование.

Стиль форматирования: Эвристика

Анализировать документ на наличие общих заголовков глав, разрывов сцен и выделенных курсивом слов и применить соответствующую разметку HTML во время конвертирования.

Стиль форматирования: Markdown

calibre также поддерживает запуск ввода TXT через препроцессор преобразования, известный как Markdown. Markdown позволяет добавлять базовое форматирование в документы TXT, такие как полужирный шрифт, курсив, заголовки разделов, таблицы, списки, оглавление и т. д. Пометить заголовки глав начальным # и установить для выражения обнаружения XPath главы значение «//h:h1 «- это самый простой способ получить правильное оглавление, сгенерированное из документа TXT. Вы можете узнать больше о синтаксисе Markdown в daringfireball [https://daringfireball.net/projects/markdown/syntax].

Стиль форматирования: Нет

К тексту не применяется никакого специального форматирования, документ конвертируется в HTML без каких-либо других изменений.

Преобразование PDF документов

PDF документы - один из худших форматов для конвертации. Они имеют фиксированный размер страницы и формат размещения текста. То есть очень трудно определить, где заканчивается один абзац и начинается другой. calibre попытается развернуть абзацы с помощью настраиваемого Line Un-Wrapping Factor. Это шкала, используемая для определения длины, на которой должна быть развернута строка. Допустимые значения: десятичное число от 0 до 1. Значение по умолчанию - 0,45, чуть меньше длины средней строки. Уменьшите это значение, чтобы включать больше текста в развертывание. Увеличьте, чтобы включать меньше. Вы можете отрегулировать это значение в настройках преобразования в разделе PDF Input.

Кроме того, их верхние и нижние колонтитулы часто включаются в основной текст документа. Используйте панель Поиск и замена для удаления верхних и нижних колонтитулов, чтобы смягчить эту проблему. Если верхние и нижние колонтитулы не удалены из текста, это может привести к разворачиванию абзаца. Чтобы узнать, как использовать параметры удаления верхнего и нижнего колонтитула, прочитайте Всё об использования регулярных выражений в calibre.

Некоторые ограничения ввода с PDF:

- Сложные документы с несколькими столбцами и изображениями не поддерживаются.

- Извлечение векторных изображений и таблиц из документа также не поддерживается

- В некоторых PDF-файлах используются специальные глифы для обозначения ll или ff или fi и т. д. Конвертация может их обработать или не обработать в зависимости от того, как они внутренне представлены в PDF.

- Ссылки и оглавления не поддерживаются

- PDF-файлы, в которых для представления неанглоязычных символов используются встроенные шрифты, отличные от Юникода, приведут к искажённому выводу этих символов.

- Некоторые PDF-файлы состоят из фотографий страницы с распознанным текстом OCR позади них. В таких случаях calibre использует распознанный текст, который может сильно отличаться от того, что вы видите при просмотре файла PDF.

- PDF-файлы, которые используются для отображения сложного текста, такого как языки с письмом справа налево и математические наборы, не будут правильно сконвертированы

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

Коллекции комиксов

Коллекция комиксов - это файл .cbc. Файл .cbc представляет собой ZIP-файл, содержащий другие файлы CBZ/CBR. Кроме того, файл .cbc должен содержать простой текстовый файл с именем comics.txt, закодированный в UTF-8. Файл comics.txt должен содержать список файлов комиксов в файле .cbc в форме имя файла:заголовок, как показано ниже:

one.cbz:Chapter One
two.cbz:Chapter Two
three.cbz:Chapter Three

Файл .cbc будет содержать:

comics.txt
one.cbz
two.cbz
three.cbz

calibre автоматически преобразует этот файл .cbc в электронную книгу с оглавлением, указывающим на каждую запись в comics.txt.

Демоверсия расширенного форматирования EPUB

Различное расширенное форматирование для файлов EPUB демонстрируется в этом демофайле demo file [https://calibre-ebook.com/downloads/demos/demo.epub]. Файл был создан из HTML-кода, написанного вручную в calibre, это - шаблон для создания ваших собственных EPUB.

Исходный HTML-код, из которого он был создан, здесь demo.zip [https://calibre-ebook.com/downloads/demos/demo.zip]. Настройки, используемые для создания EPUB из файла ZIP:

ebook-convert demo.zip .epub -vv --authors "Kovid Goyal" --language en --level1-toc '//*[@class="title"]' --disable-font-rescaling --page-breaks-before / --no-default-epub-cover

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

Конвертировать ODT документы
calibre может напрямую конвертировать файлы ODT (OpenDocument Text). Вы должны использовать стили для форматирования вашего документа и минимизировать использование прямого форматирования. При вставке изображений в документ необходимо привязать их к абзацу, изображения, привязанные к странице, в конечном итоге окажутся в начале преобразования.

Чтобы включить автоматическое определение глав, вам необходимо пометить их встроенными стилями Heading 1, Heading 2, …, Heading 6 (Heading 1 соответствует HTML tag <h1>, Heading 2 - <h2>, и т.д.). При конвертировании в calibre вы можете указать стиль, который вы использовали, в поле Определить главы в. Пример:

- Если вы помечаете главы стилем Heading 2, установите для поля „Определить главы по“ значение //h:h2`

- Для вложенного оглавления с разделами, помеченными как Heading 2 и разделами, помеченными как Heading 3, необходимо ввести //h:h2|//h:h3. На странице «Преобразование - ТОС» установите для поля Level 1 TOC значение //h:h2, а для поля Level 2 TOC - //h:h3.

Хорошо известные свойства документа (Название, Ключевые слова, Описание, Создатель) распознаются, и calibre будет использовать первое изображение (не слишком маленькое и с хорошим соотношением сторон) в качестве изображения обложки.

Существует также расширенный режим преобразования свойств, который активируется установкой настраиваемого свойства opf.metadata (тип „Yes или No“) на Yes в вашем документе ODT (Файл->Свойства->Пользовательские свойства). Если это свойство обнаружено calibre, распознаются следующие пользовательские свойства (opf.authors переопределяет создателя документа):

opf.titlesort
opf.authors
opf.authorsort
opf.publisher
opf.pubdate
opf.isbn
opf.language
opf.series
opf.seriesindex

В дополнение к этому вы можете указать изображение для использования в качестве обложки, назвав его opf.cover (щелкните правой кнопкой мыши, Изображение->Опции->Имя) в ODT. Если изображение с таким именем не найдено, используется «умный» метод. Поскольку обнаружение обложки может привести к двойным обложкам в определённых выходных форматах, процесс удалит абзац (только если содержит только обложку!) из документа. Но это работает только с именованным изображением!

Чтобы отключить обнаружение обложки, вы можете установить для настраиваемого свойства opf.nocover (тип «Yes или No») значение «Yes» в расширенном режиме.

Конвертация в PDF

Первое и наиболее важное решение для преобразования в PDF — настройка размера страницы. По умолчанию в calibre используется размер «U.S. Letter». Вы можете изменить его на другой стандартный или полностью произвольный размер страницы в разделе PDF Output диалога преобразования. Если вы создаёте PDF для определённого устройства, вместо этого можно включить опцию использования размера страницы из output profile. Так, если выходной профиль — Kindle, calibre создаст PDF-файл с размером страницы, подходящим для просмотра на маленьком экране.

Верхние и нижние колонтитулы

Вы можете вставить произвольные верхние и нижние колонтитулы на каждую страницу PDF, указав шаблоны верхнего и нижнего колонтитула. Шаблоны - это просто фрагменты HTML-кода, которые отображаются в верхнем и нижнем колонтитулах. Например, чтобы отображать номера страниц по центру внизу каждой страницы зеленым цветом, используйте следующий шаблон нижнего колонтитула:

<footer><div style="margin: auto; color: green">_PAGENUM_</div></footer>

calibre автоматически заменит _PAGENUM_ на номер текущей страницы. Вы можете даже размещать различное содержимое на четных и нечетных страницах, например, следующий шаблон заголовка покажет заголовок на нечетных страницах и автора на четных страницах:

<header style="justify-content: flex-end">
<div class="even-page">_AUTHOR_</div>
<div class="odd-page"><i>_TITLE_</i></div>
</header>

calibre автоматически заменит _TITLE_ и _AUTHOR_ на название и автора конвертируемого документа. Установка justify-content в :code:` flex-end` приведет к выравниванию текста по правому краю.

Вы также можете отображать текст по левому и правому краям и изменять размер шрифта, как показано в этом шаблоне заголовка:

<header style="justify-content: space-between; font-size: smaller">
<div>_TITLE_</div>
<div>_AUTHOR_</div>
</header>

Это отобразит заголовок слева и автора справа, размер шрифта будет меньше основного текста.

Вы также можете использовать текущий раздел в шаблонах, как показано ниже:

<header><div>_SECTION_</div></header>

_SECTION_ заменяется именем текущего раздела. Эти имена взяты из метаданных Оглавление в документе (PDF Outline). Если в документе нет оглавления, он будет заменен пустым текстом. Если на одной странице PDF есть несколько разделов, будет использоваться первый раздел на странице. Аналогично, существует переменная с именем _TOP_LEVEL_SECTION_, которая может быть использована для получения имени текущего раздела верхнего уровня.

Вы даже можете использовать JavaScript внутри шаблонов верхнего и нижнего колонтитула, например, следующий шаблон приведет к тому, что номера страниц будут начинаться с 4 вместо 1:

<footer>
<div></div>
<script>document.currentScript.parentNode.querySelector("div").innerHTML = "" + (_PAGENUM_ + 3)</script>
</footer>

Кроме того, есть ещё несколько переменных, описанных ниже для использования в верхних и нижних колонтитулах:

- _TOTAL_PAGES_ - общее количество страниц в файле PDF, полезно, например, для реализации счётчика прогресса.

- _TOP_LEVEL_SECTION_PAGES_ - общее количество страниц в текущем разделе верхнего уровня

- _TOP_LEVEL_SECTION_PAGENUM_ - номер текущей страницы в текущем разделе верхнего уровня

- _WIDTH_PIXELS_ - ширина области верхнего / колонтитула в пикселях

- _HEIGHT_PIXELS_ - высота области верхнего / колонтитула в пикселях

Примечание

При добавлении верхних и нижних колонтитулов убедитесь, что для верхних и нижних полей страницы установлены достаточно большие значения в разделе PDF Output диалога конвертации.

Оглавление для печати

Вы также можете вставить печатное оглавление в конец PDF-файла, в котором перечислены номера страниц для каждого раздела. Это очень полезно, если вы собираетесь распечатать PDF на бумаге. Если вы хотите использовать PDF на электронном устройстве, PDF Outline предоставляет эту функцию и создается по умолчанию.

Вы можете настроить внешний вид сгенерированного оглавления с помощью параметра «Дополнительное преобразование CSS» в разделе «Внешний вид» диалогового окна конвертации. Используемый css по умолчанию приведен ниже, просто скопируйте его и внесите все необходимые изменения.

.calibre-pdf-toc table { width: 100%% }

.calibre-pdf-toc table tr td:last-of-type { text-align: right }

.calibre-pdf-toc .level-0 {
font-size: larger;
}

.calibre-pdf-toc .level-1 td:first-of-type { padding-left: 1.4em }
.calibre-pdf-toc .level-2 td:first-of-type { padding-left: 2.8em }

Пользовательские поля страницы для отдельных файлов HTML

Если вы конвертируете файл EPUB или AZW3 с несколькими отдельными файлами HTML внутри него и хотите изменить поля страницы для определённого файла HTML, вы можете добавить следующий блок стилей в файл HTML с помощью редактора calibre:

<style>
@page {
margin-left: 10pt;
margin-right: 10pt;
margin-top: 10pt;
margin-bottom: 10pt;
}
</style>

Затем в разделе вывода PDF диалогового окна конвертации включите параметр Использовать поля страницы из преобразовываемого документа. Теперь все страницы, сгенерированные из этого HTML-файла, будут иметь поля 10pt.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Редактирование книг

Редактирование книг

В calibre есть встроенный редактор электронных книг, который может быть использован для редактирования книг в форматах EPUB, KEPUB (Kobo) и AZW3 (Kindle) . Редактор показывает HTML и CSS, которые использованы непосредственно в файле книги, с с живым просмотром всех сделанных изменений. Он также содержит различные автоматизированные средства для выполнения общей очистки и внесения исправлений.

Вы можете использовать этот редактор, щелкнув правой кнопкой на любой книге в calibre и выбрав: : Редактировать книгу.

Содержание

Основной рабочий процесс

Браузер файлов

- Изменение имени файлов

- Объединить файлы

- Изменение порядка текстовых файлов

- Создание обложек

- Удаление файлов

- Экспортировать файлы

- Добавление новых изображений/шрифтов/и т. д. или создание новых пустых файлов

- Замена файлов

- Эффективное связывание таблиц стилей с файлами HTML

Поиск и замена

- Сохранённые поиски

- Функциональный режим

- Поиск, игнорирующий теги HTML

Автоматизированные инструменты

- Редактирование оглавления

- Проверка книги

- Добавление обложки

- Внедрение упоминаемых шрифтов

- Сокращение внедрённых шрифтов

- Улучшение пунктуации

- Преобразование свойств CSS

- Удаление неиспользуемых CSS правил

- Исправление HTML

- Улучшение файлов

- Вставка встроенного Оглавления

- Установка семантики

- Фильтрация информации о стилях

- Обновление внутренней структуры книги

Контрольные точки

Панель живого предпросмотра

- Разделение HTML файлов

Панель живого CSS

Разные инструменты

- Вид Оглавления

- Проверка правописания слов в книге

- Вставка специальных символов

- Вид инспектор кода

- Проверка внешних ссылок

- Скачать внешние ресурсы

- Упорядочить файлы в папках по типу

- Импорт файлов других форматов электронных книг в EPUB

- Инструмент Отчётов

Специальные функции в редакторе кода

- Подсветка синтаксиса

- Контекстная помощь

- Автозавершение

- Фрагменты

Основной рабочий процесс

Примечание

Видеотур по редактору электронных книг calibre доступен здесь [https://calibre-ebook.com/ru/demo#tutorials].

Когда вы впервые откроете книгу с помощью Редактора, вам будет представлен список файлов слева. Это отдельные файлы HTML, таблицы стилей, изображения и т. д., из которых и состоит книга. Дважды щёлкните на файле, чтобы начать его редактирование. Обратите внимание, что если вы хотите сделать что-то более сложное, чем несколько небольших изменений, вам нужно знать HTML Tutorial [https://www.w3schools.com/Html/default.asp] и CSS Tutorial [https://www.w3schools.com/css/default.asp].

Изменения HTML или CSS внесённые в редакторе будут показаны на панели предварительного просмотра справа. Когда Вы будете удовлетворены ими нажмите кнопку «Сохранить» или воспользуйтесь Файл → Сохранить для сохранения изменений в электронной книге.

Полезная функция :guilabel: Контрольные точки. Прежде чем приступить к каким-либо масштабным изменениям, вы можете создать контрольную точку. Контрольная точка сохранит текущее состояние книги, а затем сделанные изменения в книге можно вернуть к состоянию при создании контрольной точки. Чтобы создать контрольную точку, используйте Правка → Создать контрольную точку. Контрольные точки будут автосоздаваться каждый раз, при запуске автоматизированного инструмента, вроде глобального поиска и замены. Функциональность контрольных точек - дополнение к обычному механизму отмены/возврата при редактировании отдельных файлов. Контрольные точки полезны, когда изменения распространяются на несколько файлов в книге.

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

Браузер файлов

The File browser gives you an overview of the various files inside the book you are editing. The files are arranged by category, with text (HTML)
files at the top, followed by stylesheet (CSS) files, images and so on. Simply double click on a file to start editing it. Editing is supported for HTML, CSS and image files. The order of text files is the same order that they would be displayed in, if you were reading the book. All other files are arranged alphabetically.

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

Многие файлы имеют особое значение в книге. Обычно у них будет значок рядом с именем, указывающий на особое значение. Например, на рисунке слева вы видите, что файлы cover_image.jpg и titlepage.xhtml имеют значок обложки рядом с ними. Это - изображение обложки книги и титульный лист. Аналогично, файл content.opf имеет значок метаданных рядом с ним, указывающий на наличие метаданных книги, а файл toc.ncx имеет значок T, указывающий, что это Таблица контента (содержание).

Вы можете совершить множество действий с каждым файлом, щелкнув по нему правой кнопкой мыши.

Изменение имени файлов

Вы можете переименовать отдельный файл щелкнув по нему правой кнопкой мыши и выбрав Переименовать. Изменение имени файла автоматически обновляет все ссылки и сноски на них в книге. Таким образом, все что Вам надо сделать - это обозначить новое имя, calibre сделает все остальное.

Вы также можете переименовать много файлов одновременно. Это полезно, для того чтобы файлы имели простой шаблон имени. Например, можно переименовать все HTML-файлы с именами Chapter-1.html, Chapter-2.html и так далее. Выберите файлы для переименования, удерживая нажатой клавишу Shift или Ctrl и щёлкая на файлах. Затем щёлкните правой кнопкой мыши и выберите Массовое переименование. Введите префикс и номер, с которого вы хотите начать автоматическую нумерацию, нажмите ОК, и всё готово. Диалоговое окно массового переименования также позволяет переименовывать файлы в порядке их появления в книге, а не в том порядке, в котором вы их выбрали, что полезно, например, для переименования всех изображений в порядке их появления.

Наконец, вы можете массово изменить расширение файла для всех выбранных файлов. Выберите несколько файлов, как указано выше, щелкните правой кнопкой мыши и выберите Изменить расширение файла для выбранных файлов.

Объединить файлы

Иногда вам может потребоваться объединить два HTML-файла или два CSS-файла вместе. Иногда бывает полезно иметь всё в одном файле. Однако будьте осторожны, если поместить много содержимого в один файл, это приведет к проблемам с производительностью при просмотре книги в типичной читалке электронных книг.

Чтобы объединить несколько файлов, выберите их, удерживая клавишу Ctrl и щёлкая по ним (убедитесь, что вы выбираете только файлы одного типа, либо все файлы HTML, либо все файлы CSS и т. д.). Затем щёлкните правой кнопкой мыши и выберите Объединить. Вот и всё, calibre объединит файлы, автоматически позаботившись о переносе всех ссылок и ссылок на объединённые файлы. Обратите внимание, что объединение файлов может иногда приводить к изменению стиля текста, поскольку отдельные файлы могли использовать разные таблицы стилей.

Вы также можете выбрать текстовые файлы, а затем перетащить текстовые файлы в другой текстовый файл, чтобы объединить перетаскиваемые текстовые файлы в целевой текстовый файл.

Изменение порядка текстовых файлов

Вы можете изменить порядок, в котором текстовые (HTML) файлы открываются при чтении книги, просто перетащив их в Браузер файлов или щёлкнув по файлу для перемещения, а затем нажав модификаторы Ctrl+Shift с помощью клавиш Вверх, Вниз, Home или End. Для технических специалистов это называется переупорядочиванием корешка книги.

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

Создание обложек

Обычно у электронных книг есть обложка. Это изображение отмечено в Браузере файлов значком с изображением коричневой книги рядом с названием. Если Вы хотите установить другое изображение в качестве обложки, это можно сделать, щёлкнув правой кнопкой мыши на файле и выбрав Отметить как обложку.

Кроме того, файлы EPUB имеют концепцию titlepage. Титульный лист - это HTML-файл, который выполняет роль титульного листа/обложки книги. Вы можете пометить HTML-файл как титульную страницу при редактировании EPUB, щёлкнув правой кнопкой мыши. Будьте внимательны, чтобы помеченный вами файл содержал только сопроводительную информацию. Если он содержит другое содержимое, такое как первая глава, то это содержимое будет потеряно, если пользователь когда-либо преобразует файл EPUB в calibre в другой формат. Это связано с тем, что при конвертации calibre предполагает, что отмеченная титульная страница содержит обложку и только её.

Удаление файлов

Вы можете удалить файлы, щёлкнув по ним правой кнопкой мыши или выбрав их и нажав клавишу Delete. Удаление файла удаляет все ссылки на файл из файла OPF, уберегая вас от этой рутинной работы. Однако ссылки в других местах не удаляются, вы можете использовать инструмент Check Book, чтобы легко найти и удалить/заменить их.

Экспортировать файлы

Вы можете экспортировать файл из книги в другое место на вашем компьютере. Это полезно, если вы хотите работать с файлом изолированно, с помощью специализированных инструментов. Для этого просто щелкните правой кнопкой мыши файл и выберите :guilabel: Экспорт.

Как только вы закончите работать с экспортированным файлом, вы можете повторно импортировать его в книгу, снова щелкнув правой кнопкой мыши на файле и выбрав Заменить на файл…, который позволит вам заменить файл в книге с ранее экспортированным файлом.

Вы также можете копировать файлы между несколькими экземплярами редактора. Выберите файлы, которые вы хотите скопировать, в :guilabel: Браузер файлов, затем щелкните правой кнопкой мыши и выберите Копировать выбранные файлы в другой экземпляр редактора. Затем в другом экземпляре редактора щелкните правой кнопкой мыши в Браузер файлов и выберите Вставить файл из другого экземпляра редактора.

Добавление новых изображений/шрифтов/и т. д. или создание новых пустых файлов

Вы можете добавить новое изображение, шрифт, таблицу стилей и т. д. со своего компьютера в книгу, нажав Файл → Новый файл. Это позволяет либо импортировать файл, нажав кнопку Импортировать файл ресурса, либо создать новый пустой файл HTML или таблицу стилей, просто введя имя файла в поле для нового файла.

Вы также можете импортировать несколько файлов в книгу одновременно, используя Файл->Импорт файлов в книгу.

Замена файлов

Вы можете легко заменить существующие файлы в книге, щёлкнув правой кнопкой мыши по файлу и выбрав заменить. Это автоматически обновит все ссылки и упоминания, если замещающий файл будет иметь другое имя, чем заменяемый файл.

Эффективное связывание таблиц стилей с файлами HTML

Для удобства вы можете выбрать несколько файлов HTML в Браузере файлов, щёлкнуть правой кнопкой мыши и выбрать ссылку на таблицы стилей, чтобы calibre автоматически вставлял <link> теги для этих таблиц стилей во все выбранные файлы HTML.

Поиск и замена

Редактир книг имеет очень мощный интерфейс поиска и замены, который позволяет вам искать и заменять текст в текущем файле, во всех файлах и даже в отмеченной области текущего файла. Вы можете искать с помощью обычного поиска или с помощью регулярных выражений. Чтобы узнать, как использовать регулярные выражения для расширенного поиска, смотрите Всё об использования регулярных выражений в calibre.

Начните поиск и замените с помощью пункта меню Поиск → Найти/заменить (вы должны редактировать файл HTML или CSS).

Введите текст, который вы хотите найти, в поле «Найти» и его замену в поле «Заменить». Вы можете нажать соответствующие кнопки, чтобы найти следующее совпадение, заменить текущее совпадение и заменить все совпадения.

Используя раскрывающиеся списки в нижней части окна, вы можете настроить поиск по текущему файлу, всем текстовым файлам, всем файлам стилей или всем файлам. Вы также можете выбрать режим поиска: обычный (строковый) поиск или поиск по регулярному выражению.

Возможен подсчёт всех совпадений для выражения поиска Поиск → Подсчитать все. Счётчик работает с любыми файлами/регионами, выбранными в выпадающем списке.

Также можно перейти к определённой строке в текущем открытом редакторе с помощью Поиск → Перейти к строке.

Примечание

Все возможности поиска и замены реализуются регулярными выражениями. Смотрите regex tutorial.

Сохранённые поиски

Вы можете сохранять часто используемые выражения поиска/замены (включая выражения в функциональном режиме) и использовать их многократно. Чтобы сохранить поиск, просто щёлкните правой кнопкой мыши в поле поиска и выберите Сохранить текущий поиск.

Вы можете вызвать сохраненные поиски через Поиск → Сохранённый поиск. Вам предоставится список выражений поиска и замены, для применения. Вы сможете выбрать несколько записей в списке, удерживая нажатой клавишу Ctrl при щелчке, для запуска нескольких выражений поиска и замены выражений в одной операции.

Функциональный режим

Функциональный режим позволяет вам писать произвольно мощные функции Python, которые запускаются при каждом поиске/замене. Вы можете выполнять практически любые манипуляции с текстом в функциональном режиме . Для получения дополнительной информации см .:doc:function_mode.

Поиск, игнорирующий теги HTML

Существует также специальный инструмент для поиска текста, игнорирующий любые HTML-теги между ними. Например, если в книге есть HTML Empahisis on a <i>word</i>, вы можете искать on a word , и он будет найден, даже если в середине есть тег <i>. Используйте этот инструмент через пункт меню :guilabel: Поиск->Поиск, игнорирующий HTML-разметку.

Автоматизированные инструменты

Редактор книг имеет различные инструменты для помощи с общими задачами. Доступ к ним осуществляется через меню Инструменты.

Редактирование оглавления

Существует специальный инструмент для облегчения редактирования оглавления. Запустите его с помощью Инструменты → Оглавление → Редактировать оглавление.

Инструмент Редактировать оглавление показывает вам текущее оглавление (если оно есть) слева. Просто дважды щелкните по любой записи, чтобы изменить её текст. Вы также можете изменить порядок записей путем перетаскивания или с помощью кнопок справа.

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

Вы можете редактировать отдельные записи, нажимая на них, а затем нажимая Изменить местоположение, на которое указывает эта запись. Откроется предварительный мини-просмотр книги, просто наведите курсор мыши на панель просмотра книги и щёлкните там, куда должна указывать запись. Толстая зелёная линия покажет вам местоположение. Нажмите OK, когда вы будете довольны местоположением.

Проверка книги

Инструмент Проверка книги проверяет книгу на наличие проблем, которые могут помешать её чтению на реальных устройствах чтения. Активируйте его через Инструменты → Проверить книгу.

О найденных проблемах сообщается в простом и удобном списке. Нажав на любую запись в списке вы получите справку об этой ошибке, и сможете автоматически её исправить, если это возможно. Также можно дважды щёлкнув по ошибке открыть её местоположение в редакторе и исправить её вручную.

Некоторые из выполненных проверок:

- Неправильная HTML-разметка. Сообщается о любой разметке HTML, которая не анализируется как правильно сформированный XML. Исправив её, вы гарантируете, что ваша разметка работает так, как задумано во всех контекстах. calibre также может автоматически исправить эти ошибки, но иногда автоматическое исправление может сбойнуть, поэтому используйте его с осторожностью. Как всегда, перед автоматическим исправлением создается контрольная точка, чтобы легко отменить все изменения. Автоматическое исправление работает анализируя разметку с использованием алгоритма HTML5, который очень устойчив к сбоям, а затем преобразовывает её в правильно сформированный XML.

- Неправильные или неизвестные стили CSS. Сообщается о любом недопустимом CSS или имеющем свойства, не определенные в стандарте CSS 2.1 (плюс некоторые из CSS 3). CSS проверяется во всех таблицах стилей, атрибутах встроенных стилей и <style>тегах в файлах HTML.

- Неработающие ссылки. Отчёт о ссылках, которые указывают на отсутствующие файлы в книге.

- Файлы без ссылок. Отчёт о файлах в книге, на которые не ссылаются никакие другие файлы или которые не находятся в корешке.

- Разные общие проблемы в файлах OPF, такие как дублирование элементов корешка или манифеста, неработающие идентификаторы или мета-покрывающие теги , пропущенные обязательные разделы и т. д.

- Разные проверки совместимости для известных проблем, которые могут вызвать сбой книги на читалках.

Добавление обложки

Вы можете легко добавить обложку в книгу через Инструменты → Добавить обложку. Это позволяет либо выбрать существующее изображение в книге в качестве обложки, либо импортировать новое изображение в книгу и сделать его обложкой. При редактировании файлов EPUB автоматически создается HTML-оболочка для обложки. Если в книге найдена существующая обложка, она заменяется. Инструмент также автоматически заботится о правильной маркировке файлов обложек как обложек в OPF.

Внедрение упоминаемых шрифтов

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

Сокращение внедрённых шрифтов

Доступ к нему осуществляется через Инструменты → Подмножество встроенных шрифтов, этот инструмент усекает все шрифты в книге, чтобы они содержали только глифы для текста, фактически присутствующие в книге. Это обычно уменьшает размер файлов шрифтов на ~ 50%. Однако имейте в виду, что если шрифты являются подмножеством, и вы добавите новый текст, символы которого ранее не присутствовали в шрифте подмножества, шрифт не будет работать для нового текста. Так что делайте это только как последний шаг в вашем рабочем процессе.

Улучшение пунктуации

Конвертировать текстовые тире, многоточия, кавычки, множественные переносы и т.д. в их типографически верные эквиваленты. Помните, что алгоритм может иногда выдавать неверные результаты, особенно в случае одиночных кавычек в начале сокращений. Доступ через Инструменты →  Умная пунктуация.

Преобразование свойств CSS

Создание правил для преобразования стиля книги. Например, создайте правило, чтобы преобразовать весь красный текст в зелёный или удвоить размер шрифта всего текста в книге, или сделать текст из определенного семейства шрифтов курсивом и т. д.

Создать правила просто, правила следуют формату естественного языка, который выглядит следующим образом:

- Если свойство color имеет значение red change его на green

- Если свойство font-size равно any value multiply значение на 2

Доступ через Инструменты → Трансформировать стили.

Удаление неиспользуемых CSS правил

Удалить все неиспользуемые CSS правила из таблицы стилей и тегов. Некоторые книги, созданные с помощью технических шаблонов, могут иметь множество дополнительных CSS правил, не совпадающих с реальным содержанием. Это замедляет работу читалок при разборе правил. Доступ через Инструменты → Удалить неиспользуемые CSS.

Исправление HTML

Этот инструмент просто конвертирует HTML, который не может быть проанализирован как XML, в правильно сформированный XML. В электронных книгах очень часто бывает неправильно сформированный XML, поэтому этот инструмент просто автоматизирует процесс исправления такого HTML. Инструмент работает, анализируя HTML с использованием алгоритма HTML5 (алгоритм, используемый во всех современных браузерах), а затем преобразовывая результат в XML. Имейте в виду, что автоматическое исправление иногда может привести к противоречивым результатам. При желании вы можете использовать инструмент Проверить книгу, рассмотренный выше, чтобы найти и вручную исправить проблемы в HTML. Доступ через Инструменты → Исправить HTML.

Улучшение файлов

Этот инструмент используется для автоматического форматирования всех файлов HTML и CSS, чтобы они «выглядели красиво». Код автоматически выравнивается с отступами, вставляются пустые строки, при необходимости и т.д. Обратите внимание, что улучшение также автоматически исправляет поврежденный HTML/CSS. Поэтому, если вы не хотите, чтобы выполнялось автоматическое исправление, сначала воспользуйтесь инструментом Проверить книгу, чтобы исправить все проблемы, и только затем запустите улучшение. Доступ через Инструменты → Улучшить все файлы.

Примечание

В HTML любой текст может иметь значительный пробел через white-space директиву CSS. Следовательно, улучшение может потенциально изменить отображение HTML. Чтобы избежать этого, алгоритм beautify улучшает только теги уровня блока, которые содержат другие теги уровня блока. Так, например, текст внутри <p>тега не будет менять свой пробел. Но <body>тег, который содержит только другие <p> и <div> теги, будет улучшен. Иногда это значит, что конкретный файл не будет затронут улучшением, поскольку у него нет подходящих тегов уровня блока. В таких случаях вы можете попробовать другие инструменты для улучшения, например: HTML Tidy [https://infohound.net/tidy/].

Вставка встроенного Оглавления

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

Если вы используете этот инструмент несколько раз, каждый вызов приведет к замене ранее созданного встроенного оглавления. Доступ к инструменту можно получить через :guilabel: Инструменты->Оглавление->Вставить встроенное оглавление.

Установка семантики

Этот инструмент используется для установки семантики в файлах EPUB. Семантика - это просто ссылки в файле OPF, которые определяют определенные места в книге как имеющие особое значение. Вы можете использовать их для определения предисловия, посвящения, обложки, оглавления и т.д. Просто выберите тип семантической информации, которую вы хотите указать, а затем выберите место в книге, на которое должна указывать ссылка. Этот инструмент доступен через Инструменты → Установить семантику.

Фильтрация информации о стилях

Этот инструмент можно использовать для простого удаления указанных свойств стиля CSS из всей книги. Вы можете сказать ему, какие свойства вы хотите удалить, например, `` color, background-color, line-height``, и он удалит их везде, где они встречаются - таблицы стилей, теги <style> и инлайновые style атрибуты. После удаления информации о стиле отображается сводка с точным списком всех сделанных изменений. Доступ к инструменту можно получить через Инструменты → Фильтровать информацию о стиле.

Обновление внутренней структуры книги

Этот инструмент может быть использован для обновления внутренней структуры книги, по возможности. Например, он обновит книги EPUB 2 до книг EPUB 3. Доступ к инструменту можно получить через Обновление внутренней структуры книги.

Контрольные точки

Контрольные точки - это способ пометить текущее состояние книги как «особенное». Затем вы можете продолжить вносить любые изменения в книгу и, если вам не нравятся результаты, вернитесь к состоянию контрольной точки. Контрольные точки создаются автоматически при каждом запуске любого из автоматизированных инструментов, описанных в предыдущем разделе.

Вы можете создать контрольную точку через Правка →  Создать контрольную точку. И вернитесь к предыдущей контрольной точке с помощью Правка →  Вернуться к…

Функциональность контрольных точек - дополнение к обычному механизму отмены/возврата при редактировании отдельных файлов. Контрольные точки особенно полезны, когда изменения распространяются на несколько файлов в книге или когда вы хотите иметь возможность отменить большую группу связанных изменений в целом.

Вы можете увидеть список доступных контрольных точек через Вид → Контрольные точки. Вы можете сравнить текущее состояние книги с указанной контрольной точкой, используя инструмент Сравнение книг - выбрав интересующую контрольную точку и нажав кнопку Сравнить. Кнопка Вернуться к… восстанавливает книгу до выбранной контрольной точки, отменяя все изменения с момента создания этой контрольной точки.

Панель живого предпросмотра

Просмотр файла дает обзор различных файлов внутри. Панель живого предпросмотра показывает изменения, которые вы вносите в реальном времени (с задержкой в одну или две секунды). При редактировании файлов HTML или CSS панель предварительного просмотра обновляется автоматически, чтобы отразить ваши изменения. Когда вы перемещаете курсор в редакторе, панель предварительного просмотра будет отслеживать его местоположение, показывая вам соответствующее местоположение в книге. Щелчок на панели предварительного просмотра приведет к тому, что курсор в редакторе будет расположен над элементом, который вы щелкнули. Если щелкнуть ссылку, указывающую на другой файл в книге, этот файл будет автоматически открыт в панели редактирования и предварительного просмотра.

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

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

Примечание

Панель предварительного просмотра не поддерживает встроенные шрифты, если имя шрифта внутри файла шрифта не совпадает с именем в правиле CSS @font-face. Вы можете использовать инструмент Проверить книгу для быстрого поиска и исправления таких проблемных шрифтов.

Разделение HTML файлов

Одним из, возможно, неочевидным, использованием панели предварительного просмотра является разделение длинных HTML-файлов. При просмотре файла, который вы хотите разделить, нажмите кнопку Режим разделения под панелью предварительного просмотра . Затем просто наведите указатель мыши на то место, где вы хотите разбить файл, и нажмите. Толстая зелёная линия покажет вам, где именно произойдет разделение при перемещении мыши. Как только вы нашли нужное место, просто нажмите и разделение будет выполнено.

Разделение файла автоматически обновит все ссылки и упоминания, указывающие на нижнюю половину файла, и откроет новый разделенный файл в редакторе.

Вы также можете автоматически разбить один HTML-файл на несколько, щёлкнув правой кнопкой мыши внутри файла в редакторе и выбрав Разделить на несколько. Это позволит вам легко разбить большой файл по всем тегам заголовков или всем тегам определённого класса и т.п.

Панель живого CSS

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

Это отличный способ быстро увидеть, какие правила стиля применимы к любому тегу. Представление также имеет интерактивные ссылки (синим цветом), которые ведут прямо к месту, где был определен стиль, на случай, если вы захотите внести какие-либо изменения в правила стиля. Отображаются правила стиля, которые применяются непосредственно к тегу, а также правила, унаследованные от родительских тегов.

Панель также показывает, каковы окончательно рассчитанные стили для тега. Свойства в списке, которые заменены правилами с более высоким приоритетом, показаны линией перечёркивания .

Вы можете включить панель Живого CSS через Вид →  Живой CSS.

Разные инструменты

Есть еще несколько инструментов, которые могут быть полезны при редактировании книги.

Вид Оглавления

Представление «Оглавление» показывает текущее оглавление в книге. Двойной щелчок по любой записи открывает место, на которое указывает запись в редакторе. Вы можете щёлкнуть правой кнопкой мыши, чтобы отредактировать оглавление, обновить представление или развернуть/свернуть все элементы. Получите доступ к этому представлению через :guilabel: Вид->Оглавление.

Проверка правописания слов в книге

Вы можете запустить проверку орфографии через Инструменты → Проверить орфографию.

Слова показаны с количеством раз, когда они встречаются в книге, и языком, к которому относится слово. Информация о языке берется из метаданных книг и атрибутов lang в файлах HTML. Это позволяет программе проверки орфографии работать хорошо даже с книгами, которые содержат текст на нескольких языках. Например, в следующем фрагменте HTML слово color будет проверено с использованием американского английского, а слово colour - с использованием британского английского:

<div lang="en_US">color <span lang="en_GB">colour</span></div>

Примечание

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

Чтобы изменить слово, просто дважды щелкните по одному из предложенных альтернативных вариантов написания справа или введите собственное исправленное написание и нажмите кнопку Заменить выбранное слово на. Это заменит все вхождения слова в книгу. Вы также можете щелкнуть правой кнопкой мыши слово в главном списке слов, чтобы удобно заменить слово в меню правой кнопки мыши.

Вы можете заставить программу проверки правописания игнорировать слово для текущего сеанса, нажав кнопку Игнорировать. Вы также можете добавить слово в пользовательский словарь, нажав кнопку Добавить в словарь. Средство проверки правописания поддерживает несколько пользовательских словарей, поэтому вы можете выбрать словарь, в который хотите добавить слово.

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

Примечание

Если вы вносите какие-либо изменения в книгу, редактируя файлы, когда инструмент проверки орфографии открыт, вы должны нажать кнопку Обновить в инструменте проверки орфографии. Если вы этого не сделаете и продолжите использовать инструмент проверки орфографии, вы можете потерять изменения, внесенные в редакторе.

Примечание

Чтобы исключить отдельный файл из проверки орфографии при запуске инструмента проверки орфографии, вы можете использовать кнопку Исключить файлы или добавить следующий комментарий сразу под открывающим тегом в файле:

<!-- calibre-no-spell-check -->

Добавление словарей

Проверка орфографии поставляется со встроенными словарями для английского и испанского языков. Вы можете установить свои собственные словари через Настройки → Редактор → Управлять орфографическими словарями. Средство проверки орфографии может использовать словари из программы LibreOffice (в формате .oxt). Вы можете загрузить эти словари из репозитория «LibreOffice Extensions» <https://extensions.libreoffice.org/?Tags%5B%5D=50>`_.

Вставка специальных символов

Вы можете вставить символы, которые сложно набрать, используя инструмент Правка → Вставить специальный символ. Это показывает все символы Юникода, просто нажмите на символ, который вы хотите ввести. Если вы удерживаете нажатой клавишу Ctrl во время нажатия, окно закроется после вставки выбранного символа. Этот инструмент можно использовать для вставки специальных символов в основной текст или в любую другую область пользовательского интерфейса, например в инструмент поиска и замены.

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

Вы также можете напрямую вводить специальные символы с помощью клавиатуры. Для этого введите код Unicode для символа (в шестнадцатеричном формате) и затем нажмите клавишу Alt+X, которая преобразует ранее набранный код в соответствующий символ. Например, чтобы ввести ÿ, вы должны ввести ff, а затем Alt+X. Чтобы ввести неразрывный пробел, нужно ввести a0, а затем Alt+X, для ввода горизонтального многоточия вы будете использовать 2026 и Alt+X и так далее.

Наконец, вы можете вводить специальные символы, используя именованные объекты HTML. Например, при вводе &nbsp; ввод текста будет заменен неразрывным пробелом. Замена происходит только при вводе точки с запятой.

Вид инспектор кода

Это представление показывает HTML-код элемента и CSS, применяемые к нему. Вы открываете его, щёлкая правой кнопкой мыши место на панели предварительного просмотра и выбирая Исследовать. Это позволяет вам увидеть HTML-код для этого элемента и, что более важно, стили CSS, которые применяются к нему. Вы даже можете динамически редактировать стили и сразу же увидеть, как повлияют ваши изменения. Обратите внимание, что редактирование стилей на самом деле не вносит изменений в содержание книги, а только позволяет быстро экспериментировать. Возможность интерактивного редактирования внутри Инспектора находится в стадии разработки.

Проверка внешних ссылок

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

Скачать внешние ресурсы

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

Упорядочить файлы в папках по типу

Часто при редактировании файлов EPUB, которые вы получаете откуда-то, вы обнаружите, что файлы внутри EPUB расположены случайным образом в разных подпапках. Этот инструмент позволяет автоматически перемещать все файлы в подпапки в зависимости от их типа. Доступ к нему осуществляется через Инструменты → Упорядочить по папкам. Обратите внимание, что этот инструмент только изменяет порядок расположения файлов внутри EPUB, но не меняет способ их отображения в Браузере файлов.

Импорт файлов других форматов электронных книг в EPUB

Редактор включает в себя возможность импортировать файлы в некоторых других форматах электронных книг напрямую, как новый EPUB, без полной конвертации. Это особенно полезно для непосредственного создания файлов EPUB из ваших собственных отредактированных вручную файлов HTML. Вы можете сделать это через Файл → Импортировать файл HTML или DOCX как новую книгу.

Инструмент Отчётов

Редактор включает в себя замечательный инструмент Отчёты (через Инструменты → Отчёты), который показывает сводку файлов, изображений, ссылок, слов, символов и стилей, используемых в книге. Каждая строка в отчете имеет горячую ссылку. Двойной щелчок по строке переходит к месту в книге, где этот элемент используется или определён (в зависимости от ситуации). Например, в представлении Ссылки можно дважды щелкнуть записи в столбце Источник, чтобы перейти туда, где определена ссылка, и записи в столбце Цель, чтобы перейти туда, где цель ссылки.

Специальные функции в редакторе кода

HTML-редактор calibre очень мощный. Он имеет много функций, которые облегчают редактирование HTML (и CSS).

Подсветка синтаксиса

Редактор HTML имеет очень сложную подсветку синтаксиса. Особенности включают в себя:

- Текст внутри тегов, выделенных жирным шрифтом, курсивом и заголовком, выделен жирным шрифтом/курсивом

- Когда вы перемещаете курсор по HTML-коду, подсвечиваются соответствующие теги HTML, и вы можете перейти к открывающему или закрывающему тегу с помощью сочетаний клавиш Ctrl+{ и Ctrl+}. Точно так же вы можете выбрать содержимое тега с помощью Ctrl+Alt+T или Ctrl+Shift+T.

- Неправильный HTML выделяется красным подчёркиванием

- Орфографические ошибки в тексте внутри тегов HTML и атрибутов, таких как заголовок, выделены. Проверка орфографии выполняется с учетом языка на основе значения атрибута lang текущего тега и общего языка книги.

- CSS, встроенный в <style> теги, подсвечивается

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

- Ссылки на другие файлы в тегах <a>, <img> и <link> имеют выделенные имена файлов. Если имя файла, на которое они указывают, не существует, оно помечается красным подчеркиванием.

Контекстная помощь

Вы можете щёлкнуть правой кнопкой мыши по имени тега HTML или имени свойства CSS, чтобы получить справку по этому тегу или свойству.

Вы также можете удерживать нажатой клавишу Ctrl и щёлкнуть по любому имени файла внутри тега ссылки, чтобы автоматически открыть этот файл в редакторе. Аналогично Ctrl щёлкнув по имени класса, вы перейдёте к первому правилу стиля, которое соответствует тегу и классу.

Щелчок правой кнопкой мыши по имени класса в HTML-файле позволит вам переименовать класс, изменив все вхождения класса во всей книге и во всех её таблицах стилей.

Автозавершение

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

При вводе имени файла редактор автоматически выдаёт подсказки. Используйте клавишу Tab, для выбора правильного имени файла. Редактор также предлагает подсказки для ссылок, указывающих на привязку внутри другого HTML-файла. После ввода символа # будет выдан список всех якорей в целевом файле с небольшим фрагментом текста, помогающим выбрать правильный якорь.

Обратите внимание, что в отличие от большинства других систем завершения, система завершения редактора использует сопоставление подпоследовательностей. Это означает, что вы можете ввести всего две или три буквы из любого места в имени файла, чтобы завершить имя файла. Например, скажем, вы хотите имя файла ../images/arrow1.png, вы можете просто набрать ia1 и нажать Tab, чтобы завершить имя файла. При поиске совпадений система завершения определяет приоритетность букв, которые находятся в начале слова или сразу после разделителя пути. Как только вы привыкнете к этой системе, вы обнаружите, что она экономит ваше время и силы.

Фрагменты

Редактор calibre поддерживает сниппеты. Сниппет - это фрагмент текста, который часто используется повторно или содержит много избыточного текста. Редактор позволяет вставить сниппет с помощью всего нескольких нажатий клавиш. Сниппеты очень мощные, со многими функциями, такими как заполнители, между которыми вы можете перемещаться, автоматическое зеркалирование повторяющегося текста и так далее. Для получения дополнительной информации смотрите Фрагменты.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Редактирование книг »

- Функциональный режим для поиска и замены в редакторе

Функциональный режим для поиска и замены в редакторе

Инструмент Search & replace в редакторе поддерживает функциональный режим. В этом режиме вы можете комбинировать регулярные выражения (см. Всё об использования регулярных выражений в calibre) с произвольно мощными функциями Python для выполнения всех видов расширенной обработки текста.

В стандартном режиме regexp для поиска и замены вы указываете как регулярное выражение для поиска, так и шаблон, который используется для замены всех найденных совпадений. В функциональном режиме вместо использования фиксированного шаблона вы указываете произвольную функцию на языке программирования Python [https://docs.python.org/]. Это позволяет вам делать много вещей, которые невозможны с простыми шаблонами.

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

Автоматическое исправление регистра заголовков в документе

Здесь мы будем использовать одну из встроенных функций в редакторе, чтобы автоматически изменять регистр всего текста внутри тегов заголовка на регистр заголовка:

Find expression: <([Hh][1-6])[^>]*>.+?</\1>

Для функции просто выберите встроенную функцию Заголовок (без тегов). Заголовки будут изменены следующим образом: <h1>some titLE</h1> на <h1>Some Title</h1>. Это будет работать, даже если внутри заголовочных тегов есть другие HTML-теги.

Ваша первая пользовательская функция - умные дефисы

Настоящая сила режима функций заключается в возможности создавать свои собственные функции для произвольной обработки текста. Инструмент Smarten Punctuation в редакторе оставляет индивидуальные дефисы в отдельности, поэтому вы можете использовать эту функцию, чтобы заменить их на тире.

Чтобы создать новую функцию, просто нажмите кнопку Create/edit, чтобы создать новую функцию и скопировать код Python снизу.

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
return match.group().replace('--', '—').replace('-', '—')

Каждая пользовательская функция Search & replace должна иметь уникальное имя и состоять из функции Python с именем replace, которая принимает все аргументы, показанные выше. На данный момент мы не будем беспокоиться о различных аргументах функции replace(). Просто сосредоточьтесь на аргументе match. Он представляет совпадение при выполнении поиска и замены. Полная документация доступна `здесь https://docs.python.org/library/re.html#match-objects`_. match.group() просто возвращает весь совпавший текст, и всё, что мы делаем, это заменяем дефисы в этом тексте длинными тире, сначала заменяя двойные дефисы, а затем одиночные дефисы.

Используйте эту функцию с регулярным выражением поиска:

>[^<>]+<

И он заменит все дефисы на тире, но только в реальном тексте, а не в определениях тегов HTML.

Сила функционального режима - использование орфографического словаря для исправления неверно написанных слов

Часто электронные книги, созданные на основе отсканированных печатных книг, содержат неправильно написанные слова - слова, которые были разбиты в конце строки на печатной странице. Мы напишем простую функцию для автоматического поиска и исправления таких слов.

import regex
from calibre import replace_entities
from calibre import prepare_string_for_xml

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):

def replace_word(wmatch):
# Try to remove the hyphen and replace the words if the resulting
# hyphen free word is recognized by the dictionary
without_hyphen = wmatch.group(1) + wmatch.group(2)
if dictionaries.recognized(without_hyphen):
return without_hyphen
return wmatch.group()

# Search for words split by a hyphen
text = replace_entities(match.group()[1:-1]) # Handle HTML entities like &amp;
corrected = regex.sub(r'(\w+)\s*-\s*(\w+)', replace_word, text, flags=regex.VERSION1 | regex.UNICODE)
return '>%s<' % prepare_string_for_xml(corrected) # Put back required entities

Используйте эту функцию с тем же выражением поиска, что и раньше, а именно:

>[^<>]+<

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

Обратите внимание, что одним из ограничений этой техники является то, что она будет работать только для одноязычных книг, потому что по умолчанию dictionaries.recognized() использует основной язык книги.

Авто нумерация разделов

Теперь мы увидим что-то немного другое. Предположим, ваш HTML-файл имеет много разделов, каждый из которых имеет заголовок в <h2> теге, который выглядит следующим образом :code: <h2>Some text</h2>. Вы можете создать пользовательскую функцию, которая будет автоматически нумеровать эти заголовки последовательными номерами разделов, чтобы они выглядели так <h2>1. Some text</h2>.

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
section_number = '%d. ' % number
return match.group(1) + section_number + match.group(2)

# Ensure that when running over multiple files, the files are processed
# in the order in which they appear in the book
replace.file_order = 'spine'

Используйте это с выражением поиска:

(?s)(<h2[^<>]*>)(.+?</h2>)

Поместите курсор вверху файла и нажмите Заменить все.

Эта функция использует еще один полезный дополнительный аргумент для replace(): аргумент `` number``. При выполнении Заменить все номер автоматически увеличивается для каждого последующего совпадения.

Другой новой функцией является использование replace.file_order - установка этого значения в 'spine' означает, что если этот поиск выполняется по нескольким HTML-файлам, файлы обрабатываются в том порядке, в котором они отображаются в книга. Смотрите Выберите порядок файлов при работе с несколькими файлами HTML для деталей.

Автоматическое создание оглавления

Наконец, давайте попробуем что-нибудь более амбициозное. Предположим, что ваша книга имеет заголовки в тегах h1 и h2, которые выглядят как <h1 id="someid">Some Text</h1>. Мы автоматически сгенерируем оглавление HTML на основе этих заголовков. Создайте пользовательскую функцию ниже:

from calibre import replace_entities from calibre.ebooks.oeb.polish.toc import TOC, toc_to_html from calibre.gui2.tweak_book import current_container from calibre.ebooks.oeb.base import xml2str def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
if match is None:
# All matches found, output the resulting Table of Contents.
# The argument metadata is the metadata of the book being edited if 'toc' in data:
toc = data['toc']
root = TOC()
for (file_name, tag_name, anchor, text) in toc:
parent = root.children[-1] if tag_name == 'h2' and root.children else root parent.add(text, file_name, anchor)
toc = toc_to_html(root, current_container(), 'toc.html', 'Table of Contents for ' + metadata.title, metadata.language)
print(xml2str(toc))
else:
print('No headings to build ToC from found')
else:
# Add an entry corresponding to this match to the Table of Contents if 'toc' not in data:
# The entries are stored in the data object, which will persist
# for all invocations of this function during a 'Replace All' operation data['toc'] = []
tag_name, anchor, text = match.group(1), replace_entities(match.group(2)), replace_entities(match.group(3))
data['toc'].append((file_name, tag_name, anchor, text))
return match.group() # We don't want to make any actual changes, so return the original matched text

# Ensure that we are called once after the last match is found so we can
# output the ToC replace.call_after_last_match = True
# Ensure that when running over multiple files, this function is called,
# the files are processed in the order in which they appear in the book replace.file_order = 'spine'

И использовать это с выражением поиска:

<(h[12]) [^<>]* id=['"]([^'"]+)['"][^<>]*>([^<>]+)

Запустите поиск по Все текстовые файлы и в конце поиска появится окно Отладка вывода из вашей функции, которое будет содержать оглавление HTML, готовое для вставки в toc.html.

Вышеприведенная функция сильно прокомментирована, поэтому ей должно быть легко следовать. Ключевой новой возможностью является использование другого полезного дополнительного аргумента для функции replace() объекта data. Объект data - это Python dict, который сохраняется между всеми последовательными вызовами ``replace()``во время одной операции Replace All.

Еще одной новой возможностью является использование call_after_last_match - установка этого параметра в `` True`` в функции ` replace()`` означает, что редактор будет вызывать replace() один раз после всех найденных совпадений. Для этого дополнительного вызова объектом сопоставления будет None.

Это была просто демонстрация мощи функционального режима: если вам действительно нужно сгенерировать оглавление из заголовков в вашей книге, вам лучше использовать специальный инструмент оглавления в :guilabel:`Инструменты->Оглавление.

API для function mode

Все функции function mode должны быть функциями Python с именем replace, со следующей сигнатурой:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
return a_string

Когда выполняется поиск/замена, для каждого найденного совпадения вызывается функция replace(), она должна возвращать строку замены для этого совпадения. Если никакие замены не должны быть сделаны, она должна вернуть match.group(), которая является исходной строкой. Различные аргументы функции replace() описаны ниже.

Аргумент match

Аргумент match представляет текущее найденное совпадение. Это объект Python Match [https://docs.python.org/library/re.html#match-objects]. Его наиболее полезным методом является group(), который можно использовать для получения сопоставленного текста, соответствующего отдельным группам захвата в регулярном выражении поиска.

Аргумент number

Аргумент number - это номер текущего совпадения. Когда вы запускаете Replace All, каждое последующее совпадение будет вызывать``replace()`` с возрастающим числом. Первое совпадение имеет номер 1.

Аргумент file_name

Это имя файла, в котором было найдено текущее совпадение. При поиске внутри помеченного текста file_name пусто. file_name находится в канонической форме, пути относительно корня книги, используя / в качестве разделителя пути.

Аргумент metadata

Это представляет метаданные текущей книги, такие как название, авторы, язык и т. д. Это объект класса calibre.ebooks.metadata.book.base.Metadata. Полезные атрибуты включают title, authors (список авторов) и language (код языка).

Аргумент dictionaries

Это представляет собой словари, используемые для проверки орфографии текущей книги. Его наиболее полезным методом является dictionaries.recognized(word), который будет возвращать True, если переданное слово распознается словарём для языка текущей книги.

Аргумент data

Это простой Python dict. Когда вы запускаете Replace all, каждое последующее совпадение будет вызывать``replace()`` с тем же dict, что и для данных. Таким образом, вы можете использовать его для хранения произвольных данных между вызовами replace() во время операции Replace all.

Аргумент functions

Аргумент functions предоставляет вам доступ ко всем другим пользовательским функциям. Это полезно для повторного использования кода. Вы можете определить служебные функции в одном месте и повторно использовать их во всех других ваших функциях. Например, предположим, что вы создаете имя функции My Function следующим образом:

def utility():
# do something

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...

Затем в другой функции вы можете получить доступ к функции utility() следующим образом:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
utility = functions['My Function']['utility']
...

Вы также можете использовать объект функций для хранения постоянных данных, которые могут быть повторно использованы другими функциями. Например, у вас может быть одна функция, которая при запуске с Replace All собирает некоторые данные, и другая функция, которая использует их при последующем запуске. Рассмотрим следующие две функции:

# Function One
persistent_data = {}

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...
persistent_data['something'] = 'some data'

# Function Two
def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
persistent_data = functions['Function One']['persistent_data']
...

Отладка ваших функций

Вы можете отлаживать созданные вами функции, используя стандартную функцию print() из Python. Результат печати будет отображаться во всплывающем окне после завершения поиска/замены. Вы видели пример использования print() для вывода всего оглавления выше.

Выберите порядок файлов при работе с несколькими файлами HTML

Когда вы запускаете Заменить все для нескольких файлов HTML, порядок, в котором файлы обрабатываются, зависит от того, какие файлы вы открыли для редактирования. Вы можете заставить поиск обрабатывать файлы в том порядке, в котором они отображаются, установив атрибут file_order в вашей функции, например так:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...

replace.file_order = 'spine'

file_order принимает два значения, spine и spine-reverse, которые заставляют поиск обрабатывать несколько файлов в порядке их появления в книге, либо вперед, либо назад, соответственно.

Вызов вашей функции дополнительно после нахождения последнего совпадения

Иногда, как в приведенном выше примере с автоматическим созданием оглавления, полезно, чтобы ваша функция вызывалась дополнительно после того, как найдено последнее совпадение. Вы можете сделать это, установив атрибут call_after_last_match в вашей функции, например так:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...

replace.call_after_last_match = True

Добавление вывода из функции к выделенному тексту

При поиске и замене отмеченного текста иногда бывает полезно добавить такой текст в конец отмеченного текста. Вы можете сделать это, установив атрибут append_final_output_to_marked в своей функции (обратите внимание, что вам также необходимо установить call_after_last_match), например так:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...
return 'some text to append'

replace.call_after_last_match = True
replace.append_final_output_to_marked = True

Подавление диалогового окна результатов при выполнении поиска по выделенному тексту

Вы также можете отключить диалоговое окно результатов (которое может замедлить повторное применение поиска/замены во многих блоках текста), установив атрибут suppress_result_dialog в вашей функции, например так:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...

replace.suppress_result_dialog = True

Больше примеров

Больше полезных примеров от пользователей calibre можно найти на форуме редактора calibre [https://www.mobileread.com/forums/showthread.php?t=237181].

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Редактирование книг »
- Фрагменты

Фрагменты

Редактор calibre поддерживает фрагменты. Фрагмент - это фрагмент текста, который часто используется повторно или содержит много избыточного текста. Редактор позволяет вставить фрагмент с помощью всего нескольких нажатий клавиш. Например, предположим, что вы часто вставляете теги ссылок при редактировании файлов HTML, тогда вы можете просто набрать <a в редакторе и нажать Control+J. Редактор расширит его до:

<a href="filename"></a>

Кроме того, будет выбрано слово filename с курсором, расположенным над ним, чтобы вы могли легко ввести настоящее имя файла, используя отличную функцию редактора Автозавершение. И как только вы закончите вводить имя файла, нажмите Control+J снова, и курсор переместится на позицию между тегами <a>, чтобы вы могли легко ввести текст для ссылки.

Система фрагментов в редакторе очень сложна, есть несколько встроенных фрагментов, и вы можете создать свой собственный в соответствии со своим стилем редактирования.

Следующее обсуждение встроенных фрагментов должно помочь проиллюстрировать мощь системы фрагментов.

Примечание

Вы также можете использовать фрагменты в полях ввода текста на панели Search & replace, однако заполнители (использующие Control+J для перемещения) не будут работать.

Встроенные фрагменты

Встроенные фрагменты описаны ниже. Обратите внимание, что вы можете переопределить их, создав собственные фрагменты с тем же текстом триггера.

Вставка заполнителя текста [Lorem]

Первый встроенный фрагмент и самый простой используется для вставки текста-заполнителя в документ. Текст заполнителя взят из De finibus bonorum et malorum [https://en.wikipedia.org/wiki/De_finibus_bonorum_et_malorum] философского труда Цицерона (перевод на английский). Чтобы использовать его, просто наберите Lorem в HTML-файле и нажмите Control+J. Он будет заменен парой абзацев заполнителя.

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

Вставка самозакрывающегося тега HTML [<>]

Теперь давайте рассмотрим простой пример мощной концепции заполнителей. Скажем, вы хотите вставить самозакрывающийся тег <hr/>. Просто введите <> и нажмите Control+J, редактор расширит фрагмент до:

<|/>

Здесь символ | представляет текущую позицию курсора. Затем вы можете набрать hr и нажать Control+J переместить курсор в конец тега. Этот фрагмент определен как:

Trigger: <>
Template: <$1/>$2

Заполнители - это просто знак доллара ($), за которым следует число. Когда фрагмент раскрывается нажатием Control+J курсор помещается на первый заполнитель (заполнитель с наименьшим номером). Когда вы нажимаете Control+J снова курсор переходит к следующему заполнителю (заполнителю со следующим большим номером).

Вставить тег ссылка HTML [<a]

Все теги HTML-ссылок имеют общую структуру. У них есть атрибут href и некоторый текст между открывающим и закрывающим тегами. Фрагмент для более эффективного их набора познакомит нас с некоторыми функциями заполнителей. Чтобы использовать этот фрагмент, просто введите <a и нажмите Control+J. Редактор расширит это до:

<a href="filename|"></a>

Кроме того, будет выбрано слово filename с курсором, расположенным над ним, чтобы вы могли легко ввести настоящее имя файла, используя отличную функцию редактора Автозавершение. И как только вы закончите вводить имя файла, нажмите Control+J снова, и курсор переместится на позицию между тегами <a>, чтобы вы могли легко ввести текст для ссылки. После того, как вы закончите ввод текста, нажмите Control+J снова, чтобы перейти к точке после закрывающего тега. Этот фрагмент определен как

Trigger: <a
Template: <a href="${1:filename}">${2*}</a>$3

Здесь есть пара новых функций. Сначала заполнитель $1 стал более сложным. Теперь он включает в себя некоторый текст по умолчанию (слово filename). Если заполнитель содержит текст по умолчанию, то текст по умолчанию заменяется заполнителем при раскрытии фрагмента. Также при переходе к заполнителю с текстом по умолчанию с помощью Control+J выбирается текст по умолчанию. Таким образом, вы можете использовать текст по умолчанию, чтобы напоминать вам о необходимости заполнения важных частей шаблона. Вы можете указать текст по умолчанию для заполнителя, используя синтаксис: ${<number>:текст по умолчанию}.

Другая новая особенность заключается в том, что второй заполнитель имеет звездочку после него (${2*}). Это означает, что любой текст, который был выбран перед расширением шаблона, заменяется заполнителем. Чтобы увидеть это в действии, выделите текст в редакторе, нажмите Control+J, введите <a и нажмите Control+J снова шаблон будет расширен до:

<a href="filename">whatever text you selected</a>

Вставить тег изображение HTML [<i]

Это очень похоже на вставку HTML-ссылки, как мы видели выше. Он позволяет быстро вводить тег <img src="filename" alt="description" /> и переходить между атрибутами src и alt:

Trigger: <i
Template: <img src="${1:filename}" alt="${2*:description}" />$3

Вставить произвольный тег HTML [<<]

Это позволяет вставлять произвольный полный тег HTML (или переносить ранее выделенный текст в теге). Чтобы использовать его, просто введите << и нажмите Control+J. Редактор расширит его до:

<|></>

Введите имя тега, например: span и нажмите Control+J, что приведет к:

<span>|</span>

Вы заметите, что закрывающий тег был автоматически заполнен span. Это достигается с помощью еще одной функции заполнителей, зеркальное отображение. Зеркалирование просто означает, что если вы указали образец заполнителя более одного раза в шаблоне, вторая и все последующие позиции будут автоматически заполнены тем, что вы вводите в первой позиции, когда вы нажимаете Control+J. Определение этого фрагмента:

Trigger: <<
Template: <$1>${2*}</$1>$3

Как видите, первый заполнитель ($1) был указан дважды, второй раз в закрывающем теге, который просто скопирует всё, что вы введете в открывающий тег.

Вставка произвольного тега HTML с атрибутом класса [<c]

Это очень похоже на пример вставки произвольного тега выше, за исключением того, что предполагается, что вы хотите указать класс для тега:

Trigger: <c
Template: <$1 class="${2:classname}">${3*}</$1>$4

Это позволит вам сначала ввести имя тега, нажать Control+J, ввести имя класса, нажать Control+J, ввести содержимое тега и нажав Control+J в последний раз, выпрыгнуть из тега. Закрывающий тег будет заполнен автоматически.

Создание собственных фрагментов

Фрагменты действительно блестящая вещь, потому что вы можете создать свой собственный, для соответствия своему стилю редактирования. Чтобы создать свои собственные фрагменты, перейдите в Edit →  Preferences →  Settings Editor →  Manage snippets в редакторе. Появится простой в использовании диалог, который поможет вам создать свои собственные фрагменты. Просто нажмите кнопку Добавить фрагмент, и вы увидите диалоговое окно, которое выглядит следующим образом:

Сначала дайте вашему фрагменту имя, что-то описательное, чтобы помочь идентифицировать фрагмент в будущем. Затем укажите триггер. Триггер - это просто текст, который вы должны напечатать в редакторе, прежде чем нажимать Control+J чтобы расширить фрагмент.

Затем укажите шаблон фрагмента. Начните с одного из приведенных выше примеров и измените его в соответствии со своими потребностями. Наконец, укажите, для каких типов файлов фрагмент будет активен. Таким образом, у вас может быть несколько фрагментов с одним и тем же текстом триггера, которые по-разному работают в разных типах файлов.

Следующим шагом является проверка вашего вновь созданного фрагмента. Используйте поле Test внизу. Введите текст триггера и нажмите Control+J чтобы расширить фрагмент и перейти между заполнителями.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Контент-сервер calibre

Контент-сервер calibre
calibre Контент-сервер позволяет вам получать доступ к своим библиотекам calibre и читать книги прямо в браузере на мобильном телефоне или планшете. В результате вам не нужно устанавливать на телефон какие-либо специальные приложения для чтения и управления книгами. Просто используйте браузер. Сервер загружает и сохраняет книгу, которую вы читаете, в автономном кэше, чтобы вы могли читать ее, даже если нет подключения к Интернету.

Содержание

- Доступ к контент-серверу с других устройств

- Доступ к серверу с устройств в вашей домашней сети

- Доступ к серверу из любой точки интернета

- Интерфейс сервера

- Список книг

- Просмотрщик книг

- Поддержка браузеров

- Включение режима автономной работы

- Управление пользовательскими аккаунтами с помощью командной строки

- Интеграция контент-сервера calibre с другими серверами

- Использование полностью виртуального хоста

- Использование префикса URL-адреса

- Создание сервиса для сервера calibre в современной системе Linux

Чтобы запустить сервер, нажмите кнопку Соединиться/поделиться и выберите :guilabel:` Запустить контент-сервер`. Вы можете получить сообщение от брандмауэра вашего компьютера или антивирусной программы с вопросом, можно ли разрешить доступ к calibre.exe. Нажмите кнопку «Разрешить» или «ОК». Затем откройте браузер (предпочтительно Chrome или Firefox) на своем компьютере и введите следующий адрес:

http://127.0.0.1:8080

Это откроет страницу в браузере, показывающую ваши библиотеки calibre, нажмите на любую и просмотрите книги в ней. Нажмите на книгу, и вы увидите все метаданные о книге, а также кнопки Читать и Скачать. Нажмите кнопку Читать, чтобы начать чтение книги.

Примечание

Адрес http: //127.0.0.1: 8080 будет работать только на компьютере, на котором запущен calibre. Для доступа к серверу с других компьютеров/телефонов/планшетов и т. д. вам нужно будет проделать чуть больше работы, как описано в следующем разделе

Доступ к контент-серверу с других устройств

Существует два типа удаленного доступа к устройству, которые вам обычно необходимы. Первый, более простой вид - изнутри вашей домашней сети. Если вы используете calibre на компьютере в вашей домашней сети, и вы также подключили другие устройства к той же домашней сети, вы сможете легко получить доступ к серверу на этих устройствах.

Доступ к серверу с устройств в вашей домашней сети

После запуска сервера calibre, как описано выше, снова нажмите кнопку Соединиться/поделиться. Вместо действия Запустить контент-сервер вы должны увидеть действие Остановить контент-сервер. Справа от этого действия будет указан IP-адрес и номер порта. Они выглядят как набор чисел, разделенных точками. Например:

Stop Content server [192.168.1.5, port 8080]

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

http://192.168.1.5:8080

Первая часть адреса всегда http://, следующая часть - это IP-адрес, который представляет собой числа перед запятой, и, наконец, у нас есть номер порта, который необходимо добавить к IP-адресу с двоеточием (:). Если вам повезет, это все, что вам нужно, и вы будете смотреть на библиотеки calibre на вашем устройстве. Если нет, то читайте дальше.

Устранение неполадок подключения к домашней сети

Если вы не можете получить доступ к серверу с вашего устройства, попробуйте выполнить следующие действия:

1. Убедитесь, что сервер работает, открыв адрес http: //127.0.0.1: 8080 в браузере, запущенном на том же компьютере, что и сервер.

2. Убедитесь, что ваш брандмауэр/антивирус разрешает подключения к вашему компьютеру через порт 8080 и к программе calibre. Самый простой способ устранить брандмауэр/антивирус как источник проблем - временно отключить их оба, а затем попробовать подключиться. Прежде чем отключать брандмауэр, сначала необходимо отключиться от Интернета, чтобы обеспечить безопасность компьютера.

3. Убедитесь, что ваше устройство и компьютер находятся в одной сети. Это означает, что они оба должны быть подключены к одному беспроводному маршрутизатору. В частности, ни один из них не должен использовать сотовую связь или интернет-провайдер с прямым подключением.

4. Если у вас нестандартные настройки сети, возможно, IP-адрес, указанный в меню Соединиться/поделиться, неверен. В таком случае вам придется самостоятельно определить, какой правильный IP-адрес использовать. К сожалению, учитывая бесконечное разнообразие возможных сетевых конфигураций, невозможно дать вам план действий для этого.

5. Если вы настроили имя пользователя и пароль, попробуйте сначала без них, чтобы увидеть, вызывает ли это проблемы. Некоторые устройства e-ink имеют браузеры, которые не поддерживают аутентификацию. Иногда это можно обойти, включив имя пользователя и пароль в URL, например: http://username: password@192.168.1.2: 8080.

6. Если вы застряли, вы всегда можете обратиться за помощью на форумы пользователей calibre [https://www.mobileread.com/forums/forumdisplay.php?f=166].

Доступ к серверу из любой точки интернета

Предупреждение

Перед этим вы должны включить защиту на сервере с указанием имени пользователя/пароля, иначе любой человек в мире сможет получить доступ к вашим книгам. Перейдите по адресу Настройки → Поделиться → Поделиться по сети и включите опцию Требуется имя пользователя и пароль для доступа к контент-серверу.

Хотя конкретные сведения о настройке доступа в Интернет различаются в зависимости от конфигурации сети и типа используемого вами компьютера, основная схема выглядит следующим образом.

1. Узнайте внешний IP-адрес компьютера, на котором вы собираетесь запустить сервер. Вы можете сделать это, посетив сайт What is my IP address [https://www.whatismyip.com/] в браузере, работающем на компьютере.

2. Если компьютер находится за маршрутизатором, включите переадресацию портов на маршрутизаторе, чтобы перенаправить порт 8080 (или любой другой порт, на котором вы хотите запустить контент-сервер calibre) на компьютер.

3. Убедитесь, что доступ к серверу calibre разрешен через любые брандмауэры/антивирусные программы на вашем компьютере.

4. Теперь вы сможете получить доступ к серверу на любом подключенном к Интернету устройстве, используя IP-адрес, который вы нашли в первом шаге. Например, если IP-адрес, который вы нашли, был 123.123.123.123, а порт, который вы используете для сервера calibre - 8080, адрес, который будет использоваться на вашем устройстве, станет: http://123.123 .123.123:8080.

5. При желании можно использовать службу типа no-ip <https://www.noip.com/free>`_, чтобы настроить легко запоминаемый адрес для использования вместо IP-адреса, найденного на первом шаге.

Примечание

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

Интерфейс сервера

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

Список книг

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

Сортировка и поиск в списке книг должны быть знакомы пользователям calibre. Доступ к ним можно получить, щелкнув их значки в правом верхнем углу. Они оба работают точно так же, как в основной программе calibre. Страница поиска даже позволяет создавать поисковые запросы, нажимая на авторов/ теги и т. д., так же, как и в Браузере тегов в основной программе.

Особо любимая особенность основной программы Виртуальные библиотеки также присутствует в интерфейсе сервера. Нажмите на три вертикальные точки в правом верхнем углу, чтобы выбрать виртуальную библиотеку.

Просмотрщик книг

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

Если вы оставите сервер содержимого включенным, вы даже сможете открыть одну и ту же книгу на нескольких устройствах, и она запомнит вашу последнюю прочитанную позицию. Если это не так, вы можете вызвать синхронизацию, нажав в верхней четверти и выбрав :guilabel: Sync.

Поддержка браузеров

В новом сервере calibre широко используются расширенные функции HTML 5 и CSS 3. Таким образом, для использования требуется современный браузер. Он был протестирован в Android Chrome и iOS Safari, а также в Chrome и Firefox на рабочем столе

Сервер использует функциональные возможности, которые либо уже стандартизированы, либо находятся в соответствии со стандартами. Если он в настоящее время не работает с вашим любимым браузером, но, вероятно, будет работать, когда этот браузер догонит его по возможностям.

Если вы используете особенно старый или ограниченный браузер или вам не нравится запускать JavaScript, вы можете использовать представление mobile, просто добавив /mobile к адресу сервера.

Примечание

В iOS Apple позволяет использовать только один движок браузера, поэтому Firefox, Chrome и Safari на самом деле являются одним и тем же браузером. Для нового серверного интерфейса требуется iOS 10.3.2 или новее. На Android сервер был протестирован с Chrome версии 58 и новее.

Включение режима автономной работы

Производители браузеров убеждают людей использовать SSL отключая расширенные функции в браузерах для простых HTTP-соединений. Одна из жертв этого - ApplicationCache для офлайн-поддержки в calibre. В результате, в настоящее время автономный режим работает только до тех пор, пока вы держите вкладку браузера открытой. Кроме того, в Firefox на Android вам нужно будет ввести about: config, создать настройку browser.tabs.useCache и переключить её на true.

Управление пользовательскими аккаунтами с помощью командной строки

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

Вы можете управлять учетными записями пользователей, используя параметр --manage-users в автономной программе calibre-server. Предположим, вы хотите сохранить базу данных пользователей в папке /srv/calibre, затем вы создаете её, запустив:

calibre-server --userdb /srv/calibre/users.sqlite --manage-users

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

calibre-server --userdb /srv/calibre/users.sqlite --enable-auth

Он будет использовать учетные записи пользователей, которые вы создали на предыдущем шаге.

Интеграция контент-сервера calibre с другими серверами

Здесь мы покажем вам, как интегрировать сервер контента calibre в другой сервер. Обычно для использования SSL или обслуживания библиотеки calibre как части более крупного сайта. Основной метод - запустить сервер calibre и настроить к нему обратный прокси с главного сервера.

Обратный прокси-сервер - это когда ваш обычный сервер принимает входящие запросы и передает их серверу calibre. Затем он считывает ответ от сервера calibre и пересылает его клиенту. Это означает, что вы можете просто запустить сервер calibre как обычно, не пытаясь тесно интегрировать его с вашим основным сервером.

Использование полностью виртуального хоста

Самая простая конфигурация - выделить полноценный виртуальный хост серверу calibre. В этом случае запустите сервер calibre как:

calibre-server

Теперь настройте виртуальный хост на вашем основном сервере, например, для nginx:

http {
client_max_body_size 64M; # needed to upload large books
}

server {
listen [::]:80;
server_name myserver.example.com;

location / {
proxy_pass http://127.0.0.1:8080;
}
}

Или для Apache:

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so

<VirtualHost *:80>
ServerName myserver.example.com
AllowEncodedSlashes On
ProxyPreserveHost On
ProxyPass "/" "http://localhost:8080/"
</VirtualHost>

Использование префикса URL-адреса

Если вы не хотите выделять полный виртуальный хост для calibre, вы можете использовать префикс URL. Запустите сервер calibre как:

calibre-server --url-prefix /calibre --port 8080

Ключевым параметром здесь является --url-prefix/calibre. В результате сервер содержимого будет обслуживать все URL-адреса с префиксом /calibre. Чтобы увидеть это в действии, посетите в браузере http://localhost:8080/calibre. Вы должны увидеть обычный веб-сайт сервера контента, но теперь он будет работать под именем /calibre.

Для nginx необходимая конфигурация:

http {
client_max_body_size 64M; # needed to upload large books
}

proxy_set_header X-Forwarded-For $remote_addr;
location /calibre/ {
proxy_buffering off;
proxy_pass http://127.0.0.1:8080$request_uri;
}
location /calibre {
# we need a trailing slash for the Application Cache to work
rewrite /calibre /calibre/ permanent;
}

Для Apache сначала включите прокси-модули в Apache, добавив следующее в httpd.conf:

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so

Точный способ включения прокси-модулей будет зависеть от вашей установки Apache. После включения прокси-модулей добавьте следующие правила в httpd.conf (или, если вы используете виртуальные хосты, в файл conf для рассматриваемого виртуального хоста):

AllowEncodedSlashes On
RewriteEngine on
RewriteRule ^/calibre/(.*) http://127.0.0.1:8080/calibre/$1 [proxy]
RedirectMatch permanent ^/calibre$ /calibre/

Вот и всё, теперь вы сможете получить доступ к серверу содержимого calibre по URL-адресу /calibre на вашем основном сервере. Вышеупомянутые правила передают все запросы из раздела /calibre серверу calibre, работающему на порту 8080, и благодаря указанному выше параметру --url-prefix сервер calibre обрабатывает их прозрачно.

Примечание

При использовании обратного прокси-сервера вы должны указать серверу контента calibre прослушивать только локальный хост, используя параметр --listen-on 127.0.0.1. Таким образом, сервер будет прослушивать соединения только с того же компьютера, то есть с обратного прокси.

Примечание

Если вы настроили SSL для вашего основного сервера, вы должны указать серверу calibre использовать базовую аутентификацию вместо digest-аутентификации, так как это быстрее. Для этого передайте параметр --auth-mode=basic в calibre-server.

Создание сервиса для сервера calibre в современной системе Linux

Вы можете легко создать службу для запуска calibre при загрузке в современной системе Linux (systemd [https://www.freedesktop.org/wiki/Software/systemd/]). Просто создайте файл /etc/systemd/system/calibre-server.service с содержимым, показанным ниже:

[Unit]
Description=calibre Content server
After=network.target

[Service]
Type=simple
User=mylinuxuser
Group=mylinuxgroup
ExecStart=/opt/calibre/calibre-server "/path/to/calibre library folder"

[Install]
WantedBy=multi-user.target

Измените mylinuxuser и mylinuxgroup на любого пользователя и группу, от имени которых будет работать сервер. Это должны быть тот же пользователь и группа, которым принадлежат файлы в папке библиотеки calibre. Обратите внимание, что обычно запускать сервер от имени пользователя root - не лучшая идея. Также измените путь к папке библиотеки calibre в соответствии с вашей системой. При необходимости вы можете добавить несколько библиотек. См. Справку по команде calibre-server.

Теперь запустите:

sudo systemctl start calibre-server

для запуска сервера. Проверьте его статус:

sudo systemctl status calibre-server

To make it start at boot, run:

sudo systemctl enable calibre-server

Примечание

Серверу calibre не нужен работающий X-сервер, но ему нужны установленные X-библиотеки, поскольку некоторые компоненты, которые он использует, связаны с ними.

Примечание

Сервер calibre также поддерживает активацию сокета systemd, так что вы также можете использовать его при необходимости.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Сравнение книг

Сравнение книг

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

Чтобы работать с ним, либо откройте электронную книгу в инструменте Редактирование книг, а затем нажмите Файл → Сравнить с другой книгой, либо воспользуйтесь панелью Описание книги. Если вы выполните преобразование из EPUB в EPUB, исходный файл сохранится как ORIGINAL_EPUB. Просто щёлкните правой кнопкой мыши по записи ORIGINAL_EPUB на панели описания книги и выберите Сравнить с форматом EPUB.

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

Что и где показано в окне сравнения

Как видно на скриншоте выше, вид сравнение показывает различия между стороне две книги о бок. Только различия, с несколькими строками контекста вокруг них приведены. Это делает его легко увидеть с первого взгляда только то, что было изменено в большом документе, как книгу.

Добавил текст отображается на зеленом фоне, удалить текст на красном фоне и измененного текста с синим фоном.

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

Один полезный метод при сравнении книги, чтобы рассказать инструмент сравнения, чтобы украсить текст и стиль файла перед вычислением разницы. Это часто может привести к чище и проще следовать различия. Чтобы сделать это, нажмите: guilabel: options кнопку в правом нижнем углу и выберите: guilabel:` Украсить файлы перед comparing`. Следует отметить, что иногда может украшении есть нежелательные эффекты, так как это может привести к недействительным разметки должна быть изменена, чтобы сделать его действительным. Вы также можете изменить количество строк контекста, показанных вокруг различий через: guilabel: options кнопки.

Вы можете искать любой текст в различиях через панель поиска внизу. Вам нужно будет указать, в какой панели искать, Left или Right.

Запуск инструмента сравнения

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

Сравнение двух файлов электронных книг

Откройте первый файл, используя Редактировать. Теперь нажмите Файл → Сравнить с другим файлом и выберите второй файл (он должен быть в том же формате, что и первый). Просмотр сравнения откроется в редактируемом файле справа и слева во втором файле.

Сравнение ORIGINAL_FMT и FMT

Когда вы в calibre преобразовываете из FMT в этот же формат, исходный файл сохраняется как ORIGINAL_FMT. Увидеть, что изменилось после преобразования, можно, щёлкнув правой кнопкой мыши по записи ORIGINAL_FMT на панели «Описание книги» в главном окне calibre и выбрав «Сравнить с FMT». Откроется окно сравнения с ORIGINAL_FMT слева и FMT справа.

Сравнение контрольной точки с текущим состоянием книги при редактировании

Инструмент Редактирование книг имеет очень полезную функцию Контрольные точки. Это позволяет сохранить текущее состояние книги в качестве именованной контрольной точки *, на которую вы можете вернуться, если вам не нравятся изменения, сделанные вами после создания контрольной точки. Контрольные точки также создаются автоматически при выполнении различных автоматических действий в редакторе. Вы можете просмотреть список контрольных точек, перейдя по ссылке View → Checkpoints, а затем используйте кнопку Compare, чтобы сравнить книгу с выбранной контрольной точкой с текущим состоянием. Инструмент сравнения покажет контрольную точку слева и текущее состояние справа.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Редактирование метаданных книги

Редактирование метаданных книги

Содержание

- Редактирование метаданных одной книги за раз

- Загрузка метаданных

- Управление форматами книг

- Всё об обложках

- Редактирование метаданных нескольких книг одновременно

- Поиск и замена

- Массовая загрузка метаданных

- Добавить дополнительные файлы данных в книгу

Электронные книги бывают разных форм и размеров, и чаще всего их метаданные (такие как название/автор/серия/издатель) являются неполными или неправильными. Самый простой способ изменить метаданные в calibre - это просто дважды щелкнуть по записи и ввести правильную замену. Для более сложного «мощного редактирования» используйте инструменты редактирования метаданных, описанные ниже.

Редактирование метаданных одной книги за раз

Нажмите на книгу, которую хотите отредактировать, а затем нажмите кнопку Редактировать метаданные или нажмите клавишу E. Откроется диалоговое окно, которое позволяет редактировать все аспекты метаданных. В нём есть различные функции для быстрого и эффективного редактирования. Список часто используемых советов:

- Вы можете нажать кнопку между заголовком и авторами, чтобы поменять их местами автоматически.

- Вы можете нажать кнопку рядом с сортировкой по автору, чтобы calibre автоматически заполнил её, используя значения сортировки, сохранённые у каждого автора. Используйте Управление авторами, чтобы увидеть и изменить значения сортировки авторов. Это диалоговое окно можно открыть, нажав и удерживая кнопку рядом с сортировкой автора.

- Вы можете нажать кнопку рядом с тегами, чтобы использовать Редактор тегов для управления тегами, связанными с книгой.

- Поле «Идентификаторы» можно использовать для ввода ISBN (и многих других типов идентификаторов), красный фон в поле, если вы введете неправильный ISBN. Зелёный фон в поле для действительных номеров ISBN.

- Поле сортировки автора будет красным, если значение сортировки автора отличается от того, что, по мнению calibre, должно быть.

Загрузка метаданных

Самая приятная особенность диалога редактирования метаданных — это возможность автозаполнения полей метаданными, полученными с различных веб-сайтов. Сейчас calibre использует Google Книги и Amazon. Загрузка метаданных может заполнить название, автора, серии, теги, оценку, описание и ISBN.

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

Управление форматами книг

В calibre с одной записью книги может быть связано много разных форматов. Например, вы, возможно, получили Полное собрание сочинений Шекспира в формате EPUB, а затем преобразовали его в MOBI для чтения на Kindle. calibre автоматически управляет несколькими форматами для вас. В разделе :guilabel: Доступные форматы диалога Редактировать метаданные вы можете управлять этими форматами. Можно добавить новый формат, удалить существующий формат, а также попросить calibre задать метаданные и заполнить запись книги метаданными в одном из форматов.

Всё об обложках

Вы можете попросить calibre загрузить обложки для вас, при условии, что книга имеет известный номер ISBN. Кроме того, вы можете указать файл на вашем компьютере для использования в качестве обложки. calibre может даже создать обложку по умолчанию с основными метаданными для вас. Вы можете перетащить изображение на обложку, чтобы изменить её, а также щелкнуть правой кнопкой мыши, чтобы скопировать/вставить изображение обложки.

Кроме того, есть кнопка для автоматической обрезки границ обложки, если изображение на обложке имеет уродливую рамку.

Редактирование метаданных нескольких книг одновременно

Сначала выберите книги для редактирования, удерживая Ctrl или Shift и щёлкая по ним. При выборе более одной книги нажатие кнопки Редактировать метаданные откроет диалоговое окно Массового редактирования метаданных. В этом окне вы можете быстро установить для набора книг одно и то же значение автора/издателя/рейтинга/тега/серии и т. д. Это особенно полезно, если вы только что импортировали несколько книг с общими метаданными. Это очень мощное диалоговое окно, например, оно имеет вкладку «Поиск и замена», которую можно использовать для выполнения массовых операций с метаданными и даже для копирования метаданных из одного столбца в другой.

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

Поиск и замена

Диалог Массовое редактирование метаданных позволяет произвольно выполнять мощные операции поиска и замены выбранных книг. По умолчанию он использует простой текстовый поиск и замену, но также поддерживает регулярные выражения. Для получения дополнительной информации о регулярных выражениях см. Всё об использования регулярных выражений в calibre.

Как отмечалось выше, существует два режима поиска и замены: сопоставление символов и регулярное выражение. Сопоставление символов - это символы, вводимые в поле «Поиск», и замена их теми символами, которые вы вводите в поле «Заменить на». Каждое вхождение символов поиска в поле будет заменено. Например, предположим, что искомое поле содержит a bad cat. Если вы ищете a для замены на HELLO, то результатом будет HELLO bHELLOd cHELLOt.

Если поле, по которому вы ищете, является «множественным» полем, подобным тегам, то каждый тег обрабатывается отдельно. Например, если ваши теги содержат Horror, Scary, поисковое выражение r, не будет ни с чем совпадать, потому что выражение сначала будет применено к Horror, а затем к Scary.

Если вы хотите, чтобы при поиске игнорировались различия в верхнем/нижнем регистре, снимите флажок «Учитывать регистр».

Вы можете указать calibre изменить регистр в результате (информация после замены), выбрав одну из функций из поля «Применить после замены». Доступные операции:

- Нижний регистр - изменить все символы в поле на нижний регистр

- Верхний регистр - изменить все символы в поле на верхний регистр

- «Регистр Заголовков» - капитализировать каждое слово в результате.

Поле «Ваш тест» предназначено для ввода текста, чтобы проверить, выполняет ли поиск/замена то, что вы хотите. В большинстве случаев достаточно блоков с книжными тестами, но возможно, что есть случай, который вы хотите проверить, который не показан в этих полях. Введите этот случай в «Ваш тест».

Режим регулярных выражений имеет некоторые отличия от режима символов, помимо (конечно) использования регулярных выражений. Во-первых, функции применяются к частям строки, совпадающим со строкой поиска, а не ко всему полю. Во-вторых, функции применяются к строке замены, а не ко всему полю.

Третье и самое важное - замена строки может ссылаться на части строки поиска с помощью обратных ссылок. Обратной ссылкой является \\n, где n - целое число, которое ссылается на n-ную группу в скобках в поисковом выражении. Например, если использовать тот же пример, что и выше, a bad cat, поисковое выражение a (…) (…) `и выражение замены `a \2 \1, результат будет a cat bad. Пожалуйста, смотрите Всё об использования регулярных выражений в calibre для получения дополнительной информации о обратных ссылках.

Один полезный шаблон: предположим, что вы хотите изменить регистр всего поля. Самый простой способ сделать это - использовать символьный режим, но давайте предположим, что вы хотите использовать режим регулярных выражений. Выражение поиска должно быть (^.*$), выражение замены должно быть \1, и должна быть выбрана нужная функция изменения регистра.

Наконец, в режиме регулярных выражений вы можете копировать значения из одного поля в другое. Просто измените поле источника и назначения. Копия может заменить поле назначения, добавить его к полю (добавить в начало) или добавить в поле (добавить в конце). Флажок «использовать запятую» указывает calibre (или не добавлять) запятую между текстом и полем назначения в режимах prepend и append. Если пунктов назначения несколько (например, теги), вы не можете снять этот флажок.

Поиск и замена выполняется после применения всех других изменений метаданных в других вкладках. Это может привести к некоторой путанице, потому что тестовые окна будут показывать информацию до других изменений, но операция будет применена после других изменений. Если у вас есть какие-либо сомнения относительно того, что произойдет, не смешивайте поиск/замену с другими изменениями.

Массовая загрузка метаданных

Если вы хотите загрузить метаданные для нескольких книг одновременно, щелкните правой кнопкой мыши кнопку :guilabel: Изменить метаданные и выберите Загрузить метаданные. Вы можете загружать только метаданные, только обложки или и то, и другое.

Добавить дополнительные файлы данных в книгу
calibre может хранить любое количество дополнительных файлов данных, связанных с книгой. Это могут быть альтернативные обложки, дополнительный материал и т. д. Их нельзя просматривать напрямую или использовать в качестве источников конверсии. Они также не индексируются полнотекстовой поисковой системой в calibre. Чтобы просмотреть/добавить/удалить их, выберите книгу и щелкните правой кнопкой мыши кнопку Редактировать метаданные и выберите Управление файлами данных. Появится всплывающее окно, в котором вы можете выполнять операции с этими файлами. Кроме того, вы можете щелкнуть правой кнопкой мыши по кнопке Добавить книги и выбрать Добавить файлы данных в выбранные записи книг, чтобы быстрее добавить файлы данных.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Часто задаваемые вопросы

Часто задаваемые вопросы

Содержание

- Конвертация форматов электронных книг

- Интеграция устройств

- Управление библиотекой

- Разное

Конвертация форматов электронных книг

Содержание

- Какие форматы конвертации поддерживает calibre?

- Каковы оптимальные исходные форматы для конвертации?

- Я преобразовал PDF файл, но в результате имеются разные проблемы?

- Как конвертировать мой файл, если он включает не английские символы или «умные» кавычки?

- Что случилось с оглавлением MOBI-файлов?

- Каким образом можно преобразовывать коллекцию HTML-файлов в определенной последовательности?

- Если EPUB, созданный в calibre, не работает?

- Как я могу использовать некоторые из расширенных возможностей инструментов конвертации?

Какие форматы конвертации поддерживает calibre?

calibre поддерживает конвертацию множества форматов из одного в другой. Возможно конвертировать каждый входной формат в приведенном ниже списке в каждый формат вывода.

Входные форматы: AZW, AZW3, AZW4, CBZ, CBR, CB7, CBC, CHM, DJVU, DOCX, EPUB, FB2, FBZ, HTML, HTMLZ, KEPUB, LIT, LRF, MOBI, ODT, PDF, PRC, PDB, PML, RB, RTF, SNB, TCR, TXT, TXTZ

Выходные форматы: AZW3, EPUB, DOCX, FB2, HTMLZ, KEPUB, OEB, LIT, LRF, MOBI, PDB, PMLZ, RB, PDF, RTF, SNB, TCR, TXT, TXTZ, ZIP

Примечание

PRC - общий формат, calibre поддерживает файлы PRC с заголовками TextRead и MOBIBook. PDB также - общий формат, calibre поддерживает файлы eReader, Plucker, PML и zTxt PDB. DJVU формат поддерживается только для преобразования DJVU-файлов, содержащих встроенный текст. Они, как правило, генерируются с помощью программного обеспечения для OCR. MOBI-книги могут быть двух типов: Mobi6 и KF8. calibre полностью поддерживает оба. MOBI-файлы часто имеют расширение .azw или .azw3. Файлы .DOCX поддерживаются начиная с Microsoft Word 2007 и все более новые версии.

Каковы оптимальные исходные форматы для конвертации?

В порядке убывания предпочтения: LIT, MOBI, AZW, EPUB, KEPUB, AZW3, FB2, FBZ, DOCX, HTML, PRC, ODT, RTF, PDB, TXT, PDF

Я преобразовал PDF файл, но в результате имеются разные проблемы?

PDF — это ужасный формат для начальной конвертации. Для списка различных вопросов, с которыми придется столкнуться при конвертировании PDF, см.: Преобразование PDF документов.

Как конвертировать мой файл, если он включает не английские символы или «умные» кавычки?

Существует два аспекта этой проблемы:

1. Знание кодировки исходного файла: calibre пробует предположить кодировку, которая используется в вашем исходном файле, но это зачастую невозможно, так что вы должны сообщить, какую кодировку использовать. В графическом интерфейсе это указывается через поле Исходная кодировка символов в Оформление → Текст в Общих настройках диалога конвертации. В утилитах командной строки - параметр ebook-convert-txt-input --input-encoding

2. При добавлении файлов HTML в calibre вам может потребоваться указать calibre, в какой кодировке находятся файлы. Для этого перейдите в Preferences-Advanced-Plugins-File type и настройте плагин HTML to ZIP , указав кодировку ваших HTML-файлов. Теперь, когда вы добавляете HTML-файлы в calibre, они будут правильно обрабатываться. HTML-файлы из разных источников часто имеют разные кодировки, поэтому вам, возможно, придется менять этот параметр несколько раз. Обычная кодировка для многих некириллических файлов из Интернета - cp1252, сначала попробуйте её. Заметьте, что при преобразовании файлов HTML нужно оставить указанную выше настройку кодировки ввода пустым. Это связано с тем, что плагин HTML to ZIP автоматически преобразует файлы HTML в стандартную кодировку (UTF-8).

Что случилось с оглавлением MOBI-файлов?

Первое, что нужно понимать, - в большинстве электронных книг два оглавление. Одно - традиционное оглавление, как в бумажных книгах. Это Оглавление - часть главного потока документа и может быть стилизовано как вам нравится. Это Оглавление называется Оглавление содержимого.

Кроме того есть Оглавление метаданных. Оглавление метаданных не является частью текста книги и обычно доступно при помощи специальной кнопки в читалке. Например в calibre viewer используется кнопка Показать оглавление. Это Оглавление не может быть стилизовано создателем книги. Его представление - задача просмотрщика.

В формате MOBI ситуация немного запутаннее. Т. к. формат MOBI не слишком хорошо поддерживает Оглавления метаданных. Книга MOBI эмулирует наличие Оглавления метаданных, добавляя дополнительное Оглавление содержимого в конце книги. Когда вы нажимаете Перейти к Оглавлению на Kindle, он показывает вам дополнительное Оглавление содержимого.

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

При конвертировании в MOBI, calibre находит Оглавление метаданных в исходом документе и генерирует Оглавление в-конце-файла в выходном файле MOBI. Вы можете отключить этот параметр в Выходных настройках MOBI. Также calibre можно указать добавлять его в начало или в конец книги через параметр в Выходных настройках MOBI. Помните, что семантически оно - Оглавление метаданных, в любом формате кроме MOBI оно не может быть частью текста. То, что это часть текста в MOBI - случайность, связанная с ограничениями формата. Если вы хотите получить Оглавление в определённом месте текста вашего документа, создайте его вручную. Также настоятельно рекомендуется оставить всё по умолчанию, то есть помещать Оглавление метаданных в конце книги. Заметьте, что если вы отключаете создание Оглавления в-конце-файла, получившийся файл MOBI может неправильно функционировать в Kindle, поскольку Kindle использует Оглавление метаданных для многих вещей, включая переворот страницы.

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

В конце, я призываю вас убрать Оглавление содержимого и оставить в ваших электронных книгах только Оглавление метаданных. Оглавление метаданных даст читателям наилучшую навигацию (исключая Kindle, где оно, по существу, совпадает с Оглавлением содержимого).

Примечание

Новый формат AZW3 имеет соответствующую поддержку Оглавления метаданных. Однако, прошивка Kindle может функционировать неправильно, если вы отключаете генерацию Оглавления в-конце-файла. Так что рекомендуется не менять сгенерированное Оглавление. Если вы создаёте файл AZW3 с Оглавлением метаданных и без Оглавления в-конце-файла, некоторые возможности, такие как Page Flip, в Kindle

Каким образом можно преобразовывать коллекцию HTML-файлов в определенной последовательности?

Чтобы преобразовать коллекцию HTML файлов в определенном порядке, вам нужно создать файл со списком содержимого. Это еще один файл HTML, содержащий ссылки на все другие файлы в желаемом порядке. Подобный файл выглядит так:

<html>
<body>
<h1>Table of Contents</h1>
<p style="text-indent:0pt">
<a href="file1.html">First File</a><br/>
<a href="file2.html">Second File</a><br/>
.
.
.
</p>
</body>
</html>

Затем просто добавьте этот файл HTML в графическом интерфейсе и нажмите кнопку Convert, чтобы создать электронную книгу. Вы можете использовать опцию в разделе Оглавление диалога конвертации, чтобы управлять созданием Оглавления.

Примечание

По умолчанию, при добавлении файлов HTML, calibre следует по ссылкам в файлах в порядке поиск в глубину. Это означает, что если файл A.html ссылается на B.html, C.html и D.html, но B.html также ссылается на D.html, то файлы будут иметь следующий порядок: A.html, B.html, D. .html, C.html. Если вместо этого вы хотите, чтобы порядок был A.html, B.html, C.html, D.html, тогда вы должны указать calibre на добавление файлов в порядке поиск в ширину. Сделайте это, перейдя в Preferences-Advanced-Plugins-File type и настроив плагин HTML to ZIP.

Если EPUB, созданный в calibre, не работает?

calibre не гарантирует, что созданный им EPUB получится валидным. Единственное, в чём можно быть уверенным - это то, что если вы скормите ему валидные XHTML 1.1 + CSS 2.1, он выдаст валидный EPUB. calibre старается убедиться, что файлы EPUB, которые он производит действительно работают как задумано на широком разнообразии устройств, что несовместимо с созданием валидных EPUB, но преследует цель, которая намного важнее для большинства пользователей. Если вам нужен инструмент, который всегда должен производить валидные EPUB, то calibre не для вас. Это означает, что если вы хотите отправить созданный calibre EPUB в онлайн магазин, использующий проверку валидности EPUB, вам следует убедиться в валидности файла самостоятельно, calibre не сделает это за вас. Другими словами, вы должны скармливать на вход calibre валидные XHTML + CSS документы.

Как я могу использовать некоторые из расширенных возможностей инструментов конвертации?

Вы можете получить помощь по любым возможностям конверторов, подведя на них курсор мыши в графическом интерфейсе или выполнив команду ebook-convert dummy.html .epub -h в командной строке. Лучше всего начать с просмотра следующего демо файла, который демонстрирует некоторые из расширенных возможностей html-demo.zip [https://calibre-ebook.com/downloads/html-demo.zip].

Интеграция устройств

Содержание

- Какие устройства поддерживает calibre?

- Как я мог помочь получению поддержки моего устройства в calibre?

- Моё устройство не определяется в calibre?

- Моё устройство нестандартное или необычное. Что я могу сделать, чтобы подключить его?

- Как я могу использовать calibre с моим iPad/iPhone/iPod touch?

- Как использовать calibre с телефоном/планшетом Android или Kindle Fire?

- Могу ли я получить доступ к своим книгам calibre с помощью веб-браузера на Kindle или другом устройстве для чтения?

- Я не могу посылать электронную почту при помощи calibre?

- Мое устройство монтируется в Linux только для чтения, поэтому calibre не может подключиться к нему?

- Почему calibre не поддерживает коллекции на Kindle или полку на Nook?

- Я получаю сообщение об ошибке, когда пытаюсь использовать calibre с моим Kobo Touch/Glo и т. д.?

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

- Обложки для книг, отправленных на мой Kindle ColorSoft и новее, не отображаются в списке книг?

- Обложки моих MOBI-файлов перестали отображаться в Kindle для PC/Kindle, Android/iPad и т.п.

- Я перекинул несколько книг в мой Kindle при помощи caibre и их не видно?

Какие устройства поддерживает calibre?

calibre может напрямую подключаться ко всем основным устройствам чтения электронных книг (и большинству второстепенных), смартфонам, планшетам и т. д. Кроме того, с помощью функции в Тонкой настройке Connect to folder можно использовать подключать любую читалку, представляющуюся как USB-диск. Наконец, можно подключиться по беспроводной сети к любому устройству с веб-браузером, используя сервер контента calibre.

Как я мог помочь получению поддержки моего устройства в calibre?

Если ваше устройство определяется операционной системой как USB диск, то добавить его поддержку в calibre не составит труда. Нам только нужно немного информации от вас:

- Полный список форматов электронных книг, которые поддерживает ваше устройство.

- Есть ли на устройстве специальная папка, в которую должны быть помещены все файлы электронных книг? Устройство обнаруживает файлы, помещенные в подпапки?

- Также нам потребуется информация о вашем устройстве, которую calibre соберёт автоматически. Сначала, если ваше устройство поддерживает карты SD, вставьте их. Затем подключите устройство к компьютеру. В calibre перейдите в Параметры → Разное и нажмите кнопку «Обнаружение устройства в режиме отладки». Это создаст некий отладочный вывод. Скопируйте его в файл и повторите процесс, на этот раз с отключенным от вашего компьютера устройством.

- Отправьте оба упомянутых вывода нам с прочей информацией и мы напишем драйвер для вашего устройства.

Как только вы отправите нам выходные данные для конкретной операционной системы, поддержка устройства в этой операционной системе появится в следующем выпуске calibre. Чтобы отправить нам результат, откройте отчет об ошибке и прикрепите к нему вывод. Смотрите how to report bugs [https://calibre-ebook.com/ru/bugs].

Моё устройство не определяется в calibre?

Следуйте этим инструкциям, чтобы найти проблему:

- Убедитесь, что одновременно к вашему компьютеру вы присоединяете только одно устройство. Не держите подключенными в это время другие поддерживаемые calibre устройства, такие как iPhone/iPad и прочие.

- Если вы подключаете Apple iDevice (iPad, iPod Touch, iPhone), Apple больше не позволяет стороннему программному обеспечению подключаться к своим устройствам с помощью кабеля USB. Вместо этого используйте беспроводное соединение через сервер контента calibre.

- Если вы подключаете 2024 Kindle или другое устройство Android, и они управляются macOS или Linux, прочтите примечание Используя кабель USB.

- В macOS, если вы получаете ошибки разрешений при подключении устройства к calibre, можно исправить это: guilabel: Системные настройки -> безопасность и конфиденциальность -> Файлы и папки.

- Убедитесь, что у вас установлена последняя версия calibre (текущая 8.7.0). Последнюю версию всегда можно загрузить с website:the calibre website <download>. Вы можете узнать, какую версию calibre вы используете в данный момент, посмотрев на нижнюю строку главного окна calibre.

- Убедитесь, что ваша операционная система видит устройство. То есть устройство должно отображаться в Проводнике (в Windows) или Finder (в macOS).

- В calibre перейдите по ссылке Параметры → Общий доступ к файлам  → Игнорируемые устройства и убедитесь, что ваше устройство не игнорируется.

- Если все вышеперечисленные шаги завершились неудачно, перейдите в Параметры → Разное и нажмите Обнаружение устройства в режиме отладки, подключив ваше устройство, после этого опубликуйте результат в виде тикета на <https://bugs.launchpad.net/calibre>`_.

Моё устройство нестандартное или необычное. Что я могу сделать, чтобы подключить его?

В дополнение к функции: guilabel: Connect to folder, находящейся под кнопкой: guilabel:` Connect/share`, calibre предоставляет подключаемый модуль устройства, определяемый пользователем, который можно использовать для подключения к любому обнаруженному USB-устройству, как к диску в операционной системе. Примечание: в Windows устройство должно иметь букву диска, чтобы calibre мог его использовать. См. в Настройки -> Плагины -> Интерфейс устройства -> Folder Device Interface и Настройки -> Разное -> Получить информацию для установки пользовательского устройства. Обратите внимание, что если используется пользовательский плагин для устройства, обычно определяемого встроенным плагином calibre, сначала нужно отключить встроенный плагин, для использования вместо него пользовательского.

Как я могу использовать calibre с моим iPad/iPhone/iPod touch?

Лёгкий способ просмотреть коллекцию calibre с устройства Apple - использовать Контент-сервер calibre, для доступа к коллекции из сети. Сначала выполните следующие шаги в calibre

- Установить предпочитаемый формат вывода как EPUB (формат вывода можно установить в Настройки → Интерфейс → Поведение)

- Установить профиль вывода на iPad (также работает для iPhone/iPod) в разделе Параметры → Конвертация → Общие настройки → Настройка страницы

- Конвертируйте книги для чтения на i-Pad/Phone/Pod в формат EPUB выделив их и нажав Преобразовать книги.

- Включите сервер содержимого, нажав кнопку Подключить/поделиться, и оставьте calibre запущенным. Также можно настроить автоматический запуск сервера контента с помощью Настройки → Сетевой доступ → Запускать сервер автоматически при запуске calibre.

Сервер содержимого позволяет читать книги непосредственно в Safari. Кроме того, для вашего iDevice есть множество приложений, которые могут подключаться к серверу содержимого calibre. Примеры: Marvin, Mapleread, iBooks.

Использование сервера контента

Запустите браузер Safari и введите IP-адрес и порт компьютера, на котором запущен сервер calibre, например:

http://192.168.1.2:8080/

Замените 192.168.1.2 локальным IP-адресом компьютера с запущенной calibre. Подробнее о том, как запустить сервер и узнать правильный IP-адрес, читайте в разделе «Контент-сервер calibre».

Вы увидите список книг в Safari, нажмите на любую книгу, и вы сможете либо загрузить её, либо прочитать в самом браузере. Если вы решите загрузить её, Safari спросит вас, хотите ли вы открыть файл с помощью iBooks.

Многие приложения для чтения поддерживают просмотр библиотеки calibre напрямую через поддержку OPDS . В таких приложениях вы можете перейти на экран онлайн-каталога и добавить IP-адрес сервера calibre для просмотра и загрузки книг из библиотеки calibre в приложении.

Как использовать calibre с телефоном/планшетом Android или Kindle Fire?

Есть два способа подключить Android-устройство к calibre. С помощью кабеля USB или по беспроводной сети. Сначала установите на Android-устройство приложение для чтения электронных книг. Есть множество бесплатных и платных приложений для чтения электронных книг для Android. Некоторые примеры (в произвольном порядке): `FBReader https://play.google.com/store/apps/details?id=org.geometerplus.zlibrary.ui .android & hl = ru`_, `Moon + https://play.google.com/store/apps/details?id=com.flyersoft.moonreader&hl=en`_,` Mantano https://play.google.com/store /apps/details?id=com.mantano.reader.android.lite&hl=en`_, `Aldiko https://play.google.com/store/apps/details?id=com.aldiko.android&hl=en`_ , `Kindle https://play.google.com/store/apps/details?id=com.amazon.kindle&feature=related_apps`_.

Используя кабель USB

Просто подключите устройство к компьютеру с помощью кабеля USB. calibre должен автоматически определить устройство, после чего вы сможете перенести на него книги, нажав кнопку Отправить на устройство. Обратите внимание, что в macOS и Linux только одна программа может подключаться к устройству Android одновременно, поэтому убедитесь, что устройство не открыто в диспетчере файлов ОС или в утилите передачи файлов Android и т.п.

Примечание

С новыми устройствами Android вам, возможно, придётся попрыгать с бубном, чтобы соединение заработало, т.к. Google действительно не хочет, вашей независимости от его облака. Сначала разблокируйте экран перед подключением кабеля USB. При подключении кабеля USB вы получите всплывающее уведомление. Убедитесь, что написано что-то вроде «Передача файлов мультимедиа» или «MTP (режим передачи мультимедиа)». Если это не так, коснитесь уведомления и измените режим на Media Transfer (MTP). На этом этапе возможно потребоваться перезапустить calibre, чтобы ваше устройство было распознано. Если на вашем устройстве всплывающее окно с запросом разрешения появляется при каждой попытке подключения calibre или операционной системы к нему - жмите OK.

Примечание

В Kindle Fire 8 или новее при подключении USB-кабеля появляется значок, показывающий, что устройство заряжается. Нажмите на него и переключите устройство в режим передачи данных, а затем запустите calibre, после чего он должен быть обнаружен.

По беспроводному соединению
в calibre есть встроенный веб-сервер :doc:`calibre Content server<server> ` для доступа к коллекции calibre из сети. Для просмотра используйте браузер или специальное приложение. Вначале выполните следующие шаги в calibre:

- Установите Предпочтительный выходной формат в EPUB для обычных устройств Android или MOBI для Kindles (формат вывода можно установить в Параметры → Интерфейс → Поведение)

- Преобразуйте книги, которые вы хотите читать на своем устройстве, в формат EPUB/MOBI, выбрав их и нажав кнопку Преобразовать книги.

- Включите Content server в настройках calibre и оставьте calibre включенным.

Теперь на вашем устройстве Android откройте браузер и перейдите на:

http://192.168.1.2:8080/

Замените 192.168.1.2 локальным IP-адресом компьютера с запущенной calibre. Подробнее о том, как запустить сервер и узнать правильный IP-адрес, читайте в разделе «Контент-сервер calibre».

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

Многие приложения для чтения поддерживают просмотр библиотеки calibre напрямую через поддержку OPDS . В таких приложениях вы можете перейти на экран онлайн-каталога и добавить IP-адрес сервера calibre для просмотра и загрузки книг из библиотеки calibre в приложении.

Могу ли я получить доступ к своим книгам calibre с помощью веб-браузера на Kindle или другом устройстве для чтения?

В calibre есть контент-сервер, который представляет книги в calibre как веб-страницы. Подробнее см. «Контент-сервер calibre».

Некоторые устройства, такие как Kindle (1/2/DX), не позволяют получить доступ к порту 8080 (порт по умолчанию, на котором работает сервер контента). В этом случае измените порт в настройках calibre на 80. (В некоторых операционных системах вы не сможете запустить сервер на порте с номером меньше 1024 из-за настроек безопасности. В этом случае самым простым решением является настройка ваш маршрутизатор для пересылки запросов с порта 80 на порт 8080).

Также некоторые устройства не имеют браузеров, достаточно продвинутых для запуска интерфейса, подобного приложениям, используемого сервером содержимого. Для таких устройств вы можете просто добавить /mobile к URL-адресу сервера, чтобы получить упрощённый интерфейс без использования JavaScript.

Я не могу посылать электронную почту при помощи calibre?

Из-за большого количества спама в электронной почте отправка электронной почты может быть сложной, поскольку разные почтовые серверы используют разные стратегии для блокировки электронной почты. Наиболее распространенная проблема - если вы отправляете электронную почту напрямую из calibre (без почтового ретранслятора). Многие серверы (например, Amazon) блокируют электронную почту, которая не исходит от известного ретранслятора. Самый надежный способ настроить отправку электронной почты в calibre - это сделать следующее:

- Создать бесплатный GMX аккаунт в GMX [https://www.gmx.com].

- Перейти к Настройки → Общий доступ к файлам → Пересылка книг по электронной почте в calibre, нажать кнопку Использовать GMX и ввести запрашиваемую информацию.

- Войти в свою учётную запись GMX на веб-сайте и включить отправку SMTP (Настройки-POP3 и IMAP-Отправлять и получать электронные письма через внешнюю программу)

- После этого calibre сможет использовать GMX для отправки почты.

- Если вы отправляете книги на Kindle, не забудьте обновить настройки электронной почты на странице Amazon Kindle, чтобы разрешить отправку электронной почты с вашего адреса электронной почты GMX. Также обратите внимание, что Amazon не разрешает доставку по электронной почте файлов MOBI формата AZW3 и нового формата (KF8). Наконец, Amazon недавно начал отправлять электронные письма с подтверждением, которые вы должны нажать обратно в свою учетную запись GMX, прежде чем книга будет фактически доставлена.

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

Примечание

Microsoft / Google / GMX может отключить вашу учетную запись, если вы используете её для отправки большого количества писем. Таким образом, при использовании этих сервисов для отправки почты calibre автоматически ограничивается отправкой одной книги каждые пять минут. Если вы не возражаете против того, чтобы ваша учётная запись была заблокирована, вы можете сократить этот интервал ожидания, перейдя по ссылке Preferences-Advanced-Tweaks в calibre.

Примечание

Google недавно умышленно отключили поддержку протокола рассылки сообщений (SMTP) чтобы заставить всех использовать их веб-интерфейс с показом вам рекламы. Они объявили SMTP незащищённым, что некорректно. Используйте дроугого провайдера электропочты.

Примечание

Если вас беспокоит предоставление доступа к своей учётной записи электронной почты для calibre, просто создайте новую бесплатную учётную запись электронной почты с помощью GMX или Outlook и используйте её только для calibre.

Мое устройство монтируется в Linux только для чтения, поэтому calibre не может подключиться к нему?

Ядра Linux монтируют устройства только для чтения, если в их файловых системах есть ошибки. Вы можете восстановить файловую систему с помощью:

sudo fsck.vfat -y /dev/sdc

Замените /dev/sdc на путь к узлу устройства вашего устройства. Вы можете найти узел устройства вашего устройства, которое всегда будет в /dev, проверив вывод:

mount

Почему calibre не поддерживает коллекции на Kindle или полку на Nook?

Ни Kindle, ни Nook не позволяют управлять коллекциями через USB-соединение. Если вы действительно заботитесь об использовании коллекций, я бы посоветовал вам продать свой Kindle/Nook и получить Kobo. Кажется, только Кобо понимает, что жизнь слишком коротка, чтобы вводить коллекции одну за другой на экране e-ink 😇

Обратите внимание, что в случае с Kindle есть способ управлять коллекциями через USB, но для этого требуется, чтобы Kindle перезагружался каждый раз при отключении от компьютера, чтобы изменения в коллекциях были распознаны. Таким образом, маловероятно, что разработчики calibre когда-либо почувствуют себя достаточно мотивированными, чтобы поддержать его. Однако существует плагин calibre, который позволяет создавать коллекции на Kindle из метаданных calibre. Он доступен `здесь https://www.mobileread.com/forums/showthread.php?t=244202`_.

Примечание

Amazon полностью удалила возможность управления коллекциями в своих новых моделях, таких как Kindle Touch и Kindle Fire, что сделало даже вышеуказанный плагин бесполезным, если вы не рутируете свой Kindle и не устанавливаете пользовательскую прошивку.

Я получаю сообщение об ошибке, когда пытаюсь использовать calibre с моим Kobo Touch/Glo и т. д.?

У Kobo очень глючная прошивка. Известно, что подключение к нему происходит случайно. Определенные комбинации материнской платы, USB-портов/кабелей/концентраторов могут усугубить эту тенденцию к отказу. Если вы получаете сообщение об ошибке при подключении с помощью calibre, попробуйте следующее, каждое из которых решает проблему для некоторых пользователей calibre.

- Подключите Kobo напрямую к компьютеру, а не через USB-концентратор.

- Попробуйте использовать другой USB-кабель и другой USB-порт на компьютере.

- Выйдите из Kobo и войдите снова, это заставит его перестроить базу данных, исправив ошибки.

- Попробуйте обновить прошивку на Kobo Touch до последней версии

- Попробуйте перезагрузить Kobo (иногда это решает проблему на некоторое время, но затем она появляется снова, и в этом случае вам придется перезагружать его снова и снова)

- Старайтесь класть на Kobo только одну или две книги за раз и не храните большие коллекции на Kobo.

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

Это происходит из-за ошибки Amazon. Они пытаются загрузить обложку для книги со своих серверов, и когда это не удается, они заменяют существующую обложку на созданную универсальную обложку. Подробности см. в этой ветке форума https://www.mobileread.com/forums/showthread.php?t=329945`_. Начиная с версии 4.17, у calibre есть обходной путь: если вы подключите Kindle к calibre после того, как Amazon уничтожит обложки, calibre восстановит их автоматически. Итак, чтобы увидеть обложки на Kindle, вам необходимо:

1. Отправьте книгу на Kindle из calibre

2. Отключите Kindle и подождите, пока Amazon уничтожит обложку.

3. Подключите Kindle к calibre.

Обратите внимание, что этот обходной путь работает только для книг, отправленных из calibre 4.17 или новее. В качестве альтернативы, просто оставьте Kindle в режиме полёта, если вы действительно не хотите, чтобы Amazon знал о каждой книге, которую вы читаете. Я рекомендую вам обратиться в службу поддержки Amazon и громко пожаловаться на эту ошибку. Может быть, Amazon прислушается.

Примечание

Если обходной путь у вас не работает, убедитесь, что у вас прошивка Kindle как минимум версии 5.12.5, выпущенной в апреле 2020 года.

Обложки для книг, отправленных на мой Kindle ColorSoft и новее, не отображаются в списке книг?

Amazon намеренно нарушила эту функциональность в своих устройствах ColorSoft и более новых версиях, чтобы отбить у вас охоту читать книги не от Amazon, на их устройствах. Подробности смотрите в „этой теме форума https://www.mobileread.com/forums/showthread.php?t=364350`__. Единственный известный обходной путь - отправить книги как „Личные документы“ на Kindle, что исправит отсутствие обложек, но нарушит работу других функций, таких как Whispersync, и книги будут отображаться на устройстве в разделе „Личные документы“, а не „Книги“. Чтобы включить это в calibre, перейдите по ссылке: guilabel: `Настройки-Параметры вывода-Вывод MOBI“ и установите флажок с надписью: guilabel: „Разрешить общий доступ к содержимому книги“. В результате все будущие книги, отправляемые на Kindle из calibre, будут помечены как личные документы.

Обложки моих MOBI-файлов перестали отображаться в Kindle для PC/Kindle, Android/iPad и т.п.

Причиной этому ошибка в программном обеспечении Amazon. Вы можете преодолеть эту проблему, перейдя в Параметры → Настройки вывода → MOBI вывод и изменить параметр «Включить обмен книжным контентом». Если вы переконвертируете преобразованную ранее книгу, вам также нужно включить параметр в диалоге конвертации для этой конкретной книги (так как настройки конвертации книги сохраняются и имеют преимущество).

Заметьте, что в таком случае созданный файл MOBI будет показыаться в списке персональных документоы вместо Книг на Kindle Fire и Amazon whispersync работать не будет, однако будут обложки. Вам решать, что для вас важнее. Я призываю вас связаться с Amazon и попросить их исправить эту ошибку.

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

Если вы передаёте файл MOBI в Kindle на электронных чернилах через USB соединение при помощи calibre, то calibre преодолевает проблему, загружая миниатюру обложки самостоятельно. Однако, такое решение возможно только при использовании соединения USB и передачи через calibre. Заметьте, что если вы посылаете файл через электронную почту, Amazon автоматически отмечает файл MOBI как Персональный документ и обложка будет показана, но и книга будет в разделе Персональных документов.

Я перекинул несколько книг в мой Kindle при помощи caibre и их не видно?

Книги, отправленные на Kindle, отображаются на Kindle только после того, как они были проиндексированы Kindle. Это может занять некоторое время. Если книга по-прежнему не появляется через некоторое время, вероятно, произошел сбой индексатора Kindle. Иногда определённая книга может вызвать сбой индексатора. К сожалению, Amazon не предоставила никакого способа определить, какая книга вызывает сбой на Kindle. Единственный выход - либо перезагрузить Kindle, либо удалить все файлы из его памяти с помощью проводника Windows (или любого другого файлового менеджера), а затем снова отправить ему книги, одну за другой, пока вы не обнаружите проблемную книгу. Как только вы найдете проблемную книгу, удалите её с Kindle и выполните преобразование файла этой книги MOBI в MOBI или MOBI в AZW3 в calibre, а затем загрузите файл обратно в Kindle. Это, скорее всего, решит проблему.

Управление библиотекой

Содержание

- Где хранятся файлы книг?

- Как calibre управляет именами авторов и сортировкой?

- Почему calibre не позволяет мне хранить книги в собственной структуре папок?

- Почему calibre не имет какого-либо необходимого мне столбца?

- Могу ли я добавить столбец с форматами или ISBN?

- Как перенести мою копию calibre с одного компьютера на другой?

- Список книг в calibre пуст!

- Я получаю ошибки, когда моя библиотека calibre находится на сетевом диске/NAS?

Где хранятся файлы книг?

Когда вы впервые запускаете calibre, она попросит вас указать папку для хранения ваших книг. Каждый раз, когда вы добавляете книгу в calibre, она копирует книгу в эту папку. Книги в папке организованы в подпапки по автору и названию. Обратите внимание, что содержимое этой папки автоматически управляется calibre, не добавляйте файлы/папки вручную в эту папку, так как они могут быть автоматически удалены. Если вы хотите добавить файл, связанный с определённой книгой, используйте для этого правую верхнюю область диалогового окна Редактировать метаданные. Затем calibre автоматически поместит этот файл в нужную папку и переместит его при изменении названия/автора.

Метаданные о книгах хранятся в файле metadata.db на верхнем уровне папки библиотеки. Этот файл представляет собой базу данных sqlite. При резервном копировании вашей библиотеки убедитесь, что вы скопировали всю папку и все её подпапки.

Папка библиотеки и все её содержимое составляют так называемую библиотеку calibre. У вас может быть несколько таких библиотек. Для управления библиотеками щелкните значок calibre на панели инструментов. Вы можете создавать новые библиотеки, удалять/переименовывать существующие и легко переключаться между библиотеками.

Вы можете копировать или перемещать книги между разными библиотеками (если у вас настроено несколько библиотек), щелкнув книгу правой кнопкой мыши и выбрав действие Копировать в библиотеку.

Как calibre управляет именами авторов и сортировкой?

Имена авторов сложны, особенно в разных культурах, см. Это примечание [https://www.w3.org/International/questions/qa-personal-names.en.php?changelang=en], чтобы узнать о некоторых сложностях. calibre имеет очень гибкую стратегию управления именами авторов. Первое, что нужно понять, - это то, что книги и авторы - отдельные сущности по размеру. У книги может быть более одного автора, а у автора может быть более одной книги. Вы можете управлять авторами книги с помощью диалогового окна редактирования метаданных. Вы можете управлять отдельными авторами, щёлкнув правой кнопкой мыши автора в браузере тегов слева от главного окна калибра и выбрав: guilabel: Управление авторами. Используя этот диалог, вы можете изменить имя автора, а также способ сортировки этого имени. Имя автора автоматически изменится во всех книгах этого автора. Если у книги несколько авторов, разделите их имена с помощью символа &.

Теперь переходим к сортировке имён авторов:

- Когда в calibre добавляется новый автор (это происходит всякий раз, когда добавляется книга нового автора), calibre автоматически вычисляет строку сортировки как для книги, так и для автора.

- Авторы в Браузере тегов сортируются по значению сортировки для авторов. Помните, что это отличается от поля сортировки по авторам для книги.

- По умолчанию этот алгоритм сортировки предполагает, что имя автора находится в формате Имя Фамилия и генерирует значение сортировки Фамилия, Имя.

- Вы можете изменить этот алгоритм, перейдя в Preferences-Advanced-Tweaks и установив настройку author_sort_copy_method.

- Вы можете заставить calibre пересчитывать значения сортировки по авторам для каждого автора, щёлкнув правой кнопкой мыши по любому автору и выбрав Управление авторами, затем нажав кнопку Пересчитать все значения сортировки по авторам. Сделайте это после того, как вы установили желаемый твик author_sort_copy_method.

- Вы можете заставить calibre пересчитать значения сортировки по авторам для всех книг с помощью диалогового окна массового редактирования метаданных (выберите все книги и щёлкните Редактировать метаданные, установите флажок Автоматически устанавливать сортировку по авторам, затем нажмите OK.)

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

- Вы можете управлять отображением в Браузере тегов авторов с использованием их имён или значений сортировки, установив настройку Categories_use_field_for_author_name в Preferences-Advanced-Tweaks

Обратите внимание, что вы можете установить любое значение сортировки для отдельного автора, используя Manage authors. Это полезно при работе с именами, которые calibre не может получить правильно, такими как сложные, состоящие из нескольких частей имена, такие как Miguel de Cervantes Saavedra, или при работе с азиатскими именами, такими как Sun Tzu.

При всей этой гибкости можно настроить calibre для управления именами авторов, как вам нравится. Например, один из распространенных запросов - указать имена авторов отображения калибра LN, FN. Для этого, и если приведенное ниже примечание к вам не относится, то:

- Установите author_sort_copy_method в copy как описано выше.

- Перезапустите calibre. Не меняйте никаких метаданных книги до выполнения оставшихся шагов.

- Измените все имена авторов на LN, FN, используя диалог Управление авторами.

- После того как вы изменили авторов, нажмите кнопку Пересчитать все значения сортировки авторов

- Нажмите ОК, в этот момент calibre изменит авторов всех ваших книг. Это может занять какое-то время.

Примечание

При изменении с FN LN на LN, FN, часто бывает, что значения в author_sort уже в формате LN, FN. Если это ваш случай, то следайте следующее:

- Установите author_sort_copy_method в copy как описано выше.

- Перезапустите calibre. Не меняйте никаких метаданных книги до выполнения оставшихся шагов.

- Откройте диалог Управление авторами. Нажмите кнопку ``копировать все данные сортировки автору `

- Пройдитесь по авторам, чтобы быть уверенным, что всё в порядке. Вы всё ещё можете нажать Отмена, чтобы отменить изменения. Если вы нажали OK, то вернуть изменения будет невозможно.

- Нажмите ОК, в этот момент calibre изменит авторов всех ваших книг. Это может занять какое-то время.

Почему calibre не позволяет мне хранить книги в собственной структуре папок?

Весь смысл функций управления библиотекой calibre заключается в том, что они предоставляют интерфейс на основе поиска и сортировки для поиска книг, который намного более эффективен, чем любая возможная схема папок, которую вы могли бы придумать для своей коллекции. Действительно, как только вы освоите интерфейс calibre для поиска, сортировки и просмотра своей коллекции, вы больше никогда не почувствуете необходимости рыться в файлах на вашем диске, чтобы снова найти книгу. Управляя книгами в собственной структуре папок, состоящей из файлов „Автор - Название - Книга“, calibre может достичь высокого уровня надежности и стандартизации. Чтобы проиллюстрировать, почему интерфейс на основе поиска / тегов превосходит папки, рассмотрим следующее. Предположим, ваша коллекция книг красиво отсортирована по папкам по следующей схеме:

Genre -> Author -> Series -> ReadStatus

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

ReadStatus -> Genre -> Author -> Series

В calibre вместо этого вы должны использовать теги для отметки жанра и статуса чтения, а затем просто использовать простой поисковый запрос, например, tag:scifi, and not tag:read. calibre даже имеет красивый графический интерфейс, поэтому вам не нужно изучать его язык поиска, вместо этого вы можете просто щелкнуть теги, чтобы включить или исключить их из поиска.

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

Если вы беспокоитесь о том, что когда-нибудь calibre перестанет развиваться, оставив все ваши книги брошенными в своей структуре папок, изучите мощную функцию Сохранить на диск в calibre, которая позволяет вам экспортировать все ваши файлы в структуру папок произвольной сложности на основе их метаданных.

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

Если вы всё ещё не уверены, то, боюсь, calibre не для вас. Поищите что-нибудь другое для каталогизации ваших книг. Просто чтобы было ясно, это не изменится. Пожалуйста, не пытайтесь заставить изменить это.

Почему calibre не имет какого-либо необходимого мне столбца?

В calibre предусмотрены столбцы для наиболее часто используемых и широко используемых полей. Кроме того, вы можете добавлять любые понравившиеся столбцы. Столбцы могут быть добавлены через Preferences-Interface-Add your own columns. Посмотрите учебное пособие UI Power tips [https://calibre-ebook.com/ru/demo#tutorials], чтобы узнать, как создавать свои собственные столбцы, или прочтите это сообщение в блоге [https://blog.calibre-ebook.com/calibre-custom-columns/].

Вы также можете создать «виртуальные столбцы», которые содержат комбинации метаданных из других столбцов. В диалоге добавления столбцов используйте ссылки на Быстрое создание для лёгкого создания столбцов для отображения ISBN или формата книги. Вы можете использовать мощный язык шаблонов calibre, чтобы сделать намного больше столбцов. Для подробностей смотрите Язык шаблонов calibre.

Могу ли я добавить столбец с форматами или ISBN?

Да, вы можете. Следуйте инструкциям в ответе выше для добавления особых столбцов.

Как перенести мою копию calibre с одного компьютера на другой?

Вы можете экспортировать все данные calibre (книги, настройки и плагины), а затем импортировать их на другой компьютер. Сначала посмотрим, как экспортировать данные:

- Щелкните правой кнопкой мыши значок calibre на главной панели инструментов calibre и выберите Экспорт / импорт всех данных calibre. Обратите внимание, что если в данный момент подключено устройство, этот пункт меню будет недоступен, поэтому отключите все подключенные устройства. Затем нажмите кнопку с надписью Экспортировать все данные calibre. Вы увидите список всех ваших библиотек calibre. Нажмите ОК и выберите пустую папку где-нибудь на вашем компьютере. Экспортированные данные будут сохранены в этой папке. Просто скопируйте эту папку на свой новый компьютер и следуйте инструкциям ниже, чтобы импортировать данные.

- Установите calibre на свой новый компьютер и запустите Welcome wizard, неважно, что вы там делаете, так как на следующем шаге вы импортируете свои старые настройки. Теперь у вас будет пустой calibre с только руководством Getting Started в библиотеке. Ещё раз щёлкните правой кнопкой мыши кнопку calibre и выберите Экспорт / импорт всех данных calibre. Затем нажмите кнопку с надписью :guilabel: Импортировать ранее экспортированные данные. Выберите папку с экспортированными данными, которые вы скопировали ранее. Теперь у вас есть список библиотек для импорта. Просмотрите список поочерёдно и выберите новое расположение для каждой библиотеки (это просто пустая папка где-то на вашем компьютере). Щелкните ОК. После завершения импорта calibre перезапустится со всеми вашими старыми библиотеками, настройками и плагинами calibre.

Примечание

Эта функция импорта / экспорта доступна только начиная с версии 2.47 калибра. Если у вас более старая версия calibre или у вас возникли проблемы с импортом / экспортом, вы можете просто скопировать папку библиотеки calibre вручную, как описано в следующем абзаце.

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

Теперь на новом компьютере запустите калибр в первый раз. Он запустит Welcome wizard и запросит у вас расположение библиотеки calibre. Наведите его на ранее скопированную папку. Если на целевом компьютере уже установлен calibre, то Welcome wizard не запустится. В этом случае щелкните правой кнопкой мыши значок calibre на панели инструментов и укажите на только что скопированную папку. Теперь у вас будет две библиотеки calibre на компьютере, и вы можете переключаться между ними, щелкая значок calibre на панели инструментов. При переносе библиотеки сохраняются все ваши метаданные, теги, настраиваемые столбцы и т. д.

Список книг в calibre пуст!

Чтобы понять, почему это произошло, вы должны понимать, что такое библиотека calibre. На самом базовом уровне библиотека calibre - это просто папка. Всякий раз, когда вы добавляете книгу в calibre, файлы этой книги копируются в эту папку (сгруппированные по подпапкам по автору и названию). Внутри папки библиотеки calibre на верхнем уровне вы увидите файл с именем metadata.db. В этом файле calibre хранит метаданные, такие как название/автор/рейтинг/теги, для каждой книги в вашей библиотеке calibre. Список книг, который отображает calibre, создается путём чтения содержимого этого файла metadata.db.

Существует две причины, по которым Calibre показывает пустой список книг:

- Папка вашей библиотеки calibre изменила свое местоположение. Это может произойти, если он был на внешнем диске и буква диска для этого диска изменилась. Или если вы случайно переместили папку. В этом случае calibre не может найти свою библиотеку и вместо этого запускается с пустой библиотекой. Для исправления щёлкните правой кнопкой мыши значок calibre на панели инструментов calibre и выберите «Переключить / создать библиотеку». Щёлкните по маленькому синему значку, чтобы выбрать новое расположение библиотеки calibre, и после выбора расположения нажмите OK. Если вы не знаете новое местоположение, найдите на своем компьютере файл metadata.db.

- Ваш файл metadata.db был удален / поврежден. В этом случае вы можете указать calibre восстановить файл metadata.db из его резервных копий. Щёлкните правой кнопкой мыши значок calibre на панели инструментов calibre и выберите «Обслуживание библиотеки - Восстановление базы данных». calibre автоматически перестроит файл metadata.db.

Я получаю ошибки, когда моя библиотека calibre находится на сетевом диске/NAS?

Не помещайте вашу библиотеку calibre на сетевой диск.

Файловая система - это сложный зверь. В большинстве сетевых файловых систем отсутствуют различные функции файловой системы, которые использует calibre. Некоторые не поддерживают блокировку файлов, некоторые не поддерживают жесткую привязку, некоторые просто нестабильны. Кроме того, calibre - это однопользовательское приложение, и если вы случайно запустите две копии calibre в одной сетевой библиотеке, произойдут неприятности. Наконец, разные операционные системы налагают разные ограничения на файловые системы, поэтому, если вы используете общий сетевой диск в разных операционных системах, опять же, могут случиться неприятности.

Рассмотрите возможность использования контент-сервера calibre, чтобы ваши книги были доступны на других компьютерах. Запустите calibre на одном компьютере и получите доступ к нему через контент-сервер или решение для удаленного рабочего стола.

Если вам необходимо предоставить общий доступ к реальной библиотеке, используйте инструмент синхронизации файлов, такой как DropBox или rsync, вместо сетевого диска. Если вы используете инструмент синхронизации файлов, важно убедиться, что и calibre, и инструмент синхронизации файлов не пытаются одновременно получить доступ к библиотеке calibre. Другими словами, не запускайте инструмент синхронизации файлов и calibre одновременно.

Даже при использовании этих инструментов существует опасность повреждения / потери данных, поэтому делайте это только в том случае, если вы готовы нести этот риск. В частности, имейте в виду, что Google Диск несовместим с calibre, если вы поместите свою библиотеку calibre в Google Диск, вы потеряете данные. Подробности смотрите в этой теме [https://www.mobileread.com/forums/showthread.php?t=205581].

Разное

Содержание

- Amazon прекращает доставку файлов MOBI по электронной почте?

- Я хочу, чтобы calibre загружал новостис моих любимых сайтов новостей.

- Почему программа называется calibre?

- Почему calibre показывает только некоторые из моих шрифтов в macOS?

- calibre не запускается в Windows?

- calibre время от времени тормозит/вылетает?

- Просмотрщик электронных книг calibre и инструменты редактирования книг не работают в Windows?

- Использование просмотрщика или выполнение любых конвертаций приводит к ошибке нехватки разрешений в Windows

- calibre не запускается/вылетает на macOS?

- Я получаю только чёрный или белый экран при запуске просмотрщика электронных книг Calibre?

- Я загрузил установщик, но он не работает?

- Мой антивирус утверждает, что calibre - вирус/троян?

- Как я могу сделать резервную копию calibre?

- Как я могу использовать купленные EPUB книги с calibre (или что делать с файлами .acsm)?

- Я получаю ошибку «Доступ запрещён»?

- Могу ли я показать метаданные комментария на моем ридере?

- Как я могу указать calibre использовать мой HTTP прокси?

- Я хочу, чтобы в calibre были добавлены некоторые возможности. Что я могу сделать?

- Почему у calibre нет автоматического обновления?

- Как лицензирована calibre?

- Как я могу запустить calibre с USB-флешки?

- Как мне запустить такие части calibre, как загрузка новостей и сервер содержимого, на моём собственном сервере Linux?

Amazon прекращает доставку файлов MOBI по электронной почте?

Amazon «объявила https://blog.the-ebook-reader.com/2022/05/03/amazon-dropping-mobi-support-on-send-to-kindle-apps/__, что прекратит принимать MOBI. файлы, отправленные по электронной почте на адреса электронной почты ``@kindle.com`. Вы можете настроить calibre на отправку EPUB вместо MOBI, перейдя в Настройки-Обмен книгами по электронной почте, а затем удалив MOBI из списка форматов для отправки на ваш адрес электронной почты @kindle.com и добавив вместо этого EPUB.

Однако обратите внимание, что прием EPUB в Amazon очень несовершенен, они отклонят ряд файлов EPUB, которые работают везде. В таких случаях можно попробовать следующий трюк:

1. Конвертируйте файл EPUB в MOBI

2. Затем конвертируйте файл MOBI обратно в EPUB и отправьте полученный файл EPUB.

Это удалит всё расширенное форматирование, встроенные шрифты и т. д., но значительно повысит шансы на то, что Amazon примет EPUB.

Примечание

Если вы ранее использовали доставку по электронной почте периодических изданий, загруженных calibre, вам будет лучше отправлять их по USB-кабелю или загружать их с сервера контента калибровки через встроенный браузер Kindle. Однако, если вы хотите продолжить использовать доставку по электронной почте, вы можете попробовать изменить формат вывода в Preferences-Behavior на EPUB, тогда caliber будет загружать новости в формате EPUB. Примет ли Amazon EPUB или нет — это совсем другой вопрос.

Я хочу, чтобы calibre загружал новостис моих любимых сайтов новостей.

Если вы достаточно опытный с компьютерами, вы можете научить calibre скачивать новости с любого сайта по вашему выбору. Чтобы узнать, как это сделать смотрите news`.

В противном случае вы можете запросить конкретный новостной сайт, разместив сообщение на форуме calibre Recipes [https://www.mobileread.com/forums/forumdisplay.php?f=228].

Почему программа называется calibre?

Выбирайте:

- Converter And LIBRary for E-books

- Продукт высокого качества (calibre)

- Дань SONY Librie, первой читалке электронных книг с экраном на основе электронных чернил

- Моя жена выбрала название ;-)

calibre произносится как кал-и-бер, не ка-ли-бре. Если вы удивлены, калибре - произношение слова caliber в Британии/Содружестве. Как для индийца, это для меня естественное произношение.

Почему calibre показывает только некоторые из моих шрифтов в macOS?

calibre встраивает шрифты в файлы электронных книг, которые он создает. Файлы электронных книг поддерживают встраивание только шрифтов TrueType и OpenType (.ttf и .otf). Большинство шрифтов в системах MacOS имеют формат .dfont, поэтому они не могут быть встроены. calibre показывает только шрифты TrueType и OpenType, имеющиеся в вашей системе. Вы можете найти множество таких шрифтов в Интернете. Просто загрузите файлы .ttf / .otf и добавьте их в папку Library/Fonts в своей домашней папке.

calibre не запускается в Windows?

Этому может быть несколько причин:

- Если вы не получаете ошибок, но окно calibre не появляется, возможно, оно появилось за пределами экрана. Вы можете собрать все окна на текущем экране, используя один из методов, описанных здесь [https://www.wikihow.com/Bring-an-Off-Screen-Window-Back-on-Windows].

- Известно, что некоторое программное обеспечение вмешивается в работу calibre, попробуйте перезагрузиться в безопасном режиме и посмотреть, работает ли оно. Известным виновником является Sunshine [https://github.com/LizardByte/Sunshine] программное обеспечение для совместного использования экрана.

- Если вы видите ошибку о том, что calibre не может открыть файл, потому что он используется другой программой, сделайте следующее:

- Удалите программу calibre

- Перезагрузите ваш компьютер.

- Переустановите calibre , но не запускайте calibre из мастера установки.

- Временно отключите ваш антивирус ( для безопасности перед этим отключитесь от интернета)

- Загляните в директорую, которую вы выбрали для библиотеки calibre . Если вы видите файл с названием metadata.db, удалите его.

- Запустите calibre

- С этого моменты calibre должна запускаться нормально.

- Если вы видите ошибку о том, что функция Python неожиданно завершилась после обновления calibre, сначала удалите calibre, потом удалите директории (если они существуют):file:C:\Program Files\Calibre and C:\Program Files\Calibre2 . Теперь переустановите calibre и всё должно быть хорошо.

- Если вы получаете ошибку в Welcome wizard при первом запуске calibre, попробуйте выбрать папку, например C:\library, в качестве библиотеки calibre (у calibre иногда есть проблемы с расположением библиотек, если путь содержит неанглийские символы или только цифры и т. д.)

- Запустите его от имени Администратора (Щёлкните правой кнопкой мыши по значку и выберите Запустить как Администратор)

Если он по-прежнему не запускается, запустите командную строку (нажмите клавишу Windows и R; затем введите: command: cmd.exe в появившемся диалоговом окне Выполнить). В командной строке введите следующую команду и нажмите Enter:

calibre-debug -g

Публикуйте любые результаты, которые вы видите в справочном сообщении, на Форуме [https://www.mobileread.com/forums/forumdisplay.php?f=166].

calibre время от времени тормозит/вылетает?

Я знаю несколько вещей, которые могут быть этому причиной:

- Недавно вы подключили внешний монитор или телевизор к компьютеру. В этом случае, при открытии calibre новых окон, например, окна редактирования метаданных или диалогового окна преобразования, они появляются на втором экране, где вы их не замечаете и думаете, что calibre завис. Отключите второй монитор и перезапустите calibre.

- Сообщалось, что следующие программы вызывают сбои в calibre: Если вы используете любую из них, закройте их перед запуском calibre или удалите их: RoboForm, Logitech SetPoint Settings, Constant Guard Protection от Xfinity, Spybot, Killer Network Manager, Интерфейс пользовательского интерфейса Nahimic, Acronis True Image.

- Вы используете USB мышь/планшет Wacom. Присутствует несовместимость между драйверами Wacom и графическим инструментарием, используемым calibre. Попробуйте использовать мышь не от Wacom.

- В некоторых 64-разрядных версиях Windows есть программное обеспечение/параметры безопасности, которые препятствуют нормальной работе 64-разрядного calibre. Если вы используете 64-разрядную версию calibre, попробуйте перейти на 32-разрядную версию.

- Если сбой происходит, когда вы пытаетесь скопировать текст из программы просмотра электронных книг calibre, скорее всего, это вызвано каким-то запущенным приложением для мониторинга/управления буфером обмена. Выключите его, и всё будет в порядке.

- Если сбои происходят именно тогда, когда вы используете диалоговое окно с файлом, например, нажимаете кнопку Добавить книги или Сохранить на диск, значит, у вас есть программное обеспечение, которое установило неработающие расширения оболочки на вашем компьютере. компьютер. Известные виновники: SpiderOak, odrive sync и Dell Backup and Recovery и NetDrive. Если у вас есть один из них, удалите его, и всё будет в порядке. Вы также можете использовать NirSoft Shell Extension Viewer [https://www.nirsoft.net/utils/shexview.html], чтобы узнать, какие расширения оболочки установлены в вашей системе, и отключить их по отдельности, если вы не хотите удалять программу. Не забудьте перезагрузить проводник или перезагрузить весь компьютер после отключения расширений оболочки.

Если ничего из вышеперечисленного не относится к вам, значит, на вашем компьютере есть какая-то другая программа, которая мешает calibre. Сначала перезагрузите компьютер в безопасном режиме, чтобы у вас было как можно меньше запущенных программ, и посмотрите, происходят ли сбои по-прежнему. Если этого не происходит, то вероятно проблема вызвана какой-то программой. Наиболее вероятным таким виновником является программа, которая изменяет поведение других программ, например антивирус, драйвер устройства, что-то вроде RoboForm (приложение для автоматического заполнения форм) или вспомогательные технологии, такие как голосовое управление или средство чтения с экрана.

Единственный способ найти виновника - удалить программы одну за другой и посмотреть, какая из них вызывает проблему. Вкратце: остановите программу, запустите calibre, проверьте, нет ли сбоев. Если они всё ещё возникают, остановите другую программу и повторите.

Просмотрщик электронных книг calibre и инструменты редактирования книг не работают в Windows?

Эти две программы используют аппаратное ускорение, поскольку они встраивают версию браузера Chrome для рендеринга HTML. Если они не работают, это будет из-за несовместимости с драйверами графического процессора вашей системы. Попробуйте сначала обновить их и перезагрузитесь. Если это не поможет, вы можете установить для переменной среды QTWEBENGINE_CHROMIUM_FLAGS значение --disable-gpu, чтобы отключить аппаратное ускорение. Подробности смотрите на этой странице [https://doc.qt.io/qt-6/qtwebengine-debugging.html].

Использование просмотрщика или выполнение любых конвертаций приводит к ошибке нехватки разрешений в Windows

Что-то на вашем компьютере мешает calibre получить доступ к собственным временным файлам. Скорее всего, права доступа к вашей папке Temp неверны. Перейдите в папку C:\Users\USERNAME\AppData\Local в проводнике Windows, а затем щелкните правой кнопкой мыши папку`Temp`, выберите Properties и перейдите во вкладку Безопасность. Убедитесь, что ваша учётная запись пользователя имеет полный контроль над этой папкой.

У некоторых пользователей выполнение следующей команды в командной строке с правами Администратора исправило разрешения. Для запуска командной строки от Администратора, найдите cmd.exe в меню «Пуск», затем щёлкните правой кнопкой мыши по найденному пункту и выберите «Запуск от имени администратора». В командной строке введите следующую команду и нажмите Enter:

icacls "%appdata%\..\Local\Temp" /reset /T

Или вы можете запустить calibre от имени Администратора, но при этом некоторые функции, такие как перетаскивание мышью, перестанут работать.

В итоге, некоторые пользователи сообщили, что отключение UAC решает проблему.

calibre не запускается/вылетает на macOS?

Одной из распространенных причин сбоев в macOS является использование технологий специальных возможностей, несовместимых с графическим инструментарием, используемым calibre. Попробуйте отключить VoiceOver, если он у вас включен. Также перейдите в Системные настройки-Система-Универсальный доступ и отключите настройку для включения доступа для вспомогательных устройств на всех вкладках. Другой причиной могут быть некоторые сторонние приложения, которые изменяют поведение системы, такие как Smart Scroll и Default Folder X.

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

Qt: internal: -108: Error ATSUMeasureTextImage text/qfontengine_mac.mm

тогда проблема, вероятно, в поврежденном кэше шрифтов. Вы можете очистить кэш, следуя этим инструкциям [https://typefaceapp.com/help/troubleshooting/macos-font-cache]. Если это не решит проблему, поищите повреждённый файл шрифта в вашей системе в ~/Library/Fonts или подобном месте. Простой способ проверить наличие поврежденных шрифтов в macOS - запустить приложение «Font Book» , выбрать все шрифты, а затем в меню File выбрать «Validate fonts».

Я получаю только чёрный или белый экран при запуске просмотрщика электронных книг Calibre?

Это происходит из-за несовместимости между Qt WebEngine, который средство просмотра использует для рендеринга, и драйверами графического процессора в вашей системе. Сначала попробуйте обновить драйверы графического процессора. Если это не поможет, вы можете попробовать отключить аппаратное ускорение в Qt WebEngine, установив для переменной среды QTWEBENGINE_CHROMIUM_FLAGS значение --disable-gpu. См. Переменные окружения, чтобы узнать, как изменить переменные среды.

Я загрузил установщик, но он не работает?

При скачивании из Интернета загруженные файлы иногда могут оказаться поврежденными. Если скачанный установочный файл calibre не запускается, попробуйте скачать его снова. Если это не помогает, скачайте его из другого расположения [https://github.com/kovidgoyal/calibre/releases/latest]. Если установщик по-прежнему не работает, значит что-то на вашем компьютере мешает его запуску.

- Попробуйте временно отключить вашу антивирусную программу (Microsoft Security Essentials, Антивирус Касперского, Norton, McAfee и проч.). Скорее всего, она и является причиной зависания процесса обновления на полпути.

- Точно так же, если установщик дает сбой/запускается и у вас работает Microsoft PowerToys, выйдите из него.

- Если вы установили программу в нестандартное расположение, попробуйте запустить программу установки от имени администратора

- Попробуйте перезагрузить компьютер и запустить программу очистки реестра, например Wise registry cleaner [https://www.wisecleaner.com].

- Попробуйте чистую установку. Деинсталлируйте calibre, удалите каталог C:\Program Files\Calibre2 (или тот, который вы указывали при установке). Переустановите calibre. Имейте в виду, что деинсталляция не затронет ваши книги или настройки.

- Попробуйте загрузить установщик в другом браузере. Например, если вы используете Microsoft Edge, попробуйте вместо этого использовать Firefox или Chrome.

- Если вы получаете сообщение об отсутствующей DLL в Windows, то, скорее всего, права доступа к вашей временной папке неверны. Перейдите в папку C:\Users\USERNAME\AppData\Local в проводнике Windows, а затем щелкните правой кнопкой мыши по папке Temp и выберите Свойства и перейдите в вкладку Безопасность. Убедитесь, что у вашей учётной записи пользователя есть полные права на эту папку.

Если вы по-прежнему не можете заставить работать установщик и работаете в Windows, вы можете использовать calibre portable install [https://calibre-ebook.com/ru/download_portable], для которого не требуется установщик (это просто ZIP файл).

Мой антивирус утверждает, что calibre - вирус/троян?

Первое, что нужно проверить, это то, что вы загружаете calibre с официального сайта: https://calibre-ebook.com/ru/<download>. Убедитесь, что вы нажимаете ссылки для скачивания слева, а не рекламу справа. calibre - очень популярная программа, и недобросовестные люди создают фальшивые веб-сайты, якобы предлагающие её для загрузки, с целью обмана неосторожных посетителей.

Если у вас есть официальная загрузка, а ваша антивирусная программа всё ещё утверждает, что calibre - это вирус, значит, ваша антивирусная программа ошибается. Антивирусные программы используют эвристику, шаблоны кода, которые «выглядят подозрительно» для обнаружения вирусов. Это скорее похоже на расовое профилирование. calibre - это продукт с полностью открытым исходным кодом. Вы можете самостоятельно просмотреть исходный код (или нанять кого-нибудь, кто сделает это за вас), чтобы убедиться, что это не вирус. Сообщите о ложной идентификации любой компании, у которой вы покупаете антивирусное программное обеспечение. Если антивирусная программа не позволяет вам загрузить/установить calibre, временно отключите её, установите calibre и снова включите.

Как я могу сделать резервную копию calibre?

Самая важная вещь для резервного копирования - это папка библиотеки calibre, которая содержит все ваши книги и метаданные. Это папка, которую вы выбрали для своей библиотеки calibre при первом запуске calibre. Вы можете получить путь к папке библиотеки, щелкнув значок calibre на главной панели инструментов. Вы должны сделать полную резервную копию этой папки со всеми её файлами и подпапками.

Вы можете переключить calibre на использование резервной папки библиотеки, просто щелкнув значок calibre на панели инструментов и выбрав папку резервной библиотеки. Резервная копия библиотеки папок создает резервные копии ваших пользовательских столбцов и сохраненных результатов поиска, а также всех ваших книг и метаданных.

Если вы хотите сделать резервную копию конфигурации/плагинов calibre, вам необходимо сделать резервную копию папки config. Вы можете найти эту конфигурационную папку через Preferences-Miscellaneous. Обратите внимание, что восстановление папок конфигурации официально не поддерживается, но в большинстве случаев должно работать. Просто скопируйте содержимое резервной папки в текущую папку конфигурации для восстановления.

Как я могу использовать купленные EPUB книги с calibre (или что делать с файлами .acsm)?

В большинстве купленных EPUB-книг имеется DRM, что не даёт calibre их открыть. Хотя вы всё ещё можете использовать calibre для их хранения и передачи на своё устройство чтения. Для начала нужно авторизовать устройство на компьютере с Windows при помощи Adobe Digital Editions. Как только это будет сделано, EPUB-книги, переданные calibre, будут отлично работать на вашей читалке. Когда вы покупаете EPUB-книгу на веб-сайте, то получаете «.acsm» файл. Его необходимо открыть в Adobe Digital Editions, и программа загрузит актуальную «.epub» книгу. Файл электронной книги будет сохранён в папке «My Digital Editions», откуда вы сможете добавить его в calibre.

Я получаю ошибку «Доступ запрещён»?

Ошибки доступа могут появляться по ряду причин, никаким образом не связанных с calibre.

- Ошибки доступа могут появляться по причине включенной защиты от записи на SD карте.

- В macOS, если вы получаете ошибки разрешений при подключении устройства к calibre, можно исправить это: guilabel: Системные настройки -> безопасность и конфиденциальность -> Файлы и папки.

- Если вы или какая-либо используемая вами программа изменили разрешения проблемных файлов на «только чтение».

- Если на устройстве произошла ошибка файловой системы, из-за которой ваша операционная система смонтировала файловую систему в режиме только для чтения или пометила конкретный файл атрибутом «только для чтения» ожидая его восстановления.

- Если владельцами файлов выставлен другой пользователь.

- Если ваш файл открыт в другой программе.

- Если файл находится на устройстве, возможно, вы достигли ограничения в 256 файлов в корневом каталоге устройства. В этом случае вам необходимо переформатировать устройство/SD-карту, указанную в сообщении об ошибке, в файловую систему FAT32 или удалить некоторые файлы с SD-карты/памяти устройства.

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

Могу ли я показать метаданные комментария на моем ридере?

Большинство читателей этого не поддерживают. Вы должны пожаловаться на это производителю, и, надеюсь, если достаточно людей будет жаловаться, все изменится. Тем временем вы можете вставить метаданные, включая комментарии, на «страницу обложки» в начале электронной книги, используя опцию «Вставить метаданные как страницу в начале книги» во время преобразования. Опция находится в разделе Определение структуры настроек преобразования. Обратите внимание, что для этого вам необходимо конвертировать книгу. Если ваша книга уже имеет формат, который не требует преобразования, вы можете преобразовать этот формат в тот же формат.

Другой альтернативой является создание каталога в форме электронной книги, содержащего список всех книг в вашей библиотеке calibre с их метаданными. Нажмите и удерживайте кнопку Convert, чтобы получить доступ к инструменту создания каталога. И прежде чем вы спросите, нет, у вас не может быть каталог с «прямой ссылкой на книги» на вашем читателе.

Как я могу указать calibre использовать мой HTTP прокси?

По умолчанию calibre использует любые настройки прокси, установленные в вашей ОС. Иногда они неверны, например, в Windows, если вы не используете Microsoft Edge, настройки прокси-сервера могут быть устаревшими. Вы можете указать calibre использовать конкретный прокси-сервер, установив переменные среды http_proxy и https_proxy. Формат переменной: http://username:password@servername, вы должны попросить вашего сетевого администратора дать вам правильное значение для этой переменной. Обратите внимание, что calibre поддерживает только HTTP-прокси, но не SOCKS-прокси. Вы можете увидеть текущие прокси, используемые calibre, в Настройках-Разное.

Я хочу, чтобы в calibre были добавлены некоторые возможности. Что я могу сделать?

У вас два выбора:

1. Создать патч путём изменения calibre и послать его мне для рассмотрения и включения. См. Разработка [https://calibre-ebook.com/ru/get-involved].

2. Откройте ошибку, запрашивающую функцию [https://calibre-ebook.com/ru/bugs]. Помните, что, хотя вы можете думать, что ваш запрос функции чрезвычайно важен / необходим, разработчики calibre могут не согласиться. К счастью, calibre имеет открытый исходный код, что означает, что у вас всегда есть возможность реализовать свою функцию самостоятельно или нанять кого-то, кто сделает это за вас. Кроме того, calibre имеет комплексную архитектуру плагинов, поэтому вы можете разработать свою функцию как плагин, см. Создание собственных плагинов для расширения функциональности calibre.

Почему у calibre нет автоматического обновления?

По многим причинам:

- Нет необходимости в еженедельном обновлении. Если вас устраивает работа calibre, отключите уведомление об обновлении и будьте сами по себе. Возвращайте уведомления раз в год или около того, чтобы посмотреть, не захотите ли обновиться. Чекбокс для отключения уведомлений об обновлении есть в самом уведомлении.

- Скачивания calibre в настоящее время используют около 150 ТБ полосы пропускания в месяц <dynamic/downloads>`. Внедрение автоматических обновлений значительно увеличит это и в конечном итоге обойдется в тысячи долларов в месяц, которые кому-то придется платить.

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

- Если вы действительно очень ненавидите загружать calibre, но всё ещё хотите использовать последнюю версию, я поддерживаю запуск из исходного кода, что делает обновление тривиальным. Инструкции доступны здесь.

- На форуме calibre <https://www.mobileread.com/forums/forumdisplay.php?f=238>`_ имеются сторонние средства автоматического обновления для calibre, созданные пользователями calibre.

- Кроме того, некоторые программы обновления сторонних производителей, такие как Norton или Avast, могут обновлять программное обеспечение за спиной пользователя. Если вы обнаружите, что calibre неожиданно обновился, проверьте их наличие.

Как лицензирована calibre?

calibre под лицензией GNU General Public License v3 (лицензия с открытым исходным кодом). Это означает, что вы можете свободно распространять calibre до тех пор, пока вы делаете исходный код доступным. Поэтому, если вы хотите поместить calibre на компакт-диск с вашим продуктом, вы также должны поместить исходный код calibre на компакт-диск. Исходный код доступен для скачивания на [https://download.calibre-ebook.com]. Вы можете использовать результаты конвертации из calibre, как хотите. Вы не можете использовать ни код, ни библиотеки от calibre в своем программном обеспечении, не сделав свое программное обеспечение открытым. Подробнее см. GNU GPL v3 [https://www.gnu.org/licenses/gpl.html].

Как я могу запустить calibre с USB-флешки?

Портативная версия calibre доступна здесь [https://calibre-ebook.com/ru/download_portable].

Как мне запустить такие части calibre, как загрузка новостей и сервер содержимого, на моём собственном сервере Linux?

Во-первых, вы должны установить calibre на свой Linux-сервер. Если на вашем сервере используется современный дистрибутив Linux, у вас не должно возникнуть проблем с установкой на него calibre.

Примечание
calibre требуется GLIBC >= 2.31 и libstdc++ >= 6.0.28. Если у вас более старый сервер, вам нужно либо скомпилировать их из исходного кода, либо использовать calibre 3.48, для которого требуется GLIBC >= 2.17, или 2.85.1, для которого требуется GLIBC >= 2.13, или calibre 1.48, для которого требуется только GLIBC >= 2.10. Кроме того, хотя утилитам командной строки calibre не требуется работающий X-сервер, некоторые из них требуют, чтобы в вашей системе были установлены библиотеки X-сервера. Это из-за Qt, который используется для различных задач обработки изображений, и ссылок на эти библиотеки. Если вы получаете сообщение ImportError о некоторых модулях Qt, вероятно не хватает некоторых X-библиотек. Типичными кандидатами являются: libxcb-cursor0, libxcb-xinerama0, libegl1, libopengl0.

Вы можете запустить сервер calibre при помощи команды:

/opt/calibre/calibre-server /path/to/the/library/you/want/to/share

Вы можете скачать новости и преобразовать их в электронную книгу с помощью команды:

/opt/calibre/ebook-convert "Title of news source.recipe" outputfile.epub

Если вы хотите сгенерировать MOBI, используйте outputfile.mobi вместо этого и используйте --output-profile kindle.

Вы можете послать загруженные новости по электронной почте командой:

/opt/calibre/calibre-smtp

Я оставляю создание точной командной строки в качестве упражнения для читателя.

В результате вы можете добавить загруженные новости в библиотеку calibre при помощи:

/opt/calibre/calibredb add --with-library /path/to/library outfile.epub

Не забудьте прочитать раздел Командная строка в руководстве пользователя calibre, чтобы узнать больше об этих и других командах.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Руководства

Руководства

Здесь вы найдёте руководства по более сложным функциям, например, XPath или шаблонам.

- Добавление любимых новостных веб-сайтов

- Управление подгруппами книг, например «genre»

- Учебник по XPath

- Язык шаблонов calibre

- Всё об использования регулярных выражений в calibre

- Создание собственных плагинов для расширения функциональности calibre

- Типографика математических выражений в электронных книгах

- Создание AZW3 • EPUB • MOBI каталогов

- Виртуальные библиотеки

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Руководства »

- Управление подгруппами книг, например «genre»

Управление подгруппами книг, например «genre»

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

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

- Установка

- Поиск

- Ограничения

- Полезные шаблонные функции

Обычно для подгрупп, таких как жанры, требуются:

- Подгруппа (например, жанр) должна содержать (указывать) книги, а не категории книг. Это то, что отличает подгруппы от пользовательских категорий calibre.

- Книга может быть в нескольких подгруппах (жанрах). Это отличает подгруппы от физических файловых папок.

- Подгруппы (жанры) должны образовывать иерархию; подгруппы могут содержать подгруппы.

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

Функция иерархии в calibre дает вам третье - способность видеть жанры в «дереве» и возможность легко искать книги в жанре или поджанре. Например, предположим, что ваша структура жанра похожа на следующую:

Genre
. History
.. Japanese
.. Military
.. Roman
. Mysteries
.. English
.. Vampire
. Science Fiction
.. Alternate History
.. Military
.. Space Opera
. Thrillers
.. Crime
.. Horror
etc.

Используя функцию иерархии, вы можете увидеть эти жанры в браузере тегов в виде дерева, как показано на изображении на экране. В этом примере внешний уровень (жанр) является настраиваемым столбцом, который содержит жанры. Жанры, содержащие поджанры, появляются с небольшим треугольником рядом с ними. Нажатие на этот треугольник откроет элемент и покажет суб-жанры, как вы можете видеть в Истории и Научной фантастике.

Нажав на жанр, вы можете искать все книги с этим жанром или потомками этого жанра. Например, нажатие на Science Fiction может дать все три дочерних жанра: Alternate History, Military и Space Opera. Нажатие на Alternate History выдаст книги в этом жанре, игнорируя те, которые находятся в Military и Space Opera . Конечно, книга может иметь несколько жанров. Если в книге есть жанры как Space Opera, так и Military, вы увидите эту книгу, если вы нажмете на любой жанр. Подробнее поиск обсуждается ниже.

Ещё одна вещь, которую вы можете видеть по изображению, состоит в том, что жанр Military появляется дважды, один раз в Истории и один раз под Научной фантастикой. Поскольку жанры находятся в иерархии, это два отдельных жанра. Книга может быть в одном, другом или (сомнительно в этом случае) и в том, и другом. Например, книга Уинстона Черчилля «Вторая мировая война» могут быть в «History.Military». Книги почёта Хэрингтона Дэвида Вебера можно найти в «Science Fiction.Military», и в этом отношении также в «Science Fiction.Space Opera».

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

Установка

К настоящему моменту ваш вопрос может быть следующим: «Как всё это настроить?» Всего три шага: 1) создать пользовательский столбец, 2) сообщить calibre, что новый столбец должен рассматриваться как иерархия, и 3) добавить жанры.

Вы создаете пользовательский столбец обычным способом, используя «Настройки»->«Добавить свои собственные столбцы». Этот пример использует «#genre» в качестве имени поиска и «Genre» в качестве заголовка столбца. Тип столбца - это Текст с разделителями-запятыми, напр. теги, отображаемые в Браузере тегов.

Затем, после рестарта, вы должны сказать calibre, что столбец должен рассматриваться как иерархия. Перейдите по ссылке Настройки  →  Look & feel  →  Tag browser  →  Иерархия и поиск» и выберите новый столбец Жанр как содержащий иерархические элементы.

На данный момент в столбце нет жанров. Остаётся последний шаг: как применить жанр к книге. Жанр не существует в calibre, пока он не появится, по крайней мере, в одной книге. Чтобы узнать, как применить жанр в первый раз, мы должны подробно рассказать о том, как выглядит жанр в метаданных для книги.

Иерархия «вещей» строится путем создания элемента, состоящего из фраз, разделённых точками. Продолжая пример жанра, эти предметы будут «History.Military», «Mysteries.Vampire», «Science Fiction.Space Opera» и т.д. Таким образом, чтобы создать новый жанр, вы выбираете книгу, которая должна иметь этот жанр, редактируете его метаданные и вводите новый жанр в созданный вами столбец. Продолжая наш пример, если вы хотите присвоить книге новый жанр «Комиксы» с поджанром «Супергерои», вы должны «редактировать метаданные» для этой книги (комикса), выбрать вкладку «Пользовательские метаданные», а затем ввести «Comics.Superheroes», как показано ниже (игнорируйте другие настраиваемые столбцы):

После выполнения вышеизложенного вы увидите в Браузере тегов:

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

Примечание

Иерархическое отображение работает только в том случае, если браузер тегов настроен на сортировку элементов по имени. Это значение по умолчанию, и его можно проверить, нажав кнопку Configure в нижней части браузера тегов.

Поиск

Простейший способ поиска жанров - использовать Браузер тегов, нажав на жанр, который вы хотите увидеть. Нажав на жанр с потомками, вы увидите книги с этим жанром и всеми жанрами-потомками. Однако это может вызвать вопрос. Просто потому, что у жанра есть потомки, это не значит, что это не собственный жанр. Например, книга может иметь жанр «History», но не «History.Military». Как вы ищете книги только с «History»?

Механизм поиска Браузера тегов знает, имеет ли элемент дочерние элементы. Если это так, щелчок по элементу циклически проходит через 5 поисковых запросов вместо обычных трёх. Первый - обычный зелёный плюс, который показывает только книги с этим жанром (например, «История»). Второй - двойной плюс (показан выше), который показывает вам книги с этим жанром и всеми суб-жанрами (например, History and History.Military). Третий - это нормальный красный минус, который показывает вам книги без этого точного жанра. Четвёртый - это удвоенный минус, который показывает вам книги без этого жанра или поджанров. Пятый возвращается к началу, без знака, что означает отсутствие поиска.

Ограничения

Если вы ищете жанр, а затем создаете сохранённый поиск, вы можете использовать поле «Ограничить», чтобы создать виртуальную библиотеку книг с этим жанром. Это полезно, если вы хотите выполнять другие поиски в жанре или управлять/обновлять метаданные для книг в жанре. Продолжая наш пример, вы можете создать сохраненный поиск с именем «History.Japanese», сначала щелкнув по жанру Japanese в браузере тегов, чтобы выполнить поиск в поле поиска, введя History.Japanese в сохраненное поле поиска, а затем нажав кнопку «Сохранить поиск» (зелёное поле с белым плюсом справа).

После создания сохраненного поиска вы можете использовать его как ограничение.

Полезные шаблонные функции

Возможно, вы захотите использовать информацию о жанре в шаблоне, например, сохранить на диск или отправить на устройство. Тогда может возникнуть вопрос: «Как получить самое внешнее имя или имена жанра?». Функция шаблонов calibre - подпункты предоставляется как раз для того чтобы сделать это проще.

Например, предположим, что вы хотите добавить внешний уровень жанра к шаблону save-to-disk, чтобы создавать папки жанра, как в «History/The Gathering Storm - Churchill, Winston» . Для этого вы должны извлечь первый уровень иерархии и добавить его в начале вместе с слэшем (косой чертой) для указания что нужно создать папку. Ниже приведён шаблон:

{#genre:subitems(0,1)||/}{title} - {authors}

См. The template language для получения дополнительной информации о шаблонах и функции subitems().

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Руководства »
- Учебник по XPath

Учебник по XPath

Этот учебник — введение в XPath [https://en.wikipedia.org/wiki/XPath], язык запросов, который можно использовать для выбора произвольных частей HTML [https://en.wikipedia.org/wiki/HTML] в calibre. XPath — широко используемый стандарт, и поиск Google выдаст тонну информации. Однако этот учебник сосредотачивается на решении с помощью XPath задач, связанных с электронными книгами, например поиске заголовков разделов в неструктурированном HTML-документе.

Содержание

- Выбор по имени тега

- Выбор по атрибутам

- Выбор по содержимому тега

- Образец электронной книги

- Встроенные функции XPath

Выбор по имени тега

Простейшая форма выбора — выбор тегов по имени. Предположим, что вы хотите выбрать все теги <h2> в документе. Запрос XPath для этого прост:

//h:h2 (Selects all <h2> tags)

Префикс // означает поиск на любом уровне документа. Теперь предположим, что вы хотите искать теги <span>, которые находятся внутри тегов <a>. Этого можно достичь с помощью:

//h:a/h:span (Selects <span> tags inside <a> tags)

Если вы хотите искать теги на определённом уровне в документе, измените префикс:

/h:body/h:div/h:p (Selects <p> tags that are children of <div> tags that are
children of the <body> tag)

Это будет соответствовать только <p>A very short e-book to demonstrate the use of XPath.</p> в разделе «Образец электронной книги», а не каким-либо другим тегам <p>. Префикс h: в вышеприведённых примерах необходим для соответствия тегам XHTML. Потому что внутри calibre всё содержимое представлено как XHTML. В тегах XHTML есть пространство имён, а h: — префикс пространства имён для HTML-тегов.

Теперь предположим, что вы хотите выбрать теги <h1> и <h2>. Для этого нам нужна конструкция XPath, называемая предикат. A предикат — это просто тест, который используется для выбора тегов. Тесты могут быть сколь угодно мощными, и по мере продвижения по этому учебнику вы увидите более мощные примеры. Предикат создаётся путём заключения тестового выражения в квадратные скобки:

//*[name()='h1' or name()='h2']

В этом выражении XPath имеется несколько новых функций. Во-первых, это использование подстановочного символа *. Это означает соответствие любому тегу. Теперь посмотрим на тестовое выражение name()='h1' or name()='h2'. name() — пример встроенной функции. Она просто вычисляет имя тега. Таким образом, с её помощью мы можем выбрать теги, имена которых либо h1, либо h2. Обратите внимание, что функция name () игнорирует пространства имён, поэтому нет необходимости в префиксе h:. XPath имеет несколько полезных встроенных функций. Некоторые из них будут показаны в этом учебнике.

Выбор по атрибутам

Чтобы выбрать теги на основе их атрибутов, необходимо использовать предикаты:

//*[@style] (Select all tags that have a style attribute)
//*[@class="chapter"] (Select all tags that have class="chapter")
//h:h1[@class="bookTitle"] (Select all h1 tags that have class="bookTitle")

Здесь оператор @ ссылается на атрибуты тега. Вы можете использовать некоторые из XPath built-in functions (встроенных функций XPath) для выполнения более сложных сопоставлений значений атрибутов.

Выбор по содержимому тега

Используя XPath, вы можете выбирать теги даже на основе содержащегося в них текста. Лучший способ сделать это — призвать силу регулярных выражений с помощью встроенной функции re:test ():

//h:h2[re:test(., 'chapter|section', 'i')] (Selects <h2> tags that contain the words chapter or
section)

Здесь оператор . ссылается на содержимое тега, так же как оператор @ ссылается на его атрибуты.

Образец электронной книги

<html>
<head>
<title>A very short e-book</title>
<meta name="charset" value="utf-8" />
</head>
<body>
<h1 class="bookTitle">A very short e-book</h1>
<p style="text-align:right">Written by Kovid Goyal</p>
<div class="introduction">
<p>A very short e-book to demonstrate the use of XPath.</p>
</div>

<h2 class="chapter">Chapter One</h2>
<p>This is a truly fascinating chapter.</p>

<h2 class="chapter">Chapter Two</h2>
<p>A worthy continuation of a fine tradition.</p>
</body>
</html>

Встроенные функции XPath

name()

Имя текущего тега.

contains()

contains(s1, s2) возвращает true, если s1 содержит s2.

re:test()

re:test(src, pattern, flags) возвращает true, если строка src соответствует регулярному выражению pattern. Особенно полезен флаг i: он отвечает за нечувствительность к регистру. Хороший учебник синтаксиса регулярных выражений для начинающих можно найти по ссылке regexp syntax [https://docs.python.org/library/re.html].

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Руководства »
- Язык шаблонов calibre

Язык шаблонов calibre

Язык шаблонов calibre — это специфичный для calibre язык, используемый в calibre для таких задач, как указание путей к файлам, форматирование значений и вычисление значений для столбцов, указанных пользователем. Примеры:

- Укажите структуру папок и имена файлов при сохранении файлов из библиотеки calibre на диск или в читалку электронных книг

- Определите правила добавления значков и цветов в список книг calibre.

- Определите виртуальные столбцы, содержащие данные из других столбцов.

- Расширенный поиск в библиотеке.

- Расширенный поиск и замена метаданных.

Язык построен на понятии «шаблон», который определяет, какие метаданные книги использовать, вычисления над этими метаданными и способ их форматирования.

Базовые шаблоны

Базовый шаблон состоит из одного или нескольких шаблонных выражений. Шаблонное выражение состоит из текста и имен в фигурных скобках ({}), которые заменяются соответствующими метаданными из обрабатываемой книги. Например, шаблон по умолчанию в calibre, используемый для сохранения книг на устройство, имеет 4 шаблонных выражения:

{author_sort}/{title}/{title} - {authors}

Для книги Айзека Азимова «Основание» «The Foundation» by «Isaac Asimov» им станет:

Asimov, Isaac/The Foundation/The Foundation - Isaac Asimov

Косые черты не являются шаблонными выражениями, потому что они находятся между ними в {}. Такой текст остается там, где он появляется. Например, если шаблон:

{author_sort} Some Important Text {title}/{title} - {authors}

затем для книги «Основание» шаблон создает:

Asimov, Isaac Some Important Text The Foundation/The Foundation - Isaac Asimov

Шаблонное выражение может получить доступ ко всем метаданным, доступным в calibre, включая настраиваемые столбцы (столбцы, которые вы создаете самостоятельно), используя имя поиска столбца. Чтобы найти поисковое имя для столбца (иногда называемых полями), наведите указатель мыши на заголовок столбца в списке книг calibre. Имена подстановки для настраиваемых столбцов всегда начинаются с символа #. Для столбцов типа серии есть дополнительное поле с именем #lookup name_index, которое является индексом серии для этой книги в серии. Например, если у вас есть столбец настраиваемой серии с именем #myseries, также будет столбец с именем #myseries_index. Индекс столбца стандартной серии называется series_index.

В дополнение к стандартным полям на основе столбцов вы также можете использовать:

- {formats}- Список форматов, доступных в библиотеке calibre для книги

- {identifiers:select(isbn)} - ISBN книги

Если метаданные для поля для данной книги не определены, тогда поле в шаблоне заменяется пустой строкой (''). Например, рассмотрим следующий шаблон:

{author_sort}/{series}/{title} {series_index}

Если книга Азимова «Второе основание» входит в серию «Основание», то шаблон дает:

Asimov, Isaac/Foundation/Second Foundation 3

Если для книги не введена серия, то шаблон выдает:

Asimov, Isaac/Second Foundation

Обработчик шаблонов автоматически удаляет несколько косых черт и начальные или конечные пробелы.

Расширенное форматирование

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

Условно включая текст

Иногда вы хотите, чтобы текст отображался в выводе, только если поле не пустое. Обычный случай - это series и series_index, когда вы не хотите ничего или два значения, разделенные дефисом. calibre обрабатывает этот случай, используя специальный синтаксис шаблонного выражения.

Например, используя приведенный выше пример Foundation, предположим, что вы хотите, чтобы шаблон создавал Foundation - 3 - Second Foundation. Этот шаблон производит такой вывод:

{series} - {series_index} - {title}

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

{field:|prefix_text|suffix_text}

Это шаблонное выражение говорит, что если field имеет значение XXXX, то результатом будет prefix_textXXXXXsuffix_text. Если поле пусто (не имеет значения), результатом будет пустая строка (ничего), потому что префикс и суффикс игнорируются. Префикс и суффикс могут содержать пробелы.

Не используйте подшаблоны (`{ … }`) или функции (см. Ниже) в префиксе или суффиксе.

Используя этот синтаксис, мы можем решить указанную выше проблему без серии с шаблоном:

{series}{series_index:| - | - }{title}

Дефисы будут добавлены только в том случае, если у книги есть индекс серии, который есть только в том случае, если в ней есть серия. Продолжая пример с Foundation снова, шаблон создаст Foundation - 1 - Second Foundation.

Примечания:

- Вы должны включить двоеточие после имени поиска, если используете префикс или суффикс.

- Вы должны использовать либо никакой, либо оба символа |. Использовать один, как в {field:| -}, не допускается.

- Допускается отсутствие текста для префикса или суффикса, например, в {series:|| -}. Шаблон {title:||} такой же, как {title}.

Форматирование

Предположим, вы хотите, чтобы series_index был отформатирован как три цифры с ведущими нулями. Это помогает:

{series_index: 0>3s} - три цифры с ведущими нулями

Для завершающих нулей используйте:

{series_index: 0<3s}- три цифры с завершающими нулями

Если вы используете индексы серий с дробными значениями, например 1.1, и нужно чтобы десятичные точки совпадали. Например, чтобы индексы 1 и 2.5 отображались как 01.00 и 02.50 и правильно сортировались на устройстве, которое выполняет лексическую сортировку. Для этого используйте:

{series_index: 05.2f} - пять символов, состоящих из двух цифр с ведущими нулями, десятичной точкой и двумя цифрами после десятичной точки.

Если вам нужны только первые две буквы данных, используйте:

{author_sort:.2} - Только первые две буквы сортировочного имени автора

Большая часть форматирования языка шаблонов calibre из Python. Дополнительные сведения о синтаксисе расширенных операций форматирования см. в Python documentation [https://docs.python.org/3/library/string.html#formatstrings].

Использование шаблонов для определения пользовательских столбцов

Шаблоны могут использоваться для отображения информации, которая не входит в метаданные calibre, или для отображения метаданных, отличных от обычного формата calibre. Например, чтобы отобразить ISBN, поле, которое штатно не отображается в calibre. Создайте настраиваемый столбец с типом Столбец, построенный из других столбцов (в дальнейшем называемый составные столбцы) и предоставьте шаблон для генерации отображаемого текста. В столбце отобразится результат оценки шаблона. Чтобы отобразить ISBN, создайте составной столбец и введите {identifiers:select (isbn)} в поле шаблона. Чтобы отобразить столбец, содержащий значения двух пользовательских столбцов серий, разделённых запятой, используйте {#series1:||,}{#series2}.

В составных столбцах можно использовать любой параметр шаблона, включая форматирование.

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

Шаблоны и коммутации

Коммутационные панели используются для изменения метаданных, записанных в книги, во время операций отправки на устройство и сохранения на диск. Коммутационная панель позволяет указать шаблон предоставления данных для записи в метаданные книги. Вы можете использовать коммутационные панели для изменения следующих полей: authors, author_sort, language, publisher, tags, title, title_sort. Эта функция помогает в использовании разных метаданных в книгах на устройствах, решении проблем сортировки или отображения.

При создании коммутационной панели вы указываете формат и устройство, для которых она будет использоваться. Предоставляется специальное устройство save_to_disk, которое используется при сохранении форматов (в отличие от их отправки на устройство). После того, как вы выбрали формат и устройство, вы выбираете поля метаданных, которые нужно изменить, предоставляя шаблоны для предоставления новых значений. Эти шаблоны присоединены к своим полям назначения, отсюда и название plugboards. Конечно, в этих шаблонах можно использовать составные столбцы.

Плагборды довольно гибкие и могут быть написаны в режиме Single Function Mode, Template Program Mode, General Program Mode, или Python Template mode.

Когда может применяться коммутационная панель (контент-сервер, сохранение на диск или отправка на устройство), calibre выполняет поиск определенных подключаемых модулей, чтобы выбрать правильный вариант для данного формата и устройства. Например, чтобы найти подходящую коммутационную панель для книги EPUB, отправляемой на устройство ANDROID, calibre выполняет поиск в коммутационных панелях, используя следующий порядок поиска:

- коммутационная панель с точным соответствием по формату и устройству, например, EPUB и ANDROID

- коммутационная панель с точным соответствием по формату и специальным выбором any device, например, EPUB и any device

- коммутационная панель со специальным выбором any format и точным соответствием на устройстве, например, any format и ANDROID

- коммутационная панель с any format и any device

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

То же самое происходит с авторами, но с использованием другого символа для сокращения, & (амперсанд) вместо запятой. Например, если в шаблоне указано значение Blogs, Joe&Posts, Susan, тогда в книге будет два автора: Blogs, Joe и Posts, Susan. Если шаблон выдает значение Blogs, Joe;Posts, Susan, то у книги будет один автор с довольно странным именем.

Плагины влияют на метаданные, записанные в книгу, когда они сохраняются на диск или записываются на устройство. Плагины не влияют на метаданные, используемые командами «сохранить на диск» и «отправить на устройство» для создания имен файлов. Вместо этого имена файлов создаются с использованием шаблонов, введенных в соответствующем окне настроек.

Использование функций в шаблонах - режим одной функции

Предположим, вы хотите отобразить значение поля в верхнем регистре, когда это поле обычно находится в регистре заголовков. Вы можете сделать это с помощью шаблонных функций. Например, чтобы отобразить заголовок в верхнем регистре, используйте функцию uppercase, как в {title:uppercase()}. Чтобы отобразить его в регистре заголовка, используйте {title:titlecase()}.

Функции входят в форматную часть шаблона после : и перед первым | или закрывающим }, если не используется префикс/суффикс. Если у вас есть и формат, и ссылка на функцию, функция появляется после второго :. Функции возвращают соответствующее измененное значение столбца, указанного в шаблоне.

Синтаксис использования функций - один из:

{lookup_name:function(arguments)}
{lookup_name:format:function(arguments)}
{lookup_name:function(arguments)|prefix|suffix}
{lookup_name:format:function(arguments)|prefix|suffix}

За именами функций всегда должны стоять открывающие и закрывающие круглые скобки. Некоторым функциям требуются дополнительные значения (аргументы), и они заключаются в круглые скобки. Аргументы разделяются запятыми. Буквальным запятым (запятым как тексту, а не разделителям аргументов) должна предшествовать обратная косая черта (\\). Последний (или единственный) аргумент не может содержать закрывающую текстовую скобку.

Функции оцениваются до спецификации формата и префикса/суффикса. См. Ниже пример использования формата и функции.

Важно: если у вас есть опыт программирования, обратите внимание, что синтаксис в Single Function Mode не соответствует вашим ожиданиям. Строки не заключаются в кавычки, а пробелы имеют значение. Все аргументы считаются константами; нет выражений.

Не используйте подшаблоны (`{ … }`) в качестве аргументов функции. Вместо этого используйте Template Program Mode и General Program Mode.

Примечания по вызову функций в режиме одиночной функции:

- Когда функции используются в режиме одиночной функции, первый параметр, value, автоматически заменяется содержимым поля, указанного в шаблоне. Например, когда обрабатывается шаблон {title:capitalize()}, содержимое поля title передается в качестве параметра value функции capitalize.

- В документации по функциям ниже обозначение [something]* означает, что something может повторяться ноль или более раз. Обозначение [something]+ означает, что something повторяется один или несколько раз (должно существовать хотя бы один раз).

- Некоторые функции используют регулярные выражения. В языке шаблонов сопоставление регулярных выражений не зависит от регистра.

Функции задокументированы в Справочник по шаблонным функциям. В документации указано, какие аргументы требуются функциям и что они делают. Например, вот документация функции ifempty.

- ifempty(value, text_if_empty) – если value не пустое, то вернуть это value, иначе вернуть text_if_empty.

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

{tags:ifempty(No tags on this book)}

показывает теги для книги, если таковые имеются. Если у неё нет тегов, то отображается В этой книге нет тегов.

Следующие функции можно использовать в режиме одиночной функции, поскольку их первым параметром является value.

capitalize(value) – возвращает значение, в котором первая буква в верхнем регистре, а остальные в нижнем регистре.

ceiling(value) – возвращает наименьшее целое число, большее или равное value. Выдаёт исключение, если value не является числом.

cmp(value, y, lt, eq, gt) – сравнивает value и y после преобразования обоих в числа.

contains(value, pattern, text_if_match, text_if_not_match) – проверяет, соответствует ли значение регулярному выражению pattern. Возвращает text_if_match при совпадении, иначе возвращает text_if_not_match.

ff_““date_arithmetic(value, calc_spec, fmt)'' -- вычислить новую дату от ``value с помощью calc_spec. Вернуть новую дату, отформатированную в соответствии с необязательным fmt: если не указано, то результат будет в формате ISO. Строка calc_spec - конкатенация пар „vW“ (valueWhat), где v - возможно отрицательное число, а W - одна из следующих букв:

- s: добавить v добавить к date

- m: добавить v минут к date

- h: добавить v часов к date

- d: добавить v дней к date

- w: добавить v недель к date

- y: добавить v лет к date, где год это 365 дней.

Пример: '1s3d-1m' добавит 1 секунду, прибавит 3 дня и вычтет 1 минуту из date.

encode_for_url(value, use_plus) – возвращает значение value закодированное для использования в URL, как указанно с use_plus. Значение URL-кодированное. Далее, если use_plus равно 0, то пробелы заменяются знаками '+' (плюс). Если это 1, то пробелы заменяются %20.

floor(value) – возвращает наибольшее целое число, меньшее или равное value. Выдаёт исключение, если value не является числом.

format_date(value, format_string) – форматирует value, которое должно быть строкой даты, используя format_string, возвращающую строку.

format_duration(value, template, [largest_unit]) – format the value, a number of seconds, into a string showing weeks, days, hours, minutes, and seconds. If the value is a float then it is rounded to the nearest integer.

format_number(value, template) – интерпретирует value как число и форматирует это число, используя шаблон форматирования Python, такой как {0:5.2f} или {0:,d}` или ``${0:5,.2f}. Шаблон форматирования должен начинаться с {0: и заканчиваться }, как в приведенных выше примерах. Исключение: вы можете опустить начальные {0: и завершающие }, если шаблон формата содержит только формат. См. [URL href=»https://manual.calibre-ebook.com/template_lang.html»] Template Language[/URL] и [URL href=»https://docs.python.org/3/library/string.html#formatstrings»] Python[/URL]. Возвращает пустую строку, если сбой форматирования.

fractional_part(value) – возвращает значение после десятичной точки.

human_readable(value) – ожидает, что значение будет числом, и возвращает строку, представляющую это число в КБ, МБ, ГБ и т. д.

ifempty(value, text_if_empty) – если value не пустое, то вернуть это value, иначе вернуть text_if_empty.

language_strings(value, localize) – вернуть значение названий языков для языковых кодов (см. здесь имена и коды [https://www.loc.gov/standards/iso639-2/php/code_list.php]) , переданных в качестве значения value. Пример: {languages:language_strings()}. Если localize равно нулю, вернуть строки на английском языке. Если localize не равно нулю, вернуть строки на языке текущей локали. lang_codes - это список, разделённый запятыми.

list_contains(value, separator, [ pattern, found_val, ]* not_found_val) – интерпретирует value как список элементов, разделенных разделителем separator, проверяя pattern для каждого элемента в списке.

list_count(value, separator) – интерпретирует значение как список элементов, разделенных символом separator и возвращает количество элементов в списке.

list_count_matching(value, pattern, separator) – интерпретирует value как список элементов, разделённых separator, возвращая количество элементов в списке, которые соответствуют регулярному выражению pattern. Псевдонимы: list_count_matching(), count_matching()

list_item(value, index, separator) – обрабатывает значение как список элементов, разделённых separator, возвращая элемент с номером index. Первый элемент имеет номер ноль. Последний элемент с номером индекса -1 как list_item(-1,separator). Если элемент не в списке, то возвращается пустое значение. Разделитель (separator) работает так же, как и в функции count, обычно запятая, но амперсанд для списков, вроде author.

list_sort(list, direction, separator) – вернуть список, отсортированный с использованием лексической сортировки без учета регистра. Если direction равно нулю, список сортируется по возрастанию, иначе по убыванию. Элементы списка разделены разделителем separator, как и элементы в возвращаемом списке.

lookup(value, [ pattern, key, ]* else_key) – Шаблоны будут проверяться по значению value по порядку. Если шаблон совпадает, возвращается значение поля, названного key. Если ни один шаблон не совпадает, возвращается значение поля с именем else_key. Смотрите функцию switch().

lowercase(value) – возвращает value в нижнем регистре.

mod(value, y) – возвращает floor остаток от value / y. Выдаёт исключение, если value или y не является числом.

rating_to_stars(value, use_half_stars) – Возвращает рейтинг как value в виде строки, состоящей из звездочек (★“). Значение должно быть числом от 0 до 5. Задайте для use_half_stars значение 1, если вы хотите, чтобы символы половинной звезды для дробных чисел были доступны в настраиваемых столбцах рейтингов.

re(value, pattern, replacement) – вернуть значение value после применения регулярного выражения. Все вхождения pattern в значении value заменяются на replacement. Язык шаблонов не регистрозависим Python regular expressions [https://docs.python.org/3/library/re.html].

re_group(value, pattern [, template_for_group]*) – вернуть строку, полученную путем применения шаблона регулярного выражения pattern к value и замены каждого сопоставленного экземпляра строкой, вычисленной путем замены каждой сопоставленной группы значением, возвращаемым соответствующим шаблоном. В [URL href=»https://manual.calibre-ebook.com/template_lang.html#more-complex-programs-in-template-expressions-template-program-mode»] программном режиме шаблона, как и в случае с template и eval, вы используете [[ для { и ]] для }. В следующем примере выполняется поиск ряда, состоящего более чем из одного слова, и первое слово переводится в верхний регистр [CODE] program: re_group(field(„series“), «(S* )(.*)», «{$:uppercase()}», «{$}»)“} [/CODE]

round(value) – возвращает ближайшее к value целое число. Выдаёт исключение, если value не является числом.

select(value, key) – обрабатывает value как разделённый запятыми список элементов вида id:id_value (формат identifier calibre). Функция находит первую пару с совпадающим с key идентификатором, и возвращает соответствующее id_value. При отсутствии совпадений возвращается пустая строка.

shorten(value, left chars, middle text, right chars) – Возвращает сокращённую версию значения``value``, состоящую из символов left chars с начала значения, за которыми следует middle text, за которыми следуют символы right chars с конца значения. left chars и right chars должны быть неотрицательными целыми числами.

Пример: предположим, что вы хотите отобразить заголовок длиной не более 15 символов. Один из шаблонов, который делает это, - {title:short(9,-,5)}. Для книги с названием «Ancient English Laws in the Times of Ivanhoe» результатом будет Ancient E-nhoe: первые 9 символов названия, -, затем последние 5 символов. Если длина значения меньше, чем left chars + right chars + длина middle text, то значение будет возвращено без изменений. Например, название The Dome не изменится.

str_in_list(separator, [ string, found_val, ]+ not_found_val) – интерпретирует значение как список элементов, разделенных separator, затем сравнивает string с каждым значением в списке. string не является регулярным выражением. Если string равна любому элементу (без учета регистра), вернуть соответствующее значение found_val. Если string содержит разделители separator, она также рассматривается как список, и каждое подзначение проверяется. Пары string и found_value могут повторяться сколько угодно раз, что позволяет возвращать разные значения в зависимости от значения строки. Если ни одна из строк не совпадает, возвращается not_found_value. Строки проверяются по порядку. Возвращается первое совпадение.

subitems(value, start_index, end_index) – Эта функция разделяет списки на иерархические элементы, подобные тегам, такие как жанры.

sublist(value, start_index, end_index, separator) – интерпретирует value как список значений, разделённых separator, возвращая новый список с началом в start_index с концом в end_index. Первый пункт - номер 0 (ноль). Если индекс - отрицательный, то его начало - от конца списка. В особых случаях end_index нуля считается длиной списка.

Примеры, использующие режим базового шаблона и предополагающие что столбец тегов (разделённый запятыми) содержит «A, B, C»:

- {tags:sublist(0,1,\,)} returns «A»

- {tags:sublist(-1,0,\,)} returns «C»

- {tags:sublist(0,-1,\,)} returns «A, B»

substr(value, start, end) – возвращает подстроку - символы строки value от start до end. Первый символ строки value имеет номер ноль. Если end отрицательный - это сколько символов будет отсчитываться с конца строки (справа). Если end равен 0 (ноль), то это - последний символ строки. Например, substr('12345', 1, 0) вернёт '2345', и substr('12345', 1, -1) вернёт '234'.

swap_around_articles(value, separator) – возвращает value значение со статьями, перемещёнными в конец, разделёнными точкой с запятой. Значение value может быть списком, тогда обрабатывается каждый элемент в списке. Если value - список, укажите separator. Если separator не указан (или его значение - пустая строка), то значение value` рассматривается как одно значение, а не как список.   `articles` используются calibre для создания ``title_sort.

swap_around_comma(value) – если значение value имеет вид B, A, возвращает A B. Это наиболее полезно для преобразования имён вида LN, FN (Ф, И О) к виду FN LN (И О Ф). Если значение не содержит запятой в value, то оно возвращается без изменений.

switch(value, [patternN, valueN,]+ else_value) – для каждой пары patternN, valueN, проверяет, соответствует ли значение регулярному выражению patternN`, и если да, возвращает связанное `` valueN``. Если нет совпадений с шаблоном, возвращается else_value. У вас может быть столько пар patternN, valueN, сколько хотите. Возвращается первое совпадение.

test(value, text_if_not_empty, text_if_empty) – возвращает text_if_not_empty если значение не пусто, иначе возвращает text_if_empty.

titlecase(value) – возвращает value в регистре ЗАГОЛОВКОВ.

transliterate(value) – возвращает строку в латинском алфавите, которая по звучанию похожа на слова исходной строки. Например, если value было Фёдор Миха́йлович Достоевский, то функция вернёт Fiodor Mikhailovich Dostoievskii.

uppercase(value) – возвращает value в верхнем регистре

Использование функций и форматирования в одном шаблоне

Предположим, у вас есть целочисленный настраиваемый столбец #myint, который вы хотите отображать с ведущими нулями, как в 003. Один из способов сделать это - использовать формат 03. Однако по умолчанию, если число (целое или с плавающей запятой) равно нулю, тогда значение отображается как пустая строка, поэтому нулевые значения будут давать пустую строку, а не 000. Если вы хотите увидеть значения 000, используйте как строку формата, так и функцию ifempty, чтобы изменить пустое значение обратно в ноль. Шаблон будет:

{#myint:0>3s:ifempty(0)}

Обратите внимание, что вы также можете использовать префикс и суффикс. Если вы хотите, чтобы номер отображался как [003] или [000], то используйте шаблон:

{#myint:0>3s:ifempty(0)|[|]}

Общий программный режим

Общий программный режим (GPM) заменяет шаблонные выражения программой, написанной на языке шаблонов. Синтаксис языка определяется следующей грамматикой:

program ::= 'program:' expression_list expression_list ::= top_expression [ ';' top_expression ]* top_expression ::= or_expression or_expression ::= and_expression [ '||' and_expression ]* and_expression ::= not_expression [ '&&' not_expression ]* not_expression ::= [ '!' not_expression ]* | concatenate_expr concatenate_expr::= compare_expr [ '&' compare_expr ]* compare_expr ::= add_sub_expr [ compare_op add_sub_expr ]
compare_op ::= '==' | '!=' | '>=' | '>' | '<=' | '<' |
'in' | 'inlist' | 'inlist_field' |
'==#' | '!=#' | '>=#' | '>#' | '<=#' | '<#'
add_sub_expr ::= times_div_expr [ add_sub_op times_div_expr ]* add_sub_op ::= '+' | '-'
times_div_expr ::= unary_op_expr [ times_div_op unary_op_expr ]* times_div_op ::= '*' | '/'
unary_op_expr ::= [ add_sub_op unary_op_expr ]* | expression expression ::= identifier | constant | function | assignment | field_reference | if_expr | for_expr | break_expr | continue_expr |
'(' expression_list ')' | function_def field_reference ::= '$' [ '$' ] [ '#' ] identifier identifier ::= id_start [ id_rest ]* id_start ::= letter | underscore id_rest ::= id_start | digit constant ::= " string " | ' string ' | number function ::= identifier '(' expression_list [ ',' expression_list ]* ')'
function_def ::= 'def' identifier '(' top_expression [ ',' top_expression ]* ')' ':'
expression_list 'fed'
assignment ::= identifier '=' top_expression if_expr ::= 'if' condition 'then' expression_list
[ elif_expr ] [ 'else' expression_list ] 'fi'
condition ::= top_expression elif_expr ::= 'elif' condition 'then' expression_list elif_expr | ''
for_expr ::= for_list | for_range for_list ::= 'for' identifier 'in' list_expr
[ 'separator' separator_expr ] ':' expression_list 'rof'
for_range ::= 'for' identifier 'in' range_expr ':' expression_list 'rof'
range_expr ::= 'range' '(' [ start_expr ',' ] stop_expr
[ ',' step_expr [ ',' limit_expr ] ] ')'
list_expr ::= top_expression break_expr ::= 'break'
continue_expr ::= 'continue'
separator_expr ::= top_expression start_expr ::= top_expression stop_expr ::= top_expression step_expr ::= top_expression limit_expr ::= top_expression

Примечания:

- top_expression всегда имеет значение. Значение expression_list - это значение последнего top_expression в списке. Например, значение списка выражений 1;2;'foobar';3 равно 3.

- В логическом контексте любое непустое значение - True

- В логическом контексте пустым значением является False

- Строки и числа могут использоваться как взаимозаменяемые. Например, 10 и '10' - это одно и то же.

- Comments are lines starting with a „#“ character, possibly preceded by blanks or tabs.

Приоритет оператора

Приоритет оператора (порядок оценки) от наивысшего (оценивается первым) до самого низкого (оценивается последним):

- Вызовы функций, константы, выражения в скобках, выражения операторов, выражения присваивания, ссылки на поля.

- Унарный плюс (+) и минус (-). Эти операторы вычисляют справа налево.

Эти и все другие арифметические операторы возвращают целые числа, если результат выражения дает дробную часть, равную нулю. Например, если выражение возвращает 3.0, оно изменяется на 3.

- Умножение (*) и деление (/). Эти операторы ассоциативны и выполняются слева направо. Используйте круглые скобки, если хотите изменить порядок оценки.

- Сложение (+) и вычитание (-). Эти операторы ассоциативны и выполняются слева направо.

- Числовые и строковые сравнения. Эти операторы возвращают 1, если сравнение прошло успешно, в противном случае - пустую строку (''). Сравнения не ассоциативны: a < b < c - синтаксическая ошибка.

- Конкатенация строк (&). Оператор & возвращает строку сформированную путем объединения левого и правого выражений. Пример: 'aaa' & 'bbb'` возвращает ``'aaabbb'. Оператор является ассоциативным и оценивает слева направо.

- Унарное логическое НЕ (!). Этот оператор возвращает '1', если выражение ложно (возвращает пустую строку), в противном случае ''.

- Логическое и (&&). Этот оператор возвращает „1“, если и левое, и правое выражения имеют значение True, или пустую строку '', если любое из них имеет значение False. Он ассоциативен, вычисляется слева направо и выполняет `short-circuiting `_.

- Логическое ИЛИ (||). Этот оператор возвращает 1, если левое или правое выражение истинно, или '', если оба ложны. Он ассоциативен, вычисляется слева направо и выполняет short-circuiting `_. Это `включающее ИЛИ, возвращающее``“1“``, если и левое, и правое выражения истинны.

Ссылки на поля
field_reference оценивает значение поля метаданных, названного именем поиска, которое следует за $ или $$. Использование $ эквивалентно использованию функции field. Использование $$ эквивалентно использованию функции raw_field. Примеры:

* $authors ==> field('authors')
* $#genre ==> field('#genre')
* $$pubdate ==> raw_field('pubdate')
* $$#my_int ==> raw_field('#my_int')

If выражения

Выражения if сначала оценивают условие. Если condition равно True (непустое значение), тогда expression_list в предложении then оценивается. Если оно равно False, то при его наличии вычисляется список выражений в предложении elif или else. Части elif и else не являются обязательными. Слова if, then, elif, else и fi зарезервированы; вы не можете использовать их в качестве имен идентификаторов. Вы можете помещать символы новой строки и пробелы везде, где они имеют смысл. condition - это top_expression, а не expression_list; точки с запятой не допускаются. expression_list - это разделенные точкой с запятой последовательности «top_expressions». Выражение if возвращает результат последнего выражения top_expression в вычисленном списке выражений или пустую строку, если список выражений не оценивался.

Примеры:

* program: if field('series') then 'yes' else 'no' fi
* program:
if field('series') then
a = 'yes';
b = 'no'
else
a = 'no';
b = 'yes'
fi;
strcat(a, '-', b)

Пример вложенного if:

program:
if field('series') then
if check_yes_no(field('#mybool'), '', '', '1') then
'yes'
else
'no'
fi
else
'no series'
fi

Как сказано выше, if выдаёт значение. Это означает, что всё следующее эквивалентно:

* program: if field('series') then 'foo' else 'bar' fi
* program: if field('series') then a = 'foo' else a = 'bar' fi; a
* program: a = if field('series') then 'foo' else 'bar' fi; a

К примеру эта программа возвращает значение столбца series, если в книге есть серия, в противном случае значение столбца title:

program: field(if field('series') then 'series' else 'title' fi)

For выражения

Выражение for выполняет итерацию по списку значений, обрабатывая их по одному за раз. list_expression должно соответствовать либо полю метаданных lookup name, например, tags или #genre, либо списку значений. Параметр :ref: ff_range генерирует список чисел. Если результатом является допустимое lookup name, то извлекается значение поля и используется разделитель, указанный для этого типа поля. Если результатом не является допустимое имя для поиска, то предполагается, что это список значений. Предполагается, что список разделяется запятыми, если не указано необязательное ключевое слово separator, и в этом случае значения списка должны быть разделены результатом вычисления separator_expr. Разделитель нельзя использовать, если список генерируется с помощью range(). Каждое значение в списке присваивается указанной переменной, после чего вычисляется expression_list. Вы можете использовать break, чтобы выйти из цикла, и continue, чтобы перейти к началу цикла для следующей итерации.

Пример: этот шаблон удаляет первое иерархическое имя для каждого значения в жанре (#genre), создавая список с новыми именами:

program:
new_tags = '';
for i in '#genre':
j = re(i, '^.*?\.(.*)$', '\1');
new_tags = list_union(new_tags, j, ',')
rof;
new_tags

Если исходный жанр - History.Military, Science Fiction.Alternate History, ReadMe, то шаблон возвращает Military, Alternate History, ReadMe. Вы можете использовать этот шаблон в calibre Массовое редактирование метаданных - Поиск и замена с помощью: guilabel:` Search for`, установленного на template, чтобы удалить первый уровень иерархии и присвоить полученное значение Жанру.

Примечание: последняя строка в шаблоне, new_tags, в этом случае не является строго необходимой, потому что for возвращает значение последнего top_expression в списке выражений. Значение присваивания - это значение его выражения, поэтому значение оператора for - это то, что было присвоено new_tags.

Определение функции

Если у вас есть повторяющийся код в шаблоне, вы можете поместить этот код в локальную функцию. Ключевое слово def запускает определение. За ним следует имя функции, список аргументов, затем код функции. Определение функции заканчивается ключевым словом fed

Аргументы позиционные. При вызове функции предоставленные аргументы сопоставляются слева направо с определенными параметрами, при этом значение аргумента присваивается параметру. Будет ошибкой указывать больше аргументов, чем определено параметров. Параметры могут иметь значения по умолчанию, например a = 25. Если для этого параметра не указан аргумент, используется значение по умолчанию, в противном случае параметру присваивается пустая строка.

Оператор return можно использовать в локальной функции.

Функция должна быть определена до того, как её можно будет использовать.

Пример. Этот шаблон вычисляет приблизительную продолжительность в годах, месяцах и днях из числа дней. Функция to_plural() форматирует вычисляемые значения. Обратите внимание, что в примере также используется оператор &:

program:
days = 2112;
years = floor(days/360);
months = floor(mod(days, 360)/30);
days = days - ((years*360) + (months * 30));

def to_plural(v, str):
if v == 0 then return '' fi;
return v & ' ' & (if v == 1 then str else str & 's' fi) & ' '
fed;

to_plural(years, 'year') & to_plural(months, 'month') & to_plural(days,'day')

Операторы отношения

Операторы отношения возвращают '1', если сравнение истинно, в противном случае - пустую строку ('').

Есть две формы операторов отношения: сравнение строк и сравнение чисел.

При сравнении строк выполняется сравнение строк без учета регистра с использованием лексического порядка. Поддерживаемые операторы сравнения строк: ==, !=, <, <=, >, >=, in и inlist. Для оператора in результат левого выражения интерпретируется как шаблон регулярного выражения. Они являются истинными, если значение регулярного выражения слева совпадает со значением выражения справа. Регулярные выражения не чувствительны к регистру.

Оператор inlist имеет значение true, если левое регулярное выражение соответствует любому из элементов в правом списке, где элементы в списке разделены запятыми. Оператор inlist_field имеет значение true, если левое регулярное выражение соответствует любому из элементов в поле (столбце), названном правым выражением, используя разделитель, определённый для поля. ПРИМЕЧАНИЕ: оператор inlist_field требует, чтобы правое выражение преобразовывалось в имя поля, в то время как оператор inlist требует, чтобы правое выражение преобразовывалось в строку, содержащую список, разделённый запятыми. Из-за этого различия inlist_field выполняется существенно быстрее, чем inlist, поскольку не выполняются преобразования строк или построения списков.

Операторы сравнения чисел: ==#, !=#, <#, <=#, >#, >=#. Левое и правое выражения должны оцениваться как числовые значения с двумя исключениями: как строковое значение «None» (неопределенное поле), так и пустая строка оцениваются как нулевое значение.

Примеры:

program: field('series') == 'foo' возвращает '1' если серия книги - foo, иначе - ''.

program: 'f.o' in field('series') вернёт '1' если серия книги соответствует регулярному выражению f.o (например, foo, Off Onyx и т. д.), иначе ''.

program: 'science' inlist $#genre вернёт '1' если какой-либо из жанров книги соответствует регулярному выражению science, например, Science, History of Science, Science Fiction и т. д.), иначе ''.

program: '^science$' inlist $#genre вернёт '1' если какой-либо из жанров книги точно соответствует регулярному выражению ^science$, например Science. Жанры History of Science и Science Fiction не совпадают.

program: 'asimov' inlist $authors вернёт '1' если какой-либо автор соответствует регулярному выражению asimov, например, Asimov, Isaac или Isaac Asimov, иначе ''.

program: 'asimov' inlist_field 'authors' вернёт '1' если какой-либо автор соответствует регулярному выражению asimov, например, Asimov, Isaac или Isaac Asimov, иначе ''.

program: 'asimov$' inlist_field 'authors' вернёт '1' если какой-либо автор соответствует регулярному выражению asimov$, например, Isaac Asimov, иначе ''. Это не соответствует Asimov, Isaac из-за привязки $ в регулярном выражении.

program: if field('series') != 'foo' then 'bar' else 'mumble' fi returns 'bar' если серия книги не foo. Иначе возвращается 'mumble'.

program: if field('series') == 'foo' || field('series') == '1632' then 'yes' else 'no' fi returns 'yes' if series is either foo or 1632, otherwise 'no'.

program: if '^(foo|1632)$'  field('series') then 'yes' else 'no' fi returns 'yes' if series is either 'foo' or '1632', otherwise 'no'.

program: if 11 > 2 then 'yes' else 'no' fi returns 'no' because the > оператор выполняет лексическое сравнение.

program: if 11 ># 2 then 'yes' else 'no' fi вернёт 'yes' потому что оператор ># выполняет числовое сравнение.

Функции в Общем программном режиме

Смотрите Справочник по шаблонным функциям для получения списка функций, встроенных в язык шаблонов.

Примечания:

- В отличие от Single Function Mode, в Общем программном режиме вы должны указать первый параметр value.

- Все параметры являются expression_lists (смотрите грамматику выше).

Более сложные программы в шаблонных выражениях - Template Program Mode

Template Program Mode (TPM) - смесь General Program Mode и :ref:` Single Function Mode`. TPM отличается от режима одиночной функции тем, что он позволяет писать шаблонные выражения, которые ссылаются на другие поля метаданных, используют вложенные функции, изменяют переменные и выполняют арифметические операции. Он отличается от General Program Mode тем, что шаблон заключен между символами``{`` и``}``и не начинается со слова program:. Программная часть шаблона - это список выражений General Program Mode.

Пример: предположим, что вы хотите, чтобы шаблон отображал серию книги, если она есть, в противном случае показывала бы значение настраиваемого поля #genre. Вы не можете сделать это в Single Function Mode, потому что вы не можете ссылаться на другое поле метаданных в выражении шаблона. В TPM это можно сделать, как демонстрирует следующее выражение:

{series:'ifempty($, $#genre)'}

Пример показывает несколько вещей:

- TPM используется, если выражение начинается с :' и заканчивается '}. Предполагается, что все остальное находится в Single Function Mode.

Если шаблон содержит префикс и суффикс, выражение заканчивается на '|, где | является разделителем префикса. Пример:

{series:'ifempty($, $#genre)'|prefix | suffix}

- функциям должны быть даны все их аргументы. Например, стандартные встроенные функции должны иметь дополнительный начальный параметр value.

- Переменная $ может использоваться в качестве аргумента value и обозначает значение поля с именем в шаблоне, в данном случае series.

- пробелы игнорируются и могут использоваться в любом месте выражения.

- константные строки заключаются в соответствующие кавычки, либо ', либо ''.

В TPM использование символов { и } в строковых литералах может привести к ошибкам или неожиданным результатам, поскольку они сбивают с толку обработчик шаблонов. Он пытается рассматривать их как границы выражения шаблона, а не как символы. В некоторых, но не во всех случаях, вы можете заменить { на [[ и } на ]]. Как правило, если ваша программа содержит символы { и ``}``, вам следует использовать General Program Mode.

Режим шаблона Python

Режим шаблонов Python (PTM) позволяет создавать шаблоны с использованием родного Python и calibre API [https://manual.calibre-ebook.com/develop.html#api-documentation-for-various-parts-of-calibre]. API базы данных будет наиболее полезным; дальнейшее обсуждение выходит за рамки данного руководства. Шаблоны PTM быстрее и могут выполнять более сложные операции, но вы должны знать, как писать код на Python, используя calibre API.

Шаблон PTM начинается с:

python:
def evaluate(book, context):
# book is a calibre metadata object
# context is an instance of calibre.utils.formatter.PythonTemplateContext,
# which currently contains the following attributes:
# db: a calibre legacy database object.
# globals: the template global variable dictionary.
# arguments: is a list of arguments if the template is called by a GPM template, otherwise None.
# funcs: used to call Built-in/User functions and Stored GPM/Python templates.
# Example: context.funcs.list_re_group()

# your Python code goes here
return 'a string'

Вы можете добавить приведенный выше текст в свой шаблон с помощью контекстного меню, доступ к которому обычно осуществляется с помощью щелчка правой кнопкой мыши. Комментарии несущественны и могут быть удалены. Вы должны использовать отступы Python.

Объект контекста поддерживает str(context), который возвращает строку содержимого контекста, и context.attributes, который возвращает список имён атрибутов в контексте.

Атрибут context.funcs позволяет вызывать встроенные и пользовательские функции шаблонов, а также Сохранённые шаблоны GPM/Python, чтобы вы могли выполнять их непосредственно в своем коде. Функции извлекаются по их именам. Если имя конфликтует с ключевым словом Python, добавьте в конце имени символ подчеркивания. Примеры:

context.funcs.list_re_group()
context.funcs.assert_()

Вот пример шаблона PTM, который создает список всех авторов серии. Список хранится как Столбец, состоящий из других столбцов, ведущих себя как теги. Он отображается в Описание книги и отмечен как Категории в отдельных строках (в Настройки → Оформление → Описание книги). Этот параметр требует, чтобы список был разделён запятыми. Чтобы удовлетворить это требование, шаблон преобразует запятые в именах авторов в точки с запятой, а затем создаёт список авторов, разделённых запятыми. Затем авторы сортируются, поэтому в шаблоне используется author_sort.

python:
def evaluate(book, context):
if book.series is None:
return ''
db = context.db.new_api
ans = set()
# Get the list of books in the series
ids = db.search(f'series:"={book.series}"', '')
if ids:
# Get all the author_sort values for the books in the series
author_sorts = (v for v in db.all_field_for('author_sort', ids).values())
# Add the names to the result set, removing duplicates
for aus in author_sorts:
ans.update(v.strip() for v in aus.split('&'))
# Make a sorted comma-separated string from the result set
return ', '.join(v.replace(',', ';') for v in sorted(ans))

Вывод в Описание книги выглядит так:

Шаблоны и URL

Вы можете использовать шаблоны для создания URL-адресов. Здесь описаны два случая:

- Пользовательский столбец: :guilabel:`Book details guilabel: URL-адреса

- Схема URL-адресов calibre

пользовательский столбец URL-адреса для поиска сведений о книге

Когда вы создаете пользовательский столбец, вы можете указать URL-адрес, который будет использоваться в Сведения о книге, используя шаблон. Например, если у вас есть пользовательский столбец для Translators, вы можете определить URL-адрес, который приведет вас на сайт для переводчиков. URL-адреса для поиска сведений о книге могут быть предоставлены для типов столбцов Текст, Перечисляемый“, „Серия“ и „Столбец, построенный из других столбцов.

При нажатии на элемент с шаблоном поиска в Сведения о книге шаблон оценивается. Ему предоставляются обычные метаданные книги. Также предоставляются три дополнительных поля:

- item_value: значение нажатого элемента.

- item_value_quoted: значение нажатого элемента, кодированный URL. Специальные символы экранируются, чтобы сделать их действительными в URL-адресах, а пробелы заменяются на знаки '+' (плюс).

- item_value_no_plus: значение нажатого элемента, кодированный URL. Специальные символы экранируются, чтобы сделать их действительными в URL-адресах, а пробелы заменяются на знаки '+' (плюс).

Существует несколько способов создания URL-адреса. Ниже в качестве примера используется Википедия.

Самый простой - это базовый шаблон:

https://en.wikipedia.org/w/index.php?search={item_value_encoded}

В некоторых случаях может потребоваться дополнительная обработка. В зависимости от сложности обработки вы можете использовать четыре шаблонные функции.

- make_url(path, [query_name, query_value]+) – эта функция - простейший способ создания URL-адреса запроса. Она использует path, веб-сайт и страницу, которые вы хотите запросить и пары query_name, query_value, из которых строится запрос. Как правило, query_value должно быть закодировано в URL. С помощью этой функции оно всегда закодировано, пробелы всегда заменяются знаками „+“.

- make_url_extended(...) – эта функция аналогична make_url() но дает вам больше контроля над компонентами URL. Компонентами URL являются
scheme:://authority/path?query string.

См. Uniform Resource Locator [https://en.wikipedia.org/wiki/URL] в Wikipedia.

У функции два варианта:

make_url_extended(scheme, authority, path, [query_name, query_value]+)

и

make_url_extended(scheme, authority, path, query_string)

- query_string([query_name, query_value, how_to_encode]+)– возвращает строку запроса URL сконструированную из триад query_name, query_value, how_to_encode. Строка запроса это серия элементов выглядящих как query_name=query_value, где query_value это URL-кодирование. Элементы запроса разделяются символами '&' (ampersand).

- encode_for_url(value, use_plus) – возвращает значение value закодированное для использования в URL, как указанно с use_plus. Значение URL-кодированное. Далее, если use_plus равно 0, то пробелы заменяются знаками '+' (плюс). Если это 1, то пробелы заменяются %20.

Например, предположим, что у вас есть пользовательский столбец Переводчики (#translators), где имена - Last name, First name. Возможно, при создании URL-адреса вам потребуется преобразовать имя в First name Last name. Для этого вы можете использовать функцию :ref: ff_make_url:

program: make_url('https://en.wikipedia.org/w/index.php', 'search', swap_around_comma($item_value))

Если мы предположим, что переводчика зовут Boy-Żeleński, Tadeusz, то приведенный выше шаблон выдает ссылку:

https://en.wikipedia.org/w/index.php?search=Tadeusz+Boy-%C5%BBele%C5%84ski

Обратите внимание, что имя пользователя теперь стоит первым, пробел - это плюс, а неанглоязычные символы в фамилии URL-кодированы.

Функции make_url_extended, query_string и encode_for_url могут быть полезны в зависимости от любой дополнительной сложности обработки.

Схема URL-адресов calibre

Calibre поддерживает несколько различных URL-адресов для навигации по вашим библиотекам calibre. В этом разделе показано, как использовать шаблоны для создания URL-адресов. Смотрите Схема URL-адресов calibre:// для получения подробной информации о доступных URL-адресах.

Переключение на определённую библиотеку. Синтаксис этого URL-адреса следующий:

calibre://switch-library/Library_Name

Library_Name необходимо заменить на название библиотеки calibre, которую вы хотите открыть. Название библиотеки отображается в строке заголовка окна. Это простое имя, а не путь к файлу библиотеки. Вы должны написать его так, как показано в строке заголовка, включая регистр букв. Символ _ (подчеркивание) обозначает текущую библиотеку. Если имя содержит какие-либо пробелы или специальные символы, то оно должно быть закодировано в шестнадцатеричном формате с использованием функции to_hex, как в следующем примере:

program: strcat('calibre://switch-library/_hex_-', to_hex(current_library_name()))

Шаблон генерирует URL-адрес:

calibre://switch-library/_hex_-4c6962726172792e746573745f736d616c6c

Вы можете заменить функцию current_library_name() фактическим именем библиотеки, как в:

program: strcat('calibre://switch-library/_hex_-', to_hex('Library.test_small'))

Ссылки для показа книг. По этим ссылкам выбирается книга в библиотеке calibre. Синтаксис этого URL-адреса следующий:

calibre://show-book/Library_Name/book_id

book id - это цифровой идентификатор книги в calibre, доступный в шаблонах как $ id. Как указано выше, название библиотеки может потребоваться в шестнадцатеричном кодировании. Вот пример:

program: strcat('calibre://show-book/_hex_-', to_hex(current_library_name()), '/', $id)

Он выдает URL-адрес:

calibre://show-book/_hex_-4c6962726172792e746573745f736d616c6c/1353

Поиск книг. Эти ссылки выполняют поиск книг в указанной библиотеке calibre. Синтаксис этого URL-адреса следующий:

calibre://search/Library_Name?q=query
calibre://search/Library_Name?eq=hex_encoded_query

где query - любое допустимое поисковое выражение calibre. Вы должны закодировать любой запрос, содержащий пробелы или специальные символы, в шестнадцатеричном формате, что обычно означает их все. Например, поисковое выражение calibre для поиска иерархического тега, начинающегося с „AA“, является tags:"=.AA". Этот шаблон создает URL-адрес поиска для этого выражения:

program: strcat('calibre://search/_hex_-', to_hex(current_library_name()), '?eq=', to_hex('tags:"=.AA"'))

Результирующий URL-адрес выглядит следующим образом:

calibre://search/_hex_-4c6962726172792e746573745f736d616c6c?eq=746167733a223d2e414122

Вот пример того же URL, созданного с использованием функции :ref:ff_make_url_extended вместо :ref:ff_strcat:

program: make_url_extended('calibre', '', 'search/_hex_-' & to_hex(current_library_name()),
'eq', to_hex('tags:"=.AA"'))

Открыть окно сведений о книге в какой-либо библиотеке. Синтаксис этого URL-адреса следующий:

calibre://book-details/Library_Name/book_id

Примером шаблона является:

program: strcat('calibre://book-details/_hex_-', to_hex(current_library_name()), '/', $id)

который выдает URL-адрес:

calibre://book-details/_hex_-4c6962726172792e746573745f736d616c6c/1353

Открыть примечания, связанные с автором/серией/и т.д. Синтаксис URL-адреса следующий:

calibre://book-details/Library_Name/Field_Name/id_Item_Id
calibre://book-details/Library_Name/Field_Name/hex_Hex_Encoded_Item_Name

Field_Name - это поисковое имя поля. Если поле представляет собой пользовательский столбец, замените символ # символом подчеркивания (_). Item_Id - это внутренний числовой идентификатор значения в поле. Не существует шаблонной функции, которая возвращала бы Item_Id, поэтому шаблоны обычно используют вторую форму, Hex_Encoded_Item_Name. Вот образец шаблона, который открывает заметку для человека Boy-Żeleński, Tadeusz в поле #authtest:

program: strcat('calibre://show-note/_hex_-', to_hex(current_library_name()),
'/_authtest/hex_', to_hex('Boy-Żeleński, Tadeusz'))

который выдает URL-адрес:

calibre://show-note/_hex_-4c6962726172792e746573745f736d616c6c/_authtest/hex_426f792dc5bb656c65c584736b692c205461646575737a

Сохранённые шаблоны

И Общий программный режим, и Режим шаблона Python поддерживают сохранение шаблонов и вызов этих шаблонов из другого шаблона, во многом аналогично вызову хранимых функций. Вы сохраняете шаблоны, используя Настройки → Расширенные → Шаблонные функции. В этом диалоговом окне - дополнительная информация. Вы вызываете шаблон так же, как вызываете функцию, передавая позиционные аргументы, если необходимо. Аргументом может быть любое выражение. Примеры вызова шаблона, предполагая, что сохраненный шаблон называется foo:

- foo() – вызов шаблона без аргументов.

- foo(a, b) вызывает шаблон, передавая значения переменных a и b.

- foo(if field('series') then field('series_index') else 0 fi) – если в книге есть series, то передать series_index, в противном случае - значение 0.

В GPM вы извлекаете аргументы, переданные при вызове сохраненного шаблона, с помощью функции arguments. Она и объявляет, и инициализирует локальные переменные, фактически параметры. Переменные являются позиционными; они получают значение параметра, заданного при вызове, в той же позиции. Если соответствующий параметр не указан в вызове, тогда arguments присваивает этой переменной предоставленное значение по умолчанию. Если значение по умолчанию отсутствует, переменной присваивается пустая строка. Например, следующая функция arguments объявляет 2 переменные, key, alternate:

arguments(key, alternate='series')

Примеры, снова предполагаем, что сохраненный шаблон назван foo:

- foo('#myseries') – аргументу key присваивается значение 'myseries', а аргументу alternate присваивается значение по умолчанию 'series'.

- foo('series', '#genre') переменной ``key присваивается значение 'series', а переменной alternate присваивается значение '#genre'.

- foo() - переменной key присваивается пустая строка, а переменной alternate присваивается значение 'series'.

В PTM аргументы передаются в параметре arguments, который представляет собой список строк. Невозможно указать значения по умолчанию. Вы должны проверить длину списка аргументов, чтобы убедиться, что количество аргументов соответствует ожидаемому.

Простой способ проверить сохраненные шаблоны - использовать диалоговое окно Тестер шаблонов. Для облегчения доступа используйте сочетание клавиш в Preferences-Advanced-Keyboard shortcuts-Template tester. Ярлык для диалогового окна Сохраненные шаблоны“ поможет быстрее переключаться между тестером и редактированием исходного кода сохраненного шаблона.

Передача шаблонам дополнительной информации

Разработчик может выбрать передачу дополнительной информации процессору шаблонов, например метаданные книги для конкретного приложения или информацию о том, что процессору предлагается сделать. Шаблон может получить доступ к этой информации и использовать ее во время оценки.

** Разработчик: как передать дополнительную информацию **

Дополнительная информация - это словарь dict Python, содержащий пары variable_name: variable_value, где значения должны быть строками. Шаблон может получить доступ к dict, создавая локальные переменные шаблона с именем variable_name, содержащие значение variable_value. Пользователь не может изменить имя, поэтому лучше использовать имена, которые не будут конфликтовать с другими локальными переменными шаблона, например, добавив к имени префикс подчеркивания _.

Этот dict передается процессору шаблонов (formatter) с использованием именованного параметра global_vars=your_dict. Полная сигнатура метода:

def safe_format(self, fmt, kwargs, error_value, book,
column_name=None, template_cache=None,
strip_results=True, template_functions=None,
global_vars={})

** Автор шаблона: как получить доступ к дополнительной информации **

Вы получаете доступ к дополнительной информации (словарю globals) в шаблоне с помощью шаблонной функции:

globals(id[=expression] [, id[=expression]]*)

где id - любое допустимое имя переменной. Эта функция проверяет, содержит ли дополнительная информация, предоставленная разработчиком, имя. Если это так, функция присваивает предоставленное значение локальной переменной шаблона с этим именем. Если имя отсутствует в дополнительной информации и если предоставлено expression, то expression оценивается, и результат присваивается локальной переменной. Если ни значение, ни выражение не предоставлены, функция присваивает пустую строку ('') локальной переменной.

Шаблон может установить значение в словаре globals с помощью шаблонной функции:

set_globals(id[=expression] [, id[=expression]]*)

Эта функция устанавливает globals dict key:value пару id:value, где value - значение шаблонной локальной переменной id. Если эта локальная переменная не существует, тогда value устанавливается равным результату вычисления выражения expression.

Замечания о разнице между режимами

Три программных режима Single Function Mode (SFM), Template Program Mode (TPM) и General Program Mode (GPM) работают по-разному. SFM задуман как „простой“, поэтому он скрывает много тонкостей в языке программирования.

Различия:

- В SFM значение столбца всегда передается как „invisible“ «невидимый» первый аргумент функции, включенной в шаблон.

- SFM не поддерживает разницу между переменными и строками; все значения являются строками.

- Следующий шаблон SFM возвращает либо название серии, либо строку «no series»:

{series:ifempty(no series)}

Эквивалентный шаблон в TPM

{series:'ifempty($, 'no series')'}

Эквивалентный шаблон в GPM

program: ifempty(field('series'), 'no series')

Первый аргумент ifempty - это значение поля series. Второй аргумент - это строка no series. В SFM автоматически передается первый аргумент, значение поля (невидимый аргумент).

- Некоторые функции-шаблоны, например, booksize() и current_library_name(), не принимают аргументов. Из-за „невидимого аргумента“ вы не можете использовать эти функции в SFM.

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

{series:uppercase(substr(0,5))}

- TPM и GPM поддерживают вложенные функции. Вышеупомянутый шаблон в TPM будет:

{series:'uppercase(substr($, 0,5))'}

В GPM это было бы:

program: uppercase(substr(field('series'), 0,5))

- Как отмечалось выше в разделе Template Program Mode, использование символов { и``}`` в строковых литералах TPM может привести к ошибкам или неожиданным результатам, поскольку они сбивают с толку обработчик шаблонов. Он пытается рассматривать их как границы шаблона, а не как символы. В некоторых, но не во всех случаях, вы можете заменить { на [[ и } на ]]. Как правило, если ваша программа содержит символы { и ``}``, вам следует использовать General Program Mode.

Пользовательские шаблонные функции Python

Вы можете добавить свои собственные функции Python в процессор шаблонов. Такие функции можно использовать в любом из трех режимов программирования шаблонов. Функции добавляются по ссылке: guilabel:Настройки - Расширенные - Функции шаблонов. Инструкции показаны в этом диалоговом окне. Обратите внимание, что вы можете использовать Шаблоны Python для аналогичной цели. Поскольку вызов пользовательских функций выполняется быстрее, чем вызов шаблона Python, пользовательские функции могут быть более эффективными в зависимости от сложности того, что выполняет функция или шаблон.

Особые указания по использованию шаблонов в различных контекстах

В графическом интерфейсе (Столбцы, созданные из других столбцов и:guilabel:Поиск по шаблону):

- Шаблоны GPM работают по-прежнему.

- Шаблон Python будет иметь полный доступ к базе данных calibre.

В правилах значков:

- шаблоны правил значков не содержат данных книги, поэтому функции на основе полей, такие как format_date_field, list_count_field, и check_yes_no не работают.

В контент-сервере:

- Шаблоны имеют доступ к новому API, но не к старому API (LibraryDatabase).

- Из-за вышесказанного, работа следующих функций форматирования в шаблонах GPM не гарантируется (составные столбцы, правила значков и т.д.), и их следует избегать, если вы используете контент-сервер:

- connected_device_name

- connected_device_uuid

- current_virtual_library_name

- is_marked

- virtual_libraries

Особые примечания для шаблонов сохранения/отправки

Специальная обработка применяется, когда шаблон используется в шаблоне Save to disk или Send to device. Значения полей очищаются, заменяя символы, специальные для файловых систем, подчеркиванием, включая косую черту. Это означает, что текст поля нельзя использовать для создания папок. Однако косые черты не изменяются в строках префиксов или суффиксов, поэтому косые черты в этих строках вызовут создание папок. Благодаря этому вы можете создать структуру папок переменной глубины.

Например, предположим, что нам нужна структура папок series/series_index - title, с оговоркой, что если серия не существует, то заголовок должен находиться в верхней папке. Шаблон для этого:

{series:||/}{series_index:|| - }{title}

Косая черта и дефис появляются только в том случае, если серия не пуста.

Функция поиска позволяет нам выполнять еще более интересную обработку. Например, предположим, что если в книге есть серия, то нам нужна структура папок series/series index - title.fmt. Если в книге нет серии, нам нужна структура папок genre/author_sort/title.fmt. Если у книги нет жанра, мы хотим использовать „Unknown“. Нам нужны два совершенно разных пути, в зависимости от значения серии.

Чтобы этого добиться, мы:

1. Создать составное поле (дать ему имя для поиска #aa), содержащее {series}/{series_index} - {title}. Если серия не пуста, тогда этот шаблон создаст series/series_index - title.

2. Создать составное поле (дать ему имя для поиска #bb), содержащее {#genre: ifempty (Unknown)}/{author_sort}/{title}. Этот шаблон создаст genre /author_sort/title, где пустой жанр заменяется на Unknown.

3. Установить для шаблона сохранения значение {series:lookup(.,#aa,#bb}. Этот шаблон выбирает составное поле #aa, если серия не пуста, и составное поле #bb, если серия пуста, поэтому у нас есть два совершенно разных пути сохранения, в зависимости от того, пуста ли series.

Советы

- Используйте тестер шаблонов для тестирования шаблонов. Добавьте тестер в контекстное меню для книг в библиотеке и/или назначьте ему сочетание клавиш.

- Шаблоны могут использовать другие шаблоны, ссылаясь на составные столбцы, построенные с помощью желаемого шаблона. В качестве альтернативы вы можете использовать сохраненные шаблоны.

- В коммутации полю можно назначить соответствующее его типу пустое значение при помощи специального шаблона {}. Он всегда соответствует пустой строке.

- Описанный выше метод отображения чисел, даже если они имеют нулевое значение, работает со стандартным полем series_index.

Справочник по шаблонным функциям

Reference for all built-in template language functions

Арифметический

- add
- ceiling
- divide
- floor
- fractional_part
- mod
- multiply
- round
- subtract

Булево

- and
- not
- or

Другое

- arguments
- assign
- globals
- is_dark_mode
- print
- set_globals

Манипулирование строками

- character
- check_yes_no
- contains
- field_exists
- ifempty
- re
- re_group
- shorten
- strcat
- strcat_max
- strlen
- substr
- swap_around_articles
- swap_around_comma
- test
- transliterate

Манипуляция списком

- list_count
- list_count_field
- list_count_matching
- list_difference
- list_equals
- list_intersection
- list_join
- list_re
- list_re_group
- list_remove_duplicates
- list_sort
- list_split
- list_union
- range
- subitems
- sublist

Относящийся

- cmp
- first_matching_cmp
- strcmp
- strcmpcase

Перебор значений

- first_non_empty
- lookup
- switch
- switch_if

Поиск по списку

- identifier_in_list
- list_contains
- list_item
- select
- str_in_list

Получить значения из метаданных

- annotation_count
- approximate_formats
- author_links
- author_sorts
- booksize
- connected_device_name
- connected_device_uuid
- current_library_name
- current_library_path
- current_virtual_library_name
- field
- formats_modtimes
- formats_path_segments
- formats_paths
- formats_sizes
- has_cover
- is_marked
- language_codes
- language_strings
- ondevice
- raw_field
- raw_list
- series_sort
- user_categories
- virtual_libraries

Рекурсия

- eval

- template

Смена регистра

- capitalize
- lowercase
- titlecase
- uppercase

Форматирование значений

- finish_formatting
- format_date
- format_date_field
- format_duration
- format_number
- human_readable
- rating_to_stars

Функции URL

- encode_for_url
- make_url
- make_url_extended
- query_string
- to_hex
- urls_from_identifiers

Функции базы данных

- book_count
- book_values
- extra_file_modtime
- extra_file_names
- extra_file_size
- get_link
- get_note
- has_extra_files
- has_note

Функции даты

- date_arithmetic
- days_between
- today

API of the Metadata objects

- Metadata

- STANDARD_METADATA_FIELDS

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Руководства »
- Язык шаблонов calibre »

- Reference for all built-in template language functions

Reference for all built-in template language functions

Here, we document all the built-in functions available in the calibre template language. Every function is implemented as a class in python and you can click the source links to see the source code, in case the documentation is insufficient. The functions are arranged in logical groups by type.

Арифметический

- add

- ceiling

- divide

- floor

- fractional_part

- mod

- multiply

- round

- subtract

Булево

- and

- not

- or

Другое

- arguments

- assign

- globals

- is_dark_mode

- print

- set_globals

Манипулирование строками

- character

- check_yes_no

- contains

- field_exists

- ifempty

- re

- re_group

- shorten

- strcat

- strcat_max

- strlen

- substr

- swap_around_articles

- swap_around_comma

- test

- transliterate

Манипуляция списком

- list_count

- list_count_field

- list_count_matching

- list_difference

- list_equals

- list_intersection

- list_join

- list_re

- list_re_group

- list_remove_duplicates

- list_sort

- list_split

- list_union

- range

- subitems

- sublist

Относящийся

- cmp

- first_matching_cmp

- strcmp

- strcmpcase

Перебор значений

- first_non_empty

- lookup

- switch

- switch_if

Поиск по списку

- identifier_in_list

- list_contains

- list_item

- select

- str_in_list

Получить значения из метаданных

- annotation_count

- approximate_formats

- author_links

- author_sorts

- booksize

- connected_device_name

- connected_device_uuid

- current_library_name

- current_library_path

- current_virtual_library_name

- field

- formats_modtimes

- formats_path_segments

- formats_paths

- formats_sizes

- has_cover

- is_marked

- language_codes

- language_strings

- ondevice

- raw_field

- raw_list

- series_sort

- user_categories

- virtual_libraries

Рекурсия

- eval

- template

Смена регистра

- capitalize

- lowercase

- titlecase

- uppercase

Форматирование значений

- finish_formatting

- format_date

- format_date_field

- format_duration

- format_number

- human_readable

- rating_to_stars

Функции URL

- encode_for_url

- make_url

- make_url_extended

- query_string

- to_hex

- urls_from_identifiers

Функции базы данных

- book_count

- book_values

- extra_file_modtime

- extra_file_names

- extra_file_size

- get_link

- get_note

- has_extra_files

- has_note

Функции даты

- date_arithmetic

- days_between

- today

API of the Metadata objects

Арифметический

add

class calibre.utils.formatter_functions.BuiltinAdd[исходный код]

add(x [, y]*) – возвращает сумму своих аргументов. Выдаёт исключение, если аргумент не является числом. В большинстве случаев эту функцию может заменить оператор +.

ceiling

class calibre.utils.formatter_functions.BuiltinCeiling[исходный код]

ceiling(value) – возвращает наименьшее целое число, большее или равное value. Выдаёт исключение, если value не является числом.

divide

class calibre.utils.formatter_functions.BuiltinDivide[исходный код]

divide(x, y) – возвращает x / y. Выдает исключение, если x или y не являются числами. Эту функцию часто можно заменить оператором /.

floor

class calibre.utils.formatter_functions.BuiltinFloor[исходный код]

floor(value) – возвращает наибольшее целое число, меньшее или равное value. Выдаёт исключение, если value не является числом.

fractional_part

class calibre.utils.formatter_functions.BuiltinFractionalPart[исходный код]

fractional_part(value) – returns the part of the value after the decimal point. For example, fractional_part(3.14) returns 0.14. Throws an exception if value is not a number.

Отображение документации на английском языке из-за ошибки FFML: Missing closing «``» for CODE_TEXT on line 4 in «fractional_part»

mod

class calibre.utils.formatter_functions.BuiltinMod[исходный код]

mod(value, y) – возвращает floor остаток от value / y. Выдаёт исключение, если value или y не является числом.

multiply

class calibre.utils.formatter_functions.BuiltinMultiply[исходный код]

multiply(x [, y]*) – возвращает произведение своих аргументов. Выдаёт исключение, если какой-либо аргумент не является числом. Эту функцию часто можно заменить оператором *.

round

class calibre.utils.formatter_functions.BuiltinRound[исходный код]

round(value) – возвращает ближайшее к value целое число. Выдаёт исключение, если value не является числом.

subtract

class calibre.utils.formatter_functions.BuiltinSubtract[исходный код]

subtract(x, y) – возвращает x - y. Выдаёт исключение, если x или y не являются числами. Эту функцию часто можно заменить оператором -.

Булево

and

class calibre.utils.formatter_functions.BuiltinAnd[исходный код]

and(value [, value]*) – возвращает строку «1», если все значения не пустые, в противном случае возвращает пустую строку. Значения - сколько захотите. Обычно можно заменить на оператор &&. Одна из причин не заменять and на && заключается в том, что короткое замыкание может изменить результаты из-за побочных эффектов. Например, and(a='',b=5) всегда будет выполнять оба присваивания, а оператор && не будет выполнять второе.

not

class calibre.utils.formatter_functions.BuiltinNot[исходный код]

not(value) – возвращает строку 1, если значение пустое, иначе возвращает пустую строку. Эту функцию обычно можно заменить унарным оператором not (!).

or

class calibre.utils.formatter_functions.BuiltinOr[исходный код]

or(value [, value]*) – возвращает строку «1», если все значения не пустые, в противном случае возвращает пустую строку. Значения - сколько захотите. Во многих случаях оператор || может заменить эту функцию. Причина, по которой нельзя заменить - короткое замыкание изменит результаты из-за побочных эффектов.

Другое

arguments

class calibre.utils.formatter_functions.BuiltinArguments[исходный код]

arguments(id[=expression] [, id [=expression]]*) – используется в сохранённом шаблоне для извлечения аргументов, переданных в вызове. Как объявляет так и инициализирует локальные переменные, фактически параметры. Переменные позиционные; они получают значение, указанное в вызове, в той же позиции. Если соответствующий параметр не указан в вызове, то аргументы присваивают этой переменной заданное значение по умолчанию. Если значение по умолчанию отсутствует, переменная устанавливается в пустую строку.

assign

class calibre.utils.formatter_functions.BuiltinAssign[исходный код]

assign(id, value) – assigns value to id, then returns value. id must be an identifier, not an expression. In most cases you can use the = operator instead of this function.

Отображение документации на английском языке из-за ошибки FFML: Missing closing «``» for CODE_TEXT on line 4 in «assign»

globals

class calibre.utils.formatter_functions.BuiltinGlobals[исходный код]

globals(id[=expression] [, id[=expression]]*) – извлекает «глобальные переменные», которые могут быть переданы в форматтер. Имя id - это имя глобальной переменной. Оно одновременно объявляет и инициализирует локальные переменные передаваемыми именами (в параметрах id глобальных переменных. Если соответствующая переменная не представлена в глобальных, то этой переменной присваивается указанное по умолчанию значение. Если нет значения по умолчанию, то эта переменная принимает значение пустой строки.)

is_dark_mode

class calibre.utils.formatter_functions.BuiltinIsDarkMode[исходный код]

is_dark_mode() – вернуть '1' если calibre запущен в тёмном режиме, иначе '' (пустая строка). Эту функцию можно использовать в расширенных настройках цвета и правилах выбора значков для выбора различных цветов / значков в зависимости от режима. Пример:

if is_dark_mode() then 'dark.png' else 'light.png' fi

print

class calibre.utils.formatter_functions.BuiltinPrint[исходный код]

print(a [, b]*) – выводит аргументы на стандартный вывод. Пока вы не запустите calibre из командной строки (calibre-debug -g), вывод идёт в чёрную дыру (black hole). Функция print всегда возвращает свой первый аргумент.

set_globals

class calibre.utils.formatter_functions.BuiltinSetGlobals[исходный код]

set_globals(id[=expression] [, id[=expression]]*) – Sets globalvariables that can be passed into the formatter. The globals are given the name of the id passed in. The value of the id is used unless an expression is provided.

Отображение документации на английском языке из-за ошибки FFML: Missing closing «``» for CODE_TEXT on line 5 in «set_globals»

Манипулирование строками

character

class calibre.utils.formatter_functions.BuiltinCharacter[исходный код]

character(character_name) - возвращает символ, названный character_name. Например, character('newline) возвращает символ новой строки ('\n'). Поддерживаемые имена символов: newline, return, tab, и backslash. Эта функция используется для ввода этих символов в выходные данные шаблонов.

check_yes_no

class calibre.utils.formatter_functions.BuiltinCheckYesNo[исходный код]

check_yes_no(field_name, is_undefined, is_false, is_true) – проверяет значение поля yes/no, названного ключом поиска field_name, на значение, указанное в параметрах, возвращая 'Yes', если совпадение найдено, иначе возвращает пустую строку. Установите параметр is_undefined, is_false или is_true в 1 (число), чтобы проверить это условие, иначе установите его равным 0 (ноль). Пример: check_yes_no("# bool", 1, 0, 1) возвращает 'Yes', если yes/no поле #bool не определено (ни True, ни False), или True.

Более одного из is_undefined, is_false или is_true может быть установлено в 1.

contains

class calibre.utils.formatter_functions.BuiltinContains[исходный код]

contains(value, pattern, text_if_match, text_if_not_match) – проверяет, соответствует ли значение регулярному выражению pattern. Возвращает text_if_match при совпадении, иначе возвращает text_if_not_match.

field_exists

class calibre.utils.formatter_functions.BuiltinFieldExists[исходный код]

field_exists(field_name) – проверяет, существует ли поле (столбец) с именем field_name, возвращая '1', если это так, и пустую строку, если нет.

ifempty

class calibre.utils.formatter_functions.BuiltinIfempty[исходный код]

ifempty(value, text_if_empty) – если value не пустое, то вернуть это value, иначе вернуть text_if_empty.

re

class calibre.utils.formatter_functions.BuiltinRe[исходный код]

re(value, pattern, replacement) – вернуть значение value после применения регулярного выражения. Все вхождения pattern в значении value заменяются на replacement. Язык шаблонов не регистрозависим Python regular expressions [https://docs.python.org/3/library/re.html].

re_group

class calibre.utils.formatter_functions.BuiltinReGroup[исходный код]

re_group(value, pattern [, template_for_group]*) – return a string made by applying the regular expression pattern to value and replacing each matched instance with the value returned by the corresponding template. In Template Program Mode [https://manual.calibre-ebook.com/template_lang.html#more-complex-programs-in-template-expressions-template-program-mode], like for the template and the eval functions, you use [[ for { and ]] for }.

The following example looks for a series with more than one word and uppercases the first word:

program: re_group(field('series'), "(\S* )(.*)", "{$:uppercase()}", "{$}")'}

Отображение документации на английском языке из-за ошибки FFML: Missing closing [/URL] near text программн on line 3 in «re_group»

shorten

class calibre.utils.formatter_functions.BuiltinShorten[исходный код]

shorten(value, left chars, middle text, right chars) – Возвращает сокращённую версию значения``value``, состоящую из символов left chars с начала значения, за которыми следует middle text, за которыми следуют символы right chars с конца значения. left chars и right chars должны быть неотрицательными целыми числами.

Пример: предположим, что вы хотите отобразить заголовок длиной не более 15 символов. Один из шаблонов, который делает это, - {title:short(9,-,5)}. Для книги с названием «Ancient English Laws in the Times of Ivanhoe» результатом будет Ancient E-nhoe: первые 9 символов названия, -, затем последние 5 символов. Если длина значения меньше, чем left chars + right chars + длина middle text, то значение будет возвращено без изменений. Например, название The Dome не изменится.

strcat

class calibre.utils.formatter_functions.BuiltinStrcat[исходный код]

strcat(a [, b]*) – возвращает строку объединяющую все аргументов. Может принимать любое количество аргументов. В большинстве случаев эту функцию может заменить оператор & .

strcat_max

class calibre.utils.formatter_functions.BuiltinStrcatMax[исходный код]

strcat_max(max, string1 [, prefix2, string2]*) – Возвращает строку, образованную объединением аргументов. Возвращаемое значение инициализируется string1. Пары prefix, string добавляются в конец значения, если результирующая длина строки меньше, чем max. string1 возвращается, даже если string1 длиннее max. Вы можете передать сколько угодно пар prefix, string.

strlen

class calibre.utils.formatter_functions.BuiltinStrlen[исходный код]

strlen(value) – Возвращает длину строки value.

substr

class calibre.utils.formatter_functions.BuiltinSubstr[исходный код]

substr(value, start, end) – возвращает подстроку - символы строки value от start до end. Первый символ строки value имеет номер ноль. Если end отрицательный - это сколько символов будет отсчитываться с конца строки (справа). Если end равен 0 (ноль), то это - последний символ строки. Например, substr('12345', 1, 0) вернёт '2345', и substr('12345', 1, -1) вернёт '234'.

swap_around_articles

class calibre.utils.formatter_functions.BuiltinSwapAroundArticles[исходный код]

swap_around_articles(value, separator) – returns the value with articles moved to the end, separated by a semicolon. The value can be a list, in which case each item in the list is processed. If the value is a list then you must provide the separator. If no separator is provided or the separator is the empty string then the value is treated as being a single value, not a list. The articles are those used by calibre to generate the title_sort.

Отображение документации на английском языке из-за ошибки FFML: Missing closing «``» for CODE_TEXT on line 7 in «swap_around_articles»

swap_around_comma

class calibre.utils.formatter_functions.BuiltinSwapAroundComma[исходный код]

swap_around_comma(value) – если значение value имеет вид B, A, возвращает A B. Это наиболее полезно для преобразования имён вида LN, FN (Ф, И О) к виду FN LN (И О Ф). Если значение не содержит запятой в value, то оно возвращается без изменений.

test

class calibre.utils.formatter_functions.BuiltinTest[исходный код]

test(value, text_if_not_empty, text_if_empty) – возвращает text_if_not_empty если значение не пусто, иначе возвращает text_if_empty.

transliterate

class calibre.utils.formatter_functions.BuiltinTransliterate[исходный код]

transliterate(value) – возвращает строку в латинском алфавите, которая по звучанию похожа на слова исходной строки. Например, если value было Фёдор Миха́йлович Достоевский, то функция вернёт Fiodor Mikhailovich Dostoievskii.

Манипуляция списком

list_count

class calibre.utils.formatter_functions.BuiltinCount[исходный код]

list_count(value, separator) – интерпретирует значение как список элементов, разделенных символом separator и возвращает количество элементов в списке. В большинстве списков используется запятая в качестве разделителя, но authors используют амперсанд (&).

Примеры: {tags:list_count(,)}, {authors:list_count(&)}.

Псевдонимы: count(), list_count()

list_count_field

class calibre.utils.formatter_functions.BuiltinFieldListCount[исходный код]

list_count_field(lookup_name)– вернуть количество элементов в поле с именем для поиска lookup_name. Поле должно быть многозначным, например authors или tags, в противном случае функция выдаст ошибку. Эта функция намного быстрее, чем list_count(), потому что она работает непосредственно с данными calibre без предварительного преобразования их в строку. Пример: list_count_field('tags').

list_count_matching

class calibre.utils.formatter_functions.BuiltinListCountMatching[исходный код]

list_count_matching(value, pattern, separator) – интерпретирует value как список элементов, разделённых separator, возвращая количество элементов в списке, которые соответствуют регулярному выражению pattern. Псевдонимы: list_count_matching(), count_matching()

list_difference

class calibre.utils.formatter_functions.BuiltinListDifference[исходный код]

list_difference(list1, list2, separator) – возвращает список, созданный удалением из списка list1 любого элемента, найденного в списке list2, используя регистронезависимое сравнение. Элементы в списках list1 и list2 разделяются разделителем separator, так же как и элементы в возвращаемом списке.

list_equals

class calibre.utils.formatter_functions.BuiltinListEquals[исходный код]

list_equals(list1, sep1, list2, sep2, yes_val, no_val) – возвращает yes_val если list1 и list2 содержат одинаковые элементы, иначе возвращает no_val. Элементы определяются разделением каждого списка с использованием соответствующего символа разделителя (sep1 или sep2). Порядок элементов в списке не важен. Сравнение регистронезависимое.

list_intersection

class calibre.utils.formatter_functions.BuiltinListIntersection[исходный код]

list_intersection(list1, list2, separator) – вернуть список, созданный удалением из list1 любого элемента, не найденного в list2, с использованием сравнения без учета регистра. Элементы в list1 и list2 разделены разделителем separator, как и элементы в возвращаемом списке.

list_join

class calibre.utils.formatter_functions.BuiltinListJoin[исходный код]

list_join(с_separator, list1, separator1 [, list2, separator2]*) – возвращает список, созданный путем объединения элементов в исходных списках (list1 и т.д.) используя with_separator между элементами в результирующем списке. Элементы в каждом из исходных списков list[123...] разделён соответствующим разделителем separator[123 ...]. Список может содержать нулевые значения. Это может быть поле типа publisher (издатель), которое является однозначным, фактически списком из одного элемента. Дубликаты удаляются с помощью сравнения без учета регистра. Детали возвращены в том порядке, как они появляются в списке источников. Если элементы в списках, отличаются только регистром букв, то используется последнее. Все разделители может быть больше, чем один символ.

Example:

program:
list_join('#@#', $authors, '&', $tags, ',')

You can use list_join on the results of previous calls to list_join as follows:

program:
a = list_join('#@#', $authors, '&', $tags, ',');
b = list_join('#@#', a, '#@#', $#genre, ',', $#people, '&', 'some value', ',')

Вы можете использовать выражения для создания списка. Например, предположим, что вам нужны элементы для authors и #genre, но с изменением жанра на слово «Genre: « далее следует первая буква жанра, т.е. «Fiction» станет «Genre: F». Получится следующее:

program:
list_join('#@#', $authors, '&', list_re($#genre, ',', '^(.).*$', 'Genre: \1'), ',')

list_re

class calibre.utils.formatter_functions.BuiltinListRe[исходный код]

list_re(src_list, separator, include_re, opt_replace) – создать список, вначале разделив src_list на элементы с помощью символа разделителя separator. Для каждого элемента в списке проверить, соответствует ли он include_re. Если это так, добавить его в список для возврата. Если opt_replace не является пустой строкой, применить замену перед добавлением элемента в возвращаемый список.

list_re_group

class calibre.utils.formatter_functions.BuiltinListReGroup[исходный код]

list_re_group(src_list, separator, include_re, search_re [, template_for_group]*) – Как и list_re, за исключением того, что замены не являются обязательными. При замене используется re_group(item, search_re, template ...).

list_remove_duplicates

class calibre.utils.formatter_functions.BuiltinListRemoveDuplicates[исходный код]

list_remove_duplicates(list, separator) – вернуть список, созданный удалением повторяющихся элементов в list. Если элементы отличаются только регистром, возвращается последний. Элементы в списке list разделены разделителем separator, как и элементы в возвращаемом списке``list``.

list_sort

class calibre.utils.formatter_functions.BuiltinListSort[исходный код]

list_sort(list, direction, separator) – вернуть список, отсортированный с использованием лексической сортировки без учета регистра. Если direction равно нулю, список сортируется по возрастанию, иначе по убыванию. Элементы списка разделены разделителем separator, как и элементы в возвращаемом списке.

list_split

class calibre.utils.formatter_functions.BuiltinListSplit[исходный код]

list_split(list_val, sep, id_prefix) – разделяет list_val на отдельные значения с помощью sep, затем присваивает значения переменным с именем id_prefix_N, где N — позиция значения в списке. Первый элемент имеет позицию 0 (ноль). Функция возвращает последний элемент в списке.

Пример:

list_split('one:two:foo', ':', 'var')

is equivalent to:

var_0 = 'one'
var_1 = 'two'
var_2 = 'foo'

list_union

class calibre.utils.formatter_functions.BuiltinListUnion[исходный код]

list_union(list1, list2, separator) – возвращает список, составленный путем слияния элементов в list1 и list2, удаляя повторяющиеся элементы с помощью сравнения без учета регистра. Если элементы отличаются регистром, используется значение из списка list1. Элементы в списках list1 и list2 разделены разделителем``separator``, как и элементы в возвращаемом списке. Псевдонимы: merge_lists(), list_union()

range

class calibre.utils.formatter_functions.BuiltinRange[исходный код]

range(start, stop, step, limit) – returns a list of numbers generated by looping over the range specified by the parameters start, stop, and step, with a maximum length of limit. The first value produced is „start“. Subsequent values next_v = current_v + step. The loop continues while next_v < stop assuming step is positive, otherwise while next_v > stop. An empty list is produced if start fails the test: start >= stop if step is positive. The limit sets the maximum length of the list and has a default of 1000. The parameters start, step, and limit are optional. Calling range() with one argument specifies stop. Two arguments specify start and stop. Three arguments specify start, stop, and step. Four arguments specify start, stop, step and limit.

Examples:

range(5) -> '0, 1, 2, 3, 4'
range(0, 5) -> '0, 1, 2, 3, 4'
range(-1, 5) -> '-1, 0, 1, 2, 3, 4'
range(1, 5) -> '1, 2, 3, 4'
range(1, 5, 2) -> '1, 3'
range(1, 5, 2, 5) -> '1, 3'
range(1, 5, 2, 1) -> error(limit exceeded)

Отображение документации на английском языке из-за ошибки FFML: Missing closing «`» for italics on line 9 in «range»

subitems

class calibre.utils.formatter_functions.BuiltinSubitems[исходный код]

subitems(value, start_index, end_index) – Эта функция разделяет списки на иерархические элементы, подобные тегам, такие как жанры. Она интерпретирует значение как разделенный запятыми список элементов, похожих на теги, где каждый элемент представляет собой список, разделенный точкой. Возвращает новый список, созданный путем извлечения из каждого элемента компонентов от start_index до end_index, затем результаты объединяются обратно. Дубликаты удаляются. Первый подпункт в списке, разделенном точкой, имеет нулевой индекс. Если индекс отрицательный, то он отсчитывается от конца списка. В качестве особого случая предполагается, что end_index , равный нулю, равен длине списка.

Примеры:

- Assuming a #genre column containing «A.B.C»:

- {#genre:subitems(0,1)} returns «A»

- {#genre:subitems(0,2)} returns «A.B»

- {#genre:subitems(1,0)} returns «B.C»

- Assuming a #genre column containing «A.B.C, D.E»:

- {#genre:subitems(0,1)} returns «A, D»

- {#genre:subitems(0,2)} returns «A.B, D.E»

sublist

class calibre.utils.formatter_functions.BuiltinSublist[исходный код]

sublist(value, start_index, end_index, separator) – интерпретирует value как список значений, разделённых separator, возвращая новый список с началом в start_index с концом в end_index. Первый пункт - номер 0 (ноль). Если индекс - отрицательный, то его начало - от конца списка. В особых случаях end_index нуля считается длиной списка.

Примеры, использующие режим базового шаблона и предополагающие что столбец тегов (разделённый запятыми) содержит «A, B, C»:

- {tags:sublist(0,1,\,)} returns «A»

- {tags:sublist(-1,0,\,)} returns «C»

- {tags:sublist(0,-1,\,)} returns «A, B»

Относящийся

cmp

class calibre.utils.formatter_functions.BuiltinCmp[исходный код]

cmp(value, y, lt, eq, gt) – сравнивает value и y после преобразования обоих в числа. Возвращает lt если value <# y, eq если value ==# y, иначе gt. Во многих случаях операторы числового сравнения (==#, <#, ># и т. п.) могут заменить эту функцию.

first_matching_cmp

class calibre.utils.formatter_functions.BuiltinFirstMatchingCmp[исходный код]

first_matching_cmp(val, [cmp1, result1,]+, else_result) – последовательно сравнивает val < cmp, возвращая ассоциированный result для первого успешного сравнения. Если сравнение не удалось, возвращает else_result. Пример:

i = 10;
first_matching_cmp(i,5,"small",10,"middle",15,"large","giant")

вернёт "large". В том же примере с первым значением 16 возвращается "giant".

strcmp

class calibre.utils.formatter_functions.BuiltinStrcmp[исходный код]

strcmp(x, y, lt, eq, gt) – does a case-insensitive lexical comparison of x and y. Returns lt if x < y, eq if x == y, otherwise gt. This function can often be replaced by one of the lexical comparison operators (==, >, <, etc.)

Отображение документации на английском языке из-за ошибки FFML: Missing closing «`» for italics on line 3 in «strcmp»

strcmpcase

class calibre.utils.formatter_functions.BuiltinStrcmpcase[исходный код]

strcmpcase(x, y, lt, eq, gt) – выполняет сравнение строк x и y с учетом регистра. Возвращает lt, если x < y, eq если x == y, иначе gt.

Примечание. Это НЕ поведение по умолчанию, используемое calibre, например, в операторах лексического сравнения (==, >, < и т. д.). Эта функция может привести к неожиданным результатам, желательно использовать strcmp(), когда это возможно.

Перебор значений

first_non_empty

class calibre.utils.formatter_functions.BuiltinFirstNonEmpty[исходный код]

first_non_empty(value [, value]*) – возвращает первое непустое значение. Если все значения пусты, возвращается пустая строка. Вы можете иметь сколько угодно значений.

lookup

class calibre.utils.formatter_functions.BuiltinLookup[исходный код]

lookup(value, [ pattern, key, ]* else_key) – Шаблоны будут проверяться по значению value по порядку. Если шаблон совпадает, возвращается значение поля, названного key. Если ни один шаблон не совпадает, возвращается значение поля с именем else_key. Смотрите функцию switch().

switch

class calibre.utils.formatter_functions.BuiltinSwitch[исходный код]

switch(value, [patternN, valueN,]+ else_value) – for each patternN, valueN pair, checks if the value matches the regular expression patternN and if so returns the associated valueN. If no patternN matches, then else_value is returned. You can have as many patternN, valueN pairs as you wish. The first match is returned.

Отображение документации на английском языке из-за ошибки FFML: Missing closing «`» for italics on line 5 in «switch»

switch_if

class calibre.utils.formatter_functions.BuiltinSwitchIf[исходный код]

switch_if([test_expression, value_expression,]+ else_expression) – для каждой пары test_expression, value_expression проверяет, является ли test_expression True (непустым), и если да, то возвращает результат value_expression. Если ни одно из test_expression не равно True, возвращается результат else_expression. У вас может быть столько пар test_expression, value_expression, сколько вы хотите.

Поиск по списку

identifier_in_list

class calibre.utils.formatter_functions.BuiltinIdentifierInList[исходный код]

identifier_in_list(val, id_name [, found_val, not_found_val]) – рассматривает val как список идентификаторов, разделённый запятыми. Идентификатор имеет формат id_name:value. id_name параметр - id_name текст для поиска, или id_name или id_name:regexp. Первый случай соответствует, если есть какой-либо идентификатор, соответствующий этому id_name. Второй случай соответствует, если id_name соответствует идентификатору, а регулярное выражение соответствует значению идентификатора. Если указаны found_val и not_found_val, то, если есть совпадение, возвращается found_val, в противном случае возвращается not_found_val. Если found_val и not_found_val не указаны, то, если есть совпадение, возвращается пара identfier:value, иначе пустая строка ('').

list_contains

class calibre.utils.formatter_functions.BuiltinInList[исходный код]

list_contains(value, separator, [ pattern, found_val, ]* not_found_val) – интерпретирует value как список элементов, разделенных разделителем separator, проверяя pattern для каждого элемента в списке. Если pattern соответствует элементу, возвращает found_val, иначе not_found_val. Пара pattern и found_value могут повторяться столько раз, сколько требуется, что позволяет возвращать разные значения в зависимости от значения элемента. Шаблоны проверяются по порядку и возвращается первое совпадение.

Псевдонимы: in_list(), list_contains()

list_item

class calibre.utils.formatter_functions.BuiltinListitem[исходный код]

list_item(value, index, separator) – обрабатывает значение как список элементов, разделённых separator, возвращая элемент с номером index. Первый элемент имеет номер ноль. Последний элемент с номером индекса -1 как list_item(-1,separator). Если элемент не в списке, то возвращается пустое значение. Разделитель (separator) работает так же, как и в функции count, обычно запятая, но амперсанд для списков, вроде author.

select

class calibre.utils.formatter_functions.BuiltinSelect[исходный код]

select(value, key) – обрабатывает value как разделённый запятыми список элементов вида id:id_value (формат identifier calibre). Функция находит первую пару с совпадающим с key идентификатором, и возвращает соответствующее id_value. При отсутствии совпадений возвращается пустая строка.

str_in_list

class calibre.utils.formatter_functions.BuiltinStrInList[исходный код]

str_in_list(separator, [ string, found_val, ]+ not_found_val) – интерпретирует значение как список элементов, разделенных separator, затем сравнивает string с каждым значением в списке. string не является регулярным выражением. Если string равна любому элементу (без учета регистра), вернуть соответствующее значение found_val. Если string содержит разделители separator, она также рассматривается как список, и каждое подзначение проверяется. Пары string и found_value могут повторяться сколько угодно раз, что позволяет возвращать разные значения в зависимости от значения строки. Если ни одна из строк не совпадает, возвращается not_found_value. Строки проверяются по порядку. Возвращается первое совпадение.

Получить значения из метаданных

annotation_count

class calibre.utils.formatter_functions.BuiltinAnnotationCount[исходный код]

annotation_count() – вернуть общее количество аннотаций всех типов, прикрепленных к текущей книге. Эта функция работает только в графическом интерфейсе и контент-сервере.

approximate_formats

class calibre.utils.formatter_functions.BuiltinApproximateFormats[исходный код]

approximate_formats() – возвращает разделенный запятыми список форматов, связанных с книгой. Поскольку список берется из базы данных calibre, а не из файловой системы, нет гарантии, что список правильный, но обычно это верно. Обратите внимание, что результирующие названия форматов всегда написаны заглавными буквами, как в EPUB.

Эта функция работает только в графическом интерфейсе. Если нужно использовать эти значения в шаблоне для сохранения на диск или отправки на устройство, то создайте пользовательский столбец типа «Столбец состоящий из других столбцов» и используйте функцию в нём, а затем используйте значение этого столбца в ваших шаблонах сохранения/отправки.

author_links

class calibre.utils.formatter_functions.BuiltinAuthorLinks[исходный код]

author_links(val_separator, pair_separator) – возвращает строку, содержащую список авторов и значения ссылок на этих авторов в форме: author1 val_separator author1_link pair_separator author2 val_separator author2_link и т.д.

Автор отделяется от значения ссылки строкой val_separator без добавленных пробелов. Предполагая, что val_separator является двоеточием, пары author:link value разделяются строковым аргументом pair_separator без добавленных пробелов. Выберите разделители, которые не встречаются в именах авторов или ссылках. Автор будет указан, даже если ссылка на автора пуста.

author_sorts

class calibre.utils.formatter_functions.BuiltinAuthorSorts[исходный код]

author_sorts(val_separator) – возвращает строку, содержащую список значений сортировок автора для авторов книги. Сортировка - метаданные автора (отличающиеся от author_sort в книгах). Возвращаемый список имет формат author_sort1 val_separator author_sort2 и т.д. без добавляемых пробелов. Значения author_sort в этом списке в том же порядке, как и авторы в книге. Если вам нужны пробелы вокруг разделителя val_separator, включите их в строку val_separator.

booksize

class calibre.utils.formatter_functions.BuiltinBooksize[исходный код]

booksize() — возвращает значение поля size. Возвращает „“, если книга не имеет форматов.

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

connected_device_name

class calibre.utils.formatter_functions.BuiltinConnectedDeviceName[исходный код]

connected_device_name(storage_location_key) – если устройство подключено, вернуть имя устройства, в противном случае вернуть пустую строку. Каждое место хранения на устройстве может иметь другое имя. Названия storage_location_key - 'main', 'carda' и 'cardb'. Эта функция работает только в графическом интерфейсе.

connected_device_uuid

class calibre.utils.formatter_functions.BuiltinConnectedDeviceUUID[исходный код]

connected_device_uuid(storage_location_key) – если устройство подключено, вернуть uuid устройства (уникальный идентификатор), иначе вернуть пустую строку. У каждого места хранения на устройстве свой uuid. Названия локаций - 'main', 'carda' и 'cardb'. Эта функция работает только в графическом интерфейсе.

current_library_name

class calibre.utils.formatter_functions.BuiltinCurrentLibraryName[исходный код]

current_library_name() – возвращает последнее имя в пути к текущей библиотеке calibre.

current_library_path

class calibre.utils.formatter_functions.BuiltinCurrentLibraryPath[исходный код]

current_library_path() – возвращает полный путь к текущей библиотеке calibre.

current_virtual_library_name

class calibre.utils.formatter_functions.BuiltinCurrentVirtualLibraryName[исходный код]

current_virtual_library_name() – return the name of the current virtual library if there is one, otherwise the empty string. Library name case is preserved. Example:

program: current_virtual_library_name()

This function works only in the GUI.

Отображение документации на английском языке из-за ошибки FFML: Missing closing «`» for italics on line 4 in «current_virtual_library_name»

field

class calibre.utils.formatter_functions.BuiltinField[исходный код]

field(lookup_name) – returns the value of the metadata field with lookup name lookup_name. The $ prefix can be used instead of the function, as in $tags.

Отображение документации на английском языке из-за ошибки FFML: Missing closing «``» for CODE_TEXT on line 3 in «field»

formats_modtimes

class calibre.utils.formatter_functions.BuiltinFormatsModtimes[исходный код]

formats_modtimes(date_format_string) – возвращает разделённый запятыми список разделённых двоеточиями элементов FMT:DATE, представляющих собой время изменения форматов книги. Параметр date_format_string задаёт формат даты. Смотрите функцию format_date() для подробностей. Вы можете использовать функцию select() для получения времени модификации в заданном формате. Названия форматов всегда в верхнем регистре, как в EPUB.

formats_path_segments

class calibre.utils.formatter_functions.BuiltinFormatsPathSegments[исходный код]

formats_path_segments(with_author, with_title, with_format, with_ext, sep) – return parts of the path to a book format in the calibre library separated by sep. The parameter sep should usually be a slash ('/'). One use is to be sure that paths generated in Save to disk and Send to device templates are shortened consistently. Another is to be sure the paths on the device match the paths in the calibre library.

A book path consists of 3 segments: the author, the title including the calibre database id in parentheses, and the format (author - title). Calibre can shorten any of the three because of file name length limitations. You choose which segments to include by passing 1 for that segment. If you don’t want a segment then pass 0 or the empty string for that segment. For example, the following returns just the format name without the extension:

formats_path_segments(0, 0, 1, 0, '/')

Because there is only one segment the separator is ignored.

If there are multiple formats (multiple extensions) then one of the extensions will be picked at random. If you care about which extension is used then get the path without the extension then add the desired extension to it.

Examples: Assume there is a book in the calibre library with an epub format by Joe Blogs with title „Help“. It would have the path

Joe Blogs/Help - (calibre_id)/Help - Joe Blogs.epub

The following shows what is returned for various parameters:

- formats_path_segments(0, 0, 1, 0, '/') returns Help - Joe Blogs

- formats_path_segments(0, 0, 1, 1, '/') returns Help - Joe Blogs.epub

- formats_path_segments(1, 0, 1, 1, '/') returns Joe Blogs/Help - Joe Blogs.epub

- formats_path_segments(1, 0, 1, 0, '/') returns Joe Blogs/Help - Joe Blogs

- formats_path_segments(0, 1, 0, 0, '/') returns Help - (calibre_id)

formats_paths

class calibre.utils.formatter_functions.BuiltinFormatsPaths[исходный код]

formats_paths([separator]) – return a separator-separated list of colon-separated items FMT:PATH giving the full path to the formats of a book. The separator argument is optional. If not supplied then the separator is ', ' (comma space). If the separator is a comma then you can use the select() function to get the path for a specific format. Note that format names are always uppercase, as in EPUB.

formats_sizes

class calibre.utils.formatter_functions.BuiltinFormatsSizes[исходный код]

formats_sizes() – return a comma-separated list of colon-separated FMT:SIZE items giving the sizes of the formats of a book in bytes. You can use the select() function to get the size for a specific format. Note that format names are always uppercase, as in EPUB.

has_cover

class calibre.utils.formatter_functions.BuiltinHasCover[исходный код]

has_cover() – возвращает 'Yes', если у книги есть обложка, иначе возвращает пустую строку.

is_marked

class calibre.utils.formatter_functions.BuiltinIsMarked[исходный код]

is_marked() – проверить, помечена ли (marked) книга в calibre. Если это так, то вернуть значение метки, либо 'true'``(lower case), либо  список именованных меток, разделенных запятыми. Возврат ``'', если книга не отмечена. Эта функция работает только в графическом интерфейсе.

language_codes

class calibre.utils.formatter_functions.BuiltinLanguageCodes[исходный код]

language_codes(lang_strings) – возвращает коды языка language codes [https://www.loc.gov/standards/iso639-2/php/code_list.php] для строк, взятых из lang_strings. Строки должны быть на языке текущей локализации. lang_strings - это список, разделённый запятыми.

language_strings

class calibre.utils.formatter_functions.BuiltinLanguageStrings[исходный код]

language_strings(value, localize) – вернуть значение названий языков для языковых кодов (см. здесь имена и коды [https://www.loc.gov/standards/iso639-2/php/code_list.php]) , переданных в качестве значения value. Пример: {languages:language_strings()}. Если localize равно нулю, вернуть строки на английском языке. Если localize не равно нулю, вернуть строки на языке текущей локали. lang_codes - это список, разделённый запятыми.

ondevice

class calibre.utils.formatter_functions.BuiltinOndevice[исходный код]

ondevice() – возвращает строку 'Yes' если параметр ondevice установлен, иначе возвращает пустую строку. Эта функция работает только в графическом интерфейсе. Если вы желаете использовать её значение в шаблоне для сохранения на диск или отправки на устройство, то создайте пользовательский столбец типа Столбец, состоящий из других столбцов и используйте функцию в нём, а затем используйте значение этого столбца в ваших шаблонах сохранения/отправки.

raw_field

class calibre.utils.formatter_functions.BuiltinRawField[исходный код]

raw_field(lookup_name [, optional_default]) – возвращает поле метаданных с именем lookup_name без применения форматирования. Вычисляет и возвращает необязательный второй аргумент optional_default, если значение поля не определено (None). Префикс $$ может использоваться вместо функции, как в $$pubdate.

raw_list

class calibre.utils.formatter_functions.BuiltinRawList[исходный код]

raw_list(lookup_name, separator) – возвращает список метаданных с именем lookup_name без применения какого-либо форматирования или сортировки и с элементами, разделёнными разделителем separator.

series_sort

class calibre.utils.formatter_functions.BuiltinSeriesSort[исходный код]

series_sort() – возвращает сортировочное значение серии.

user_categories

class calibre.utils.formatter_functions.BuiltinUserCategories[исходный код]

user_categories() – возвращает разделённый запятыми список категорий пользователя, которые содержат эту книгу. Эта функция работает только в GUI. Если вы хотите использовать эти значения в шаблонах save-to-disk или send-to-device, вы должны создать пользовательский «Столбец, состоящий из других столбцов», использовать функцию в шаблоне этого столбца и использовать значение этого столбца в ваших шаблонах сохранения/отправки

virtual_libraries

class calibre.utils.formatter_functions.BuiltinVirtualLibraries[исходный код]

virtual_libraries() – возвращает разделённый запятыми список виртуальных библиотек, которые содержат эту книгу. Эта функция работает только в графическом интерфейсе. Если вы желаете использовать её значение в шаблоне для сохранения на диск или отправки на устройство, то нужно создать пользовательский столбец типа «Столбец, состоящий из других столбцов» и использовать функцию в нём, а затем использовать значение этого столбца в своих шаблонах сохранения/отправки.

Рекурсия

eval

class calibre.utils.formatter_functions.BuiltinEval[исходный код]

eval(string) – оценивает строку как программу, передавая локальные переменные. Это позволяет использовать шаблонный процессор для построения сложных результатов из локальных переменных. В Template Program Mode [https://manual.calibre-ebook.com/template_lang.html#more-complex-programs-in-template-expressions-template-program-mode], поскольку символы { и } интерпретируются до вычисления шаблона, вы должны использовать``[[`` для символа { и ]] для } . Они конвертируются автоматически. Также обратите внимание, что префиксы и суффиксы (синтаксис |prefix|suffix ) не могут использоваться в аргументе этой функции при использовании Template Program Mode.

template

class calibre.utils.formatter_functions.BuiltinTemplate[исходный код]

template(x) – обрабатывает x как шаблон. Сравнение выполняется в контексте, то есть переменные не передаются между вызовом и обработкой шаблона. Если не используется General Program Mode, поскольку символы { и } являются специальными, вы должны использовать [[ для символа { и ]] для символа }; они преобразуются автоматически. Например,``template('[[title_sort]]')`` вычислит шаблон {title_sort} и вернёт его значение. Обратите также внимание, что префиксы и суффиксы (синтаксис |prefix |suffix) не могут использоваться в аргументе этой функции при использовании template program mode.

Смена регистра

capitalize

class calibre.utils.formatter_functions.BuiltinCapitalize[исходный код]

capitalize(value) – возвращает значение, в котором первая буква в верхнем регистре, а остальные в нижнем регистре.

lowercase

class calibre.utils.formatter_functions.BuiltinLowercase[исходный код]

lowercase(value) – возвращает value в нижнем регистре.

titlecase

class calibre.utils.formatter_functions.BuiltinTitlecase[исходный код]

titlecase(value) – возвращает value в регистре ЗАГОЛОВКОВ.

uppercase

class calibre.utils.formatter_functions.BuiltinUppercase[исходный код]

uppercase(value) – возвращает value в верхнем регистре

Форматирование значений

finish_formatting

class calibre.utils.formatter_functions.BuiltinFinishFormatting[исходный код]

finish_formatting(value, format, prefix, suffix) – применяет format, prefix и suffix к value так же как в шаблонах finish_formatting(value, format, prefix, suffix) {series_index:05.2f| - |- }. Эта функция предусмотрена для упрощения преобразования сложных шаблонов с одной функцией или шаблонного программного режима в шаблоны GPM. Например, следующая программа выдает тот же результат, что и вышеупомянутый шаблон:

program: finish_formatting(field("series_index"), "05.2f", " - ", " - ")

Другой пример: для шаблона:

{series:re(([^\s])[^\s]+(\s|$),\1)}{series_index:0>2s| - | - }{title}

используйте:

program:
strcat(
re(field('series'), '([^\s])[^\s]+(\s|$)', '\1'),
finish_formatting(field('series_index'), '0>2s', ' - ', ' - '),
field('title')
)

format_date

class calibre.utils.formatter_functions.BuiltinFormatDate[исходный код]

format_date(value, format_string) – форматирует value, которое должно быть строкой даты, используя format_string, возвращающую строку. Лучше всего, если дата указана в формате ISO, поскольку использование других форматов даты часто приводит к ошибкам, поскольку фактическое значение даты не может быть определено однозначно. Обратите внимание, что функция format_date_field() работает быстрее и надёжнее.

Коды форматирования:

- d : день как число без начального нуля (1 до 31)

- dd : день как число с начальным нулём (01 до 31)

- ddd : локализованная аббревиатура названия дня (Напр. «Пн» до «Вскр»)

- dddd : полное локализованное название дня недели (напр. «Понедельник» до «Воскресенье»)

- M : номер месяца в году без начального нуля (1 to 12)

- MM : номер месяца в году с начальным нулём (01 to 12)

- MMM : сокращённое локализованное название месяца (напр. «Янв» до «Дек»)

- MMMM : полное локализованное название месяца (от «Январь» до «Декабрь»)

- yy : две последние цифры года (от 00 до 99

- yyyy : полный номер года из четырёх цифр

- h : часы без начального 0 (от 0 до 11 или от 0 до 23, в зависимости от am/pm (12/24) формата времени

- hh : часы с начальным 0 (от 00 до 11 или от 00 до 23, в зависимости от am/pm)

- m : минуты без начального 0 (от 0 до 59)

- mm : минуты с начальным 0 (00 to 59)

- s : секунды без начального 0 (0 to 59)

- ss : секунды с начальным 0 (00 to 59)

- ap : использовать 12-часовой формат вместо 24-часового, с «ap» заменённым на локализованную строку для am или pm

- AP : использовать 12-часовой формат вместо 24-часового, с «AP» заменённым на локализованную строку для AM или PM

- aP : использовать 12-часовой формат вместо 24-часового, с «aP» заменённым на локализованную строку для AM или PM

- Ap : использовать 12-часовой формат вместо 24-часового, с «Ap» заменённым на локализованную строку для AM или PM

- iso : дата, время и временная зона (должен быть единственным форматом)

- to_number : преобразовать дату & время в число с плаввающей запятой (timestamp)

- from_number : преобразуйте число с плавающей запятой (timestamp) в дату в формате ISO. Если вам нужен другой формат даты, добавьте желаемую строку форматирования после from_number и двоеточия (:). Пример:

format_date(val, 'from_number:MMM dd yyyy')

Вы можете получить неожиданные результаты, если форматируемая дата содержит локализованные названия месяцев, что может произойти, если вы изменили формат даты на содержащий``MMMM``. Используйте format_date_field() для предотвращения этой проблемы.

format_date_field

class calibre.utils.formatter_functions.BuiltinFormatDateField[исходный код]

format_date_field(field_name, format_string) – форматирует значение в поле field_name, которое должно быть искомым именем поля даты, стандартным или настраиваемым. См. format_date() для кодов форматирования. Эта функция намного быстрее, чем format_date(), и её следует использовать при форматировании значения в поле (столбце). Её нельзя использовать для вычисляемых дат или дат в строковых переменных. Примеры:

format_date_field('pubdate', 'yyyy.MM.dd')
format_date_field('#date_read', 'MMM dd, yyyy')

format_duration

class calibre.utils.formatter_functions.BuiltinFormatDuration[исходный код]

format_duration(value, template, [largest_unit]) – format the value, a number of seconds, into a string showing weeks, days, hours, minutes, and seconds. If the value is a float then it is rounded to the nearest integer. You choose how to format the value using a template consisting of value selectors surrounded by [ and ] characters. The selectors are:

- [w]: weeks

- [d]: days

- [h]: hours

- [m]: minutes

- [s]: seconds

You can put arbitrary text between selectors.

The following examples use a duration of 2 days (172,800 seconds) 1 hour (3,600 seconds) and 20 seconds, which totals to 176,420 seconds.

- format_duration(176420, '[d][h][m][s]') will return the value 2d 1h 0m 20s.

- format_duration(176420, '[h][m][s]') will return the value 49h 0m 20s.

- format_duration(176420, 'Your reading time is [d][h][m][s]') returns the value Your reading time is 49h 0m 20s.

- format_duration(176420, '[w][d][h][m][s]') will return the value 2d 1h 0m 20s. Note that the zero weeks value is not returned.

If you want to see zero values for items such as weeks in the above example, use an uppercase selector. For example, the following uses 'W' to show zero weeks:

format_duration(176420, '[W][d][h][m][s]') returns 0w 2d 1h 0m 20s.

By default the text following a value is the selector followed by a space. You can change that to whatever text you want. The format for a selector with your text is the selector followed by a colon followed by text segments separated by '|' characters. You must include any space characters you want in the output.

You can provide from one to three text segments.

- If you provide one segment, as in [w: weeks ] then that segment is used for all values.

- If you provide two segments, as in [w: weeks | week ] then the first segment is used for 0 and more than 1. The second segment is used for 1.

- If you provide three segments, as in [w: weeks | week | weeks ] then the first segment is used for 0, the second segment is used for 1, and the third segment is used for more than 1.

The second form is equivalent to the third form in many languages.

For example, the selector:

- [w: weeks | week | weeks ] produces '0 weeks ', '1 week ', or '2 weeks '.

- [w: weeks | week ] produces '0 weeks ', '1 week ', or '2 weeks '.

- [w: weeks ] produces 0 weeks ', 1 weeks ', or 2 weeks '.

The optional largest_unit parameter specifies the largest of weeks, days, hours, minutes, and seconds that will be produced by the template. It must be one of the value selectors. This can be useful to truncate a value.

format_duration(176420, '[h][m][s]', 'd') will return the value 1h 0m 20s instead of 49h 0m 20s.

format_number

class calibre.utils.formatter_functions.BuiltinFormatNumber[исходный код]

format_number(value, template) – interprets the value as a number and formats that number using a Python formatting template such as {0:5.2f} or {0:,d} or ${0:5,.2f}. The formatting template must begin with {0: and end with } as in the above examples. Exception: you can leave off the leading «{0:» and trailing «}» if the format template contains only a format. See the Template Language [https://manual.calibre-ebook.com/template_lang.html] and the Python [https://docs.python.org/3/library/string.html#formatstrings] documentation for more examples. Returns the empty string if formatting fails.

Отображение документации на английском языке из-за ошибки FFML: Missing closing «``» for CODE_TEXT on line 7 in «format_number»

human_readable

class calibre.utils.formatter_functions.BuiltinHumanReadable[исходный код]

human_readable(value) – ожидает, что значение будет числом, и возвращает строку, представляющую это число в КБ, МБ, ГБ и т. д.

rating_to_stars

class calibre.utils.formatter_functions.BuiltinRatingToStars[исходный код]

rating_to_stars(value, use_half_stars) – Возвращает рейтинг как value в виде строки, состоящей из звездочек (★“). Значение должно быть числом от 0 до 5. Задайте для use_half_stars значение 1, если вы хотите, чтобы символы половинной звезды для дробных чисел были доступны в настраиваемых столбцах рейтингов.

Функции URL

encode_for_url

class calibre.utils.formatter_functions.BuiltinEncodeForURL[исходный код]

encode_for_url(value, use_plus) – возвращает значение value закодированное для использования в URL, как указанно с use_plus. Значение URL-кодированное. Далее, если use_plus равно 0, то пробелы заменяются знаками '+' (плюс). Если это 1, то пробелы заменяются %20.

Если вы не хотите, чтобы значение было кодированным, но чтобы пробелы были заменены, используйте функцию re() , как в re ($ series, ' ', '%20')

Смотрите также функции make_url(), make_url_extended() и query_string().

make_url

class calibre.utils.formatter_functions.BuiltinMakeUrl[исходный код]

make_url(path, [query_name, query_value]+) – эта функция - простейший способ создания URL-адреса запроса. Она использует path, веб-сайт и страницу, которые вы хотите запросить и пары query_name, query_value, из которых строится запрос. Как правило, query_value должно быть закодировано в URL. С помощью этой функции оно всегда закодировано, пробелы всегда заменяются знаками „+“.

Должна быть предоставлена хотя бы одна пара query_name, query_value.

Пример: конструирование поискового URL-запроса в Wikipedia для автора Niccolò Machiavelli:

make_url('https://en.wikipedia.org/w/index.php', 'search', 'Niccolò Machiavelli')

returns

https://en.wikipedia.org/w/index.php?search=Niccol%C3%B2+Machiavelli

Если вы пишете пользовательский URL-шаблон сведений о книге столбцов, используйте $item_name или field('item_name') чтобы получить значение поля, на которое был нажат клик. Пример: если Niccolò Machiavelli было нажато затем можно сконструировать запрос URL:

make_url('https://en.wikipedia.org/w/index.php', 'search', $item_name)

См. функции make_url_extended(), query_string() и encode_for_url().

make_url_extended

class calibre.utils.formatter_functions.BuiltinMakeUrlExtended[исходный код]

make_url_extended(...) – эта функция аналогична make_url() но дает вам больше контроля над компонентами URL. Компонентами URL являются
scheme:://authority/path?query string.

См. Uniform Resource Locator [https://en.wikipedia.org/wiki/URL] в Wikipedia.

У функции два варианта:

make_url_extended(scheme, authority, path, [query_name, query_value]+)

и

make_url_extended(scheme, authority, path, query_string)

Эта функция возвращает URL-адрес, созданный на основе scheme, authority, path, и либо query_string либо строки запроса, построенной из пар аргументов запроса. authority можен быть пустым, в случае calibre scheme URL. Вы должны указать либо query_string, либо хотя бы одну пару query_name, query_value. Если вы укажете query_string и оно будет пустым, то результирующий URL-адрес не будет содержать раздела строки запроса.

Пример 1: constructing a Wikipedia search URL for the author Niccolò Machiavelli:

make_url_extended('https', 'en.wikipedia.org', '/w/index.php', 'search', 'Niccolò Machiavelli')

returns

https://en.wikipedia.org/w/index.php?search=Niccol%C3%B2+Machiavelli

См. функцию query_string() для примера ипользующего make_url_extended() с query_string.

Если вы создаёте пользовательский шаблон URL-адреса сведений о столбцах книги, используйте $item_name или field ('item_name'), чтобы получить значение нажатого поля. Пример: если был нажат Niccolò Machiavelli, то вы можете создать URL-адрес, используя :

make_url_extended('https', 'en.wikipedia.org', '/w/index.php', 'search', $item_name')

См. функции make_url(), query_string() and encode_for_url().

query_string

class calibre.utils.formatter_functions.BuiltinQueryString[исходный код]

query_string([query_name, query_value, how_to_encode]+)– возвращает строку запроса URL сконструированную из триад query_name, query_value, how_to_encode. Строка запроса это серия элементов выглядящих как query_name=query_value, где query_value это URL-кодирование. Элементы запроса разделяются символами '&' (ampersand).

Если how_to_encode это 0 затем query_value это кодированное и пробелы заменяются символом '+' (plus). Если how_to_encode это 1 затем query_value это кодированное с пробелами заменёнными на %20. Если how_to_encode это 2 затем query_value возвращается неизменным; нет кодирования и пробелы не заменяются. Если вы хотите чтобы query_value не кодировалась но пробелы заменялись используйте re() function, как в re($series, ' ', '%20')

Вы используете эту функцию, если вам нужен конкретный контроль над тем, как создаются части строки запроса . Затем вы могли бы использовать результирующую строку запроса в make_url_extended(), as in

make_url_extended(
'https', 'your_host', 'your_path',
query_string('encoded', 'Hendrik Bäßler', 0, 'unencoded', 'Hendrik Bäßler', 2))

giving you

https://your_host/your_path?encoded=Hendrik+B%C3%A4%C3%9Fler&unencoded=Hendrik Bäßler

У вас должен быть хотя бы одна триада query_name, query_value, how_to_encode, но можете использовать их столько, сколько пожелаете.

Возвращаемое значение представляет собой строку запроса URL со всеми указанными элементами, например: name1=val1[&nameN=valN]*. Заметьте что '?' path / query string разделитель не включён в возвращаемый результат.

Если вы пишете пользовательский URL-шаблон сведений о столбцах книги, используйте $item_name or field('item_name') чтобы получить некодированное значение поля, по которому был произведен щелчок. У вас также есть item_value_quoted, где значение уже закодировано со знаками плюс замена пробелов и item_value_no_plus, где значение уже закодировано с заменой пробелов %20.

См. функции make_url(), make_url_extended() и encode_for_url().

to_hex

class calibre.utils.formatter_functions.BuiltinToHex[исходный код]

to_hex(val) – возвращает строку val, закодированную в шестнадцатеричном формате. Полезно при создании URL-ов calibre.

urls_from_identifiers

class calibre.utils.formatter_functions.BuiltinUrlsFromIdentifiers[исходный код]

urls_from_identifiers(identifiers, sort_results) – задаёт список идентификаторов, разделённых запятыми, где identifier (идентификато)р представляет собой пару значений, разделённых двоеточиями (id_name:id_value), возвращает список URL-адресов HTML, разделённых запятыми, сгенерированных из идентификаторов. Список не отсортирован, если sort_results равен 0 (символ или число), в противном случае он сортируется в алфавитном порядке по имени идентификатора. URL-адреса генерируются так же, как столбец встроенных идентификаторов, когда они отображаются в Подробности книги.

Функции базы данных

book_count

class calibre.utils.formatter_functions.BuiltinBookCount[исходный код]

book_count(query, use_vl) – возвращает количество книг, найденных с помощью поиска для query. Если use_vl равно 0 (нулю), то виртуальные библиотеки игнорируются. Эта функция и её компаньон book_values() особенно полезны в поиске по шаблону, поддерживающем поиск, объединяющий информацию из многих книг, например, поиск серий только по одной книге. Его нельзя использовать в составных столбцах если для настройки allow_template_database_functions_in_composites не задано значение True. Можно использовать только в GUI. Например, этот поиск по шаблону использует эту функцию и её дополнение для поиска всех серий, содержащих только одну книгу:

- Определить хранимый шаблон (используя Preferences → Advanced → Template functions) названный series_only_one_book (имя - произвольное). Шаблон:

program:
vals = globals(vals='');
if !vals then
all_series = book_values('series', 'series:true', ',', 0);
for series in all_series:
if book_count('series:="' & series & '"', 0) == 1 then
vals = list_join(',', vals, ',', series, ',')
fi
rof;
set_globals(vals)
fi;
str_in_list(vals, ',', $series, 1, '')

При первом запуске шаблона (первая проверенная книга) он сохраняет результаты поиска в базе данных в global глобальной переменной шаблона с именем vals. Эти результаты используются для проверки последующих книг без повторного поиска.

- Использовать сохранённый шаблон при поиске по шаблону:

template:"program: series_only_one_book()#@#:n:1"

Использование сохранённого шаблона вместо помещения шаблона в поиск устраняет проблемы, вызванные требованием экранировать кавычки в поисковом выражении.

Функция используется только в GUI и контент-сервере.

book_values

class calibre.utils.formatter_functions.BuiltinBookValues[исходный код]

book_values(column, query, sep, use_vl) – возвращает список уникальных значений, содержащихся в столбце column (имя поиска), разделенных sep, в найденных книгах путем поиска query. Если use_vl равно 0 (ноль), тогда виртуальные библиотеки игнорируются. Эта функция и её компаньон book_count() особенно полезны при поиске по шаблону, поддерживая поиск, объединяющий информацию из многих книг, например, поиск серий только с одной книгой. Нельзя использовать в составных столбцах, если для настройки allow_template_database_functions_in_composites установлено значение True. Можно использовать только в GUI и контент-сервере.

extra_file_modtime

class calibre.utils.formatter_functions.BuiltinExtraFileModtime[исходный код]

extra_file_modtime(file_name, format_spec) – возвращает время модификации дополнительного файла file_name в папке data/ книги, если он существует, иначе -1. Modtime форматируется в соответствии с format_spec (см. format_date()). Если format_string пусто, возвращает modtime как количество секунд с плавающей запятой с начала эпохи. См. также функции has_extra_files(), extra_file_names() и extra_file_size(). Эпоха зависит от ОС. Эту функцию можно использовать только в GUI и контент-сервере.

extra_file_names

class calibre.utils.formatter_functions.BuiltinExtraFileNames[исходный код]

extra_file_names(sep [, pattern]) – возвращает разделенный сепараторами (sep) список дополнительных файлов в папке книги data/. Если указан необязательный параметр pattern, регулярное выражение, то список фильтруется для файлов, соответствующих pattern. Сопоставление с pattern нечувствительно к регистру. См. также функции has_extra_files(), extra_file_modtime() и extra_file_size(). Эту функцию можно использовать только в GUI и контент-сервере.

extra_file_size

class calibre.utils.formatter_functions.BuiltinExtraFileSize[исходный код]

extra_file_size(file_name) – возвращает размер в байтах дополнительного файла file_name в папке data/ книги, если он существует, иначе -1. См. также функции has_extra_files(), extra_file_names() и extra_file_modtime(). Эту функцию можно использовать только в GUI и контент-сервере.

get_link

class calibre.utils.formatter_functions.BuiltinGetLink[исходный код]

get_link(field_name, field_value) – извлечь ссылку для поля field_name со значением field_value. Если прикрепленной ссылки нет, вернуть пустую строку. Примеры:

- Следующее возвращает ссылку, прикрепленную к тегу Fiction:

get_link('tags', 'Fiction')

- Этот шаблон создаёт список ссылок для всех тегов, связанных с книгой в форме value:link, ...:

program:
ans = '';
for t in $tags:
l = get_link('tags', t);
if l then
ans = list_join(', ', ans, ',', t & ':' & get_link('tags', t), ',')
fi
rof;
ans

Эта функция работает только в графическом интерфейсе и на сервере контента.

get_note

class calibre.utils.formatter_functions.BuiltinGetNote[исходный код]

get_note(field_name, field_value, plain_text) – получить примечание для поля field_name со значением field_value. Если plain_text пустой, вернуть HTML примечания. Если „plain_text“ 1 (или '1'), вернуть простой текст примечания. Если примечание не существует, вернуть „“ (пустая строка) в обоих случаях. Пример:

has_extra_files

class calibre.utils.formatter_functions.BuiltinHasExtraFiles[исходный код]

has_extra_files([pattern]) – возвращает количество дополнительных файлов, иначе „“ (пустая строка). Если указан необязательный параметр pattern (регулярное выражение), то список фильтруется по файлам, которые соответствуют pattern, прежде чем файлы будут подсчитаны. Сопоставление с pattern нечувствительно к регистру. См. также функции extra_file_names(), extra_file_size() и extra_file_modtime(). Можно использовать только в GUI и контент-сервере.

has_note

class calibre.utils.formatter_functions.BuiltinHasNote[исходный код]

has_note(field_name, field_value). Проверьте, есть ли в поле примечание. Эта функция имеет два варианта:

- если field_value не '' (пустая строка) вернуть '1' если значение field_value в поле field_name имеет примечание, иначе ''. Если field_value равно '', то вернуть список значений в field_name у которых есть примечание. Если ни у одного элемента в поле нет примечания, вернуть ''. Этот вариант полезен для отображения значков столбцов, если какое-либо значение в поле имеет примечание, а не конкретное значение. Пример: has_note('authors', '') возвращает список авторов имеющих примечания или '' если нет авторов с примечаниями.

Вы можете проверить, все ли значения в field_name имеют примечание, сравнив длину списка возвращаемого значения этой функции с длиной списка значений в field_name. Пример:

list_count(has_note('authors', ''), '&') ==# list_count_field('authors')

Можно использовать только в GUI и контент-сервере.

Функции даты

date_arithmetic

class calibre.utils.formatter_functions.BuiltinDateArithmetic[исходный код]

„“date_arithmetic(value, calc_spec, fmt)““ – вычислить новую дату от value с помощью calc_spec. Вернуть новую дату, отформатированную в соответствии с необязательным fmt: если не указано, то результат будет в формате ISO. Строка calc_spec - конкатенация пар „vW“ (valueWhat), где v - возможно отрицательное число, а W - одна из следующих букв:

- s: добавить v добавить к date

- m: добавить v минут к date

- h: добавить v часов к date

- d: добавить v дней к date

- w: добавить v недель к date

- y: добавить v лет к date, где год это 365 дней.

Пример: '1s3d-1m' добавит 1 секунду, прибавит 3 дня и вычтет 1 минуту из date.

days_between

class calibre.utils.formatter_functions.BuiltinDaysBetween[исходный код]

days_between(date1, date2) – возвращает количество дней между date1 и date2. Число положительно, если date1 больше date2, в противном случае отрицательно. Если date1 или date2 не являются датой, функция возвращает пустую строку.

today

class calibre.utils.formatter_functions.BuiltinToday[исходный код]

today() – возвращает текущий день в виде строки. Это значение предназначено для использования в format_date или days_between, но можно использовать как и любую другую строку. Дата в формате ISO [https://en.wikipedia.org/wiki/ISO_8601]

API of the Metadata objects

The python implementation of the template functions is passed in a Metadata object. Knowing it’s API is useful if you want to define your own template functions.

class calibre.ebooks.metadata.book.base.Metadata(title, authors=('Неизвестный',), other=None, template_cache=None, formatter=None)[исходный код]

A class representing all the metadata for a book. The various standard metadata fields are available as attributes of this object. You can also stick arbitrary attributes onto this object.

Metadata from custom columns should be accessed via the get() method, passing in the lookup name for the column, for example: «#mytags».

Use the is_null() method to test if a field is null.

This object also has functions to format fields into strings.

The list of standard metadata fields grows with time is in STANDARD_METADATA_FIELDS.

Please keep the method based API of this class to a minimum. Every method becomes a reserved field name.

is_null(field)[исходный код]

Return True if the value of field is null in this object.
„null“ means it is unknown or evaluates to False. So a title of
_(„Unknown“) is null or a language of „und“ is null.

Be careful with numeric fields since this will return True for zero as well as None.

Also returns True if the field does not exist.

deepcopy(class_generator=<function Metadata.<lambda>>)[исходный код]

Do not use this method unless you know what you are doing, if you want to create a simple clone of this object, use deepcopy_metadata() instead. Class_generator must be a function that returns an instance of Metadata or a subclass of it.

get_identifiers()[исходный код]

Return a copy of the identifiers dictionary.
The dict is small, and the penalty for using a reference where a copy is needed is large. Also, we don’t want any manipulations of the returned dict to show up in the book.

set_identifiers(identifiers)[исходный код]

Set all identifiers. Note that if you previously set ISBN, calling this method will delete it.

set_identifier(typ, val)[исходный код]

If val is empty, deletes identifier of type typ

standard_field_keys()[исходный код]

return a list of all possible keys, even if this book doesn’t have them

custom_field_keys()[исходный код]

return a list of the custom fields in this book

all_field_keys()[исходный код]

All field keys known by this instance, even if their value is None

metadata_for_field(key)[исходный код]

return metadata describing a standard or custom field.

all_non_none_fields()[исходный код]

Return a dictionary containing all non-None metadata fields, including the custom ones.

get_standard_metadata(field, make_copy)[исходный код]

return field metadata from the field if it is there. Otherwise return
None. field is the key name, not the label. Return a copy if requested, just in case the user wants to change values in the dict.

get_all_standard_metadata(make_copy)[исходный код]

return a dict containing all the standard field metadata associated with the book.

get_all_user_metadata(make_copy)[исходный код]

return a dict containing all the custom field metadata associated with the book.

get_user_metadata(field, make_copy)[исходный код]

return field metadata from the object if it is there. Otherwise return
None. field is the key name, not the label. Return a copy if requested, just in case the user wants to change values in the dict.

set_all_user_metadata(metadata)[исходный код]

store custom field metadata into the object. Field is the key name not the label

set_user_metadata(field, metadata)[исходный код]

store custom field metadata for one column into the object. Field is the key name not the label

remove_stale_user_metadata(other_mi)[исходный код]

Remove user metadata keys (custom column keys) if they don’t exist in „other_mi“, which must be a metadata object

template_to_attribute(other, ops)[исходный код]

Takes a list [(src,dest), (src,dest)], evaluates the template in the context of other, then copies the result to self[dest]. This is on a best-efforts basis. Some assignments can make no sense.

smart_update(other, replace_metadata=False)[исходный код]

Merge the information in other into self. In case of conflicts, the information in other takes precedence, unless the information in other is NULL.

format_field(key, series_with_index=True)[исходный код]

Returns the tuple (display_name, formatted_value)

to_html()[исходный код]

A HTML representation of this object.

calibre.ebooks.metadata.book.base.STANDARD_METADATA_FIELDS

The set of standard metadata fields.

'''
All fields must have a NULL value represented as None for simple types, an empty list/dictionary for complex types and (None, None) for cover_data
'''

SOCIAL_METADATA_FIELDS = frozenset((
'tags', # Ordered list
'rating', # A floating point number between 0 and 10
'comments', # A simple HTML enabled string
'series', # A simple string
'series_index', # A floating point number
# Of the form { scheme1:value1, scheme2:value2}
# For example: {'isbn':'123456789', 'doi':'xxxx', ... }
'identifiers',
))

'''
The list of names that convert to identifiers when in get and set.
'''

TOP_LEVEL_IDENTIFIERS = frozenset((
'isbn',
))

PUBLICATION_METADATA_FIELDS = frozenset((
'title', # title must never be None. Should be _('Unknown')
# Pseudo field that can be set, but if not set is auto generated
# from title and languages
'title_sort',
'authors', # Ordered list. Must never be None, can be [_('Unknown')]
'author_sort_map', # Map of sort strings for each author
# Pseudo field that can be set, but if not set is auto generated
# from authors and languages
'author_sort',
'book_producer',
'timestamp', # Dates and times must be timezone aware
'pubdate',
'last_modified',
'rights',
# So far only known publication type is periodical:calibre
# If None, means book
'publication_type',
'uuid', # A UUID usually of type 4
'languages', # ordered list of languages in this publication
'publisher', # Simple string, no special semantics
# Absolute path to image file encoded in filesystem_encoding
'cover',
# Of the form (format, data) where format is, e.g. 'jpeg', 'png', 'gif'...
'cover_data',
# Either thumbnail data, or an object with the attribute
# image_path which is the path to an image file, encoded
# in filesystem_encoding
'thumbnail',
))

BOOK_STRUCTURE_FIELDS = frozenset((
# These are used by code, Null values are None.
'toc', 'spine', 'guide', 'manifest',
))

USER_METADATA_FIELDS = frozenset((
# A dict of dicts similar to field_metadata. Each field description dict
# also contains a value field with the key #value#.
'user_metadata',
))

DEVICE_METADATA_FIELDS = frozenset((
'device_collections', # Ordered list of strings
'lpath', # Unicode, / separated
'size', # In bytes
'mime', # Mimetype of the book file being represented
))

CALIBRE_METADATA_FIELDS = frozenset((
'application_id', # An application id, currently set to the db_id.
'db_id', # the calibre primary key of the item.
'formats', # list of formats (extensions) for this book
# a dict of user category names, where the value is a list of item names
# from the book that are in that category
'user_categories',
# a dict of items to associated hyperlink
'link_maps',
))

ALL_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union(
PUBLICATION_METADATA_FIELDS).union(
BOOK_STRUCTURE_FIELDS).union(
USER_METADATA_FIELDS).union(
DEVICE_METADATA_FIELDS).union(
CALIBRE_METADATA_FIELDS)

# All fields except custom fields
STANDARD_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union(
PUBLICATION_METADATA_FIELDS).union(
BOOK_STRUCTURE_FIELDS).union(
DEVICE_METADATA_FIELDS).union(
CALIBRE_METADATA_FIELDS)

# Metadata fields that smart update must do special processing to copy.
SC_FIELDS_NOT_COPIED = frozenset(('title', 'title_sort', 'authors',
'author_sort', 'author_sort_map',
'cover_data', 'tags', 'languages',
'identifiers'))

# Metadata fields that smart update should copy only if the source is not None
SC_FIELDS_COPY_NOT_NULL = frozenset(('device_collections', 'lpath', 'size', 'comments', 'thumbnail'))

# Metadata fields that smart update should copy without special handling
SC_COPYABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
PUBLICATION_METADATA_FIELDS).union(
BOOK_STRUCTURE_FIELDS).union(
DEVICE_METADATA_FIELDS).union(
CALIBRE_METADATA_FIELDS) - \ SC_FIELDS_NOT_COPIED.union(
SC_FIELDS_COPY_NOT_NULL)

SERIALIZABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
USER_METADATA_FIELDS).union(
PUBLICATION_METADATA_FIELDS).union(
CALIBRE_METADATA_FIELDS).union(
DEVICE_METADATA_FIELDS) - \ frozenset(('device_collections', 'formats',
'cover_data'))
# these are rebuilt when needed

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Руководства »

- Всё об использования регулярных выражений в calibre

Всё об использования регулярных выражений в calibre

Регулярные выражения - это функции, используемые во многих местах в calibre для выполнения сложных манипуляций с содержимым электронной книги и метаданными. Этот учебник представляет собой краткое введение в использование регулярных выражений calibre.

Содержание

- Во-первых, слово предупреждения и слово мужества

- Где в calibre вы можете использовать регулярные выражения?

- Что на земле является регулярным выражением?

- Хотите объяснений?

- Это звучит не так уж плохо. Что дальше?

- Эй, аккуратно! Это начинает иметь смысл!

- Ну, эти специальные символы очень аккуратные и все такое, но что, если я хочу сопоставить точку или знак вопроса?

- Итак, какие наборы наиболее полезны?

- Но если бы у меня было несколько переменных строк, которые я хотел бы сопоставить, все усложнялось?

- Вы пропустили…

- В начале вы сказали, что есть способ сделать регулярное выражение нечувствительным к регистру?

- Я думаю, что начинаю понимать эти регулярные выражения сейчас … как я могу использовать их в calibre?

- Конвертации

- Добавление книг

- Массовое редактирование метаданных

- Краткая справка

- Признательность

Во-первых, слово предупреждения и слово мужества

Это неизбежно будет чем-то техническим, ведь регулярные выражения - это технический инструмент для выполнения технических задач. Мне придется использовать жаргон и понятия, которые могут показаться сложными или запутанными. Я попытаюсь объяснить эти концепции настолько ясно, насколько смогу, но на самом деле не могу обойтись без их использования. Это, как говорится, не обескураживайся никаким жаргоном, так как я пытался объяснить все новое. И хотя регулярные выражения сами по себе могут показаться тайной, черной магией (или, если быть более прозаичным, случайной цепочкой букв и знаков мумбо-юмбо), я обещаю, что они не так уж и сложны. Даже те, кто действительно хорошо понимает регулярные выражения, испытывают затруднения при чтении более сложных, но написание их не так сложно - вы строите выражение шаг за шагом. Итак, сделайте шаг и следуйте за мной в кроличью нору.

Где в calibre вы можете использовать регулярные выражения?

Есть несколько мест, где calibre использует регулярные выражения. В опциях конвертации есть :guilabel:«Поиск и замена», обнаружение метаданных по именам файлов в настройках импорта и «Поиск и замена» при массовом редактировании метаданных книг. Редактор книг calibre также может использовать регулярные выражения при поиске и замене. Наконец, вы можете использовать регулярные выражения при поиске в списке книг и при поиске в просмотрщике электронных книг.

Что на земле является регулярным выражением?

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

Примечание

Я использую здесь строку в том смысле, в каком она используется в языках программирования: строка из одного или нескольких символов, символов, включая действительные символы, цифры, знаки препинания и так называемые пробелы (переносы строк, табуляторы и т. д.). Обратите внимание, что обычно прописные и строчные буквы не считаются одинаковыми, поэтому «а» - это символ, отличный от «А» и т. д. В calibre регулярные выражения не чувствительны к регистру в строке поиска, но не в параметрах преобразования. Есть способ сделать каждое регулярное выражение нечувствительным к регистру, но мы обсудим это позже. Это усложняется тем, что регулярные выражения допускают изменения в соответствующих им строках, поэтому одно выражение может соответствовать нескольким строкам, поэтому люди вообще не хотят их использовать. Подробнее об этом чуть позже.

Хотите объяснений?

Ну, вот почему мы здесь. Во-первых, это самая важная концепция в регулярных выражениях: Сама строка является регулярным выражением, которое соответствует самому себе. То есть, если бы я хотел сопоставить строку "Hello, World!" С помощью регулярного выражения, используемое регулярное выражение было бы Hello, World!. И да, это действительно так просто. Однако заметьте, что это соответствует только точной строке "Hello, World!", а не, например. "Hello, wOrld!" или ``»hello, world!» `` или любому другому подобному варианту.

Это звучит не так уж плохо. Что дальше?

Следующее - начало действительно хороших вещей. Помните, где я говорил, что регулярные выражения могут соответствовать нескольким строкам? Здесь все становится немного сложнее. Скажем, в качестве более практичного упражнения электронная книга, которую вы хотели преобразовать, имела неприятный нижний колонтитул, подсчитывающий страницы, например «Страница 5 из 423». Очевидно, что номер страницы увеличится с 1 до 423, поэтому вам нужно будет сопоставить 423 различных строки, верно? Неправильно, на самом деле: регулярные выражения позволяют вам определять наборы символов, которые соответствуют: Чтобы определить набор, вы помещаете все символы, в набор в квадратных скобках. Так, например, набор [abc] будет соответствовать символу «a», «b» или «c». * Наборы всегда будут соответствовать только одному из символов в наборе *. Они «понимают» диапазоны символов, то есть, если вы хотите сопоставить все символы нижнего регистра, вы должны использовать набор [az] для символов нижнего и верхнего регистра, которые вы используете [a-zA -Z] и так далее. Есть идея? Таким образом, очевидно, что с помощью выражения Page [0-9] of 423 вы сможете сопоставить первые 9 страниц, сократив количество выражений до трех: Второе выражение Page [0-9] [0-9] of 423 будет соответствовать всем двузначным номерам страниц, и я уверен, что вы можете догадаться, как будет выглядеть третье выражение. Да, начинайте. Запишите это

Эй, аккуратно! Это начинает иметь смысл!

Я надеялся, что вы скажете это. Но приготовьтесь, теперь становится ещё лучше! Мы только что увидели, что используя наборы, мы можем сопоставить один из нескольких символов одновременно. Но вы даже можете повторить символ или набор, сократив число выражений, необходимых для обработки приведенного выше примера с номером страницы, до одного. Да, ОДИН! Вы в восторге? А должны быть! Это работает так: некоторые так называемые специальные символы «+», «?» и «*», повторяют единственный элемент, предшествующий им. (Элемент означает либо один символ, набор символов, escape-последовательность или группу (мы узнаем о последних двух позже), короче говоря, любой отдельный объект в регулярном выражении.) Эти символы называются подстановочными знаками или квантификаторами. Чтобы быть более точным, «?» соответствует 0 или 1 предыдущего элемента, «*» соответствует 0 или более предыдущего элемента, а «+» соответствует 1 или более предыдущего элемента. Несколько примеров: выражение a? будет соответствовать либо «» (это пустая строка, в данном случае не совсем полезная), либо «a», выражение a* будет соответствовать «», «a», «aa» или любому количеству a в строке, и, наконец, выражение a+ будет соответствовать «a», «aa» или любому количеству a в строке (Примечание: это не будет соответствует пустой строке!). То же самое для множеств: выражение [0-9]+ будет соответствовать каждому целому числу! Я знаю, о чем вы думаете, и вы правы: если вы используете это в приведенном выше случае совпадения номеров страниц, разве это не будет единственным выражением, которое будет соответствовать всем номерам страниц? Да, выражение Page [0-9]+ of 423 будет соответствовать каждому номеру страницы в этой книге!

Примечание

Примечание к этим квантификаторам: они обычно пытаются найти как можно больше текста, поэтому будьте осторожны при их использовании. Это называется «жадным поведением» - я уверен, вы понимаете, почему. Это становится проблематичным, когда вы, скажем, пытаетесь сопоставить тег. Рассмотрим, например, строку "<p class="calibre2">Title here</p>" и, допустим, вы хотите сопоставить открывающий тег (часть между первой парой угловых скобок, чуть подробнее о тегах позже). Можно подумать, что выражение <p.*> Будет соответствовать этому тегу, но на самом деле оно соответствует всей строке! (Символ «.» является другим специальным символом. Он соответствует чему угодно кроме переносов строк, поэтому, по сути, выражение .* будет соответствовать любой отдельной строке, о которой вы можете подумать.) Вместо этого попробуйте использовать <p.*?>, что делает квантификатор "*" не жадным. Это выражение будет соответствовать только первому открывающему тегу, как и предполагалось. На самом деле есть еще один способ сделать это: выражение <p[^>]*> будет соответствовать тому же открывающему тегу - вы поймете почему после следующего раздела. Просто отметьте, что довольно часто существует несколько способов написания регулярного выражения.

Ну, эти специальные символы очень аккуратные и все такое, но что, если я хочу сопоставить точку или знак вопроса?

Конечно, вы можете сделать это: просто поставьте обратную косую черту перед любым специальным символом, и он будет интерпретирован как буквальный символ без какого-либо специального значения. Эта пара с обратной косой чертой, за которой следует один символ, называется escape-последовательностью, а действие по добавлению обратной косой черты перед специальным символом называется экранированием этого символа. Экранирующая последовательность интерпретируется как отдельный элемент. Конечно, существуют escape-последовательности, которые делают больше, чем просто экранирование специальных символов, например, "\t" означает табулятор. Мы вернемся к некоторым escape-последовательностям позже. Да, и, кстати, в отношении этих специальных символов: рассмотрите любой символ, который мы обсуждаем в этом введении, как имеющий некоторую функцию, которая должна быть особенной и, следовательно, должна быть экранирована, если вы хотите буквальный символ.

Итак, какие наборы наиболее полезны?

Знал, что ты спросишь. Некоторые полезные наборы: [0-9] соответствует одному числу, [az] соответствует одной строчной букве, [AZ] соответствует одной заглавной букве, [a-zA-Z] сопоставление одной буквы и [a-zA-Z0-9] сопоставление одной буквы или числа. Также можете использовать escape-последовательность в качестве сокращения:

\d

эквивалентно [0-9]

\w

эквивалентно [a-zA-Z0-9_]

\s

эквивалентно любому пробелу

Примечание

«Пробел» (Whitespace) - это термин для всего, что не будет напечатано. Эти символы включают пробел (space), табулятор (tabulator), перевод строки (line feed), перевод формы (form feed), возврат каретки (carriage return), неразрывные пробелы (non-breaking spaces) и т.п.

Примечание

Наборы верхнего и нижнего регистра могут соответствовать как верхнему, так и нижнему регистру, если включена настройка, позволяющая сделать поиск нечувствительным к регистру. Такие настройки находятся, например, в Настройки-Поиск в самом calibre и на панели Поиска в calibre Просмотрщик электронных книг, а также в инструменте calibre Редактировать книгу.

В качестве последней заметки о наборах вы также можете определить набор как любой символ, но из набора. Вы делаете это путем включения символа "^" в качестве самого первого символа в наборе. Таким образом, [^a] будет соответствовать любому символу, кроме «a». Это называется дополнением набора. Те сокращенные последовательности escape-последовательностей, которые мы видели ранее, также могут быть дополнены: "\D" означает любой нечисловой символ, что эквивалентно [^0-9]. Другие сокращения можно дополнить, как вы уже догадались, используя соответствующую заглавную букву вместо строчной. Итак, возвращаясь к примеру <p[^>]*> из предыдущего раздела, теперь вы можете видеть, что используемый им набор символов пытается соответствовать любому символу, кроме закрывающей угловой скобки.

Но если бы у меня было несколько переменных строк, которые я хотел бы сопоставить, все усложнялось?

Не бойся, жизнь всё ещё хороша и легка. Рассмотрим пример: в книге, которую вы конвертируете, на каждой нечетной странице написано «Title», а на каждой четной странице - «Author». Отлично смотрится в печати, верно? Но в электронных книгах это раздражает. Вы можете сгруппировать целые выражения в нормальных скобках, и символ "|" позволит вам сопоставить либо выражение справа, либо слева. Объедините их, и всё готово. Слишком быстро для тебя? Хорошо, во-первых, мы группируем выражения для нечетных и четных страниц, получая, таким образом, (Title)(Author) как два наших необходимых выражения. Теперь мы упростили ситуацию с помощью вертикальной черты ("|" называется символом вертикальной черты): если вы используете выражение (Title|Author), вы либо получите совпадение для «Title» (на нечетных страницах) или вы должны соответствовать «Author» (на четных страницах). Ну, разве не просто?

Конечно, вы можете использовать вертикальную черту, не используя групповые скобки. Помните, когда я говорил, что квантификаторы повторяют элемент, предшествующий им? Вертикальная черта работает немного по-другому: выражение «Title|Author» также будет соответствовать либо строке «Title», либо строке «Author», как в приведенном выше примере с использованием группировки. Вертикальная черта выбирает во всём выражении, предшествующее и следующее за ней. Итак, если вы хотите сопоставить строки «Calibre» и «calibre» и хотите выбирать только между прописными и строчными буквами «c», вам нужно использовать выражение (c|C)alibre, где группировка гарантирует, что будет выбрана только буква «с». Если бы вы использовали c|Calibre, вы бы получили совпадение в строке «c» или в строке «Calibre», что не то, что мы хотели. Вкратце: если есть сомнения, используйте группировку вместе с вертикальной чертой.

Вы пропустили…

… подождите минутку, есть еще одна действительно полезная вещь, которую вы можете сделать с группами. Если у вас есть группа, которую вы ранее сопоставляли, вы можете использовать ссылки на эту группу позже в выражении: Группы нумеруются, начиная с 1, и вы ссылаетесь на них, экранируя номер группы, на которую хотите сослаться, таким образом, пятая группа будет ссылаться как \5. Итак, если вы искали ([^]+)\1 в строке «Test Test», вы бы соответствовали всей строке!

В начале вы сказали, что есть способ сделать регулярное выражение нечувствительным к регистру?

Да, спасибо, что уделили мне внимание и напомнили. Вы можете сказать calibre, как вы хотите, чтобы определенные вещи обрабатывались с помощью чего-то, называемого флагами. Вы включаете флаги в свое выражение, используя специальную конструкцию (?flags go here), где, очевидно, вы бы заменили «flags go here» на конкретные флаги, которые вы хотите. Для игнорирования регистра флаг i, таким образом, вы включаете (?I) в свое выражение. Таким образом, (?I)test будет соответствовать «Test», «tEst», «TEst» и любым вариантам, которые вы можете придумать.

Другой полезный флаг позволяет точке соответствовать любому символу, * включая* перевод строки, флаг s. Если вы хотите использовать несколько выражений в выражении, просто поместите их в один и тот же оператор: (?is) проигнорирует регистр и сделает точку подходящей для всех. Неважно, какой флаг вы указали первым, (?si) будет эквивалентно приведенному выше.

Я думаю, что начинаю понимать эти регулярные выражения сейчас … как я могу использовать их в calibre?

Конвертации

Начнем с настроек конвертации, которые действительно аккуратны. В части Search & replace вы можете ввести регулярное выражение (сокращение от регулярного выражения), которое описывает строку, которая будет заменена во время преобразования. Аккуратная часть - это волшебник. Нажмите на посох волшебника, и вы получите предварительный просмотр того, что calibre «видит» в процессе конвертации. Прокрутите вниз до строки, которую хотите удалить, выберите и скопируйте её, вставьте в поле регулярных выражений в верхней части окна. Если есть переменные части, такие как номера страниц или вроде того, используйте наборы и квантификаторы, чтобы покрыть их, и пока вы это делаете, не забудьте экранировать специальные символы, если они есть. Нажмите кнопку с надписью Test, и calibre выделит части, которые он заменит, если вы будете использовать регулярное выражение. Как только вы удовлетворены, нажмите ОК и конвертируйте. Будьте осторожны, если в вашем источнике конвертацииии есть теги, подобные следующему:

Maybe, but the cops feel like you do, Anita. What's one more dead vampire?
New laws don't change that. </p>
<p class="calibre4"> <b class="calibre2">Generated by ABC Amber LIT Conv
<a href="http://www.processtext.com/abclit.html" class="calibre3">erter,
http://www.processtext.com/abclit.html</a></b></p>
<p class="calibre4"> It had only been two years since Addison v. Clark.
The court case gave us a revised version of what life was

(бессовестно вырванный из этой темы <https://www.mobileread.com/forums/showthread.php?t=75594 «>`_). Вам также придется удалить некоторые теги. В этом примере , я бы рекомендовал начать с тэга <b class="calibre2">, теперь вы должны закончить соответствующим закрывающим тэгом (открывающие тэги - <tag>, закрывающие тэги - </tag>), который просто является следующим </b> в этом случае. (Обратитесь к хорошему руководству по HTML или спросите на форуме, если вы не уверены в этом.) Открывающий тег можно описать с помощью``<b.*?>``, закрывающий тег с помощью </b>, таким образом, мы могли бы удалить все между этими тегами, используя <b. *?>.*?</b>. Но использование этого выражения было бы плохой идеей, потому что оно удаляет всё, что заключено в <b> -теги - (что, кстати, рендерит вложенный текст жирным шрифтом), и будет справедливо поспорить, что мы удалим части книги таким образом. Вместо этого включите также начало вложенной строки, создав регулярное выражение <b.*?>\s*Generated\s+by\s+ABC\s+Amber\s+LIT.*?</b>. \s с квантификаторами находятся в здесь вместо того, чтобы явно использовать пробелы, как видно в строке, чтобы поймать любые варианты строки, которые могут возникнуть. Не забудьте проверить, какой из них calibre удалит, чтобы убедиться, что вы не удаляете те части, которые хотите сохранить, если тестируете новое выражение. Если вы проверите только одно вхождение, вы можете пропустить несоответствие где-то ещё в тексте. Также обратите внимание, что, если вы случайно удалите больше или меньше тегов, чем вы на самом деле хотели, calibre попытается восстановить повреждённый код после удаления.

Добавление книг

Другая вещь, для которой вы можете использовать регулярные выражения, - это извлечение метаданных из имён файлов. Вы можете найти эту функцию в разделе «Добавление книг» в настройках. Здесь есть особенность: вы можете использовать имена полей для полей метаданных, например, (?P<title>) будет означать, что calibre использует эту часть строки в качестве названия книги. Разрешенные имена полей перечислены в окнах вместе с другим хорошим тестовым полем. Пример: скажем, вы хотите импортировать целую кучу файлов с такими именами, как Classical Texts: The Divine Comedy by Dante Alighieri.mobi (Классические тексты: Божественная комедия Данте Алигьери.mobi). (Очевидно, это уже есть в вашей библиотеке, так как мы все любим классическую итальянскую поэзию) или Science Fiction epics: The Foundation Trilogy by Isaac Asimov.epub (Фантастические эпопеи: трилогия Фонда» Исаака Азимова.epub). Это, очевидно, схема именования, из которой calibre не будет извлекать какие-либо значимые данные - стандартное выражение для извлечения метаданных - (?P<title>.+)-(?P<author>[^_]+) ``. Регулярное выражение, которое работает здесь, будет ``[a-zA-Z]+:(?P<title>. +) by (?P<author>.+). Обратите внимание, что внутри группы для поля метаданных вам нужно использовать выражения для описания того, что поле действительно соответствует. Также обратите внимание, что при использовании тестового поля calibre вам необходимо добавить расширение файла к вашему тестовому имени файла, иначе вы не получите никаких совпадений, несмотря на использование рабочего выражения.

Массовое редактирование метаданных

Последняя часть - поиск и замена регулярных выражений в полях метаданных. Вы можете получить доступ к этому, выбрав несколько книг в библиотеке и используя массовое редактирование метаданных. Будьте очень осторожны при использовании этой последней функции, поскольку она может сделать Очень плохие вещи для вашей библиотеки! Дважды проверьте, что ваши выражения делают то, что вы хотите, используя тестовые поля, и отмечайте только те книги, которые вы действительно хотите изменить! В режиме поиска по регулярному выражению вы можете искать в одном поле, заменять текст чем-либо и даже записывать результат в другое поле. Практический пример: скажем, ваша библиотека содержала книги из серии Фрэнка Герберта «Дюны», названные после моды Dune 1 - Dune, Dune 2 - Dune Messiah и так далее. Теперь вы хотите получить Dune в поле серии. Вы можете сделать это, выполнив поиск (.*?) \d+ - .* в поле заголовка и заменив его на \1 в поле серии. Видишь, что я там сделал? Это ссылка на первую группу, которой вы заменяете поле серии. Теперь, когда у вас есть все серии, вам нужно только выполнить другой поиск .*?- в поле заголовка и замените его на "" (пустую строку), снова в поле заголовка, и ваши метаданные будут аккуратными и чистыми. Разве это не здорово? Кстати, вместо замены всего поля вы также можете добавить или добавить к нему поле, так что, если вы хотите добавить к названию книги информацию о серии, вы также можете это сделать. Как вы, несомненно, уже заметили, есть флажок с надписью Чувствительный к регистру, поэтому вам не придется использовать флаги для выбора поведения здесь.

Ну, вот и заканчивается очень краткое введение в регулярные выражения. Надеюсь, я покажу вам достаточно, чтобы, по крайней мере, начать работу и продолжить самостоятельное обучение - хорошей отправной точкой будет документация Python для регулярных выражений <https://docs.python.org/library /re.html>`_.

Последнее слово предостережения: регулярные выражения мощная вещь, но в них также легко ошибиться. calibre предоставляет отличные возможности тестирования, чтобы увидеть, ведут ли себя ваши выражения так, как вы ожидаете. Используйте их. Старайтесь не стрелять себе в ногу. (Боже, мне нравится это выражение …) Но если вы, несмотря на предупреждение, пораните ногу (или любые другие части тела), постарайтесь извлечь из этого уроки.

Краткая справка

- Краткая справка о синтаксисе регулярных выражений

- Классы символов
- Сокращенные классы символов
- Квантификаторы
- Жадность
- Перечисление
- Исключение
- Якоря
- Группы
- Взгляды по сторонам
- Рекурсия
- Специальные символы
- Мета-символы
- Режимы

Признательность

Спасибо за помощь с советами, исправлениями и всем таким:

- ldolse

- ovidgoyal

- chaley

- dwanthny

- kacir

- Starson17

- Orpheu

Для получения дополнительной информации о регулярных выражениях см. Руководство пользователя Python [https://docs.python.org/library/re.html]. Актуальная библиотека регулярных выражений, используемая calibre: regex [https://bitbucket.org/mrabarnett/mrab-regex/src/hg/], поддерживает несколько полезных улучшений по сравнению со стандартной библиотекой Python.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Руководства »

- Всё об использования регулярных выражений в calibre »

- Краткая справка о синтаксисе регулярных выражений

Краткая справка о синтаксисе регулярных выражений

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

Содержание

- Классы символов

- Сокращенные классы символов

- Квантификаторы

- Жадность

- Перечисление

- Исключение

- Якоря

- Группы

- Взгляды по сторонам

- Рекурсия

- Специальные символы

- Мета-символы

- Режимы

Классы символов

Классы символов полезны для краткого представления различных групп символов.

Примеры:

Обозначение

Класс

[а-я]

Строчные буквы. Не включает символы с ударением и лигатурами.

[а-я0-9]

Строчные буквы от а до я а также цифры от 0 до 9.

[А-Яа-я-]

Заглавные, строчные буквы, а также тире. Чтобы включить тире в классе, вы должны поместить его в начале или в конце, чтобы не путать его с дефисом, который обозначает диапазон символов.

[^0-9]

Любые символы, кроме цифр. Каретка (^), помещенная в начале класса, исключает символы класса (дополненный класс).

[[a-z]--[aeiouy]] [[а-я]--[аеёиоуыэюя]]

Строчные согласные. Класс может быть включен в класс. Символы -- исключают то, что следует за ними

[\w--[\d_]]

Все буквы (включая иностранные акцентированные символы). Сокращенные классы могут использоваться внутри класса

Пример:

<[^<>]+> to select an HTML tag

Сокращенные классы символов

Обозначение

Класс

\d

Цифра (то же, что и [0-9])

\D

Любой нецифовой символ (то же, что и [^0-9])

\w

Буквенно-цифровой символ ([a-zA-Z0-9]), включая символы с меткой ударения и лигатуры

\W

Любой “non-word” («несловесный») символ

\s

Space (пробел), non-breaking space (неразрывный пробел), tab (табуляция), return line (обратная строка)

\S

Любой символ «без пробелов»

.

Любой символ кроме новой строки. Используйте флажок «dot all» или модификатор regexp (?s), чтобы включить символ новой строки.

Квантификаторы

Квантификатор

Количество вхождений выражения, предшествующего квантификатору

?

0 или 1 повторение регулярного выражения. Эквивалентно {0,1}.

+

Одно или более повторение регулярного выражения. Эквивалентно {1,}.

*

Любое - 0, 1 или более - количество повторений регулярного выражения. Эквивалентно {0,}.

{n}

Ровно n повторений регулярного выражения.

{min,max}

Количество повторений регулярного выражения, не менее min раз и не более max раз.

{min,}

Не менее min повторений регулярного выражения.

{,max}

Количество вхождений между 0 и включенным максимальным значением

Жадность

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

Остерегайтесь вложенных квантификаторов, например, шаблона (a*)*, поскольку он экспоненциально увеличивает время обработки.

Перечисление

Символ | в регулярном выражении соответствует логическому ИЛИ (OR). Т. е. могут совпадать либо предыдущее, либо последующее выражения.

Исключение

Метод 1
шаблон_исключения(*SKIP)(*FAIL)|шаблон_для_поиска

Пример:

"Blabla"(*SKIP)(*FAIL)|Blabla
выбирает Blabla, в строках Blabla или «Blabla или Blabla», но не в «Blabla».

Метод 2
шаблон_для_исключения\K|(шаблон_для_отбора)

"Blabla"\K|(Blabla)

выбирает Blabla, в строках Blabla или «Blabla или Blabla», но не в «Blabla».

Якоря

Якорь - это способ сопоставления логического расположения в строке, а не символа. Наиболее полезными якорями для обработки текста являются:

\b

Обозначает границу слова, то есть переход от пробела к непропробельному символу. Например, вы можете использовать \bsurd для сопоставления с surd, но не absurd.

^

Соответствует началу строки (в многострочном режиме, который используется по умолчанию)

$

Соответствует концу строки (в многострочном режиме, который используется по умолчанию)

\K

Сбрасывает начальную позицию выделения в его позицию в шаблоне. Некоторые движки регулярных выражений (но не calibre) не позволяют просматривать данные переменной длины, особенно с квантификаторами. Когда вы можете использовать \K с этими движками, это также позволяет вам избавиться от этого предела, написав эквивалент положительного взгляда позади длины переменной.

Группы

(выражение)

Группа захвата, которая хранит выбор и может быть вызвана позже в шаблонах search или replace на \n, где `` n`` - порядковый номер группы захвата (начиная с 1 в порядке чтения)

(?:выражение)

Группа, которая не захватывает выбор

(?>expression)

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

(?|expression)

Группа сброса ветвей: ветви чередований, включенные в выражение, имеют одинаковые номера групп

(?<name>expression)

Группа под названием «имя». Выбор может быть вызван позже в шаблоне search с помощью (?P=name) и в replace на \g<name>. Две разные группы могут использовать одно и то же имя.

Взгляды по сторонам

Взгляд по сторонам

Смысл

?=

Позитивный lookahead (взгляд вперёд) (будет размещен после выделения)

?!

Негативный lookahead (взгляд вперёд) (ставится после выделения)

?<=

Позитивный lookbehind (взгляд назад) (ставится перед выделением)

?<!

Негативный lookbehind (взгляд назад) (ставится перед выделением)

Lookaheads (взгляды вперед) и lookbehinds (взгляды назад) не потребляют символы, они имеют нулевую длину и не захватывают. Они являются атомарными группами: как только утверждение выполнено, движок регулярных выражений проходит, и если не удастся остальная часть шаблона, он не будет возвращаться в обход, чтобы попробовать другие комбинации.

При поиске нескольких совпадений в строке, в начальной позиции каждой попытки сопоставления, взгляд назад может проверять символы перед текущей позицией. Поэтому в строке 123 шаблон (?<=\D)\d (цифра, перед которой стоит цифра) должен теоретически выбрать 2 и 3. С другой стороны, \d\K\d может выбрать только 2, потому что начальная позиция после первого выбора находится непосредственно перед 3, и для второго совпадения недостаточно цифр. Точно так же \d(\d) только захватывает 2. В практике движка регулярных выражений calibre положительный взгляд назад (positive lookbehind) ведёт себя таким же образом и выбирает только 2, вопреки теории.

Группы могут быть помещены в lookarounds (взгляды по сторонам), но захват редко полезен. Тем не менее, если это будет полезно, необходимо будет быть очень осторожным при использовании квантификатора во взгляде назад: жадность, связанная с отсутствием обратного отслеживания, может дать неожиданный захват. По этой причине, используйте \K, а не положительный взгляд назад (positive lookbehind), когда у вас есть квантификатор (или, что еще хуже, несколько) в группе захвата положительного взгляда назад (positive lookbehind).

Пример негативного взгляда вперёд:

(?![^<>{}]*[>}])

Помещенный в конец шаблона препятствует выделению внутри тега или стиля, встроенного в файл.

Когда бы ни было возможно, всегда лучше «закрепить» обходные пути, чтобы уменьшить количество шагов, необходимых для получения результата.

Рекурсия

Обозначение

Смысл

(?R)

Рекурсия всего шаблона

(?1)

Рекурсия единственного шаблона нумерованной группы захвата, здесь группа 1

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

start-pattern(?>atomic sub-pattern|(?R))*end-pattern

Чтобы выбрать строку между двойными кавычками, не останавливаясь на встроенной строке:

“((?>[^“”]+|(?R))*[^“”]+)”

Этот шаблон также можно использовать для изменения пар тегов, которые могут быть встроены, таких как теги <div>.

Специальные символы

Обозначение

Символ

\t

табуляция

\n

Разрыв строки

\x20

(разрываемый) пробел

\xa0

неразрываемый пробел

Мета-символы

Мета-символы - это те, которые имеют особое значение для движка регулярных выражений. Из них двенадцати должен предшествовать escape-символ, обратная косая черта (\), чтобы потерять свое особое значение и снова стать обычным символом:

^ . [ ] $ ( ) * + ? | \

Семи другим метасимволам не обязательно должен предшествовать обратный слеш (но он может быть без каких-либо других последствий):

{ } ! < > = :

Специальные символы теряют свой статус, если они используются внутри класса (между скобками []). Закрывающая скобка и тире имеют особый статус в классе. Вне класса тире - это простой литерал, закрывающая скобка остается метасимволом.

Косая черта (/) и знак числа (или хэш-символ) (#) не являются метасимволами, их не нужно экранировать.

В некоторых инструментах, таких как regex101.com с движком Python, двойные кавычки имеют специальный статус разделителя и должны быть экранированы, либо параметры изменены. Это не так в редакторе calibre.

Режимы

(?s)

Заставляет точку (.) совпадать с символами новой строки

(?m)

Заставляет якоря ^ и $ соответствовать началу и концу строк вместо начала и конца всей строки.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Руководства »

- Создание собственных плагинов для расширения функциональности calibre

Создание собственных плагинов для расширения функциональности calibre
calibre имеет очень модульную конструкцию. Практически все функциональные возможности calibre поставляются в виде плагинов. Плагины используются для преобразования, для загрузки новостей (там они называются рецептами), для различных компонентов пользовательского интерфейса, для подключения к различным устройствам, для обработки файлов при добавлении их в calibre и так далее. Вы можете получить полный список всех встроенных плагинов в calibre, перейдя к :guilabel: Preferences-> Advanced-> Plugins.

Здесь мы научим вас, как создавать свои собственные плагины чтобы добавить новые функции в calibre.

Содержание

- Анатомия плагина calibre

- Плагин пользовательского интерфейса

- __init__.py

- ui.py

- main.py

- Получение ресурсов из плагина ZIP-файла

- Включение пользовательской конфигурации вашего плагина

- Редактировать книжные плагины

- main.py

- Добавление переводов в ваш плагин

- API плагина

- Отладка плагинов

- Больше примеров плагинов

- Поделиться своими плагинами с другими

Примечание

Это относится только к calibre releases >= 0.8.60

Анатомия плагина calibre

Плагин calibre очень прост, это просто ZIP-файл, который содержит некоторый код Python и любые другие ресурсы, такие как файлы изображений, необходимые плагину. Без дальнейших церемоний, давайте посмотрим на основной пример.

Предположим, у вас установлена calibre, которую вы используете для самостоятельной публикации различных электронных документов в форматах EPUB и MOBI. Вы хотите, чтобы для всех файлов, сгенерированных calibre, их издатель был установлен как «Hello world», вот как это сделать. Создайте файл с именем __init__.py (это специальное имя, которое всегда должно использоваться для основного файла вашего плагина) и введите в него следующий код Python:

from calibre.customize import FileTypePlugin

class HelloWorld(FileTypePlugin):

name = 'Hello World Plugin' # Name of the plugin
description = 'Set the publisher to Hello World for all new conversions'
supported_platforms = ['windows', 'osx', 'linux'] # Platforms this plugin will run on
author = 'Acme Inc.' # The author of this plugin
version = (1, 0, 0) # The version number of this plugin
file_types = {'epub', 'mobi'} # The file types that this plugin will be applied to
on_postprocess = True # Run this plugin after conversion is complete
minimum_calibre_version = (0, 7, 53)

def run(self, path_to_ebook):
from calibre.ebooks.metadata.meta import get_metadata, set_metadata
with open(path_to_ebook, 'r+b') as file:
ext = os.path.splitext(path_to_ebook)[-1][1:].lower()
mi = get_metadata(file, ext)
mi.publisher = 'Hello World'
set_metadata(file, mi, ext)
return path_to_ebook

Вот и всё. Чтобы добавить этот код в calibre как плагин, просто запустите в папке, в которой вы создали, следующее __init __.py:

calibre-customize -b .

Примечание

В macOS инструменты командной строки находятся внутри пакета calibre, например, если вы установили calibre в /Applications, то инструменты командной строки будут в /Applications/calibre.app/Contents/MacOS/.

Вы можете скачать плагин Hello World из helloworld_plugin.zip [https://calibre-ebook.com/downloads/helloworld_plugin.zip].

Каждый раз, когда вы используете calibre для конвертации книги, вызывается метод плагина run(), и для конвертированной книги издателю присваивается значение «Hello World». Это тривиальный плагин, давайте перейдем к более сложному примеру, который фактически добавляет компонент в пользовательский интерфейс.

Плагин пользовательского интерфейса

Этот плагин будет распространяться на несколько файлов (чтобы сохранить код в чистоте). Он покажет вам, как получить ресурсы (изображения или файлы данных) из ZIP-файла плагина, позволит пользователям настраивать ваш плагин, как создавать элементы в пользовательском интерфейсе calibre и как получить доступ и запросить базу данных книг в calibre.

Вы можете скачать этот плагин из interface_demo_plugin.zip [https://calibre-ebook.com/downloads/interface_demo_plugin.zip]

Первое, на что нужно обратить внимание, это то, что в этом ZIP-файле содержится гораздо больше файлов. Как объясняется ниже, обратите особое внимание на plugin-import-name-interface_demo.txt.

plugin-import-name-interface_demo.txt

Пустой текстовый файл, используемый для включения магии многофайлового плагина. Этот файл должен присутствовать во всех плагинах, которые используют более одного файла .py. Он должен быть пустым, а его имя файла должно иметь вид: plugin-import-name-** some_name**.txt. Наличие этого файла позволяет вам импортировать код из файлов .py, присутствующих в ZIP-файле, используя оператор вроде:

from calibre_plugins.some_name.some_module import some_object

Всегда должен присутствовать префикс calibre_plugins. some_name происходит от имени пустого текстового файла. some_module относится к файлу some_module.py внутри ZIP-файла. Обратите внимание, что этот импорт столь же эффективен, как и обычный импорт Python. Вы можете создавать пакеты и подпакеты модулей .py внутри ZIP-файла, как обычно (путем определения __init__.py в каждой подпапке), и все должно «просто работать».

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

__init__.py

Как и прежде, файл, который определяет класс плагина

main.py

Этот файл содержит реальный код, который делает что-то полезное

ui.py

Этот файл определяет интерфейсную часть плагина

images/icon.png

Значок для этого плагина

about.txt

Текстовый файл с информацией о плагине

translations

Папка, содержащая файлы .mo с переводами пользовательского интерфейса вашего плагина на разные языки. Смотрите ниже для деталей.

Теперь давайте посмотрим на код.

__init__.py

Во-первых, обязательный __init__.py для определения метаданных плагина:

from calibre.customize import InterfaceActionBase

class InterfacePluginDemo(InterfaceActionBase):
'''
This class is a simple wrapper that provides information about the actual
plugin class. The actual interface plugin class is called InterfacePlugin
and is defined in the ui.py file, as specified in the actual_plugin field
below.

The reason for having two classes is that it allows the command line
calibre utilities to run without needing to load the GUI libraries.
'''
name = 'Interface Plugin Demo'
description = 'An advanced plugin demo'
supported_platforms = ['windows', 'osx', 'linux']
author = 'Kovid Goyal'
version = (1, 0, 0)
minimum_calibre_version = (0, 7, 53)

#: This field defines the GUI plugin class that contains all the code
#: that actually does something. Its format is module_path:class_name
#: The specified class must be defined in the specified module.
actual_plugin = 'calibre_plugins.interface_demo.ui:InterfacePlugin'

def is_customizable(self):
'''
This method must return True to enable customization via
Preferences->Plugins
'''
return True

def config_widget(self):
'''
Implement this method and :meth:`save_settings` in your plugin to
use a custom configuration dialog.

This method, if implemented, must return a QWidget. The widget can have
an optional method validate() that takes no arguments and is called
immediately after the user clicks OK. Changes are applied if and only
if the method returns True.

If for some reason you cannot perform the configuration at this time,
return a tuple of two strings (message, details), these will be
displayed as a warning dialog to the user and the process will be
aborted.

The base class implementation of this method raises NotImplementedError
so by default no user configuration is possible.
'''
# It is important to put this import statement here rather than at the
# top of the module as importing the config class will also cause the
# GUI libraries to be loaded, which we do not want when using calibre
# from the command line
from calibre_plugins.interface_demo.config import ConfigWidget
return ConfigWidget()

def save_settings(self, config_widget):
'''
Save the settings specified by the user with config_widget.

:param config_widget: The widget returned by :meth:`config_widget`.
'''
config_widget.save_settings()

# Apply the changes
ac = self.actual_plugin_
if ac is not None:
ac.apply_settings()

Единственная заслуживающая внимания особенность - это поле actual_plugin. Так как calibre имеет и командную строку, и интерфейсы GUI, плагины GUI, подобные этому, не должны загружать библиотеки GUI в __init__.py. Поле actual_plugin сделает это за вас, сообщив calibre, что фактический плагин находится в другом файле внутри вашего ZIP-архива, который будет загружен только в контексте графического интерфейса пользователя.

Помните, что для этого вам понадобится файл plugin-import-name-some_name.txt в вашем ZIP-файле плагина, как обсуждалось выше.

Также есть несколько способов включения пользовательской конфигурации плагина. Они обсуждаются ниже.

ui.py

Теперь давайте посмотрим на ui.py, который определяет реальный плагин GUI. Исходный код тщательно прокомментирован и должен быть понятен:

from calibre.gui2.actions import InterfaceAction
from calibre_plugins.interface_demo.main import DemoDialog

class InterfacePlugin(InterfaceAction):

name = 'Interface Plugin Demo'

# Declare the main action associated with this plugin
# The keyboard shortcut can be None if you don't want to use a keyboard
# shortcut. Remember that currently calibre has no central management for
# keyboard shortcuts, so try to use an unusual/unused shortcut.
action_spec = ('Interface Plugin Demo', None,
'Run the Interface Plugin Demo', 'Ctrl+Shift+F1')

def genesis(self):
# This method is called once per plugin, do initial setup here

# Set the icon for this interface action
# The get_icons function is a builtin function defined for all your
# plugin code. It loads icons from the plugin zip file. It returns
# QIcon objects, if you want the actual data, use the analogous
# get_resources builtin function.
#
# Note that if you are loading more than one icon, for performance, you
# should pass a list of names to get_icons. In this case, get_icons
# will return a dictionary mapping names to QIcons. Names that
# are not found in the zip file will result in null QIcons.
icon = get_icons('images/icon.png', 'Interface Demo Plugin')

# The qaction is automatically created from the action_spec defined
# above
self.qaction.setIcon(icon)
self.qaction.triggered.connect(self.show_dialog)

def show_dialog(self):
# The base plugin object defined in __init__.py
base_plugin_object = self.interface_action_base_plugin
# Show the config dialog
# The config dialog can also be shown from within
# Preferences->Plugins, which is why the do_user_config
# method is defined on the base plugin class
do_user_config = base_plugin_object.do_user_config

# self.gui is the main calibre GUI. It acts as the gateway to access
# all the elements of the calibre user interface, it should also be the
# parent of the dialog
d = DemoDialog(self.gui, self.qaction.icon(), do_user_config)
d.show()

def apply_settings(self):
from calibre_plugins.interface_demo.config import prefs
# In an actual non trivial plugin, you would probably need to
# do something based on the settings in prefs
prefs

main.py

Фактическая логика для реализации диалогового окна Interface Plugin Demo.

from calibre_plugins.interface_demo.config import prefs class DemoDialog(QDialog):

def __init__(self, gui, icon, do_user_config):
QDialog.__init__(self, gui)
self.gui = gui self.do_user_config = do_user_config

# The current database shown in the GUI
# db is an instance of the class LibraryDatabase from db/legacy.py
# This class has many, many methods that allow you to do a lot of
# things. For most purposes you should use db.new_api, which has
# a much nicer interface from db/cache.py self.db = gui.current_db self.l = QVBoxLayout()
self.setLayout(self.l)

self.label = QLabel(prefs['hello_world_msg'])
self.l.addWidget(self.label)

self.setWindowTitle('Interface Plugin Demo')
self.setWindowIcon(icon)

self.about_button = QPushButton('About', self)
self.about_button.clicked.connect(self.about)
self.l.addWidget(self.about_button)

self.marked_button = QPushButton(
'Show books with only one format in the calibre GUI', self)
self.marked_button.clicked.connect(self.marked)
self.l.addWidget(self.marked_button)

self.view_button = QPushButton(
'View the most recently added book', self)
self.view_button.clicked.connect(self.view)
self.l.addWidget(self.view_button)

self.update_metadata_button = QPushButton(
"Update metadata in a book's files", self)
self.update_metadata_button.clicked.connect(self.update_metadata)
self.l.addWidget(self.update_metadata_button)

self.conf_button = QPushButton(
'Configure this plugin', self)
self.conf_button.clicked.connect(self.config)
self.l.addWidget(self.conf_button)

self.resize(self.sizeHint())

def about(self):
# Get the about text from a file inside the plugin zip file
# The get_resources function is a builtin function defined for all your
# plugin code. It loads files from the plugin zip file. It returns
# the bytes from the specified file.
#
# Note that if you are loading more than one file, for performance, you
# should pass a list of names to get_resources. In this case,
# get_resources will return a dictionary mapping names to bytes. Names that
# are not found in the zip file will not be in the returned dictionary.
text = get_resources('about.txt')
QMessageBox.about(self, 'About the Interface Plugin Demo', text.decode('utf-8'))

def marked(self):
''' Show books with only one format '''
db = self.db.new_api matched_ids = {book_id for book_id in db.all_book_ids() if len(db.formats(book_id)) == 1}
# Mark the records with the matching ids
# new_api does not know anything about marked books, so we use the full
# db object self.db.set_marked_ids(matched_ids)

# Tell the GUI to search for all marked records self.gui.search.setEditText('marked:true')
self.gui.search.do_search()

def view(self):
''' View the most recently added book '''
most_recent = most_recent_id = None db = self.db.new_api for book_id, timestamp in db.all_field_for('timestamp', db.all_book_ids()).items():
if most_recent is None or timestamp > most_recent:
most_recent = timestamp most_recent_id = book_id if most_recent_id is not None:
# Get a reference to the View plugin view_plugin = self.gui.iactions['View']
# Ask the view plugin to launch the viewer for row_number view_plugin._view_calibre_books([most_recent_id])

def update_metadata(self):
'''
Set the metadata in the files in the selected book's record to match the current metadata in the database.
'''
from calibre.ebooks.metadata.meta import set_metadata from calibre.gui2 import error_dialog, info_dialog

# Get currently selected books rows = self.gui.library_view.selectionModel().selectedRows()
if not rows or len(rows) == 0:
return error_dialog(self.gui, 'Cannot update metadata',
'No books selected', show=True)
# Map the rows to book ids ids = list(map(self.gui.library_view.model().id, rows))
db = self.db.new_api for book_id in ids:
# Get the current metadata for this book from the db mi = db.get_metadata(book_id, get_cover=True, cover_as_data=True)
fmts = db.formats(book_id)
if not fmts:
continue for fmt in fmts:
fmt = fmt.lower()
# Get a python file object for the format. This will be either
# an in memory file or a temporary on disk file ffile = db.format(book_id, fmt, as_file=True)
ffile.seek(0)
# Set metadata in the format set_metadata(ffile, mi, fmt)
ffile.seek(0)
# Now replace the file in the calibre library with the updated
# file. We don't use add_format_with_hooks as the hooks were
# already run when the file was first added to calibre.
db.add_format(book_id, fmt, ffile, run_hooks=False)

info_dialog(self, 'Updated files', f'Updated the metadata in the files of {len(ids)} book(s)', show=True)

def config(self):
self.do_user_config(parent=self)
# Apply the changes self.label.setText(prefs['hello_world_msg'])

Получение ресурсов из плагина ZIP-файла

Система загрузки плагинов calibre определяет несколько встроенных функций, которые позволяют вам удобно получать файлы из ZIP-файла плагина.

get_resources(name_or_list_of_names)

Эта функция должна вызываться со списком путей к файлам внутри ZIP-файла. Например, чтобы получить доступ к файлу icon.png в папке images в ZIP-файле, вы должны использовать: images/icon.png. Всегда используйте косую черту в качестве разделителя пути, даже в Windows. Когда вы передаете одно имя, функция вернет необработанные байты этого файла или None, если имя не было найдено в ZIP-файле. Если вы передаете более одного имени, возвращается dict, сопоставляющий имена с байтами. Если имя не найдено, его не будет в возвращаемом dict.

get_icons(name_or_list_of_names, plugin_name=““)

Обёртка для get_resources(), которая создает объекты QIcon из необработанных байтов, возвращенных get_resources. Если имя не найдено в ZIP-файле, соответствующий QIcon будет нулевым. Чтобы поддерживать тему значков, передайте человекопонятное имя вашего плагина как plugin_name. Если пользователь использует тему значков с иконками для вашего плагина, они будут загружены в первую очередь.

Включение пользовательской конфигурации вашего плагина

Чтобы позволить пользователям настраивать ваш плагин, вы должны определить три метода в вашем базовом классе плагинов, is_customizable, config_widget и save_settings, как показано ниже:

def is_customizable(self):
'''
This method must return True to enable customization via
Preferences->Plugins
'''
return True

def config_widget(self):
'''
Implement this method and :meth:`save_settings` in your plugin to
use a custom configuration dialog.

This method, if implemented, must return a QWidget. The widget can have
an optional method validate() that takes no arguments and is called
immediately after the user clicks OK. Changes are applied if and only
if the method returns True.

If for some reason you cannot perform the configuration at this time,
return a tuple of two strings (message, details), these will be
displayed as a warning dialog to the user and the process will be
aborted.

The base class implementation of this method raises NotImplementedError
so by default no user configuration is possible.
'''
# It is important to put this import statement here rather than at the
# top of the module as importing the config class will also cause the
# GUI libraries to be loaded, which we do not want when using calibre
# from the command line
from calibre_plugins.interface_demo.config import ConfigWidget
return ConfigWidget()

def save_settings(self, config_widget):
'''
Save the settings specified by the user with config_widget.

:param config_widget: The widget returned by :meth:`config_widget`.
'''
config_widget.save_settings()

# Apply the changes
ac = self.actual_plugin_
if ac is not None:
ac.apply_settings()

calibre имеет много разных способов хранения данных конфигурации (наследие его длинной истории). Рекомендуемый способ - использовать класс JSONConfig, который хранит информацию о вашей конфигурации в файле .json.

Код для управления данными конфигурации в демо-плагине находится в config.py:

from calibre.utils.config import JSONConfig

# This is where all preferences for this plugin will be stored
# Remember that this name (i.e. plugins/interface_demo) is also
# in a global namespace, so make it as unique as possible.
# You should always prefix your config file name with plugins/,
# so as to ensure you don't accidentally clobber a calibre config file
prefs = JSONConfig('plugins/interface_demo')

# Set defaults
prefs.defaults['hello_world_msg'] = 'Hello, World!'

class ConfigWidget(QWidget):

def __init__(self):
QWidget.__init__(self)
self.l = QHBoxLayout()
self.setLayout(self.l)

self.label = QLabel('Hello world &message:')
self.l.addWidget(self.label)

self.msg = QLineEdit(self)
self.msg.setText(prefs['hello_world_msg'])
self.l.addWidget(self.msg)
self.label.setBuddy(self.msg)

def save_settings(self):
prefs['hello_world_msg'] = self.msg.text()

Объект prefs теперь доступен во всем коде плагина с помощью простого:

from calibre_plugins.interface_demo.config import prefs

Вы можете увидеть объект prefs, используемый в main.py:

def config(self):
self.do_user_config(parent=self)
# Apply the changes
self.label.setText(prefs['hello_world_msg'])

Редактировать книжные плагины

Теперь давайте немного поменяем механизмы и посмотрим на создание плагина для добавления инструментов в редактор книг calibre. Плагин доступен здесь:download_file:editor_demo_plugin.zip.

Первым шагом, как и для всех плагинов, является создание пустого текстового файла с именем для импорта, как описано above. Мы назовем файл plugin-import-name-editor_plugin_demo.txt.

Теперь мы создаем обязательный файл __init__.py, который содержит метаданные о плагине – его имя, автора, версию и т. д.

class DemoPlugin(EditBookToolPlugin):

name = 'Edit Book plugin demo'
version = (1, 0, 0)
author = 'Kovid Goyal'
supported_platforms = ['windows', 'osx', 'linux']
description = 'A demonstration of the plugin interface for the ebook editor'
minimum_calibre_version = (1, 46, 0)

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

Все инструменты должны быть определены в файле main.py вашего плагина. Каждый инструмент - это класс, который наследуется от класса calibre.gui2.tweak_book.plugin.Tool. Давайте посмотрим на main.py из демо-плагина, исходный код тщательно прокомментирован и не требует пояснений. Прочитайте документы API класса calibre.gui2.tweak_book.plugin.Tool для получения более подробной информации.

main.py

Здесь мы увидим определение единственного инструмента, который умножит все размеры шрифта в книге на число, предоставленное пользователем. Этот инструмент демонстрирует различные важные концепции, которые вам понадобятся при разработке ваших собственных плагинов, поэтому вы должны внимательно прочитать (сильно комментированный) исходный код.

from css_parser.css import CSSRule from qt.core import QAction, QInputDialog from calibre import force_unicode from calibre.ebooks.oeb.polish.container import OEB_DOCS, OEB_STYLES, serialize from calibre.gui2 import error_dialog

# The base class that all tools must inherit from from calibre.gui2.tweak_book.plugin import Tool class DemoTool(Tool):

#: Set this to a unique name it will be used as a key name = 'demo-tool'

#: If True the user can choose to place this tool in the plugins toolbar allowed_in_toolbar = True

#: If True the user can choose to place this tool in the plugins menu allowed_in_menu = True def create_action(self, for_toolbar=True):
# Create an action, this will be added to the plugins toolbar and
# the plugins menu ac = QAction(get_icons('images/icon.png'), 'Magnify fonts', self.gui) # noqa: F821 if not for_toolbar:
# Register a keyboard shortcut for this toolbar action. We only
# register it for the action created for the menu, not the toolbar,
# to avoid a double trigger self.register_shortcut(ac, 'magnify-fonts-tool', default_keys=('Ctrl+Shift+Alt+D',))
ac.triggered.connect(self.ask_user)
return ac def ask_user(self):
# Ask the user for a factor by which to multiply all font sizes factor, ok = QInputDialog.getDouble( self.gui, 'Enter a magnification factor', 'Allow font sizes in the book will be multiplied by the specified factor', value=2, min=0.1, max=4
)
if ok:
# Ensure any in progress editing the user is doing is present in the container self.boss.commit_all_editors_to_container()
try:
self.magnify_fonts(factor)
except Exception:
# Something bad happened report the error to the user import traceback error_dialog(self.gui, _('Failed to magnify fonts'), _(
'Failed to magnify fonts, click "Show details" for more info'), det_msg=traceback.format_exc(), show=True)
# Revert to the saved restore point self.boss.revert_requested(self.boss.global_undo.previous_container)
else:
# Show the user what changes we have made, allowing her to
# revert them if necessary self.boss.show_current_diff()
# Update the editor UI to take into account all the changes we
# have made self.boss.apply_container_update_to_gui()

def magnify_fonts(self, factor):
# Magnify all font sizes defined in the book by the specified factor
# First we create a restore point so that the user can undo all changes
# we make.
self.boss.add_savepoint('Before: Magnify fonts')

container = self.current_container # The book being edited as a container object

# Iterate over all style declarations in the book, this means css
# stylesheets, <style> tags and style="" attributes for name, media_type in container.mime_map.items():
if media_type in OEB_STYLES:
# A stylesheet. Parsed stylesheets are css_parser CSSStylesheet
# objects.
self.magnify_stylesheet(container.parsed(name), factor)
container.dirty(name) # Tell the container that we have changed the stylesheet elif media_type in OEB_DOCS:
# A HTML file. Parsed HTML files are lxml elements for style_tag in container.parsed(name).xpath('//*[local-name="style"]'):
if style_tag.text and style_tag.get('type', None) in {None, 'text/css'}:
# We have an inline CSS <style> tag, parse it into a
# stylesheet object sheet = container.parse_css(style_tag.text)
self.magnify_stylesheet(sheet, factor)
style_tag.text = serialize(sheet, 'text/css', pretty_print=True)
container.dirty(name) # Tell the container that we have changed the stylesheet for elem in container.parsed(name).xpath('//*[@style]'):
# Process inline style attributes block = container.parse_css(elem.get('style'), is_declaration=True)
self.magnify_declaration(block, factor)
elem.set('style', force_unicode(block.getCssText(separator=' '), 'utf-8'))

def magnify_stylesheet(self, sheet, factor):
# Magnify all fonts in the specified stylesheet by the specified
# factor.
for rule in sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE):
self.magnify_declaration(rule.style, factor)

def magnify_declaration(self, style, factor):
# Magnify all fonts in the specified style declaration by the specified
# factor val = style.getPropertyValue('font-size')
if not val:
return
# see if the font-size contains a number num = re.search(r'[0-9.]+', val)
if num is not None:
num = num.group()
val = val.replace(num, f'{float(num)*factor:f}')
style.setProperty('font-size', val)
# We should also be dealing with the font shorthand property and
# font sizes specified as non numbers, but those are left as exercises
# for the reader

Давайте разберемся с main.py. Мы видим, что он определяет один инструмент с именем Magnify fonts. Этот инструмент запросит у пользователя число и умножит все размеры шрифтов в книге на это число.

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

Следующая важная точка входа - это calibre.gui2.tweak_book.plugin.Tool.create_action(). Этот метод создает объекты QAction, которые появляются на панели инструментов плагинов и меню плагинов. Также, по желанию, назначается сочетание клавиш, которое пользователь может настроить. Инициированный сигнал от QAction подключается к методу ask_user(), который запрашивает у пользователя множитель размера шрифта, а затем запускает код увеличения.

Код увеличения хорошо прокомментирован и довольно прост. Главное, на что следует обратить внимание, это то, что вы получаете ссылку на окно редактора как self.gui, а редактор Boss как self.boss. Boss - это объект, который управляет пользовательским интерфейсом редактора. У него есть много полезных методов, которые описаны в классе calibre.gui2.tweak_book.boss.Boss.

Наконец, есть self.current_container, который является ссылкой на книгу, редактируемую как объект calibre.ebooks.oeb.polish.container.Container. Это представляет книгу как коллекцию составляющих её HTML/CSS/графических файлов и имеет удобные методы для выполнения многих полезных задач. Контейнерный объект и различные полезные служебные функции, которые можно повторно использовать в коде вашего плагина, описаны в Документация API инструмента редактирования электронных книг.

Добавление переводов в ваш плагин

Вы можете перевести и отобразить все строки пользовательского интерфейса в вашем плагине на любом языке, установленном для основного пользовательского интерфейса.

Первый шаг - просмотреть исходный код вашего плагина и пометить все видимые пользователю строки как переводимые, заключив их в _(). Например:

action_spec = (_('My plugin'), None, _('My plugin is cool'), None)

Затем используйте какую-нибудь программу для генерации .po файлов из исходного кода вашего плагина. Для каждого языка, на который вы хотите перевести, должен быть один .po-файл. Например: de.po для немецкого, fr.po для французского и так далее. Для этого вы можете использовать программу Poedit [https://poedit.net/].

Отправьте эти .po файлы своим переводчикам. Как только вы получите их обратно, скомпилируйте их в файлы .mo. Вы можете снова использовать Poedit для этого или просто сделать:

calibre-debug -c "from calibre.translations.msgfmt import main; main()" filename.po

Поместите файлы .mo в папку translations вашего плагина.

Последний шаг - просто вызвать функцию load_translations() вверху .py файлов вашего плагина. Из соображений производительности эту функцию следует вызывать только в тех файлах .py, которые на самом деле содержат переводимые строки. Таким образом, в типичном плагине интерфейса пользователя вы бы вызвали его в верхней части ui.py, но не в __init __.py.

Вы можете протестировать переводы ваших плагинов, изменив язык пользовательского интерфейса в calibre в разделе Preferences-Interface-Look & feel или запустив calibre с установленной переменной окружения CALIBRE_OVERRIDE_LANG. Например:

CALIBRE_OVERRIDE_LANG=de

Замените de на код языка, который вы хотите проверить.

Для переводов с множественным числом используйте функцию ngettext() вместо _(). Например:

ngettext('Delete a book', 'Delete {} books', num_books).format(num_books)

API плагина

Как вы могли заметить выше, плагин calibre - это класс. Существуют разные классы для разных типов плагинов в calibre. Подробности о каждом классе, включая базовый класс всех плагинов, можно найти в Документация по API для плагинов.

Ваш плагин почти наверняка будет использовать код из calibre. Чтобы узнать, как найти различные функциональные возможности в кодовой базе calibre, прочитайте раздел о calibre Макет кода.

Отладка плагинов

Первый, самый важный шаг - запустить calibre в режиме отладки. Вы можете сделать это из командной строки с помощью:

calibre-debug -g

Или изнутри calibre, щелчком правой кнопки мыши по Preferences или через сочетание клавиш Ctrl+Shift+R.

При запуске из командной строки выходные данные отладки будут выводиться на консоль, а при запуске из calibre выходные данные будут передаваться в текстовый файл.

Вы можете вставить операторы печати в любом месте вашего кода плагина, они будут выводиться в режиме отладки. Помните, что это Python, вам действительно не нужно ничего, кроме операторов print для отладки;) Я разработал все версии calibre, используя только эту технику отладки.

Вы можете быстро протестировать изменения в вашем плагине, используя следующую командную строку:

calibre-debug -s; calibre-customize -b /path/to/your/plugin/folder; calibre

Это отключит работающий calibre, дождется завершения выключения, затем обновит ваш плагин в calibre и перезапустит calibre.

Больше примеров плагинов

Вы можете найти список множества сложных плагинов calibre здесь [https://www.mobileread.com/forums/showthread.php?t=118764].

Поделиться своими плагинами с другими

Если вы хотите поделиться плагинами, которые вы создали, с другими пользователями calibre, опубликуйте свой плагин в новой теме на `форуме плагинов calibre <https://www.mobileread.com/forums/forumdisplay.php?f=237. >`_.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Руководства »

- Типографика математических выражений в электронных книгах

Типографика математических выражений в электронных книгах

Просмотрщик электронных книг calibre умеет отображать математические выражения, встроенные в электронные книги (файлы EPUB и HTML). Вы можете набирать их с помощью TeX, или MathML, или AsciiMath. Просмотрщик эл.книг calibre использует отличную библиотеку MathJax [https://www.mathjax.org]. Это краткое руководство по созданию электронных книг со встроенными формулами, которые хорошо работают в просмотрщике эл. книг calibre.

Простой HTML-файл с формулами

Вы можете вписать математические выражения внутрь простого HTML-файла, а читалка calibre превратит их в правильно набранные формулы. В примере ниже для математических выражений применяется нотация TeX. Вы увидите, что можно использовать обычные команды TeX, пусть с небольшой оговоркой: амперсанды и знаки «меньше» и «больше» должны записываться как &amp;, &lt; и &gt; соответственно.

Сперва необходимо сообщить calibre, что документ содержит формулы. Для этого в раздел <head> HTML-файла добавляется следующий фрагмент кода:

<script type="text/x-mathjax-config"></script>

И всё, теперь можно писать формулы так же, как в .tex файле. К примеру, вот уравнения Лоренца:

<h2>The Lorenz Equations</h2>

<p>
\begin{align}
\dot{x} &amp; = \sigma(y-x) \\
\dot{y} &amp; = \rho x - y - xz \\
\dot{z} &amp; = -\beta z + xy
\end{align}
</p>

Этот фрагмент в читалке calibre выглядит как снимок экрана ниже.

Уравнения Лоренца

Ниже приведён полный HTML-файл с большим количеством уравнений и встроенной математики. Вы можете преобразовать этот HTML-файл в EPUB в calibre, чтобы в итоге получить электронную книгу, которую легко распространять среди других людей.

<!DOCTYPE html>
<html>
<!-- Copyright (c) 2012 Design Science, Inc. -->
<head>
<title>Math Test Page</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />

<!-- This script tag is needed to make calibre's ebook-viewer recpgnize that this file needs math typesetting -->
<script type="text/x-mathjax-config">
// This line adds numbers to all equations automatically, unless explicitly suppressed.
MathJax.tex = {tags: 'all'};
</script>

<style> h1 {text-align:center}
h2 { font-weight: bold; background-color: #DDDDDD; padding: .2em .5em; margin-top: 1.5em; border-top: 3px solid #666666; border-bottom: 2px solid #999999;
}
</style>
</head>
<body>

<h1>Sample Equations</h1>

<h2>The Lorenz Equations</h2>

<p> \begin{align}
\dot{x} &amp; = \sigma(y-x) \label{lorenz}\\
\dot{y} &amp; = \rho x - y - xz \\
\dot{z} &amp; = -\beta z + xy
\end{align} </p>

<h2>The Cauchy-Schwarz Inequality</h2>

<p>\[
\left( \sum_{k=1}^n a_k b_k \right)^{\!\!2} \leq
\left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)
\]</p>

<h2>A Cross Product Formula</h2>

<p>\[
\mathbf{V}_1 \times \mathbf{V}_2 =
\begin{vmatrix}
\mathbf{i} &amp; \mathbf{j} &amp; \mathbf{k} \\
\frac{\partial X}{\partial u} &amp; \frac{\partial Y}{\partial u} &amp; 0 \\
\frac{\partial X}{\partial v} &amp; \frac{\partial Y}{\partial v} &amp; 0 \\
\end{vmatrix}
\]</p>

<h2>The probability of getting \(k\) heads when flipping \(n\) coins is:</h2>

<p>\[P(E) = {n \choose k} p^k (1-p)^{ n-k} \]</p>

<h2>An Identity of Ramanujan</h2>

<p>\[
\frac{1}{(\sqrt{\phi \sqrt{5}}-\phi) e^{\frac25 \pi}} =
1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}}
{1+\frac{e^{-8\pi}} {1+\ldots} } } }
\]</p>

<h2>A Rogers-Ramanujan Identity</h2>

<p>\[
1 + \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots =
\prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})},
\quad\quad \text{for $|q|&lt;1$}.
\]</p>

<h2>Maxwell's Equations</h2>

<p> \begin{align}
\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} &amp; = \frac{4\pi}{c}\vec{\mathbf{j}} \\
\nabla \cdot \vec{\mathbf{E}} &amp; = 4 \pi \rho \\
\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} &amp; = \vec{\mathbf{0}} \\
\nabla \cdot \vec{\mathbf{B}} &amp; = 0
\end{align} </p>

<h2>Inline Mathematics</h2>

<p>While display equations look good for a page of samples, the ability to mix math and text in a paragraph is also important. This expression \(\sqrt{3x-1}+(1+x)^2\) is an example of an inline equation. As you see, equations can be used this way as well, without unduly disturbing the spacing between lines.</p>

<h2>References to equations</h2>

<p>Here is a reference to the Lorenz Equations (\ref{lorenz}). Clicking on the equation number will take you back to the equation.</p>

</body>
</html>

Примечание

Просмотрщик calibre поддерживает MathML, а также TeX, но вы должны включить строку <script type="text/x-mathjax-config"></script> в свой HTML-файл, иначе MathML не будет отображаться.

Дополнительная информация

Поскольку промотрщик эл. книг calibre для отрисовки формул использует библиотеку MathJax, лучшее место, где можно узнать больше о математике в электронных книгах и получить помощь, — это веб-сайт MathJax [https://www.mathjax.org].

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Руководства »
- Создание AZW3 • EPUB • MOBI каталогов

Создание AZW3 • EPUB • MOBI каталогов

Функция создания каталога calibre позволяет вам создавать каталог вашей библиотеки в различных форматах. Этот файл справки описывает параметры каталогизации при создании каталога в форматах AZW3, EPUB и MOBI.

- Выбор книг для каталогизации

- Включенные разделы

- Префиксы

- Исключены книги

- Исключенные жанры

- Другие настройки

- Настраиваемый каталог обложек

- Дополнительные справочные ресурсы

Выбор книг для каталогизации

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

Если Вы хотите каталогизировать только часть вашей библиотеки, у Вас есть два варианта:

- Выберите книги, которые вы хотите каталогизировать. При выборе более чем одной книги в главном окне Calibre будут каталогизированы только выбранные книги.

- Используйте поле поиска или браузер тегов для фильтрации отображаемых книг. Каталогизируются только отображаемые книги.

Чтобы начать создание каталога, выберите пункт меню Конвертировать книги > Создать каталог книг в библиотеке calibre. Вы также можете добавить :guilabel: кнопку Создать каталог на панели инструментов в Настройки > Интерфейс > Панели инструментов для упрощения доступа к диалогу Создать каталог.

В Опции каталога, выберите AZW3, EPUB или MOBI в качестве формата Каталога. В поле Catalog title укажите имя, которое будет использоваться для сгенерированного каталога. Если каталог с таким же именем и форматом уже существует, он будет заменен вновь созданным каталогом.

Включение Отправить каталог на устройство автоматически будет загружать сгенерированный каталог на подключенное устройство по завершении.

Включенные разделы

Разделы, включенные с помощью галочки, будут включены в сгенерированный каталог:

- Authors - все книги, отсортированные по автору, представлены в виде списка. Книги, не относящиеся к серии, перечислены перед книгами серии.

- Titles - все книги, отсортированные по названию, представлены в виде списка.

- Series - все книги, входящие в серию, отсортированные по сериям, представлены в виде списка.

- Genres- отдельные жанры, представленные в списке, отсортированные по автору и серии.

- Recently Added- все книги, отсортированные в обратном хронологическом порядке. Список включает книги, добавленные за последние 30 дней, а затем ежемесячные списки добавленных книг.

- Descriptions- подробная страница описания каждой книги, включая миниатюру обложки и комментарии. Сортировка по автору, с книгами, не относящимися к серии, перечисленными перед книгами серии.

Префиксы

Префиксные правила позволяют добавлять префикс в списки книг при выполнении определённых критериев. Например, вы можете пометить прочитанные книги с помощью галочки или книги в своем списке желаний с помощью X.

Флажок в первом столбце разрешает это правило. Name - это имя правила, которое вы предоставляете. Field или Tags, или пользовательский столбец из вашей библиотеки. Value- содержимое Field для совпадения. При выполнении префиксного правила книга помечается выбранным Prefix.

В приведенном выше примере приведены три правила префикса:

1. Read book указывает, что книга с любой датой в специальном столбце с именем Last read будет иметь префикс с символом галочки.

2. Wishlist`элемент указывает, что любая книга с тегом guilabel:`Wishlist будет иметь префикс с символом X.

3. Library указывает, что любая книга со значением True (или Yes) в настраиваемом столбце :guilabel:`Available in Library“ будет иметь префикс с символом двойной стрелки.

Первое совпадение с правилом содержит префикс. Отключенные или неполные правила игнорируются.

Исключены книги

Правила исключения позволяют вам указывать книги, которые не будут каталогизированы.

Флажок в первом столбце разрешает это правило. Name - это имя правила, которое вы предоставляете. Field`или :guilabel:`Tags, или пользовательский столбец в вашей библиотеке. Value - содержимое Field для соответствия. Когда правило исключения будет выполнено, книга будет исключена из сгенерированного каталога.

В приведенном выше примере были указаны два правила исключения:

1. Правило Catalogs`указывает, что любая книга с тегом guilabel:`Catalog будет исключена из сгенерированного каталога.

2. Правило Archived указывает, что любая книга со значением Archived в настраиваемом столбце Status будет исключена из сгенерированного каталога.

Все правила оцениваются для каждой книги. Отключенные или неполные правила игнорируются.

Исключенные жанры

При создании каталога, теги в вашей базе данных - это жанры. Например теги Fiction и Nonfiction становятся жанрами в сгенерированном каталоге, с книгами, перечисленными в соответствующих списках жанров, на основе назначенных им тегам. Книга будет указана в каждом жанре, для которого у неё есть соответствующий тег.

Вы можете использовать определённые теги для других целей, возможно, +, чтобы указать читаемую книгу, или тег в скобках, вроде [Amazon Freebie], чтобы указать источник книги. : guilabel:Excluded genres „ regex позволяет указать теги, которые вы не хотите использовать в качестве жанров в сгенерированном каталоге. Шаблон регулярного исключения исключений `[.+] +`` исключает любые теги формы [tag], а также исключает знак + (умолчальный тег для чтения книг) от его использования в качестве жанров в сгенерированном каталоге.

Вы также можете использовать точное имя тега в регулярном выражении. Например, [Amazon Freebie] или [Project Gutenberg]. Если вы хотите перечислить несколько точных тегов для исключения, поместите между ними символ пайпа (вертикальной полосы): [Amazon Freebie]|[Project Gutenberg].

Results of regex shows you which tags will be excluded when the catalog is built, based on the tags in your database and the regex pattern you enter. The results are updated as you modify the regex pattern.

Другие настройки

Catalog cover указывает, следует ли создавать новую обложку или использовать существующую. Можно создать пользовательскую обложку для ваших каталогов - см. custom catalog covers для получения дополнительной информации. Если вы создали пользовательскую обложку, и хотите её повторно использовать, выберите Использовать существующую обложку. В противном случае выберите :guilabel: Сгенерировать новую обложку.

Extra Description note`указывает содержимое настраиваемого столбца, которое должно быть вставлено на страницу «Описание», рядом с миниатюрой обложки. Например, вы можете отобразить дату, когда вы последний раз читали книгу, используя пользовательский столбец :guilabel:`Last Read. Для расширенного использования функции Description note см. этот пост на форуме calibre <https://www.mobileread.com/forums/showpost.php?p=1335767&postcount=395>`_.

:guilabel:`Thumb width`задает предпочтение ширины для эскизов обложки, включенных в страницы описаний. Миниатюры кэшируются для повышения производительности. Чтобы поэкспериментировать с разной шириной, попробуйте создать каталог всего для нескольких книг, пока вы не определите предпочитаемую ширину, а затем сгенерируйте полный каталог. В первый раз, когда каталог создается с новой шириной эскиза, производительность будет медленнее, но последующие сборки этого каталога будут использовать кэш эскизов.

Merge with Comments`определяет настраиваемый столбец, содержимое которого будет недеструктивно слито с метаданными комментариев во время генерации каталога. Например, у вас может быть собственный столбец :guilabel:`Author bio, который вы хотите добавить к метаданным комментариев. Вы можете вставлять содержимое пользовательского столбца до или после раздела «Комментарии» и, при необходимости, разделять добавленный контент с помощью горизонтального разделителя. Настраиваемые типы столбцов - «текст, комментарии и составные».

Настраиваемый каталог обложек

С установленным плагином Generate Cover [https://www.mobileread.com/forums/showthread.php?t=124219] вы можете создавать собственные обложки для своего каталога. Чтобы установить плагин, перейдите по ссылке Настройки > Дополнительно > Плагины > Получить новые плагины.

Дополнительные справочные ресурсы

Для получения дополнительной информации о функции Каталога в calibre см. форум MobileRead Creating Catalogs - Start here [https://www.mobileread.com/forums/showthread.php?t=118556], где вы можете найти информацию о том, как настроить шаблоны каталогов и как отправить отчет об ошибке.

Чтобы задать вопросы или обсудить функции каталогизации calibre с другими пользователями, посетите форум MobileRead Library Management [https://www.mobileread.com/forums/forumdisplay.php?f=236].

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Руководства »
- Виртуальные библиотеки

Виртуальные библиотеки

В calibre Виртуальная библиотека - это способ указать calibre открывать только подмножество обычной библиотеки. Например, вы можете работать только с книгами определённого автора или с книгами, имеющими только определённый тег. Использование виртуальных библиотек является предпочтительным способом разделения вашей большой коллекции книг на небольшие подколлекции. Лучше разделить вашу библиотеку на несколько небольших библиотек, так как, когда вы хотите выполнить поиск по всей вашей коллекции, вы можете просто вернуться к полной библиотеке. В calibre нет возможности поиска одновременно по нескольким отдельным библиотекам.

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

Создание виртуальных библиотек

Чтобы использовать Виртуальную библиотеку, нажмите кнопку Виртуальная библиотека, расположенную слева от панели поиска, и выберите параметр Создать виртуальную библиотеку. В качестве первого примера давайте создадим виртуальную библиотеку, которая показывает нам только книги определенного автора. Нажмите на ссылку Authors, как показано на рисунке ниже, выберите автора, которого хотите использовать, и нажмите OK.

Диалоговое окно «Создать виртуальную библиотеку» заполнено для вас. Нажмите «ОК», и вы увидите, что новая виртуальная библиотека была создана, автоматически выбрана и отображает только книги выбранного автора. Что касается calibre, это значит, что ваша виртуальная библиотека содержит только книги выбранного автора.

Вы можете в любой момент вернуться в полную библиотеку, снова щелкнув по Virtual library и выбрав запись с именем: guilabel:<None>.

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

Примеры полезных виртуальных библиотек

- Книги добавленные в calibre в последний день

date:>1daysago

- Книги добавленные в calibre за последний месяц

date:>30daysago

- Книги с рейтингом 5 звезд

rating:5

- Книги с рейтингом не менее 4 звезд

rating:>=4

- Книги без рейтинга

rating:false

- Периодические издания, загруженные функцией Fetch News в calibre:

tags:=News and author:=calibre

- Книги без тегов:

tags:false

- Книги без обложек:

cover:false

Работа с виртуальными библиотеками

Вы можете отредактировать ранее созданную Виртуальную библиотеку или удалить ее, щелкнув по Виртуальная библиотека и выбрав соответствующее действие.

Вы можете сказать calibre, что вы всегда хотите применять определенную виртуальную библиотеку, когда открыта текущая библиотека, перейдя по адресу :guilabel: Настройки->Интерфейс->Поведение.

Вы можете быстро использовать текущий поиск в качестве временной Виртуальной библиотеки, нажав кнопку Виртуальная библиотека и выбрав запись * current search.

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

Использование виртуальных библиотек в поиске

Вы можете искать книги в виртуальной библиотеке, используя префикс vl:. Например, vl: Read найдет все книги в виртуальной библиотеке Read. Поиск vl: Read и vl:"Science Fiction" найдет все книги, которые находятся в виртуальных библиотеках Read и Science Fiction.

Значение, следующее за vl: должно быть именем виртуальной библиотеки. Если имя виртуальной библиотеки содержит пробелы, заключите его в кавычки.

Одно из применений поиска в виртуальной библиотеке - это сервер содержимого. В Preferences-Sharing over the net-Require username and password вы можете ограничить библиотеки calibre, видимые пользователю. Для каждой видимой библиотеки вы можете указать поисковое выражение, чтобы дополнительно ограничить количество просматриваемых книг. Используйте vl:"Virtual library name", чтобы ограничить количество книг теми, что находятся в виртуальной библиотеке.

Использование дополнительных ограничений

Вы можете дополнительно ограничить книги, показанные в виртуальной библиотеке, используя Дополнительные ограничения. Дополнительным ограничением является сохраненный ранее созданный вами поиск, который можно применить к текущей виртуальной библиотеке, чтобы дополнительно ограничить книги, отображаемые в виртуальной библиотеке. Например, если у вас есть виртуальная библиотека для книг, помеченных как Historical Fiction, и сохраненный поиск, который показывает непрочитанные книги, вы можете нажать кнопку Virtual Library и выбрать Дополнительные ограничения - для показа только непрочитанных книг по исторической фантастике. Чтобы узнать о сохраненных поисках, смотрите ved_searches.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Схема URL-адресов calibre://

Схема URL-адресов calibre://
calibre регистрируется как программа-обработчик для URL-адресов calibre://. Таким образом, вы можете использовать их для выполнения таких действий, как открытие книг, поиск книг и т. д. из других программ / документов или через командную строку. Например, запустив в командной строке следующее:

calibre calibre://switch-library/Some_Library

Откроет calibre с библиотекой под названием Some Library. Имена библиотек - это имя папки библиотеки с пробелами, заменёнными подчеркиванием. Специальное значение _ означает текущую библиотеку. Ниже описаны различные типы URL-адресов.

Вы можете размещать такие ссылки даже внутри HTML-файлов, документов Word или чего-то подобного, и операционная система автоматически запустит calibre для выполнения указанного действия.

- Переключение на определённую библиотеку

- Отображение определённой книги в calibre

- Открытие определённой книги в читалке электронных книг в указанном месте

- Поиск книг

- Открыть окно сведений о книге в какой-либо библиотеке

- Открыть примечания, связанные с автором/серией/и т.д.

- Шестнадцатеричное кодирование параметров URL-адреса

Переключение на определённую библиотеку

Синтаксис URL-адресов:

calibre://switch-library/Library_Name

Имена библиотеки - это имя папки библиотеки с пробелами, замененными подчеркиванием. Специальное значение``_`` означает текущую библиотеку. Вы также можете использовать: hex encoding для имен библиотек, это полезно, если в именах библиотек есть специальные символы, которые потребовали бы URL-кодирования имён. Имена библиотек в шестнадцатеричном формате выглядят так:

_hex_-AD23F4BC

Где часть после _hex_- префикса - это имя библиотеки, закодированное в UTF-8, каждый байт представлен двумя шестнадцатеричными символами.

Отображение определённой книги в calibre

Синтаксис URL-адресов:

calibre://show-book/Library_Name/book_id

Показ книги с `` book_id „“ (число) в calibre. Идентификаторы книг можно увидеть в интерфейсе calibre, наведя курсор на ссылку Нажмите, чтобы открыть на панели Book details. Это число в скобках в конце пути к папке с книгами.

Вы можете скопировать ссылку на текущую книгу, показанную в calibre, щёлкнув правой в Панели описания книги и выбрав Копировать ссылку на книгу.

Если поиск активен и книга не соответствует поиску, поиск очищается.

Если выбрана виртуальная библиотека, calibre будет использовать её при показе книги. Если книга не найдена в этой виртуальной библиотеке, виртуальная библиотека очищается.

Если вы хотите переключиться на определённую виртуальную библиотеку, используйте:

calibre://show-book/Library_Name/book_id?virtual_library=Library%20Name
or
calibre://show-book/Library_Name/book_id?encoded_virtual_library=hex_encoded_virtual_library_name

замена пробелов в имени виртуальной библиотеки на %20. Если книга не найдена в этой виртуальной библиотеке, виртуальная библиотека игнорируется.

Открытие определённой книги в читалке электронных книг в указанном месте

Синтаксис URL-адресов:

calibre://view-book/Library_Name/book_id/book_format?open_at=location

Здесь book_format - это формат книги, например, EPUB или MOBI, а location - это опциональное местоположение внутри книги. Простейший способ получить эти ссылки - открыть книгу в просмотрщике, затем в элементах управления просмотрщиком выбрать Go to → Location, и там будет ссылка, которую вы можете скопировать/вставить где-либо.

Поиск книг

Синтаксис URL-адресов:

calibre://search/Library_Name?q=query
calibre://search/Library_Name?eq=hex_encoded_query

Здесь допустим любой запрос search expression. Если поисковое выражение сложное, encode it as a hex string и используйте вместо этого eq. Если не указать запрос, текущий поиск будет очищен.

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

calibre://search/Library_Name?q=query&virtual_library=_

Если вы хотите переключиться на определённую виртуальную библиотеку, используйте::

calibre://search/Library_Name?virtual_library=Library%20Name
or
calibre://search/Library_Name?encoded_virtual_library=hex_encoded_virtual_library_name

заменяя пробелы в названии виртуальной библиотеки на %20.

Если вы выполняете поиск в calibre и хотите сгенерировать для него ссылку, щёлкните правой кнопкой мыши по строке поиска и выберите Копировать поиск как URL.

Открыть окно сведений о книге в какой-либо библиотеке

Синтаксис URL-адресов:

calibre://book-details/Library_Name/book_id

Это открывает окно сведений о книге для указанной книги из указанной библиотеки без изменения текущей библиотеки или выбранной книги.

Открыть примечания, связанные с автором/серией/и т.д.

Синтаксис URL-адресов:

calibre://book-details/Library_Name/Field_Name/id_Item_Id

Это открывает окно, показывающее примечания указанного элемента. Самый простой способ создать такие URL-адреса - показать примечания, желаемые в calibre, и нажать кнопку Copy URL, чтобы скопировать URL в буфер обмена и вставить его туда, где вам нужно.

Здесь Field_Name - это имя столбцов, таких как authors или tags. Для созданных пользователем столбцов замените ведущий символ # в имени столбца на подчеркивание, так что #mytags станет _mytags.

Дополнительно к указанию элементов по идентификатору с помощью Item_Id, вы также можете указывать их по имени с использованием val_Item_Name или hex_Hex_Encoded_Item_Name. Например:

calibre://book-details/Library_Name/authors/val_John%20Doe

Шестнадцатеричное кодирование параметров URL-адреса

Шестнадцатеричное кодирование параметров URL-адреса выполняется сначала путем кодирования параметра как байтов UTF-8, а затем замены каждого байта двумя шестнадцатеричными символами, представляющими байт. Например, строка «abc» - это байты «0x61 0x62 и 0x63» в UTF-8, поэтому закодированная версия - это строка: «616263».

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Настройка calibre

Настройка calibre
calibre имеет высокомодульную конструкцию. Все его части можно настроить. Здесь вы узнаете:

- как использовать переменные среды и твики для настройки поведения calibre,

- как указать свои собственные статические ресурсы, такие как значки и шаблоны, чтобы переопределить умолчальные значения

- как использовать плагины для добавления функциональности в calibre.

- как делиться темами значков и плагинами с другими пользователями calibre.

- Чтобы узнать, как создавать рецепты для добавления новых источников онлайн-контента в calibre, посетите Раздел Добавление любимых новостных веб-сайтов.

Примечание

Обратите внимание, что, хотя темы значков и плагины индексируются и загружаются с помощью встроенного средства обновления calibre, они не являются частью calibre, и их исходный код и поддержка находятся на форумах Mobileread [https://www.mobileread.com/forums/forumdisplay.php?f=166] в своих ветках поддержки.

- Переменные окружения

- Твики

- Переопределение значков, шаблонов и т. д.

- Создание собственной темы значков для calibre

- Настройка calibre с помощью плагинов

Переменные окружения

- CALIBRE_CONFIG_DIRECTORY - установить папку, в которой файлы конфигурации хранятся/читаются.

- CALIBRE_TEMP_DIR- установить временную папку, используемую calibre

- CALIBRE_CACHE_DIRECTORY - установить папку calibre, используемую для кэширования постоянных данных между сеансами

- CALIBRE_OVERRIDE_DATABASE_PATH - указать полный путь к metadata.db. Используя эту переменную, вы можете хранить metadata.db вне папки библиотеки. Полезно, если папка вашей библиотеки находится на сетевом диске, который не поддерживает блокировку файлов.

- CALIBRE_DEVELOP_FROM - используется для запуска из среды разработки calibre. Смотрите Настройка среды разработки calibre.

- CALIBRE_OVERRIDE_LANG - используется для принудительного выбора языка интерфейса (ISO 639 код языка)

- CALIBRE_TEST_TRANSLATION - используется для тестирования файла перевода .po (должен быть путь к файлу .po)

- CALIBRE_NO_NATIVE_FILEDIALOGS - заставляет calibre не использовать собственные файловые диалоги для выбора файлов/папок.

- CALIBRE_NO_NATIVE_MENUBAR - заставляет calibre не создавать собственное (глобальное) меню в Ubuntu Unity и аналогичных средах рабочего стола Linux. Вместо этого меню, как обычно, помещается внутри окна.

- CALIBRE_USE_SYSTEM_THEME - по умолчанию в Linux calibre использует собственный встроенный стиль Qt. Это сделано для того, чтобы избежать сбоев и зависаний, вызванных несовместимостью между версией Qt calibre, на которой построена версия, и общесистемной Qt. Обратной стороной является то, что calibre может не соответствовать внешнему виду системы. Если вы установите эту переменную среды в Linux, это приведет к тому, что calibre будет использовать системную тему - остерегайтесь сбоев и зависаний.

- CALIBRE_SHOW_DEPRECATION_WARNINGS - заставляет calibre печатать предупреждения об устаревании в стандартный вывод. Полезно для разработчиков calibre.

- CALIBRE_NO_DEFAULT_PROGRAMS - не позволять calibre автоматически регистрировать типы файлов, с которыми он может работать в Windows.

- CALIBRE_USE_SYSTEM_CERTIFICATES - заставьте calibre использовать системное хранилище сертификатов для проверки SSL-сертификатов вместо собственного хранилища сертификатов в Windows и macOS.

- CALIBRE_NO_ICONS_IN_MENUS - Отключить значки в меню

- QT_QPA_PLATFORM - в Linux установите для этого параметра значение wayland, чтобы заставить calibre использовать Wayland, и xcb, чтобы принудительно использовать X11.

- SYSFS_PATH - использовать, если sysfs монтируется где-то, кроме /sys

- http_proxy, https_proxy - используется в Linux для указания HTTP(S) прокси

Смотрите Как настроить переменные окружения в Windows [https://www.computerhope.com/issues/ch000549.htm]. Если вы на macOS можно описать переменные окружения в ~/Library/Preferences/calibre/macos-env.txt указав по одной переменной окружения на строку, к примеру:

CALIBRE_DEVELOP_FROM=$HOME/calibre-src/src
CALIBRE_NO_NATIVE_FILEDIALOGS=1
CALIBRE_CONFIG_DIRECTORY=~/.config/calibre

Твики

Твики — это небольшие изменения, через которые вы можете управлять различными аспектами поведения calibre. Вы можете изменить их, перейдя в меню «Параметры->Расширенные->Твики». Ниже приведены значения настроек по умолчанию

#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
# License: GPLv3 Copyright: 2010, Kovid Goyal <kovid at kovidgoyal.net>

# Contains various tweaks that affect calibre behavior. Only edit this file if
# you know what you are doing. If you delete this file, it will be recreated from
# defaults.

#: Auto increment series index
# The algorithm used to assign a book added to an existing series a series number.
# New series numbers assigned using this tweak are always integer values, except
# if a constant non-integer is specified.
# Possible values are:
# next - First available integer larger than the largest existing number
# first_free - First available integer larger than 0
# next_free - First available integer larger than the smallest existing number
# last_free - First available integer smaller than the largest existing number. Return largest existing + 1 if no free number is found
# const - Assign the number 1 always
# no_change - Do not change the series index
# a number - Assign that number always. The number is not in quotes. Note that 0.0 can be used here.
# Examples:
# series_index_auto_increment = 'next'
# series_index_auto_increment = 'next_free'
# series_index_auto_increment = 16.5
#
# Set the use_series_auto_increment_tweak_when_importing tweak to True to
# use the above values when importing/adding books. If this tweak is set to
# False (the default) then the series number will be set to 1 if it is not
# explicitly set during the import. If set to True, then the
# series index will be set according to the series_index_auto_increment setting.
# Note that the use_series_auto_increment_tweak_when_importing tweak is used
# only when a value is not provided during import. If the importing regular
# expression produces a value for series_index, or if you are reading metadata
# from books and the import plugin produces a value, then that value will
# be used irrespective of the setting of the tweak.
series_index_auto_increment = 'next'
use_series_auto_increment_tweak_when_importing = False

#: Add separator after completing an author name
# Set this if the completion separator should be appended to the end of the
# completed text to automatically begin a new completion operation for authors.
# It can be either True or False authors_completer_append_separator = False

#: Author sort name algorithm
# The algorithm used to copy author to author_sort.
# Possible values are:
# invert: use "fn ln" -> "ln, fn"
# copy : copy author to author_sort without modification
# comma : use 'copy' if there is a ',' in the name, otherwise use 'invert'
# nocomma : "fn ln" -> "ln fn" (without the comma)
# When this tweak is changed, the author_sort values stored with each author
# must be recomputed by right-clicking on an author in the left-hand tags
# panel, selecting 'Manage authors', and pressing
# 'Recalculate all author sort values'.
#
# The author_name_suffixes are words that are ignored when they occur at the
# end of an author name. The case of the suffix is ignored and trailing
# periods are automatically handled.
#
# The same is true for author_name_prefixes.
#
# The author_name_copywords are a set of words which, if they occur in an
# author name, cause the automatically generated author sort string to be
# identical to the author's name. This means that the sort for a string like
# "Acme Inc." will be "Acme Inc." instead of "Inc., Acme".
#
# If author_use_surname_prefixes is enabled, any of the words in
# author_surname_prefixes will be treated as a prefix to the surname, if they
# occur before the surname. So for example, "John von Neumann" would be sorted
# as "von Neumann, John" and not "Neumann, John von".
author_sort_copy_method = 'comma'
author_name_suffixes = ('Jr', 'Sr', 'Inc', 'Ph.D', 'Phd',
'MD', 'M.D', 'I', 'II', 'III', 'IV',
'Junior', 'Senior')
author_name_prefixes = ('Mr', 'Mrs', 'Ms', 'Dr', 'Prof')
author_name_copywords = (
'Agency', 'Corporation', 'Company', 'Co.', 'Council',
'Committee', 'Inc.', 'Institute', 'National', 'Society', 'Club', 'Team',
'Software', 'Games', 'Entertainment', 'Media', 'Studios',
)
author_use_surname_prefixes = False author_surname_prefixes = ('da', 'de', 'di', 'la', 'le', 'van', 'von')

#: Splitting multiple author names
# By default, calibre splits a string containing multiple author names on
# ampersands and the words "and" and "with". You can customize the splitting
# by changing the regular expression below. Strings are split on whatever the
# specified regular expression matches, in addition to ampersands.
# Default: r'(?i),?\s+(and|with)\s+'
authors_split_regex = r'(?i),?\s+(and|with)\s+'

#: Use author_sort and/or series_sort for names in Tag browser
# Set which author or series field is used to display as the item name in the
# Tag browser (the list of authors, series, publishers etc on the left hand
# side). The choices for author are 'author' and 'author_sort'. The choices for
# series are 'series' and 'series_sort'. This tweak affects only what is
# displayed under the category in the Tag browser and Content server. Please
# note that if you set this to …_sort, it is possible to see duplicate names in
# the list because although it is guaranteed that author and series names are
# unique, there is no such guarantee for their sort values. Showing duplicates
# won't break anything but it could lead to some confusion. The tooltip for an
# item will show the item's name no matter how you set this tweak.
# Examples:
# categories_use_field_for_author_name = 'author'
# categories_use_field_for_author_name = 'author_sort'
# categories_use_field_for_series_name = 'series'
# categories_use_field_for_series_name = 'series_sort'
categories_use_field_for_author_name = 'author'
categories_use_field_for_series_name = 'series'

#: Control partitioning of Tag browser
# When partitioning the Tag browser, the format of the subcategory label is
# controlled by a template: categories_collapsed_name_template if sorting by
# name, categories_collapsed_rating_template if sorting by average rating, and
# categories_collapsed_popularity_template if sorting by popularity. There are
# two variables available to the template: first and last. The variable 'first'
# is the initial item in the subcategory, and the variable 'last' is the final
# item in the subcategory. Both variables are 'objects'; they each have multiple
# values that are obtained by using a suffix. For example, first.name for an
# author category will be the name of the author. The sub-values available are:
# name: the printable name of the item
# count: the number of books that references this item
# avg_rating: the average rating of all the books referencing this item
# sort: the sort value. For authors, this is the author_sort for that author
# category: the category (e.g., authors, series) that the item is in.
# Note that the "r'" in front of the { is necessary if there are backslashes
# (\ characters) in the template. It doesn't hurt anything to leave it there
# even if there aren't any backslashes.
categories_collapsed_name_template = r'{first.sort:shorten(4,,0)} - {last.sort:shorten(4,,0)}'
categories_collapsed_rating_template = r'{first.avg_rating:4.2f:ifempty(0)} - {last.avg_rating:4.2f:ifempty(0)}'
categories_collapsed_popularity_template = r'{first.count:d} - {last.count:d}'

#: Specify columns to sort the booklist by on startup
# Provide a set of columns to be sorted on when calibre starts.
# The argument is None if saved sort history is to be used
# otherwise it is a list of column,order pairs. Column is the
# lookup/search name, found using the tooltip for the column
# Order is 0 for ascending, 1 for descending.
# For example, set it to [('authors',0),('title',0)] to sort by
# title within authors.
sort_columns_at_startup = None

#: Control how dates are displayed
# Format to be used for publication date and the timestamp (date).
# A string controlling how the publication date is displayed in the GUI
# d the day as number without a leading zero (1 to 31)
# dd the day as number with a leading zero (01 to 31)
# ddd the abbreviated localized day name (e.g. 'Mon' to 'Sun')
# dddd the long localized day name (e.g. 'Monday' to 'Sunday')
# M the month as number without a leading zero (1-12)
# MM the month as number with a leading zero (01-12)
# MMM the abbreviated localized month name (e.g. 'Jan' to 'Dec')
# MMMM the long localized month name (e.g. 'January' to 'December')
# yy the year as two digit number (00-99)
# yyyy the year as four digit number
# h the hours without a leading 0 (0 to 11 or 0 to 23, depending on am/pm)
# hh the hours with a leading 0 (00 to 11 or 00 to 23, depending on am/pm)
# m the minutes without a leading 0 (0 to 59)
# mm the minutes with a leading 0 (00 to 59)
# s the seconds without a leading 0 (0 to 59)
# ss the seconds with a leading 0 (00 to 59)
# ap use a 12-hour clock instead of a 24-hour clock, with "ap" replaced by the lowercase localized string for am or pm
# AP use a 12-hour clock instead of a 24-hour clock, with "AP" replaced by the uppercase localized string for AM or PM
# aP use a 12-hour clock instead of a 24-hour clock, with "aP" replaced by the localized string for am or pm
# Ap use a 12-hour clock instead of a 24-hour clock, with "Ap" replaced by the localized string for AM or PM
# iso the date with time and timezone. Must be the only format present
# For example, given the date of 9 Jan 2010, the following formats show
# MMM yyyy ==> Jan 2010 yyyy ==> 2010 dd MMM yyyy ==> 09 Jan 2010
# MM/yyyy ==> 01/2010 d/M/yy ==> 9/1/10 yy ==> 10
#
# publication default if not set: MMM yyyy
# timestamp default if not set: dd MMM yyyy
# last_modified_display_format if not set: dd MMM yyyy gui_pubdate_display_format = 'MMM yyyy'
gui_timestamp_display_format = 'dd MMM yyyy'
gui_last_modified_display_format = 'dd MMM yyyy'

#: Control sorting of titles and series in the library display
# Control title and series sorting in the library view. If set to
# 'library_order', the title sort field will be used instead of the title.
# Unless you have manually edited the title sort field, leading articles such as
# The and A will be ignored. If set to 'strictly_alphabetic', the titles will be
# sorted as-is (sort by title instead of title sort). For example, with
# library_order, The Client will sort under 'C'. With strictly_alphabetic, the
# book will sort under 'T'.
# This flag affects calibre's library display. It has no effect on devices. In
# addition, titles for books added before changing the flag will retain their
# order until the title is edited. Editing a title and hitting Enter
# without changing anything is sufficient to change the sort. Or you can use
# the 'Update title sort' action in the Bulk metadata edit dialog to update
# it for many books at once.
title_series_sorting = 'library_order'

#: Control formatting of title and series when used in templates
# Control how title and series names are formatted when saving to disk/sending
# to device. The behavior depends on the field being processed. If processing
# title, then if this tweak is set to 'library_order', the title will be
# replaced with title_sort. If it is set to 'strictly_alphabetic', then the
# title will not be changed. If processing series, then if set to
# 'library_order', articles such as 'The' and 'An' will be moved to the end. If
# set to 'strictly_alphabetic', the series will be sent without change.
# For example, if the tweak is set to library_order, "The Lord of the Rings"
# will become "Lord of the Rings, The". If the tweak is set to
# strictly_alphabetic, it would remain "The Lord of the Rings". Note that the
# formatter function raw_field will return the base value for title and
# series regardless of the setting of this tweak.
save_template_title_series_sorting = 'library_order'

#: Set the list of words considered to be "articles" for sort strings
# Set the list of words that are to be considered 'articles' when computing the
# title sort strings. The articles differ by language. By default, calibre uses
# a combination of articles from English and whatever language the calibre user
# interface is set to. In addition, in some contexts where the book language is
# available, the language of the book is used. You can change the list of
# articles for a given language or add a new language by editing
# per_language_title_sort_articles. To tell calibre to use a language other
# than the user interface language, set, default_language_for_title_sort. For
# example, to use German, set it to 'deu'. A value of None means the user
# interface language is used. The setting title_sort_articles is ignored
# (present only for legacy reasons).
per_language_title_sort_articles = {
# English
'eng': (r'A\s+', r'The\s+', r'An\s+'),
# Esperanto
'epo': (r'La\s+', r"L'", 'L´'),
# Spanish
'spa': (r'El\s+', r'La\s+', r'Lo\s+', r'Los\s+', r'Las\s+', r'Un\s+', r'Una\s+', r'Unos\s+', r'Unas\s+'),
# French
'fra': (r'Le\s+', r'La\s+', r"L'", r'L´', r'L’', r'Les\s+', r'Un\s+', r'Une\s+', r'Des\s+', r'De\s+La\s+', r'De\s+', r"D'", r'D´', r'D’'),
# Polish
'pol': (),
# Italian
'ita': (r'Lo\s+', r'Il\s+', r"L'", r'L´', r'La\s+', r'Gli\s+', r'I\s+', r'Le\s+', r'Uno\s+', r'Un\s+', r'Una\s+', "rUn'", r'Un´', r'Dei\s+', r'Degli\s+', r'Delle\s+', r'Del\s+', r'Della\s+', r'Dello\s+', r"Dell'", r'Dell´'),
# Portuguese
'por': (r'A\s+', r'O\s+', r'Os\s+', r'As\s+', r'Um\s+', r'Uns\s+', r'Uma\s+', r'Umas\s+'),
# Romanian
'ron': (r'Un\s+', r'O\s+', r'Nişte\s+'),
# German
'deu': (r'Der\s+', r'Die\s+', r'Das\s+', r'Den\s+', r'Ein\s+', r'Eine\s+', r'Einen\s+', r'Dem\s+', r'Des\s+', r'Einem\s+', r'Eines\s+'),
# Dutch
'nld': (r'De\s+', r'Het\s+', r'Een\s+', r"'n\s+", r"'s\s+", r'Ene\s+', r'Ener\s+', r'Enes\s+', r'Den\s+', r'Der\s+', r'Des\s+', r"'t\s+"),
# Swedish
'swe': (r'En\s+', r'Ett\s+', r'Det\s+', r'Den\s+', r'De\s+'),
# Turkish
'tur': (r'Bir\s+',),
# Afrikaans
'afr': (r"'n\s+", r'Die\s+'),
# Greek
'ell': (r'O\s+', r'I\s+', r'To\s+', r'Ta\s+', r'Tus\s+', r'Tis\s+', r"'Enas\s+", r"'Mia\s+", r"'Ena\s+", r"'Enan\s+"),
# Hungarian
'hun': (r'A\s+', r'Az\s+', r'Egy\s+'),
}
default_language_for_title_sort = None title_sort_articles = r'^(A|The|An)\s+'

#: Specify a folder calibre should connect to at startup
# Specify a folder that calibre should connect to at startup using
# connect_to_folder. This must be a full path to the folder. If the folder does
# not exist when calibre starts, it is ignored.
# Example for Windows:
# auto_connect_to_folder = 'C:/Users/someone/Desktop/testlib'
# Example for other operating systems:
# auto_connect_to_folder = '/home/dropbox/My Dropbox/someone/library'
auto_connect_to_folder = ''

#: Specify renaming rules for SONY collections
# Specify renaming rules for SONY collections. This tweak is only applicable if
# metadata management is set to automatic. Collections on SONYs are named
# depending upon whether the field is standard or custom. A collection derived
# from a standard field is named for the value in that field.
#
# For example, if the standard 'series' column contains the value 'Darkover', then the
# collection name is 'Darkover'. A collection derived from a custom field will
# have the name of the field added to the value. For example, if a custom series
# column named 'My Series' contains the name 'Darkover', then the collection
# will by default be named 'Darkover (My Series)'. For purposes of this
# documentation, 'Darkover' is called the value and 'My Series' is called the
# category. If two books have fields that generate the same collection name,
# then both books will be in that collection.
#
# This set of tweaks lets you specify for a standard or custom field how
# the collections are to be named. You can use it to add a description to a
# standard field, for example 'Foo (Tag)' instead of the 'Foo'. You can also use
# it to force multiple fields to end up in the same collection.
#
# For example, you could force the values in 'series', '#my_series_1', and
# '#my_series_2' to appear in collections named 'some_value (Series)', thereby
# merging all of the fields into one set of collections.
#
# There are two related tweaks. The first determines the category name to use
# for a metadata field. The second is a template, used to determines how the
# value and category are combined to create the collection name.
# The syntax of the first tweak, sony_collection_renaming_rules, is:
# {'field_lookup_name':'category_name_to_use', 'lookup_name':'name', ...}
#
# The second tweak, sony_collection_name_template, is a template. It uses the
# same template language as plugboards and save templates. This tweak controls
# how the value and category are combined together to make the collection name.
# The only two fields available are {category} and {value}. The {value} field is
# never empty. The {category} field can be empty. The default is to put the
# value first, then the category enclosed in parentheses, it isn't empty:
# '{value} {category:|(|)}'
#
# Examples: The first three examples assume that the second tweak
# has not been changed.
#
# 1) I want three series columns to be merged into one set of collections. The
# column lookup names are 'series', '#series_1' and '#series_2'. I want nothing
# in the parenthesis. The value to use in the tweak value would be:
# sony_collection_renaming_rules={'series':'', '#series_1':'', '#series_2':''}
#
# 2) I want the word '(Series)' to appear on collections made from series, and
# the word '(Tag)' to appear on collections made from tags. Use:
# sony_collection_renaming_rules={'series':'Series', 'tags':'Tag'}
#
# 3) I want 'series' and '#myseries' to be merged, and for the collection name
# to have '(Series)' appended. The renaming rule is:
# sony_collection_renaming_rules={'series':'Series', '#myseries':'Series'}
#
# 4) Same as example 2, but instead of having the category name in parentheses
# and appended to the value, I want it prepended and separated by a colon, such
# as in Series: Darkover. I must change the template used to format the category name
#
# The resulting two tweaks are:
# sony_collection_renaming_rules={'series':'Series', 'tags':'Tag'}
# sony_collection_name_template='{category:||: }{value}'
sony_collection_renaming_rules = {}
sony_collection_name_template = '{value}{category:| (|)}'

#: Specify how SONY collections are sorted
# Specify how SONY collections are sorted. This tweak is only applicable if
# metadata management is set to automatic. You can indicate which metadata is to
# be used to sort on a collection-by-collection basis. The format of the tweak
# is a list of metadata fields from which collections are made, followed by the
# name of the metadata field containing the sort value.
# Example: The following indicates that collections built from pubdate and tags
# are to be sorted by the value in the custom column '#mydate', that collections
# built from 'series' are to be sorted by 'series_index', and that all other
# collections are to be sorted by title. If a collection metadata field is not
# named, then if it is a series- based collection it is sorted by series order,
# otherwise it is sorted by title order.
# [(['pubdate', 'tags'],'#mydate'), (['series'],'series_index'), (['*'], 'title')]
# Note that the bracketing and parentheses are required. The syntax is
# [ ( [list of fields], sort field ) , ( [ list of fields ] , sort field ) ]
# Default: empty (no rules), so no collection attributes are named.
sony_collection_sorting_rules = []

#: Control how tags are applied when copying books to another library
# Set this to True to ensure that tags in 'Tags to add when adding
# a book' are added when copying books to another library add_new_book_tags_when_importing_books = False

#: Set the maximum number of sort 'levels'
# Set the maximum number of sort 'levels' that calibre will use to resort the
# library after certain operations such as searches or device insertion. Each
# sort level adds a performance penalty. If the database is large (thousands of
# books) the penalty might be noticeable. If you are not concerned about multi-
# level sorts, and if you are seeing a slowdown, reduce the value of this tweak.
maximum_resort_levels = 5

#: Choose whether dates are sorted using visible fields
# Date values contain both a date and a time. When sorted, all the fields are
# used, regardless of what is displayed. Set this tweak to True to use only
# the fields that are being displayed.
sort_dates_using_visible_fields = False

#: Fuzz value for trimming covers
# The value used for the fuzz distance when trimming a cover.
# Colors within this distance are considered equal.
# The distance is in absolute intensity units.
cover_trim_fuzz_value = 10

#: Control behavior of the book list
# You can control the behavior of double clicks and pressing Enter on the books
# list. Choices: open_viewer, do_nothing, show_book_details,
# show_locked_book_details, edit_cell, edit_metadata. Selecting anything other
# than open_viewer, show_book_details, or show_locked_book_details has the side
# effect of disabling editing a field using a single click.
# Default: open_viewer.
# Example: doubleclick_on_library_view = 'do_nothing'
# You can also control whether the book list scrolls per item or
# per pixel. Default is per item.
doubleclick_on_library_view = 'open_viewer'
enter_key_behavior = 'do_nothing'
horizontal_scrolling_per_column = False vertical_scrolling_per_row = False

#: Language to use when sorting
# Setting this tweak will force sorting to use the
# collating order for the specified language. This might be useful if you run
# calibre in English but want sorting to work in the language where you live.
# Set the tweak to the desired ISO 639-1 language code, in lower case.
# You can find the list of supported locales at
# https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
# Default: locale_for_sorting = '' -- use the language calibre displays in
# Example: locale_for_sorting = 'fr' -- sort using French rules.
# Example: locale_for_sorting = 'nb' -- sort using Norwegian rules.
locale_for_sorting = ''

#: The number of seconds to wait before sending emails
# The number of seconds to wait before sending emails when using a
# public email server like GMX/Hotmail/Gmail. Default is: 5 minutes
# Setting it to lower may cause the server's SPAM controls to kick in,
# making email sending fail. Changes will take effect only after a restart of
# calibre. You can also change the list of hosts that calibre considers
# to be public relays here. Any relay host ending with one of the suffixes
# in the list below will be considered a public email server.
public_smtp_relay_delay = 301 public_smtp_relay_host_suffixes = ['gmail.com', 'live.com', 'gmx.com', 'outlook.com']

#: The maximum width and height for covers saved in the calibre library
# All covers in the calibre library will be resized, preserving aspect ratio,
# to fit within this size. This is to prevent slowdowns caused by extremely
# large covers maximum_cover_size = (1650, 2200)

#: Where to send downloaded news
# When automatically sending downloaded news to a connected device, calibre
# will by default send it to the main memory. By changing this tweak, you can
# control where it is sent. Valid values are "main", "carda", "cardb". Note
# that if there isn't enough free space available on the location you choose,
# the files will be sent to the location with the most free space.
send_news_to_device_location = 'main'

#: Unified toolbar on macOS
# If you enable this option and restart calibre, the toolbar will be 'unified'
# with the titlebar as is normal for macOS applications. However, doing this has
# various bugs, for instance the minimum width of the toolbar becomes twice
# what it should be and it causes other random bugs on some systems, so turn it
# on at your own risk!
unified_title_toolbar_on_osx = False

#: Save original file when converting/polishing from same format to same format
# When calibre does a conversion from the same format to the same format, for
# example, from EPUB to EPUB, the original file is saved, so that in case the
# conversion is poor, you can tweak the settings and run it again. By setting
# this to False you can prevent calibre from saving the original file.
# Similarly, by setting save_original_format_when_polishing to False you can
# prevent calibre from saving the original file when polishing.
save_original_format = True save_original_format_when_polishing = True

#: Number of recently viewed books to show
# Right-clicking the "View" button shows a list of recently viewed books. Control
# how many should be shown, here.
gui_view_history_size = 15

#: Change the font size of the Book details panel in the interface
# Change the font size at which book details are rendered in the side panel and
# comments are rendered in the metadata edit dialog. Set it to a positive or
# negative number to increase or decrease the font size.
change_book_details_font_size_by = 0

#: What format to default to when using the "Unpack book" feature
# The "Unpack book" feature of calibre allows direct editing of a book format.
# If multiple formats are available, calibre will offer you a choice
# of formats, defaulting to your preferred output format if it is available.
# Set this tweak to a specific value of 'EPUB' or 'AZW3' to always default
# to that format rather than your output format preference.
# Set to a value of 'remember' to use whichever format you chose last time you
# used the "Unpack book" feature.
# Examples:
# default_tweak_format = None (Use output format)
# default_tweak_format = 'EPUB'
# default_tweak_format = 'remember'
default_tweak_format = None

#: Do not preselect a completion when editing authors/tags/series/etc.
# This means that you can make changes and press Enter and your changes will
# not be overwritten by a matching completion. However, if you wish to use the
# completions you will now have to press Tab to select one before pressing
# Enter. Which technique you prefer will depend on the state of metadata in
# your library and your personal editing style.
#
# If preselect_first_completion is False and you want Tab to accept what you
# typed instead of the first completion then set tab_accepts_uncompleted_text
# to True. If you do this then to select from the completions you must press
# the Down or Up arrow keys. The tweak tab_accepts_uncompleted_text is ignored
# if preselect_first_completion is True preselect_first_completion = False tab_accepts_uncompleted_text = False

#: Completion mode when editing authors/tags/series/etc.
# By default, when completing items, calibre will show you all the candidates
# that start with the text you have already typed. You can instead have it show
# all candidates that contain the text you have already typed. To do this, set
# completion_mode to 'contains'. For example, if you type asi it will match both
# Asimov and Quasimodo, whereas the default behavior would match only Asimov.
completion_mode = 'prefix'

#: Sort the list of libraries alphabetically
# The list of libraries in the Copy to library and Quick switch menus are
# normally sorted by most used. However, if there are more than a certain
# number of such libraries, the sorting becomes alphabetic. You can set that
# number here. The default is ten libraries.
many_libraries = 10

#: Choose available output formats for conversion
# Restrict the list of available output formats in the conversion dialogs.
# For example, if you only want to convert to EPUB and AZW3, change this to
# restrict_output_formats = ['EPUB', 'AZW3']. The default value of None causes
# all available output formats to be present.
restrict_output_formats = None

#: Set the thumbnail image quality used by the Content server
# The quality of a thumbnail is largely controlled by the compression quality
# used when creating it. Set this to a larger number to improve the quality.
# Note that the thumbnails get much larger with larger compression quality
# numbers.
# The value can be between 50 and 99 content_server_thumbnail_compression_quality = 75

#: Image file types to treat as e-books when dropping onto the "Book details" panel
# Normally, if you drop any image file in a format known to calibre onto the
# "Book details" panel, it will be used to set the cover. If you want to store
# some image types as e-books instead, you can set this tweak.
# Examples:
# cover_drop_exclude = {'tiff', 'webp'}
cover_drop_exclude = ()

#: Exclude fields when copy/pasting metadata
# You can ask calibre to not paste some metadata fields when using the
# Edit metadata->Copy metadata/Paste metadata actions. For example,
# exclude_fields_on_paste = ['cover', 'timestamp', '#mycolumn']
# to prevent pasting of the cover, Date and custom column, mycolumn.
# You can also add a shortcut in Preferences->Shortcuts->Edit metadata
# to paste metadata ignoring this tweak.
exclude_fields_on_paste = []

#: Skip internet connected check
# Skip checking whether the internet is available before downloading news.
# Useful if for some reason your operating systems network checking
# facilities are not reliable (for example NetworkManager on Linux).
skip_network_check = False

#: Tab stop width in the template editor
# Sets the width of the tab stop in the template editor in "average characters".
# For example, a value of 1 results in a space with the width of one average character.
template_editor_tab_stop_width = 4

#: Value for undefined numbers when sorting
# Sets the value to use for undefined numbers when sorting.
# For example, the value -10 sorts undefined numbers as if they were set to -10.
# Use 'maximum' for the largest possible number. Use 'minimum' for the smallest
# possible number. Quotes are optional if entering a number.
# Examples:
# value_for_undefined_numbers_when_sorting = -100
# value_for_undefined_numbers_when_sorting = '2'
# value_for_undefined_numbers_when_sorting = -0.01
# value_for_undefined_numbers_when_sorting = 'minimum'
# value_for_undefined_numbers_when_sorting = 'maximum'
value_for_undefined_numbers_when_sorting = 0

#: Allow template database functions in composite columns
# If True then the template database functions book_values() and book_count()
# can be used in composite custom columns. Note: setting this tweak to True and
# using these functions in composites can be very slow.
# Default: False allow_template_database_functions_in_composites = False

#: Change the programs that are run when opening files/URLs
# By default, calibre passes URLs to the operating system to open using
# whatever default programs are configured there. Here you can override
# that by specifying the program to use, per URL type. For local files,
# the type is "file" and for web links it is "http*". For example:
# openers_by_scheme = { "http*": "firefox %u" } will make calibre run Firefox
# for https://whatever URLs. %u is replaced by the URL to be opened. The scheme
# takes a glob pattern allowing a single entry to match multiple URL types.
openers_by_scheme = {}

#: Set the first day of the week for calendar popups
# It must be one of the values Default, Sunday, Monday, Tuesday, Wednesday,
# Thursday, Friday, or Saturday, all in English, spelled exactly as shown.
calendar_start_day_of_week = 'Default'

#: East Asian language to use for transliteration
# Setting this tweak will make calibre use the specified language as the "base"
# language when transliterating East Asian languages to English. This might be
# useful if you run calibre in English but want text transliterated to
# Japanese instead of Chinese. The valid values are:
# 'ja' for Japanese
# 'kr' for Korean
# 'vn' for Vietnamese
# 'zh' for Chinese
# Any other value will use the language set in calibre preferences as the base
# language. A base language other than those in the above list causes transliteration
# with a base language of Chinese.
# Example: east_asian_base_language = 'ja'
east_asian_base_language = ''

Переопределение значков, шаблонов и т. д.

Примечание

У calibre есть прямая поддержка тем значков, несколько из этих тем можно использовать, перейдя в Параметры → Интерфейс → Оформление → Изменить набор значков. Темы значков используют описанный ниже механизм для переопределения статических ресурсов.

calibre позволяет переопределять статические ресурсы, такие как значки, JavaScript и шаблоны для оболочки метаданных, каталогов и т. д., с помощью настраиваемых версий, которые вам нравятся. Все статические ресурсы хранятся в подпапке ресурсов места установки calibre. В Windows это обычно C:\Program Files\Calibre2\app\resources. В macOS /Applications/calibre.app/Contents/Resources/resources/. В Linux, если вы используете бинарный установщик с веб-сайта калибра, это будет /opt/calibre/resources. Эти пути могут меняться в зависимости от того, где вы решили установить calibre.

Вы не должны изменять файлы в этой папке ресурсов, так как ваши изменения будут перезаписаны при следующем обновлении calibre. Вместо этого перейдите в Preferences-Advanced-Miscellaneous и щелкните Открыть папку конфигурации calibre. В этой папке конфигурации создайте подпапку с именем resources и поместите в неё файлы, которые вы хотите переопределить. Поместите файлы в соответствующие подпапки, например поместите изображения в resources/images и т. д. calibre автоматически будет использовать ваш пользовательский файл вместо встроенного при следующем запуске.

Например, если вы хотите изменить значок для действия :guilabel: Remove books, вы сначала должны заглянуть во встроенную папку ресурсов и увидеть, что соответствующий файл resources/images/remove_books.png `. Предполагая, что у вас есть альтернативный значок в формате PNG с именем :file:`my_remove_books.png, вы должны сохранить его в папке конфигурации как resources/images/remove_books.png. Все значки, используемые пользовательским интерфейсом calibre, находятся в resources/images и её подпапках. Размещение файла переопределения здесь будет иметь даже более высокий приоритет, чем пользовательская тема значков.

Создание собственной темы значков для calibre

Если вы создали красивый набор значков и хотите поделиться ими с другими пользователями calibre с помощью встроенной поддержки тем значков calibre, вы можете легко упаковать свои значки в тему. Для этого перейдите в Preferences-Miscellaneous-Create icon theme, выберите папку, в которую вы поместили свои значки (обычно это папка resources/images в папке конфигурации calibre, как описано выше) . Затем заполните метаданные темы и нажмите ОК. Создастся ZIP-файл, содержащий значки темы. Вы можете загрузить его на форум calibre по адресу Mobileread [https://www.mobileread.com/forums/forumdisplay.php?f=166], а затем я сделаю вашу тему доступной через встроенную систему тем значков calibre. По умолчанию только что созданная тема значков также будет установлена как текущая тема в calibre. Если вы тестируете свою тему, не забудьте удалить изображения из папки resources/images, чтобы использовались значки из темы.

Начиная с calibre 6, у вас могут быть собственные значки для светлого и тёмного режима. Просто создайте две версии значка и назовите файлы с суффиксом -for-dark-theme и -for-light-theme. Например, modified-for-dark-theme.png и modified-for-light-theme.png. Затем calibre автоматически использует соответствующий значок в зависимости от текущей темы.

Настройка calibre с помощью плагинов
calibre имеет модульную конструкцию. Практически все функциональные возможности calibre поставляются в виде плагинов. Плагины используются для преобразования форматов, для загрузки новостей (хотя и называются рецептами), для различных компонентов пользовательского интерфейса, для подключения к различным устройствам, для обработки файлов при добавлении их в calibre и так далее. Вы можете получить полный список всех встроенных в calibre плагинов, перейдя в Параметры → Расширенные → Плагины.

Вы можете писать свои плагины чтобы настроить и расширить поведение calibre. Архитектура плагинов calibre очень проста, ознакомьтесь с руководством Создание собственных плагинов для расширения функциональности calibre.

После того, как вы написали плагин, вы можете загрузить его на форум плагинов calibre по адресу Mobileread [https://www.mobileread.com/forums/forumdisplay.php?f=237], и он будет доступен через встроенный апдейтер плагинов calibre.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Настройка calibre »
- Документация по API для плагинов

Документация по API для плагинов

Определяет различные абстрактные основные классы, которые можно разделить на подклассы, для создания эффективных плагинов. К полезным классам относятся:

- Плагин

- FileTypePlugin

- Плагины метаданных

- Плагины каталогизации

- Плагины загрузки метаданных

- Плагины преобразования

- Драйверы устройств

- Действия в пользовательском интерфейсе

- Плагины параметров

Плагин
class calibre.customize.Plugin(plugin_path)[исходный код]

Плагин calibre. Включая эти полезные плагины:

- self.installation_type: Хранит способ установки плагина.

- self.plugin_path: Хранит путь к ZIP-файлу, содержащему

этот плагин или None, если это встроенный плагин

- self.site_customization: Сохраняет введенную строку настройки

пользователем.

Методы, которые необходимо переопределить в подклассах:

- initialize()

- customization_help()

Полезные методы:

- temporary_file()

- __enter__()

- load_resources()

supported_platforms = []

Список платформ, на которых работает этот плагин. Например: ['windows', 'osx', 'linux']

name = 'Trivial Plugin'

Название этого плагина. Вы должны установить для него что-то другое, кроме Trivial Plugin, чтобы он работал.

version = (1, 0, 0)

Версия этого плагина в виде 3 выпусков (основной, дополнительный, ревизия)

description = 'Ничего не делает'

Короткая строка, описывающая, что делает этот плагин

author = 'Неизвестный'

Автор этого плагина

priority = 1

Если для одного типа файла существует несколько подключаемых модулей, они запускаются в порядке убывания приоритета. Плагины с более высоким приоритетом будут запущены первыми. Максимально возможный приоритет - sys.maxsize. Приоритет по умолчанию = 1.

minimum_calibre_version = (0, 4, 118)

Минимальная требуемая для этого плагина версия calibre

installation_type = None

Способ установки этого плагина

can_be_disabled = True

При False пользователь не сможет отключить этот плагин. Используйте с осторожностью.

type = 'Основной'

Тип плагина. Используется для категоризации в графической оболочке.

initialize()[исходный код]

Вызывается один раз при инициализации подключаемых модулей calibre. Плагины повторно инициализируются каждый раз, когда добавляется новый плагин. Также обратите внимание, что если плагин запускается в рабочем процессе, например, добавлении книг, то плагин будет инициализирован для каждого нового рабочего процесса.

Выполните здесь любую инициализацию конкретного плагина, например, извлечение ресурсов из ZIP-файла плагина. Путь к ZIP-файлу доступен как self.plugin_path.

Обратите внимание, что на данном этапе self.site_customization сейчас недоступен.

config_widget()[исходный код]

Реализуйте этот метод и save_settings() в своем плагине, чтобы использовать настраиваемое диалоговое окно конфигурации, а не полагаться на простую настройку по умолчанию на основе строк.

Этот метод, если он реализован, должен возвращать QWidget. Виджет может иметь необязательный метод validate(), который не принимает аргументов и вызывается сразу после того, как пользователь нажимает кнопку ОК. Изменения применяются тогда и только тогда, когда метод возвращает True.

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

save_settings(config_widget)[исходный код]

Сохраните настройки, указанные пользователем, с помощью config_widget.

Параметры:

config_widget – Виджет, возвращаемый config_widget().

do_user_config(parent=None)[исходный код]

Этот метод показывает диалог конфигурации для этого плагина. Он возвращает True, если пользователь нажимает OK, в противном случае - False. Изменения применяются автоматически.

load_resources(names)[исходный код]

Если этот плагин поставляется в виде ZIP-файла (плагин, добавленный пользователем), этот метод позволит вам загружать ресурсы из ZIP-файла.

Например, чтобы загрузить изображение:

pixmap = QPixmap()
pixmap.loadFromData(self.load_resources(['images/icon.png'])['images/icon.png'])
icon = QIcon(pixmap)

Параметры:

names – Список путей к ресурсам в ZIP-файле с использованием разделителя /

Результат:

Словарь вида {name: file_contents}. Любые имена, которые не были найдены в ZIP-файле, не будут представлены в словаре.

customization_help(gui=False)[исходный код]

Вернуть строку с информацией о том, как настроить этот плагин. По умолчанию вызывается NotImplementedError, что указывает на то, что плагин не требует настройки.

Если вы повторно реализуете этот метод в своем подклассе, пользователю будет предложено ввести строку в качестве настройки для этого плагина. Строка настройки будет доступна как self.site_customization.

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

Параметры:

gui – Если True, вернуть HTML-справку, иначе вернуть текстовую справку.

temporary_file(suffix)[исходный код]

Возвращает файловый объект, который является временным файлом в файловой системе. Этот файл останется доступным даже после закрытия и будет удален только при завершении работы интерпретатора. Используйте член name возвращаемого объекта, чтобы получить полный путь к созданному временному файлу

Параметры:

suffix – Суффикс временного файла.

cli_main(args)[исходный код]

Этот метод является основной точкой входа для интерфейса командной строки ваших плагинов. Он вызывается, когда пользователь делает: calibre-debug -r «Plugin Name». Любые переданные аргументы присутствуют в переменной args.

FileTypePlugin
class calibre.customize.FileTypePlugin(plugin_path)[исходный код]

Базовые классы: Plugin

Плагин, связанный с определенным набором типов файлов.

file_types = {}

Набор типов файлов, для которых следует запускать этот плагин. Используйте „*“ для всех типов файлов. Например: {'lit', 'mobi', 'prc'}

on_import = False

Если True, этот плагин запускается при добавлении книг в базу данных

on_postimport = False

Если True, этот плагин запускается после добавления книг в базу данных. В этом случае вызываются методы плагина postimport и postadd.

on_postconvert = False

Если True, этот плагин запускается после конвертации книги. В этом случае вызывается postconvert метод плагина.

on_postdelete = False

Если True, этот плагин запускается после удаления файла книги из базы данных. В этом случае вызывается postdelete метод плагина.

on_preprocess = False

Если True, этот плагин запускается непосредственно перед конвертацией.

on_postprocess = False

Если True, этот плагин запускается после конвертации в финальный файл, созданный плагином вывода конвертации.

type = 'Тип файла'

Тип плагина. Используется для категоризации в графической оболочке.

run(path_to_ebook)[исходный код]

Запустить плагин. Должен быть реализован в подклассах. Он должен выполнить все необходимые изменения в электронной книге и вернуть абсолютный путь к измененной электронной книге. Если никаких изменений не требуется, он должен вернуть путь к исходной электронной книге. Если обнаружена ошибка, должно возникнуть исключение. Реализация по умолчанию просто возвращает путь к исходной электронной книге. Обратите внимание, что путь к исходному файлу (до запуска плагинов любого типа файла доступен как self.original_path_to_file).

Изменённый файл электронной книги должен быть создан с помощью метода temporary_file()

Параметры:

path_to_ebook – Абсолютный путь к электронной книге.

Результат:

Абсолютный путь к изменённой электронной книге.

postimport(book_id, book_format, db)[исходный код]

Вызывается пост-импортом, т.е. после того, как файл книги был добавлен в базу данных. Обратите внимание, что это отличается от postadd(), который вызывается при первом создании записи книги. Этот метод вызывается всякий раз, когда новый файл добавляется к записи книги. Полезно для изменения записи книги на основе содержимого вновь добавленного файла.

Параметры:

- book_id – Идентификатор базы данных добавленной книги.

- book_format – Тип файла добавленной книги.

- db – Библиотека базы данных.

postconvert(book_id, book_format, db)[исходный код]

Вызывается после конвертации, т. е. после того, как файл выходной книги конвертации был добавлен в базу данных. Обратите внимание, что он запускается только после преобразования, а не после добавления книги. Это полезно для изменения записи книги на основе содержимого вновь добавленного файла.

Параметры:

- book_id – Идентификатор базы данных добавленной книги.

- book_format – Тип файла добавленной книги.

- db – Библиотека базы данных.

postdelete(book_id, book_format, db)[исходный код]

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

Параметры:

- book_id – Идентификатор базы данных добавленной книги.

- book_format – Тип файла добавленной книги.

- db – Библиотека базы данных.

postadd(book_id, fmt_map, db)[исходный код]

Вызывается post add, т.е. после того, как книга была добавлена в БД. Обратите внимание, что это отличается от postimport(), который вызывается после того, как в книгу был добавлен отдельный файл книги. postadd() вызывается только тогда, когда вся запись книги, возможно, с более чем одним файлом книги, была создана впервые. Это полезно, если вы хотите изменить запись книги в базе данных при первом добавлении книги в calibre

Параметры:

- book_id – Идентификатор базы данных добавленной книги.

- fmt_map – Сопоставление формата файла с путем, из которого был добавлен формат файла. Обратите внимание, что это может указывать, а может и не указывать на существующий файл, поскольку иногда файлы добавляются в виде потоков. В этом случае это может быть фиктивное значение или несуществующий путь.

- db – База данных библиотеки

Плагины метаданных

class calibre.customize.MetadataReaderPlugin(*args, **kwargs)[исходный код]

Базовые классы: Plugin

Плагин, реализующий чтение метаданных из набора типов файлов.

file_types = {}

Набор типов файлов, для которых следует запускать этот плагин. Например: set(['lit', 'mobi', 'prc'])

supported_platforms = ['windows', 'osx', 'linux']

Список платформ, на которых работает этот плагин. Например: ['windows', 'osx', 'linux']

version = (8, 7, 0)

Версия этого плагина в виде 3 выпусков (основной, дополнительный, ревизия)

author = 'Kovid Goyal'

Автор этого плагина

type = 'Чтение метаданных'

Тип плагина. Используется для категоризации в графической оболочке.

get_metadata(stream, type)[исходный код]

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

Параметры:

type – Тип файла. Гарантированно будет одной из записей в file_types.

Результат:

Объект calibre.ebooks.metadata.book.Metadata

class calibre.customize.MetadataWriterPlugin(*args, **kwargs)[исходный код]

Базовые классы: Plugin

Плагин, реализующий чтение метаданных из набора типов файлов.

file_types = {}

Набор типов файлов, для которых следует запускать этот плагин. Например: set(['lit', 'mobi', 'prc'])

supported_platforms = ['windows', 'osx', 'linux']

Список платформ, на которых работает этот плагин. Например: ['windows', 'osx', 'linux']

version = (8, 7, 0)

Версия этого плагина в виде 3 выпусков (основной, дополнительный, ревизия)

author = 'Kovid Goyal'

Автор этого плагина

type = 'Запись метаданных'

Тип плагина. Используется для категоризации в графической оболочке.

set_metadata(stream, mi, type)[исходный код]

Установить метаданные для файла, представленного потоком (объект, подобный файлу, который поддерживает чтение). Вызвать исключение при ошибке во входных данных.

Параметры:

- type – Тип файла. Гарантированно будет одной из записей в file_types.

- mi – Объект calibre.ebooks.metadata.book.Metadata

Плагины каталогизации

class calibre.customize.CatalogPlugin(plugin_path)[исходный код]

Базовые классы: Plugin

Плагин, реализующий генератор каталогов.

file_types = {}

Тип выходного файла, для которого следует запустить этот плагин. Например: „epub“ или „xml“.

type = 'Генератор каталога'

Тип плагина. Используется для категоризации в графической оболочке.

cli_options = []

Параметры CLI парсера, специфичные для этого плагина, объявленные как namedtuple Option:

из коллекций импортировать namedtuple Option = namedtuple(„Option“, „option, default, dest, help“) cli_options = [Option(“–catalog-title“, default = „My Catalog“, dest = „catalog_title“, help = (_(„Title of generated catalog. nDefault:“) + « „» + „%default“ + «“»))] cli_options parsed в calibre.db.cli.cmd_catalog:option_parser()

initialize()[исходный код]

Если подключаемый модуль не является встроенным, скопируйте файлы .ui и .py подключаемого модуля из ZIP-файла в $TMPDIR. Вкладка будет динамически сгенерирована и добавлена в диалог Параметры каталога в calibre.gui2.dialogs.catalog.py:Catalog.

run(path_to_output, opts, db, ids, notification=None)[исходный код]

Запустить плагин. Должно быть реализовано в подклассах. Он должен сгенерировать каталог в формате, указанном в file_types, и вернуть абсолютный путь к сгенерированному файлу каталога. Если обнаружена ошибка, должно возникнуть исключение.

Сгенерированный файл каталога должен быть создан с помощью метода temporary_file().

Параметры:

- path_to_output – Абсолютный путь к сгенерированному файлу каталога.

- opts – Словарь аргументов ключевых слов

- db – Объект LibraryDatabase2

Плагины загрузки метаданных
class calibre.ebooks.metadata.sources.base.Source(*args, **kwargs)[исходный код]

Базовые классы: Plugin

type = 'Источник метаданных'

Тип плагина. Используется для категоризации в графической оболочке.

author = 'Kovid Goyal'

Автор этого плагина

supported_platforms = ['windows', 'osx', 'linux']

Список платформ, на которых работает этот плагин. Например: ['windows', 'osx', 'linux']

capabilities = frozenset({})

Набор возможностей, поддерживаемых этим плагином. Полезные возможности: „identify“, „cover“

touched_fields = frozenset({})

Список полей метаданных, которые потенциально могут быть загружены этим плагином на этапе идентификации

has_html_comments = False

Установите значение True, если ваш плагин возвращает комментарии в формате HTML.

supports_gzip_transfer_encoding = False

Установка этого значения в True означает, что объект браузера укажет, что он поддерживает кодировку передачи gzip. Это может ускорить загрузку, но сначала убедитесь, что источник действительно поддерживает кодировку передачи gzip.

ignore_ssl_errors = False

Установите значение True, чтобы игнорировать ошибки сертификата HTTPS при подключении к этому источнику.

cached_cover_url_is_reliable = True

Кешированные URL-адреса обложек иногда могут быть ненадежными (т.е. загрузка может завершиться сбоем или возвращенное изображение может быть поддельным. Если такое часто случается с этим источником, установите значение False

options = ()

Список объектов Option. Они будут использоваться для автоматического создания виджета конфигурации для этого плагина.

config_help_message = None

Строка, которая отображается в верхней части виджета конфигурации для этого плагина.

can_get_multiple_covers = False

Если True, этот источник может вернуть несколько обложек для данного запроса.

auto_trim_covers = False

Если установлено значение True, обложки, загруженные этим плагином, автоматически обрезаются.

prefer_results_with_isbn = True

Если установлено значение True, и этот источник возвращает несколько результатов для запроса, с ISBN и без, результаты без ISBN будут проигнорированы.

is_configured()[исходный код]

Верните False, если ваш плагин нужно настроить, прежде чем его можно будет использовать. Например, может потребоваться имя пользователя/пароль/ключ API.

customization_help()[исходный код]

Вернуть строку с информацией о том, как настроить этот плагин. По умолчанию вызывается NotImplementedError, что указывает на то, что плагин не требует настройки.

Если вы повторно реализуете этот метод в своем подклассе, пользователю будет предложено ввести строку в качестве настройки для этого плагина. Строка настройки будет доступна как self.site_customization.

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

Параметры:

gui – Если True, вернуть HTML-справку, иначе вернуть текстовую справку.

config_widget()[исходный код]

Реализуйте этот метод и save_settings() в своем плагине, чтобы использовать настраиваемое диалоговое окно конфигурации, а не полагаться на простую настройку по умолчанию на основе строк.

Этот метод, если он реализован, должен возвращать QWidget. Виджет может иметь необязательный метод validate(), который не принимает аргументов и вызывается сразу после того, как пользователь нажимает кнопку ОК. Изменения применяются тогда и только тогда, когда метод возвращает True.

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

save_settings(config_widget)[исходный код]

Сохраните настройки, указанные пользователем, с помощью config_widget.

Параметры:

config_widget – Виджет, возвращаемый config_widget().

get_author_tokens(authors, only_first_author=True)[исходный код]

Взять список авторов и вернуть список токенов, полезных для поискового запроса AND. Эта функция пытается вернуть токены в порядке имени, отчества, фамилии, предполагая, что если в имени автора стоит запятая, имя находится в фамилии, в другой форме.

get_title_tokens(title, strip_joiners=True, strip_subtitle=False)[исходный код]

Взять заголовок и веруть список токенов, полезных для поискового запроса AND. Исключает связки (необязательно) и знаки препинания.

split_jobs(jobs, num)[исходный код]

Разделить список заданий на максимальное количество групп, как можно равномернее.

test_fields(mi)[исходный код]

Вернуть первое поле из self.touched_fields, которое имеет значение null для объекта mi

clean_downloaded_metadata(mi)[исходный код]

Вызовите этот метод в методе идентификации вашего плагина, чтобы нормализовать метаданные перед помещением объекта Metadata в result_queue. Можно использовать собственный алгоритм, подходящий для вашего источника метаданных.

get_book_url(identifiers)[исходный код]

Вернуть 3-кортеж или None. Кортеж из трёх элементов имеет форму: (identifier_type, identifier_value, URL). URL-адрес - это URL-адрес книги, идентифицированный идентификаторами в этом источнике. identifier_type, identifier_value указывают идентификатор, соответствующий URL-адресу. Этот URL-адрес должен быть доступен для просмотра человеком, использующим браузер. Он предназначен для предоставления пользователю интерактивной ссылки, чтобы пользователь мог легко перейти на страницу с книгами в этом источнике. Если URL-адрес не найден, вернуть None. Этот метод должен быть быстрым и последовательным, поэтому используйте его только в том случае, если возможно построить URL-адрес из известной схемы с заданными идентификаторами.

get_book_url_name(idtype, idval, url)[исходный код]

Вернуть человекочитаемое имя из возвращаемого значения get_book_url ().

get_book_urls(identifiers)[исходный код]

Переопределите этот метод, если вы хотите вернуть несколько URL-адресов для этой книги. Вернуть список 3-tuples (кортеж из трех элементов). По умолчанию этот метод просто вызывает get_book_url().

get_cached_cover_url(identifiers)[исходный код]

Возвращает кешированный URL-адрес обложки для книги, идентифицированной словарем идентификаторов, или None, если такой URL-адрес не существует.

Обратите внимание, что этот метод должен возвращать только проверенные URL-адреса, то есть не те URL-адреса, которые могут привести к общему изображению обложки или ошибке «Не найдено».

id_from_url(url)[исходный код]

Проанализировать URL-адрес и вернуть кортеж в форме: (identifier_type, identifier_value). Если URL-адрес не соответствует шаблону для источника метаданных, вернуть None.

identify_results_keygen(title=None, authors=None, identifiers={})[исходный код]

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

Эти ключи используются для сортировки результатов вызова identify().

Подробнее об алгоритме по умолчанию см. InternalMetadataCompareKeyGen. Повторно реализуйте эту функцию в своем плагине, если алгоритм по умолчанию не подходит.

identify(log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=30)[исходный код]

Определить книгу по её названию/автору/ISBN/и т.д.

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

Если этот источник метаданных также предоставляет обложки, URL-адрес обложки должен быть кэширован, чтобы при последующем вызове API получения обложек с тем же ISBN/специальным идентификатором не нужно было снова получать URL-адрес обложки. Используйте для этого API кеширования.

Каждый объект Metadata, помещенный в result_queue этим методом, должен иметь атрибут source_relevance, который является целым числом, указывающим порядок, в котором результаты были возвращены источником метаданных для этого запроса. Это целое число будет использоваться compare_identify_results(). Если порядок не важен, установите его в 0 (ноль) для каждого результата.

Прежде чем объект Metadata будет помещен в result_queue, убедитесь, что вся информация сопоставления обложки/ISBN закэширована.

Параметры:

- log – Объект журнала, используйте его для вывода отладочной информации/ошибок

- result_queue – Очередь результатов, результаты должны быть помещены в неё. Каждый результат - это объект метаданных.

- abort – Если abort.is_set() возвращает True, прервать дальнейшую обработку и вернутся как можно скорее.

- title – Название книги может быть None

- authors – Список авторов книги может быть None

- identifiers – Словарь других идентификаторов, чаще всего {„isbn“: „1234 …“}

- timeout – Тайм-аут в секундах, ни один сетевой запрос не должен зависать дольше таймаута.

Результат:

None, если ошибок не было, иначе - представление ошибки в Юникоде, подходящее для показа пользователю.

download_cover(log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False)[исходный код]

Скачать обложку и поместить её в result_queue. Все параметры имеют то же значение, что и для identify(). Поместить (self, cover_data) в result_queue.

Этот метод должен использовать кешированные URL-адреса обложек для повышения эффективности, при возможности. Когда кэшированных данных нет, большинство плагинов просто вызывают идентификацию и используют её результаты.

Если параметр get_best_cover имеет значение True и этот плагин может получить несколько обложек, он должен получить только «лучшую».

class calibre.ebooks.metadata.sources.base.InternalMetadataCompareKeyGen(mi, source_plugin, title, authors, identifiers)[исходный код]

Сгенерировать ключ сортировки для сравнения релевантности объектов метаданных с учётом поискового запроса. Используется только для сравнения результатов из одного источника метаданных, но не из разных источников.

Ключ сортировки гарантирует, что сортировка в порядке возрастания является сортировкой по убыванию релевантности.

Алгоритм такой:

- Предпочитать результаты, у которых есть хотя бы один идентификатор, такой же как у запроса

- Предпочитать результаты с кешированным URL обложки

- Предпочитать результаты со всеми заполненными полями

- Предпочитать результаты на том же языке, что и текущий язык пользовательского интерфейса

- Предпочитать результаты, которые точно соответствуют названию запроса

- Предпочитать результаты с более длинными комментариями, более чем на 10% длиннее

- Используйте релевантность результата, полученную в результате поиска источника метаданных

движок

Плагины преобразования
class calibre.customize.conversion.InputFormatPlugin(*args)[исходный код]

Базовые классы: Plugin

InputFormatPlugins отвечают за преобразование документа в HTML + OPF + CSS + и т. д. Результаты преобразования должны быть закодированы в UTF-8. Основное действие происходит в convert().

type = 'Ввод для конвертации'

Тип плагина. Используется для категоризации в графической оболочке.

can_be_disabled = False

При False пользователь не сможет отключить этот плагин. Используйте с осторожностью.

supported_platforms = ['windows', 'osx', 'linux']

Список платформ, на которых работает этот плагин. Например: ['windows', 'osx', 'linux']

file_types = {}

Набор типов файлов, для которых следует запускать этот плагин. Например: set (['azw', 'mobi', 'prc'])

is_image_collection = False

Если True, этот плагин ввода генерирует коллекцию изображений, по одному на файл HTML. Устанавливается динамически в методе convert, если входные файлы могут быть как коллекциями изображений, так и коллекциями без изображений. Если вы установите для него значение True, то реализуйте метод get_images(), который возвращает список изображений.

core_usage = 1

Количество ядер процессора, используемых этим плагином. Значение -1 означает, что он использует все доступные ядра.

for_viewer = False

Если установлено значение True, плагин ввода будет выполнять специальную обработку, чтобы сделать его вывод подходящим для просмотра.

output_encoding = 'utf-8'

Кодировка, в которой этот модуль ввода создает файлы. Значение None означает, что кодировка не определена и должна определяться индивидуально.

common_options = {<calibre.customize.conversion.OptionRecommendation object>}

Параметры, общие для всех плагинов формата ввода. Не переопределять в подклассах. Вместо этого используйте options. Каждая опция должна быть экземпляром OptionRecommendation.

options = {}

Параметры для настройки поведения этого плагина. Каждая опция должна быть экземпляром OptionRecommendation.

recommendations = {}

Набор из 3-tuples (трех кортежей) формы (option_name, recommended_value, recommendation_level)

get_images()[исходный код]

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

convert(stream, options, file_ext, log, accelerators)[исходный код]

Этот метод должен быть реализован в подклассах. Он должен возвращать путь к созданному файлу OPF или экземпляру OEBBook. Весь вывод должен содержаться в текущей папке. Если этот плагин создает файлы вне текущей папки, их необходимо удалить/пометить для удаления, прежде чем этот метод вернёт значение.

Параметры:

- stream – Файловый объект, содержащий входной файл.

- options – Параметры для настройки процесса конвертации. Гарантированно наличие атрибутов, соответствующих всем параметрам, заявленным этим плагином. Кроме того, будет подробный атрибут, принимающий целые значения от нуля и выше. Большие числа означают более подробную информацию. Ещё один полезный атрибут - input_profile, который является экземпляром :class:calibre.customize.profiles.InputProfile.

- file_ext – Расширение (без .) входного файла. Гарантируется, что это будет один из file_types, поддерживаемых этим плагином.

- log – Объект calibre.utils.logging.Log. Весь вывод должен использовать этот объект.

- accelerators – Словарь различной информации, которую плагин ввода может легко получить, что ускорит последующие этапы преобразования.

postprocess_book(oeb, opts, log)[исходный код]

Вызывается, чтобы разрешить плагину ввода выполнять постобработку после анализа книги.

specialize(oeb, opts, log, output_fmt)[исходный код]

Вызывается, чтобы позволить плагину ввода специализировать анализируемую книгу для определенного формата вывода. Вызывается после postprocess_book и перед выполнением любых преобразований в проанализированной книге.

gui_configuration_widget(parent, get_option_by_name, get_option_help, db, book_id=None)[исходный код]

Вызывается для создания виджета, используемого для настройки этого плагина в графическом интерфейсе calibre. Виджет должен быть экземпляром класса PluginWidget. См. примеры встроенных модулей ввода.

class calibre.customize.conversion.OutputFormatPlugin(*args)[исходный код]

Базовые классы: Plugin

OutputFormatPlugins отвечают за преобразование документа OEB (OPF + HTML) в выходную электронную книгу.

Предполагается, что документ OEB закодирован в UTF-8. Основное действие происходит в convert().

type = 'Вывод для конвертации'

Тип плагина. Используется для категоризации в графической оболочке.

can_be_disabled = False

При False пользователь не сможет отключить этот плагин. Используйте с осторожностью.

supported_platforms = ['windows', 'osx', 'linux']

Список платформ, на которых работает этот плагин. Например: ['windows', 'osx', 'linux']

file_type = None

Тип файла (расширение без начальной точки), который выводит этот плагин.

common_options = {<calibre.customize.conversion.OptionRecommendation object>}

Параметры, общие для всех плагинов формата ввода. Не переопределять в подклассах. Вместо этого используйте options. Каждая опция должна быть экземпляром OptionRecommendation.

options = {}

Параметры для настройки поведения этого плагина. Каждая опция должна быть экземпляром OptionRecommendation.

recommendations = {}

Набор из 3-tuples (трех кортежей) формы (option_name, recommended_value, recommendation_level)

property description

str(object=““) -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Создать новый строковый объект из данного объекта. Если указаны кодировка или ошибки, то объект должен предоставить буфер данных, который будет декодирован с использованием данной кодировки и обработчика ошибок. В противном случае возвращает результат object.__str__() (если определено) или repr(object). кодировка по умолчанию „utf-8“. ошибки по умолчанию „strict“.

convert(oeb_book, output, input_plugin, opts, log)[исходный код]

Отрендерить содержимое oeb_book (который является экземпляром calibre.ebooks.oeb.OEBBook) в файл, указанный в output.

Параметры:

- output – Либо файл, подобный объекту, либо строка. Если это строка, это путь к папке, которая может существовать, а может и не существовать. Плагин вывода должен записывать свой вывод в эту папку. Если это объект, подобный файлу, плагин вывода должен записывать свой вывод в файл.

- input_plugin – Плагин ввода, который использовался в начале конвейера конвертации.

- opts – Опции конвертации. Гарантировано наличие атрибутов, соответствующих рекомендациям OptionRecommendations этого плагина.

- log – Логгер. Печатать отладочные/информационные сообщения и т. д.

specialize_options(log, opts, input_fmt)[исходный код]

Может использоваться для изменения значений параметров конвертации, используемых конвейером конвертации.

specialize_css_for_output(log, opts, item, stylizer)[исходный код]

Может использоваться для внесения изменений в CSS во время процесса уплощения CSS.

Параметры:

- item – HTML-файл элемента, который обрабатывается

- stylizer – Объект Stylizer, содержащий уплощённые стили для элемента. Вы можете получить стиль для любого элемента с помощью stylizer.style(element).

gui_configuration_widget(parent, get_option_by_name, get_option_help, db, book_id=None)[исходный код]

Вызывается для создания виджета, используемого для настройки этого плагина в графическом интерфейсе calibre. Виджет должен быть экземпляром класса PluginWidget. См. примеры встроенных модулей ввода.

Драйверы устройств

Базовый класс для всех драйверов устройств DevicePlugin. Однако, если ваше устройство представляет себя для операционной системы как USBMS-накопитель, следует использовать вместо него класс USBMS, поскольку он реализует всю логику, необходимую для поддержки таких типов устройств.

class calibre.devices.interface.DevicePlugin(plugin_path)[исходный код]

Базовые классы: Plugin

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

type = 'Интерфейс устройства'

Тип плагина. Используется для категоризации в графической оболочке.

FORMATS = ['lrf', 'rtf', 'pdf', 'txt']

Упорядоченный список поддерживаемых форматов

VENDOR_ID = 0

VENDOR_ID может быть целым числом, списком целых чисел или словарем. Если это словарь, то это должен быть словарь словарей в форме:

{
integer_vendor_id : { product_id : [list of BCDs], ... },
...
}

PRODUCT_ID = 0

Целое число или список целых чисел

BCD = None

BCD может иметь значение None, чтобы не различать устройства на основе BCD, или это может быть список BCD номеров всех устройств, поддерживаемых этим драйвером.

THUMBNAIL_HEIGHT = 68

Высота миниатюр на устройстве

THUMBNAIL_COMPRESSION_QUALITY = 75

Качество сжатия миниатюр. Значение ближе к 100 - миниатюры лучшего качества с минимумом артефактов сжатия. Размер файлов миниатюр увеличивается.

WANTS_UPDATED_THUMBNAILS = False

Значение параметра True, если устройство поддерживает обновление миниатюр обложек во время sync_booklists. При значении True у device.py запрашивается обновление миниатюр обложек при сопоставлении книг.

CAN_SET_METADATA = ['title', 'authors', 'collections']

Можно ли установить метаданные книг через графический интерфейс.

CAN_DO_DEVICE_DB_PLUGBOARD = False

Может ли устройство обрабатывать коммутации метаданных device_db

path_sep = '/'

Разделитель путей для путей к книгам на устройстве

icon = 'reader.png'

Значок для этого устройства

UserAnnotation

псевдоним для Annotation

OPEN_FEEDBACK_MESSAGE = None

В графическом интерфейсе это отображается как сообщение, если в строке статуса не None. Полезно, если открытие может занять много времени

VIRTUAL_BOOK_EXTENSIONS = frozenset({})

Набор расширений, которые являются «виртуальными книгами» на устройстве и поэтому не могут быть просмотрены/сохранены/добавлены в библиотеку. Например: frozenset(['kobo'])

VIRTUAL_BOOK_EXTENSION_MESSAGE = None

Сообщение, отображаемое пользователю для расширений виртуальных книг.

NUKE_COMMENTS = None

Удалять ли комментарии в копии книги, отправленной на устройство. Если не None, это должна быть короткая строка, которой будут заменены комментарии.

MANAGES_DEVICE_PRESENCE = False

Если True указывает, что этот драйвер полностью управляет обнаружением устройства, извлечением и т. д. При установке для него значение True, вы должны реализовать методы detect_managed_devices и debug_managed_device_detection. Драйвер, для которого установлено значение True, отвечает за обнаружение устройств, управление черным списком устройств, списком извлеченных устройств и т. д. calibre будет периодически вызывать метод detect_managed_devices(), и если он возвращает обнаруженное устройство, calibre вызовет open(). open() будет вызываться каждый раз, когда устройство возвращается, даже если предыдущие вызовы open() завершились неудачно, поэтому драйвер должен поддерживать свой собственный черный список отказавших устройств. Точно так же при извлечении calibre вызовет eject(), а затем, предполагая, что следующий вызов detect_managed_devices() вернет None, вызовет post_yank_cleanup().

SLOW_DRIVEINFO = False

Если установлено True, calibre вызовет метод get_driveinfo() после загрузки списков книг для получения информации о диске.

ASK_TO_ALLOW_CONNECT = False

Если установлено значение True, при первом обнаружении устройства calibre спросит пользователя, хочет ли он управлять устройством с помощью calibre. Если установить параметру значение True, то вы должны реализовать get_device_uid() и ignore_connected_device() и get_user_blacklisted_devices() и set_user_blacklisted_devices()

user_feedback_after_callback = None

Установите значение как словарь формы {„title“: title, „msg“: msg, „det_msg“: detail_msg}, чтобы calibre отображал всплывающее сообщение для пользователя после выполнения некоторых обратных вызовов (в настоящее время только upload_books). Не спамьте пользователя потоком сообщений. Эта переменная проверяется после каждого обратного вызова, поэтому устанавливайте её только при необходимости.

classmethod get_open_popup_message()[исходный код]

GUI отображает это как немодальное всплывающее окно. Должен быть экземпляром OpenPopupMessage

classmethod model_metadata() → tuple[ModelMetadata, ...][исходный код]

Метаданные обо всех реальных моделях устройств, поддерживаемых этим драйвером

is_usb_connected(devices_on_system, debug=False, only_presence=False)[исходный код]

Вернуть True, device_info, если в данный момент подключено устройство, управляемое этим плагином.

Параметры:

devices_on_system – Список подключенных устройств

detect_managed_devices(devices_on_system, force_refresh=False)[исходный код]

Вызывается, только если у MANAGES_DEVICE_PRESENCE значение True.

Просканировать устройства, с которыми может работать этот драйвер. Должен возвращать объект устройства, если устройство найдено. Этот объект будет передан методу open() как connected_device. Если устройство не найдено, вернуть None. Возвращаемый объект может быть любым, calibre его не использует, он передается только в open().

Этот метод периодически вызывается графическим интерфейсом пользователя, поэтому убедитесь, что он не слишком ресурсоемкий. Используйте кеш, чтобы избежать повторного сканирования системы.

Параметры:

- devices_on_system – Набор USB-устройств, обнаруженных в системе.

- force_refresh – Если True и драйвер использует кеш для предотвращения повторного сканирования, то кеш должен быть очищен.

debug_managed_device_detection(devices_on_system, output)[исходный код]

Вызывается, только если у MANAGES_DEVICE_PRESENCE значение True.

Должен записывать информацию об устройствах, обнаруженных в системе в вывод, представляющий собой файл, подобный объекту.

Должен возвращать True, если устройство было обнаружено и успешно открыто, в противном случае - False.

reset(key='-1', log_packets=False, report_progress=None, detected_device=None)[исходный код]

Параметры:

- key – Ключ для разблокировки устройства

- log_packets – Если true, поток пакетов к/от устройства логируется.

- report_progress – Функция, которая вызывается с % прогресса (число от 0 до 100) для различных задач. Если она вызывается с -1, это означает, что задача не имеет информации о ходе выполнения.

- detected_device – Device information from the device scanner

can_handle_windows(usbdevice, debug=False)[исходный код]

Необязательный метод для выполнения дальнейших проверок устройства, чтобы узнать, способен ли этот драйвер обрабатывать его. Если это не так, он должен вернуть False. Этот метод вызывается только после того, как поставщик, идентификаторы продуктов и bcd совпадают, поэтому он может выполнять некоторые относительно трудоемкие проверки. Реализация по умолчанию возвращает True. Этот метод вызывается только в Windows. См. также can_handle().

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

Параметры:

usbdevice – USB-устройство, возвращенное calibre.devices.winusb.scan_usb_devices()

can_handle(device_info, debug=False)[исходный код]

Версия для Unix can_handle_windows().

Параметры:

device_info – Кортеж из (vid, pid, bcd, производитель, продукт, серийный номер)

open(connected_device, library_uuid)[исходный код]

Выполнить любую инициализацию конкретного устройства. Вызывается после обнаружения устройства, но перед любыми другими функциями, которые обмениваются данными с устройством. Например: для устройств, которые представляют себя как запоминающие устройства USB, этот метод будет отвечать за установку устройства или, если устройство было автоматически смонтировано, за определение места его установки. Метод calibre.devices.usbms.device.Device.open() имеет реализацию этой функции, которая должна служить хорошим примером для запоминающих устройств USB.

Этот метод может вызвать исключение OpenFeedback для отображения сообщения пользователю.

Параметры:

- connected_device – Устройство, которое мы пытаемся открыть. Это кортеж из (идентификатор поставщика, идентификатор продукта, bcd, название производителя, название продукта, серийный номер устройства). Однако у некоторых устройств нет серийного номера, и в Windows присутствуют только первые три поля, остальные - None.

- library_uuid – UUID текущей библиотеки calibre. Может иметь значение None, если библиотеки нет (например, при использовании из командной строки).

eject()[исходный код]

Размонтировать/извлечь устройство из ОС. Не проверяется есть ли ожидающие задания графического интерфейса пользователя, которым необходимо взаимодействовать с устройством.

ПРИМЕЧАНИЕ. Этот метод нельзя вызывать в том же потоке, что и остальные методы устройства.

post_yank_cleanup()[исходный код]

Вызывается, если пользователь дергает устройство, не извлекая его предварительно.

set_progress_reporter(report_progress)[исходный код]

Установить функцию для сообщения информации о ходе выполнения.

Параметры:

report_progress – Функция, которая вызывается с % прогресса (число от 0 до 100) для различных задач. Если она вызывается с -1, это означает, что задача не имеет информации о ходе выполнения.

get_device_information(end_session=True)[исходный код]

Спросить у устройства информацию об устройстве. См. L{DeviceInfoQuery}.

Результат:

(имя устройства, версия устройства, версия программного обеспечения на устройстве, тип MIME). Кортеж может дополнительно иметь пятый элемент, который является словарем информации о накопителе. См. пример usbms.driver.

get_driveinfo()[исходный код]

Вернуть словарь driveinfo. Обычно вызывается из get_device_information(), но если загрузка информации о диске для этого драйвера происходит медленно, то следует установить SLOW_DRIVEINFO. В этом случае этот метод будет вызываться calibre после загрузки списков книг. Обратите внимание, что он не вызывается в потоке устройства, поэтому драйвер должен кэшировать информацию о диске в методе books(), и эта функция должна возвращать кэшированные данные.

card_prefix(end_session=True)[исходный код]

Возвращает список из 2 элементов префикса для путей на карточках. Если карта отсутствует, для префикса карты устанавливается значение None. НАПРИМЕР („/place“, „/place2“) (None, „place2“) („place“, None) (None, None)

total_space(end_session=True)[исходный код]

Получить общее пространство, доступное на точках монтирования:

1. Основная память

2. Карта памяти A

3. Карта памяти B

Результат:

Список из трех элементов с общим объёмом в байтах (1, 2, 3). Если на конкретном устройстве нет ни одного из этих местоположений, оно должно вернуть 0.

free_space(end_session=True)[исходный код]

Получить свободное место на точках монтирования:

1. Основная память

2. Карта 1

3. Карта 2

Результат:

Список из трех элементов со свободным пространством в байтах (1, 2, 3). Если конкретное устройство не имеет ни одного из этих местоположений, оно должно вернуть -1.

books(oncard=None, end_session=True)[исходный код]

Вернуть список электронных книг на устройстве.

Параметры:

oncard – Если „carda“ или „cardb“ возвращают список электронных книг на определенной карте памяти, в противном случае возвращает список электронных книг в основной памяти устройства. Если карта указана и на ней нет книг, вернуть пустой список.

Результат:

СписокКниг.

upload_books(files, names, on_card=None, end_session=True, metadata=None)[исходный код]

Загрузите список книг на устройство. Если файл уже существует на устройстве, его следует заменить. Этот метод должен вызывать FreeSpaceError, если на устройстве недостаточно свободного места. Текст FreeSpaceError должен содержать слово «card», если on_card не равно None, в противном случае он должен содержать слово «memory».

Параметры:

- files – Список путей

- names – Список имен файлов книг которые были уже загружены на устройство. len(names) == len(files)

- metadata – Если не None, это список объектов Metadata. Идея состоит в том, чтобы использовать метаданные, чтобы определить, где на устройстве разместить книгу. len(metadata) == len(files). Помимо обычной обложки (путь к обложке), также может быть атрибут эскиза, который следует использовать в предпочтении. Атрибут эскиза имеет форму (ширина, высота, данные обложки в формате jpeg).

Результат:

Список трехэлементных кортежей. Список предназначен для передачи в add_books_to_metadata().

classmethod add_books_to_metadata(locations, metadata, booklists)[исходный код]

Добавить местоположения в списки книг. Эта функция не должна связываться с устройством.

Параметры:

- locations – Результат звонка L {upload_books}

- metadata – Список объектов Metadata, такой же, как для upload_books().

- booklists – Кортеж, содержащий результат вызовов (books (oncard = None)(),: meth:books(oncard=“carda“),: meth`books(oncard=“cardb“)`).

delete_books(paths, end_session=True)[исходный код]

Удалять книги по путям на устройстве.

classmethod remove_books_from_metadata(paths, booklists)[исходный код]

Удалить книги из списка метаданных. Эта функция не должна связываться с устройством.

Параметры:

- paths – пути к книгам на устройстве.

- booklists – Кортеж, содержащий результат вызовов (books (oncard = None)(),: meth:books(oncard=“carda“),: meth`books(oncard=“cardb“)`).

sync_booklists(booklists, end_session=True)[исходный код]

Обновить метаданные на устройстве.

Параметры:

booklists – Кортеж, содержащий результат вызовов (books (oncard = None)(),: meth:books(oncard=“carda“),: meth`books(oncard=“cardb“)`).

get_file(path, outfile, end_session=True)[исходный код]

Прочитать файл по пути на устройстве и записать его в outfile.

Параметры:

outfile – файловый объект, например sys.stdout, или результат вызова open().

classmethod config_widget()[исходный код]

Должен вернуть QWidget. QWidget содержит настройки интерфейса устройства.

classmethod save_settings(settings_widget)[исходный код]

Должно сохранить настройки на диск. Принимает виджет, созданный в config_widget(), и сохраняет все настройки на диск.

classmethod settings()[исходный код]

Должно вернуть объект opts. Объект opts должен иметь как минимум один атрибут format_map, представляющий собой упорядоченный список форматов для устройства.

set_plugboards(plugboards, pb_func)[исходный код]

предоставить драйверу текущий набор коммутационных панелей и функцию для выбора конкретной коммутационной панели. Этот метод вызывается непосредственно перед add_books и sync_booklists.

pb_func - это вызываемый объект со следующей подписью:

def pb_func(device_name, format, plugboards)

You give it the current device name (either the class name or
DEVICE_PLUGBOARD_NAME), the format you are interested in (a „real“ format or „device_db“), and the plugboards (you were given those by set_plugboards, the same place you got this method).

Результат:

Нет или один экземпляр коммутационной панели.

set_driveinfo_name(location_code, name)[исходный код]

Задать имя устройства в файле информации о диске „name“. Этот параметр будет действовать до тех пор, пока файл не будет создан заново или имя не будет изменено снова.

Недисковые устройства должны реализовывать этот метод на основе кодов местоположения, возвращаемых методом get_device_information().

prepare_addable_books(paths)[исходный код]

Учитывая список путей, возвращает другой список путей. Эти пути указывают на добавляемые версии книг.

Если при подготовке книги произошла ошибка, то вместо пути в возвращаемом списке для этой книги должна быть позиция из трёх кортежей: (исходный_путь, экземпляр исключения, трассировка)

startup()[исходный код]

Вызывается, когда calibre запускает устройство. Выполните любую требуемую инициализацию. Обратите внимание, что можно создать несколько экземпляров класса, и, следовательно, __init__ можно вызывать несколько раз, но только один экземпляр будет вызывать этот метод. Этот метод вызывается в потоке устройства, а не в потоке графического интерфейса пользователя.

shutdown()[исходный код]

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

get_device_uid()[исходный код]

Должен возвращать уникальный идентификатор для текущего подключенного устройства (вызывается сразу после успешного вызова open()). Вы должны реализовать этот метод, если вы установите ASK_TO_ALLOW_CONNECT = True

ignore_connected_device(uid)[исходный код]

Должно игнорировать устройство, идентифицированное uid (результат вызова get_device_uid()) в будущем. Вы должны реализовать этот метод, если вы установите ASK_TO_ALLOW_CONNECT = True. Обратите внимание, что эта функция вызывается сразу после open(), поэтому, если open() кэширует какое-то состояние, драйвер должен сбросить это состояние.

get_user_blacklisted_devices()[исходный код]

Вернуть маппинг uid устройства к понятному имени для всех устройств, которые пользователь попросил игнорировать.

set_user_blacklisted_devices(devices)[исходный код]

Задать список идентификаторов устройств, которые должны игнорироваться этим драйвером.

specialize_global_preferences(device_prefs)[исходный код]

Реализуйте этот метод, если ваше устройство хочет переопределить конкретную настройку. Вы должны убедиться, что все сайты вызовов, которым требуется переопределение настроек, используют device_prefs[„something“] вместо prefs[„something“]. Ваш метод должен вызывать device_prefs.set_overrides(pref = val, pref = val, …). В настоящее время используется для: управления метаданными (prefs[„manage_device_metadata“])

set_library_info(library_name, library_uuid, field_metadata)[исходный код]

Реализуйте этот метод, если вам нужна информация о текущей библиотеке calibre. Этот метод вызывается при запуске и при изменении библиотеки calibre при подключении.

is_dynamically_controllable()[исходный код]

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

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

start_plugin()[исходный код]

Этот метод вызывается для запуска плагина. Плагин должен начать принимать подключения устройств, если он это делает. Если плагин уже принимает соединения, ничего не делайте.

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

stop_plugin()[исходный код]

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

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

get_option(opt_string, default=None)[исходный код]

Вернуть значение параметра, указанного в opt_string. Этот метод можно вызвать, когда плагин не запущен. Вернуть None, если параметр не существует.

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

set_option(opt_string, opt_value)[исходный код]

Установить значение параметра, обозначенного opt_string. Этот метод можно вызвать, когда плагин не запущен.

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

is_running()[исходный код]

Вернуть True, если плагин запущен, иначе false

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

synchronize_with_db(db, book_id, book_metadata, first_call)[исходный код]

Вызывается во время сопоставления книг, когда книга на устройстве сопоставляется с книгой в базе данных calibre. Метод отвечает за синхронизацию данных с устройства в базу данных calibre (при необходимости).

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

Второе значение само по себе является кортежем с двумя значениями. Первое значение в кортеже указывает, следует ли отправлять на устройство формат книги. Цель состоит в том, чтобы разрешить проверку того, что книга на устройстве совпадает с книгой в calibre. Это значение должно быть None, если книга не отправляется, в противном случае возвращается базовое имя файла на устройстве (строка, вроде foobar.epub). Обязательно укажите расширение в имени. Подсистема устройства создаст задание send_books для всех книг, не возвращающих значений not-None. Примечание: кроме последующего извлечения расширения, имя игнорируется в тех случаях, когда устройство использует шаблон для генерации имени файла, в большинстве случаев так и происходит. Второе значение в возвращенном кортеже указывает, датирован ли формат будущей датой. Вернуть True, если это так, в противном случае вернуть False. calibre отобразит для пользователя диалоговое окно со списком всех книг датированных будущим.

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

book_id: идентификатор calibre книги в базе данных. book_metadata: объект метаданных для книги, поступающий с устройства. first_call: True, если это первый вызов во время синхронизации, в противном случае - False.

class calibre.devices.interface.BookList(oncard, prefix, settings)[исходный код]

Базовые классы: list

Список книг. Каждый объект Book должен иметь поля

1. название

2. авторы

3. размер (размер файла книги)

4. datetime (кортеж времени в формате UTC)

5. путь (путь на устройстве к книге)

6. thumbnail (может быть None). thumbnail - это либо объект str/bytes с данными изображения, либо он должен иметь атрибут image_path, в котором хранится абсолютный (собственный для платформы) путь к изображению.

7. теги (список строк, может быть пустым).

supports_collections()[исходный код]

Вернуть True, если устройство поддерживает коллекции для этого списка книг.

add_book(book, replace_metadata)[исходный код]

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

remove_book(book)[исходный код]

Удалить книгу из списка книг. Исправить любые метаданные устройства одновременно

get_collections(collection_attributes)[исходный код]

Вернуть словарь коллекций, созданных из collection_attributes. Каждая запись в словаре имеет название коллекции форм:[список книг]

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

Параметры:

collection_attributes – Список атрибутов объекта Book

Устройства на базе USB Mass Storage

Базовый класс для таких устройств calibre.devices.usbms.driver.USBMS. Этот класс, в свою очередь, наследует часть своих функций от своих основ, описанных ниже. Типичный базовый драйвер на основе USBMS выглядит так:

from calibre.devices.usbms.driver import USBMS

class PDNOVEL(USBMS):
name = 'Pandigital Novel device interface'
gui_name = 'PD Novel'
description = _('Communicate with the Pandigital Novel')
author = 'Kovid Goyal'
supported_platforms = ['windows', 'linux', 'osx']
FORMATS = ['epub', 'pdf']

VENDOR_ID = [0x18d1]
PRODUCT_ID = [0xb004]
BCD = [0x224]

THUMBNAIL_HEIGHT = 144

EBOOK_DIR_MAIN = 'eBooks'
SUPPORTS_SUB_DIRS = False

def upload_cover(self, path, filename, metadata):
coverdata = getattr(metadata, 'thumbnail', None)
if coverdata and coverdata[2]:
with open('%s.jpg' % os.path.join(path, filename), 'wb') as coverfile:
coverfile.write(coverdata[2])

class calibre.devices.usbms.device.Device(plugin_path)[исходный код]

Базовые классы: DeviceConfig, DevicePlugin

Этот класс обеспечивает логику, общую для всех драйверов для устройств, которые экспортируют себя как запоминающие устройства USB. Предоставляет реализации для установки/извлечения устройств USBMS на всех платформах.

VENDOR_ID = 0

VENDOR_ID может быть целым числом, списком целых чисел или словарем. Если это словарь, то это должен быть словарь словарей в форме:

{
integer_vendor_id : { product_id : [list of BCDs], ... },
...
}

PRODUCT_ID = 0

Целое число или список целых чисел

BCD = None

BCD может иметь значение None, чтобы не различать устройства на основе BCD, или это может быть список BCD номеров всех устройств, поддерживаемых этим драйвером.

WINDOWS_MAIN_MEM = None

Строка, определяющая основную память устройства в строках идентификатора Windows PnP. Это может быть None, строка, список строк или скомпилированное регулярное выражение.

WINDOWS_CARD_A_MEM = None

Строка, идентифицирующая первую карту устройства в строках идентификатора Windows PnP. Это может быть None, строка, список строк или скомпилированное регулярное выражение.

WINDOWS_CARD_B_MEM = None

Строка, идентифицирующая вторую карту устройства в строках идентификатора Windows PnP. Это может быть None, строка, список строк или скомпилированное регулярное выражение.

OSX_MAIN_MEM_VOL_PAT = None

Используется при обнаружении нового драйвера для устранения неоднозначности основной памяти и карт памяти. Должно быть регулярное выражение, которое соответствует точке монтирования основной памяти, назначенной macOS.

MAX_PATH_LEN = 250

Максимальная длина путей, созданных на устройстве

NEWS_IN_FOLDER = True

Поместите новости в отдельную папку

classmethod model_metadata() → tuple[ModelMetadata, ...][исходный код]

Метаданные обо всех реальных моделях устройств, поддерживаемых этим драйвером

reset(key='-1', log_packets=False, report_progress=None, detected_device=None)[исходный код]

Параметры:

- key – Ключ для разблокировки устройства

- log_packets – Если true, поток пакетов к/от устройства логируется.

- report_progress – Функция, которая вызывается с % прогресса (число от 0 до 100) для различных задач. Если она вызывается с -1, это означает, что задача не имеет информации о ходе выполнения.

- detected_device – Device information from the device scanner

set_progress_reporter(report_progress)[исходный код]

Установить функцию для сообщения информации о ходе выполнения.

Параметры:

report_progress – Функция, которая вызывается с % прогресса (число от 0 до 100) для различных задач. Если она вызывается с -1, это означает, что задача не имеет информации о ходе выполнения.

card_prefix(end_session=True)[исходный код]

Возвращает список из 2 элементов префикса для путей на карточках. Если карта отсутствует, для префикса карты устанавливается значение None. НАПРИМЕР („/place“, „/place2“) (None, „place2“) („place“, None) (None, None)

total_space(end_session=True)[исходный код]

Получить общее пространство, доступное на точках монтирования:

1. Основная память

2. Карта памяти A

3. Карта памяти B

Результат:

Список из трех элементов с общим объёмом в байтах (1, 2, 3). Если на конкретном устройстве нет ни одного из этих местоположений, оно должно вернуть 0.

free_space(end_session=True)[исходный код]

Получить свободное место на точках монтирования:

1. Основная память

2. Карта 1

3. Карта 2

Результат:

Список из трех элементов со свободным пространством в байтах (1, 2, 3). Если конкретное устройство не имеет ни одного из этих местоположений, оно должно вернуть -1.

windows_sort_drives(drives)[исходный код]

Вызывается для устранения неоднозначности основной памяти и карты памяти для устройств, которые не различают их на основе WINDOWS_CARD_NAME. Например: EB600

can_handle_windows(usbdevice, debug=False)[исходный код]

Необязательный метод для выполнения дальнейших проверок устройства, чтобы узнать, способен ли этот драйвер обрабатывать его. Если это не так, он должен вернуть False. Этот метод вызывается только после того, как поставщик, идентификаторы продуктов и bcd совпадают, поэтому он может выполнять некоторые относительно трудоемкие проверки. Реализация по умолчанию возвращает True. Этот метод вызывается только в Windows. См. также can_handle().

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

Параметры:

usbdevice – USB-устройство, возвращенное calibre.devices.winusb.scan_usb_devices()

open(connected_device, library_uuid)[исходный код]

Выполнить любую инициализацию конкретного устройства. Вызывается после обнаружения устройства, но перед любыми другими функциями, которые обмениваются данными с устройством. Например: для устройств, которые представляют себя как запоминающие устройства USB, этот метод будет отвечать за установку устройства или, если устройство было автоматически смонтировано, за определение места его установки. Метод calibre.devices.usbms.device.Device.open() имеет реализацию этой функции, которая должна служить хорошим примером для запоминающих устройств USB.

Этот метод может вызвать исключение OpenFeedback для отображения сообщения пользователю.

Параметры:

- connected_device – Устройство, которое мы пытаемся открыть. Это кортеж из (идентификатор поставщика, идентификатор продукта, bcd, название производителя, название продукта, серийный номер устройства). Однако у некоторых устройств нет серийного номера, и в Windows присутствуют только первые три поля, остальные - None.

- library_uuid – UUID текущей библиотеки calibre. Может иметь значение None, если библиотеки нет (например, при использовании из командной строки).

eject()[исходный код]

Размонтировать/извлечь устройство из ОС. Не проверяется есть ли ожидающие задания графического интерфейса пользователя, которым необходимо взаимодействовать с устройством.

ПРИМЕЧАНИЕ. Этот метод нельзя вызывать в том же потоке, что и остальные методы устройства.

post_yank_cleanup()[исходный код]

Вызывается, если пользователь дергает устройство, не извлекая его предварительно.

sanitize_callback(path)[исходный код]

Обратный вызов, позволяющий отдельным драйверам устройств отменять очистку пути, используемую create_upload_path().

filename_callback(default, mi)[исходный код]

Обратный вызов, позволяющий драйверам изменять имя файла по умолчанию, установленное create_upload_path().

sanitize_path_components(components)[исходный код]

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

get_annotations(path_map)[исходный код]

Разрешить path_map в annotation_map файлов, найденных на устройстве

add_annotation_to_library(db, db_id, annotation)[исходный код]

Добавить аннотацию в библиотеку calibre

class calibre.devices.usbms.cli.CLI[исходный код]

class calibre.devices.usbms.driver.USBMS(plugin_path)[исходный код]

Базовые классы: CLI, Device

Базовый класс для всех устройств USBMS. Реализует логику отправки/получения/обновления метаданных/кеширования метаданных и т. д.

description = 'Соединиться с читалкой.'

Короткая строка, описывающая, что делает этот плагин

author = 'John Schember'

Автор этого плагина

supported_platforms = ['windows', 'osx', 'linux']

Список платформ, на которых работает этот плагин. Например: ['windows', 'osx', 'linux']

booklist_class

псевдоним для BookList

book_class

псевдоним для Book

FORMATS = []

Упорядоченный список поддерживаемых форматов

CAN_SET_METADATA = []

Можно ли установить метаданные книг через графический интерфейс.

get_device_information(end_session=True)[исходный код]

Спросить у устройства информацию об устройстве. См. L{DeviceInfoQuery}.

Результат:

(имя устройства, версия устройства, версия программного обеспечения на устройстве, тип MIME). Кортеж может дополнительно иметь пятый элемент, который является словарем информации о накопителе. См. пример usbms.driver.

set_driveinfo_name(location_code, name)[исходный код]

Задать имя устройства в файле информации о диске „name“. Этот параметр будет действовать до тех пор, пока файл не будет создан заново или имя не будет изменено снова.

Недисковые устройства должны реализовывать этот метод на основе кодов местоположения, возвращаемых методом get_device_information().

books(oncard=None, end_session=True)[исходный код]

Вернуть список электронных книг на устройстве.

Параметры:

oncard – Если „carda“ или „cardb“ возвращают список электронных книг на определенной карте памяти, в противном случае возвращает список электронных книг в основной памяти устройства. Если карта указана и на ней нет книг, вернуть пустой список.

Результат:

СписокКниг.

upload_books(files, names, on_card=None, end_session=True, metadata=None)[исходный код]

Загрузите список книг на устройство. Если файл уже существует на устройстве, его следует заменить. Этот метод должен вызывать FreeSpaceError, если на устройстве недостаточно свободного места. Текст FreeSpaceError должен содержать слово «card», если on_card не равно None, в противном случае он должен содержать слово «memory».

Параметры:

- files – Список путей

- names – Список имен файлов книг которые были уже загружены на устройство. len(names) == len(files)

- metadata – Если не None, это список объектов Metadata. Идея состоит в том, чтобы использовать метаданные, чтобы определить, где на устройстве разместить книгу. len(metadata) == len(files). Помимо обычной обложки (путь к обложке), также может быть атрибут эскиза, который следует использовать в предпочтении. Атрибут эскиза имеет форму (ширина, высота, данные обложки в формате jpeg).

Результат:

Список трехэлементных кортежей. Список предназначен для передачи в add_books_to_metadata().

upload_cover(path, filename, metadata, filepath)[исходный код]

Загрузить обложку книги на устройство. Реализация по умолчанию ничего не делает.

Параметры:

- path – Полный путь к папке, в которой находится соответствующая книга.

- filename – Имя файла книги без расширения.

- metadata – метаданные, принадлежащие книге. Используйте metadata.thumbnail для обложки

- filepath – Полный путь к файлу электронной книги

add_books_to_metadata(locations, metadata, booklists)[исходный код]

Добавить местоположения в списки книг. Эта функция не должна связываться с устройством.

Параметры:

- locations – Результат звонка L {upload_books}

- metadata – Список объектов Metadata, такой же, как для upload_books().

- booklists – Кортеж, содержащий результат вызовов (books (oncard = None)(),: meth:books(oncard=“carda“),: meth`books(oncard=“cardb“)`).

delete_books(paths, end_session=True)[исходный код]

Удалять книги по путям на устройстве.

remove_books_from_metadata(paths, booklists)[исходный код]

Удалить книги из списка метаданных. Эта функция не должна связываться с устройством.

Параметры:

- paths – пути к книгам на устройстве.

- booklists – Кортеж, содержащий результат вызовов (books (oncard = None)(),: meth:books(oncard=“carda“),: meth`books(oncard=“cardb“)`).

sync_booklists(booklists, end_session=True)[исходный код]

Обновить метаданные на устройстве.

Параметры:

booklists – Кортеж, содержащий результат вызовов (books (oncard = None)(),: meth:books(oncard=“carda“),: meth`books(oncard=“cardb“)`).

classmethod normalize_path(path)[исходный код]

Вернуть путь с нативными разделителями путей платформы

Действия в пользовательском интерфейсе

Если вы добавляете свой собственный плагин в ZIP-файл, вы должны создать подкласс как InterfaceActionBase, так и InterfaceAction. Метод load_actual_plugin() вашего подкласса InterfaceActionBase должен возвращать экземпляр объекта вашего подкласса InterfaceBase.

class calibre.gui2.actions.InterfaceAction(parent, site_customization)[исходный код]

Базовые классы: QObject

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

Обратите внимание, что этот класс является базовым классом для этих подключаемых модулей, однако, чтобы интегрировать подключаемый модуль с системой подключаемых модулей calibre, вам необходимо создать класс-оболочку, который ссылается на фактический подключаемый модуль. См. Примеры модуля calibre.customize.builtins.

Если два объекта InterfaceAction имеют одинаковое имя, приоритет имеет объект с более высоким приоритетом.

Подклассы должны реализовывать методы genesis(), library_changed(), location_selected(), shutting_down(), initialization_complete() и tag_browser_context_action()

После инициализации этот плагин получает доступ к графическому интерфейсу основного calibre через член gui. Вы можете получить доступ к другим плагинам по имени, например:

self.gui.iactions['Save To Disk']

Чтобы получить доступ к собственному плагину, используйте атрибут interface_action_base_plugin, этот атрибут становится доступным только после инициализации плагина. Полезно, если вы хотите использовать методы из класса плагина, например do_user_config().

QAction, указанный в action_spec, создается автоматически и становится доступным как self.qaction.

name = 'Implement me'

Название плагина. Если присутствуют два плагина с одинаковым именем, приоритет имеет тот, который имеет более высокий приоритет.

priority = 1

Приоритет плагина. Если присутствуют два плагина с одинаковым именем, приоритет имеет тот, который имеет более высокий приоритет.

popup_type = 1

Тип всплывающего меню, когда этот плагин добавляется на панель инструментов.

auto_repeat = False

Следует ли автоматически повторять это действие при удерживании его сочетания клавиш.

action_spec = ('text', 'icon', None, None)

Форма: (текст, путь к значку, всплывающая подсказка, сочетание клавиш) значок, всплывающая подсказка и сочетание клавиш могут иметь значение None, сочетание клавиш должно быть строкой, None или кортежем сочетаний клавиш. Если None, сочетание клавиш, соответствующее действию, не регистрируется. Если вы передаете пустой кортеж, ярлык регистрируется без привязки клавиш по умолчанию.

action_shortcut_name = None

Если не None, используется для имени, отображаемого пользователю при настройке сочетаний клавиш для указанной выше спецификации действия вместо action_spec[0]

action_add_menu = False

Если True, меню создается автоматически и добавляется в self.qaction.

action_menu_clone_qaction = False

Если True, клон self.qaction добавляется в меню self.qaction. Если вы хотите, чтобы текст этого действия отличался от текста self.qaction, установите для этой переменной новый текст.

dont_add_to = frozenset({})

Набор локаций, в которые нельзя добавлять это действие. См. all_locations для списка возможных местоположений.

dont_remove_from = frozenset({})

Набор локаций, из которых нельзя удалять это действие. См. all_locations для списка возможных местоположений.

action_type = 'global'

Тип действия „current“ означает действие в текущем представлении, „global“ означает действие, которое действует не на текущее представление, а, скорее, на calibre в целом.

accepts_drops = False

Если True, то у этого InterfaceAction будет возможность взаимодействовать с событиями перетаскивания. Подробнее см. в методах accept_enter_event(), accept_drag_move_event(), drop_event().

accept_enter_event(event, mime_data)[исходный код]

Этот метод должен возвращать True, если это действие интерфейса способно обрабатывать событие перетаскивания. Не вызывайте accept/ignore для события, о котором позаботится пользовательский интерфейс calibre.

accept_drag_move_event(event, mime_data)[исходный код]

Этот метод должен возвращать True, если это действие интерфейса способно обрабатывать событие перетаскивания. Не вызывайте accept/ignore для события, о котором позаботится пользовательский интерфейс calibre.

drop_event(event, mime_data)[исходный код]

Этот метод должен выполнять некоторые полезные действия и возвращать True, если это действие интерфейса способно обрабатывать событие drop. Не вызывайте accept/ignore для события, о котором позаботится пользовательский интерфейс calibre. В этой функции не следует выполнять блокирующие/длительные операции. Вместо этого отправьте сигнал или используйте QTimer.singleShot и быстро вернитесь. См. примеры встроенных действий.

create_menu_action(menu, unique_name, text, icon=None, shortcut=None, description=None, triggered=None, shortcut_name=None, persist_shortcut=False)[исходный код]

Удобный метод простого добавления действий в QMenu. Возвращает созданный QAction. Это действие имеет один дополнительный атрибут calibre_shortcut_unique_name, который, если не None, относится к уникальному имени, под которым это действие зарегистрировано диспетчером клавиатуры.

Параметры:

- menu – QMenu вновь созданное действие будет добавлено в

- unique_name – Уникальное имя для этого действия, оно должно быть уникальным во всем мире, поэтому сделайте его как можно более описательным. Если сомневаетесь, добавьте к нему UUID.

- text – Текст действия.

- icon – Либо QIcon, либо имя файла. Имя файла передается встроенной функции QIcon.ic(), поэтому вам не нужно передавать полный путь к папке с изображениями.

- shortcut – Строка, список строк, None или False. Если значение равно False, для этого действия не регистрируется комбинация клавиш. Если None, то регистрируется сочетание клавиш без привязки клавиш по умолчанию. Строка и список строк регистрируют ярлык с привязкой клавиш по умолчанию, как указано.

- description – Описание этого действия. Используется для установки всплывающих подсказок.

- triggered – Вызываемый объект, связанный с сработавшим сигналом созданного действия.

- shortcut_name – Текст, отображаемый пользователю при настройке сочетаний клавиш для этого действия. По умолчанию установлено значение text.

- persist_shortcut – Ярлыки для действий, которые не всегда появляются или зависят от библиотеки, могут исчезнуть при редактировании других сочетаний клавиш, если для параметра persist_shortcut установлено значение True.

load_resources(names)[исходный код]

Если этот плагин поставляется в виде ZIP-файла (плагин, добавленный пользователем), этот метод позволит вам загружать ресурсы из ZIP-файла.

Например, чтобы загрузить изображение:

pixmap = QPixmap()
pixmap.loadFromData(tuple(self.load_resources(['images/icon.png']).values())[0])
icon = QIcon(pixmap)

Параметры:

names – Список путей к ресурсам в ZIP-файле с использованием разделителя /

Результат:

Словарь формы „{name: file_contents}“. Любые имена, которые не были найдены в ZIP-файле, не будут присутствовать в словаре.

genesis()[исходный код]

Установка этого плагина. Вызывается только один раз во время инициализации. self.gui доступен. Действие, указанное в action_spec, доступно как self.qaction.

location_selected(loc)[исходный код]

Вызывается при изменении отображаемого списка книг в calibre. В настоящее время значения loc следующие: library, main, card и cardb.

Этот метод должен включать/отключать это действие и его вспомогательные действия в зависимости от местоположения.

library_about_to_change(olddb, db)[исходный код]

Вызывается при изменении текущей библиотеки.

Параметры:

- olddb – LibraryDatabase, соответствующая предыдущей библиотеке.

- db – LibraryDatabase, соответствующая новой библиотеке.

library_changed(db)[исходный код]

Вызывается при изменении текущей библиотеки.

Параметры:

db – LibraryDatabase, соответствующая текущей библиотеке.

gui_layout_complete()[исходный код]

Вызывается один раз за действие, когда макет основного графического интерфейса завершен. Если ваше действие требует внесения изменений в макет, они должны быть выполнены здесь, а не в initialization_complete().

initialization_complete()[исходный код]

Вызывается один раз для каждого действия после завершения инициализации основного графического интерфейса.

tag_browser_context_action(index)[исходный код]

Вызывается при отображении контекстного меню в браузере тегов. index - это QModelIndex, который указывает на элемент браузера тегов, нажатый правой кнопкой мыши. Проверьте его на достоверность с помощью index.valid () и получите базовый объект TagTreeItem с помощью index.data (Qt.ItemDataRole.UserRole). Любые объекты действия, полученные этим методом, будут добавлены в контекстное меню.

shutting_down()[исходный код]

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

class calibre.customize.InterfaceActionBase(*args, **kwargs)[исходный код]

Базовые классы: Plugin

supported_platforms = ['windows', 'osx', 'linux']

Список платформ, на которых работает этот плагин. Например: ['windows', 'osx', 'linux']

author = 'Kovid Goyal'

Автор этого плагина

type = 'Действие в пользовательском интерфейсе'

Тип плагина. Используется для категоризации в графической оболочке.

can_be_disabled = False

При False пользователь не сможет отключить этот плагин. Используйте с осторожностью.

load_actual_plugin(gui)[исходный код]

Этот метод должен возвращать фактический объект плагина действия интерфейса.

Плагины параметров
class calibre.customize.PreferencesPlugin(plugin_path)[исходный код]

Базовые классы: Plugin

Плагин, представляющий виджет, отображаемый в диалоговом окне «Настройки».

У этого плагина есть только один важный метод create_widget(). Различные поля плагина определяют его категоризацию в пользовательском интерфейсе.

supported_platforms = ['windows', 'osx', 'linux']

Список платформ, на которых работает этот плагин. Например: ['windows', 'osx', 'linux']

author = 'Kovid Goyal'

Автор этого плагина

type = 'Параметры'

Тип плагина. Используется для категоризации в графической оболочке.

can_be_disabled = False

При False пользователь не сможет отключить этот плагин. Используйте с осторожностью.

config_widget = None

Путь импорта к модулю, который содержит класс с именем ConfigWidget, который реализует интерфейс ConfigWidgetInterface. Используется create_widget().

category_order = 100

Где в списке категорий должна быть category этого плагина

name_order = 100

Где в списке имен в категории должен быть gui_name этого плагина

category = None

Категория, в которую должен входить этот плагин

gui_category = None

Название категории, отображаемое пользователю для этого плагина.

gui_name = None

Имя, отображаемое пользователю для этого плагина.

icon = None

Значок этого плагина должен быть абсолютным путем

description = None

Описание, используемое для всплывающих подсказок и т.п.

create_widget(parent=None)[исходный код]

Создать и вернуть фактический виджет Qt, используемый для установки этой группы предпочтений. Виджет должен реализовывать calibre.gui2.preferences.ConfigWidgetInterface.

Реализация по умолчанию использует config_widget для создания экземпляра виджета.

class calibre.gui2.preferences.ConfigWidgetInterface[исходный код]

Этот класс определяет интерфейс, который должны реализовывать все виджеты, отображаемые в диалоговом окне «Настройки». Смотрите ConfigWidgetBase, чтобы узнать о базовом классе, который реализует этот интерфейс и также определяет различные удобные методы.

changed_signal = None

Этот сигнал должен излучаться всякий раз, когда пользователь изменяет значение в этом виджете.

supports_restoring_to_defaults = True

Установить значение True, если реализован метод restore_to_defaults().

restore_defaults_desc = 'Восстановление параметров по умолчанию. Вы должны нажать кнопку Применить, чтобы сохранить параметры по умолчанию.'

Подсказка для кнопки «Восстановить по умолчанию»

restart_critical = False

Если True, диалоговое окно Preferences не позволит пользователю устанавливать какие-либо другие предпочтения. Действует только в том случае, если commit() возвращает True.

genesis(gui)[исходный код]

Вызывается один раз перед отображением виджета, должен выполнить все необходимые настройки.

Параметры:

gui – Главный графический интерфейс пользователя calibre

initialize()[исходный код]

Следует установить для всех значений конфигурации их начальные значения (значения, хранящиеся в файлах конфигурации). Оператор возврата не является обязательным. Вернуть False, если диалоговое окно не должно отображаться.

restore_defaults()[исходный код]

Должно установить для всех значений конфигурации значения по умолчанию.

commit()[исходный код]

Сохранить все изменённые настройки. Вернуть True, если изменения требуют перезапуска, в противном случае - False. Вызвать исключение AbortCommit, чтобы указать, что произошла ошибка. Вы несете ответственность за предоставление пользователю отзыва о том, в чём заключается ошибка и как её исправить.

refresh_gui(gui)[исходный код]

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

initial_tab_changed()[исходный код]

Вызывается, если изначально отображаемая вкладка изменена до показа виджета, но после его инициализации.

class calibre.gui2.preferences.ConfigWidgetBase(parent=None)[исходный код]

Базовый класс, содержащий код для простого добавления стандартных виджетов конфигурации, таких как флажки, поля со списком, текстовые поля и т. д. См. метод register().

Этот класс автоматически обрабатывает уведомление об изменении, сброс до значения по умолчанию, перевод между объектами gui и объектами конфигурации и т. д. для зарегистрированных настроек.

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

changed_signal

Этот сигнал должен излучаться всякий раз, когда пользователь изменяет значение в этом виджете.

supports_restoring_to_defaults = True

Установить значение True, если реализован метод restore_to_defaults().

restart_critical = False

Если True, диалоговое окно Preferences не позволит пользователю устанавливать какие-либо другие предпочтения. Действует только в том случае, если commit() возвращает True.

register(name, config_obj, gui_name=None, choices=None, restart_required=False, empty_string_is_None=True, setting=<class 'calibre.gui2.preferences.Setting'>)[исходный код]

Зарегистрировать настройки.

Параметры:

- name – Название настройки

- config_obj – Объект конфигурации, который читает/записывает параметр

- gui_name – Имя объекта GUI, представляющего интерфейс для изменения настройки. По умолчанию предполагается, что это 'opt_' + name.

- choices – Если этот параметр основан на множественном выборе (поле со списком), то это - список вариантов. Список представляет собой список из двух кортежей элементов формы: `` [(имя графического интерфейса, значение), …] ``

- setting – Класс, отвечающий за управление этим параметром. Класс по умолчанию обрабатывает почти все случаи, поэтому этот параметр используется редко.

initialize()[исходный код]

Следует установить для всех значений конфигурации их начальные значения (значения, хранящиеся в файлах конфигурации). Оператор возврата не является обязательным. Вернуть False, если диалоговое окно не должно отображаться.

commit(*args)[исходный код]

Сохранить все изменённые настройки. Вернуть True, если изменения требуют перезапуска, в противном случае - False. Вызвать исключение AbortCommit, чтобы указать, что произошла ошибка. Вы несете ответственность за предоставление пользователю отзыва о том, в чём заключается ошибка и как её исправить.

restore_defaults(*args)[исходный код]

Должно установить для всех значений конфигурации значения по умолчанию.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Командная строка

Командная строка

Примечание

В macOS инструменты командной строки находятся в пакете Calibre, например, если вы установили Calibre в /Applications инструменты командной строки находятся в /Applications/calibre.app/Contents/MacOS/. Так, например, для запуска ebook-convert вы можете использовать /Applications/calibre.app/Contents/MacOS/ebook-convert.

Документированные команды

- calibre
- calibre-customize
- calibre-debug
- calibre-server
- calibre-smtp
- calibredb
- ebook-convert
- ebook-edit
- ebook-meta
- ebook-polish
- ebook-viewer
- fetch-ebook-metadata
- lrf2lrs
- lrfviewer
- lrs2lrf
- web2disk

Недокументированные команды

- ebook-device

- markdown-calibre

Вы можете узнать, как использовать недокументированные команды, выполнив их без аргументов в терминале.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Командная строка »
- calibre

calibre

calibre [параметры] [path_to_ebook или calibre url ...]

Запустить основной графический интерфейс пользователя и, при желании, добавить электронную книгу по path_to_ebook в базу данных. Также возможно указать URL-адреса calibre для выполнения других действий. Например:

calibre://view-book/test_library/1842/epub
откроет книгу с id 1842 в формате EPUB из библиотеки
«test_library» в просмотрщике электронных книг. Имена библиотек - это имена папок библиотеки с пробелами, заменёнными подчеркиванием. Полное описание действий на основе URL-адресов приведены в Руководстве пользователя.

Когда вы передаёте аргументы с пробелами в calibre, заключайте их в кавычки. Например: «/some path/with spaces»

[опции]

--detach

Отвязаться от управляющего терминала (только Linux)

--help, -h

показать это сообщение и выйти

--ignore-plugins

Игнорировать пользовательские плагины. Полезно, если вы установили плагин, препятствующий запуску Calibre

--no-update-check

Не проверять наличие обновлений

--shutdown-running-calibre, -s

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

--start-in-tray

Запускать свёрнутым в системный лоток.

--verbose, -v

Не учитывается, поэтому не используйте. Оставлено только для совместимости

--version

показать версию программы и выйти

--with-library

Использовать библиотеку по указанному пути.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Командная строка »
- calibre-customize

calibre-customize

calibre-customize [опции]

Настроить calibre с помощью загрузки внешних плагинов.

Когда вы передаёте аргументы с пробелами в calibre-customize, заключайте их в кавычки. Например: «/some path/with spaces»

[опции]

--add-plugin, -a

Добавить плагин, указав путь к содержащему его ZIP файлу.

--build-plugin, -b

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

--customize-plugin

Настроить плагин. Укажите имя плагина и строку настройки через запятую. Строка настройки такая же, как и при настройке плагина в графическом интерфейсе.

--disable-plugin

Отключить указанный плагин

--enable-plugin

Включить указанный плагин

--help, -h

показать это сообщение и выйти

--list-plugins, -l

Список всех установленных плагинов

--remove-plugin, -r

Удалить пользовательский плагин по имени. Не влияет на встроенные плагины

--version

показать версию программы и выйти

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Командная строка »
- calibre-debug

calibre-debug

calibre-debug [options]

Разные интерфейсы командной строки, полезные для отладки calibre. Без вариантов, эта команда запускает встроенный интерпретатор Python. Также можно запустить основной
GUI calibre, просмотрщик электронных книг calibre и редактор calibre в режиме отладки.

Он также содержит интерфейсы для различных опций calibre, для которых нет отдельных инструментов командной строки, например выделение подмножества шрифтов, инструмент сравнения электронных книг и т. д.

Вы также можете использовать calibre-debug для запуска автономных скриптов. Для этого используйте его так:

calibre-debug -e myscript.py -- --option1 --option2 file1 file2 …

Всё, что после -- передаётся в скрипт. Вы также можете использовать calibre-debug как шебанг (shebang) в скриптах, вот так:

#!/usr/bin/env -S calibre-debug -e -- –

Когда вы передаёте аргументы с пробелами в calibre-debug, заключайте их в кавычки. Например: «/some path/with spaces»

[опции]

--add-simple-plugin

Добавить простой плагин (т.е. плагин, который состоит только из .py файла), указав путь к .py файлу, содержащему код плагина.

--command, -c

Запустить Python код

--debug-device-driver, -d

Отладка обнаружения устройства

--default-programs

Добавить (убрать) Calibre в список программ по умолчанию Windows --default-programs = (register|unregister)

--diff

Запустить инструмент сравнения. Например: calibre-debug --diff file1 file2

--edit-book

Запустите инструмент "Редактировать книгу" в режиме отладки.

--exec-file, -e

Запустить Python код в файле.

--explode-book, -x

Взорвать книгу в указанную папку. Использование: -x file.epub output_dir Экспортирует книгу как набор файлов HTML и метаданных, которые можно редактировать с помощью стандартных инструментов редактирования HTML. Работает с файлами EPUB, AZW3, HTMLZ и DOCX.

--export-all-calibre-data

Экспортировать все данные calibre (книги/настройки/плагины). Обычно вас попросят указать папку экспорта и библиотеки для экспорта. Вы также можете указать их в качестве аргументов командной строки, чтобы пропустить вопросы. Используйте абсолютные пути для папки экспорта и библиотек. Специальное ключевое слово all можно использовать для экспорта всех библиотек. Примеры: calibre-debug --export-all-calibre-data # для интерактивного использования calibre-debug --export-all-cale-data /путь/к/пустому/экспорту/папке /путь/к/библиотеке/папке1 /путь/к/библиотеке2 calibre-debug --export-all-dication-data /export/folder all # экспортировать все известные библиотеки

--gui, -g

Запустить графический интерфейс с включенной отладкой. Отладочная информация будет выводиться в потоки stdout и stderr

--gui-debug

Запустить графический интерфейс с отладочной консолью, фиксируя указанный путь. Только для внутреннего использования. Используйте ключ -g чтобы запустить графический интерфейс в отладочном режиме.

--help, -h

показать это сообщение и выйти

--implode-book, -i

Исправить ранее развернутую книгу. Использование: -i output_dir file.epub Импортировать книгу из файлов в output_dir, которые созданы предыдущим вызовом -explode-book. Используйте тот же тип файла, который был при разворачивании.

--import-calibre-data

Импорт ранее экспортированных данных calibre

--inspect-mobi, -m

Посмотреть MOBI файл(файлы) по указанному пути(путях)

--kepubify

Конвертировать указанный файл EPUB в KEPUB без полной конвертации. Это то, что делает драйвер Kobo при отправке файлов на устройство.

--paths

Вывести пути, необходимые для установки окружения для calibre

--run-plugin, -r

Запустить плагин, обеспечивающий интерфейс командной строки. Например: calibre-debug -r "Plugin name" -- file1 --option1 Всё после -- будет передано плагину в качестве параметров.

--run-test, -t

Запустить упомянутый(-е) тест(-ы). Специальное значение all - запустить все тесты. Если имя теста начинается с точки, предполагается, что это имя модуля. Если имя теста начинается с @, предполагается, что это имя категории.

--run-without-debug

Не запускать с установленным флагом DEBUG

--shutdown-running-calibre, -s

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

--subset-font, -f

Сократить указанный шрифт. Добавьте -- после параметра, чтобы передать параметр программе выделения подмножества шрифта.

--test-build

Тест бинарных модулей в сборке

--un-kepubify

Конвертировать указанный файл KEPUB в EPUB без полной конвертации. Это то, что делает драйвер Kobo при импорте файлов с устройства.

--version

показать версию программы и выйти

--viewer, -w

Запустить читалку в режиме отладки

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Командная строка »
- calibre-server

calibre-server

calibre-server [опции] [путь к папке библиотеки…]

Запустить контент-сервер calibre. Контент-сервер показывает ваши библиотеки calibre через интернет. Вы можете указать пути к папкам библиотек в качестве аргументов для calibre-server. Если вы не укажете какие-либо пути, будут использоваться все библиотеки, о которых знает основная программа calibre.

Когда вы передаёте аргументы с пробелами в calibre-server, заключайте их в кавычки. Например: «/some path/with spaces»

[опции]

--access-log

Путь к файлу журнала доступа. Журнал содержит информацию о клиентах, подключающихся к серверу и делающих запросы. По умолчанию журнал доступа не ведётся.

--ajax-timeout

Время (в секундах), ожидания ответа от сервера при выполнении запросов.

--auth-mode

Выбрать тип используемой авторизации. Установка используемого сервером режима проверки подлинности HTTP. Выберите «basic», если размещаете этот сервер за SSL-прокси. Или оставьте «auto», который будет использовать «basic», если SSL настроен, а в противном случае — «digest».

--auto-reload

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

--ban-after

Количество неудачных попыток входа в систему. Количество неудачных входов, после которого IP-адрес запрещается

--ban-for

Запретить IP-адреса с повторными ошибками входа. Временно запрещает доступ для IP-адресов с повторными ошибками входа в течение указанного количества минут. Полезно для предотвращения попыток подбора паролей. Если установлено в ноль, запрет не выполняется.

--book-list-mode

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

--compress-min-size

Мин. размер, для использования сжатия данных в ответах (в байтах).

--custom-list-template

Путь к файлу JSON, содержащему шаблон для настраиваемого режима списка книг. Простейший способ создать такой файл шаблона - перейти в calibre Настройки -> Обмен через сеть -> Шаблон списка книг , создать шаблон и экспортировать его.

--daemonize

Запустить процесс в фоне как демон (только Linux).

--displayed-fields

Ограничить отображаемые пользовательские поля. Список разделенных пользователем полей метаданных, которые будут отображаться контент-сервером в представлениях /opds и /mobile. Если вы укажете этот параметр, любые поля, не входящие в этот список, не будут отображаться. Например: my_rating,my_tags

--enable-allow-socket-preallocation, --disable-allow-socket-preallocation

Предварительное выделение сокета, например, systemd активация сокета. По умолчанию эта опция включена.

--enable-auth, --disable-auth

Аутентификация на основе пароля для доступа к серверу. Как правило, сервер неограничен, что позволяет всем получить к нему доступ. Эта опция ограничит доступ предопределенными пользователями. По умолчанию эта опция отключена.

--enable-fallback-to-detected-interface, --disable-fallback-to-detected-interface

Откат к автоматически определяемому интерфейсу. Если по какой-то причине сервер неспособен присоединиться к интерфейсу, заданному в настройке listen_on, он попробует определить интерфейс, который подключён к внешнему миру, и присоединиться к нему. По умолчанию эта опция включена.

--enable-local-write, --disable-local-write

Разрешить не аутентифицированным локальным подключениям вносить изменения. Обычно, если вы не включаете аутентификацию, сервер работает в режиме только для чтения, чтобы анонимные пользователи не вносили изменений в ваши библиотеки. Эта опция позволяет любому пользователю подключаться к серверу calibre для внесения изменений. Небезопасно запускать сервер без проверки подлинности, с использованием calibredb для внесения изменений в ваши библиотеки. Обратите внимание, что включение этой опции означает, что ЛЮБАЯ программа, запущенная на компьютере, может вносить изменения в ваши библиотеки. По умолчанию эта опция отключена.

--enable-log-not-found, --disable-log-not-found

Логировать HTTP 404 (Not Found) запросы. Обычно сервер регистрирует все HTTP-запросы для ресурсов, которые не найдены.Это может привести к большому количеству спама в журнале, если на ваш сервер нацелились боты. Используйте этот параметр, чтобы отключить логирование. По умолчанию эта опция включена.

--enable-use-bonjour, --disable-use-bonjour

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

--enable-use-sendfile, --disable-use-sendfile

Zero-copy передача файлов для увеличения производительности. Включение zero-copy для передач внутри ядра при отправке файлов по сети с целью увеличения производительности. Однако, это может привести к возникновению ошибок при передаче файлов в неверные файловые системы. Если вы столкнулись с повреждением файлов при передаче, отключите эту опцию. По умолчанию эта опция включена.

--help, -h

показать это сообщение и выйти

--ignored-fields

Игнорировать определённые пользователем поля метаданных. Список разделенных запятой пользовательских полей метаданных, не отображаемые контент-сервером в представлениях /opds и /mobile. Например: my_rating,my_tags

--listen-on

Интерфейс для прослушивания входящих соединений. По умолчанию прослушивание осуществляется на всех доступных интерфейсах IPv6 и IPv4. Вы можете изменить это, например, на "127.0.0.1", чтобы прослушивать только IPv4-соединения с локальной машины, или на "0.0.0.0", чтобы прослушивать все входящие IPv4-соединения.

--log

Путь к файлу журнала сервера. Журнал содержит информацию сервера и ошибки, а не журналы доступа. По умолчанию пишется в stdout.

--manage-users

Управляйте базой данных пользователей, которым разрешено подключаться к этому серверу. Возможно использование в автоматическом режиме добавив –. Подробнее см. calibre-server --manage-users -- help. См. также параметр --userdb.

--max-header-line-size

Макс. размер одного заголовка HTTP (в КБ).

--max-job-time

Макс. время жизни рабочих процессов. Максимально допустимое время жизни запущенного рабочего процесса (в минутах). Установите в ноль для снятия ограничения по времени.

--max-jobs

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

--max-log-size

Максимальный размер файла журнала (в МБ). Максимальный размер файлов журнала /лог-файлов/, создаваемых сервером. Как только размер лог-файла превышает заданный, содержимое файла автоматически ротируется. Установка в 0 отключает ротацию.

--max-opds-items

Максимальное число книг в фиде OPDS. Максимальное число книг, которое сервер вернёт в одном принимающем фиде OPDS.

--max-opds-ungrouped-items

Максимальное число несгрупированных элементов в фидах OPDS. Группировать элементы по категориям, таким как автор/теги по первой букве, когда элементов больше, чем это число. Установите в 0 для отключения.

--max-request-body-size

Макс. допустимый размер файлов, загружаемых на сервер (в МБ).

--num-per-page

Количество книг для показа на одной странице. Количество книг, отображаемых на одной странице в браузере.

--pidfile

Записать PID процесса в указанный файл

--port

Порт для входящих соединений.

--search-the-net-urls

Путь к файлу JSON, содержащему URL-адреса для функции Поиск в Интернет. Самый простой способ создать такой файл - перейти в Настройки -> Обмен через сеть -> Поиск в Интернет, создать URL-адреса и экспортировать их.

--shutdown-timeout

Общее время ожидания «чистого» отключения, в секундах .

--ssl-certfile

Путь к файлу сертификата SSL.

--ssl-keyfile

Путь к файлу секретного ключа SSL.

--timeout

Время (в секундах) до отключения холостого соединения.

--trusted-ips

Разрешить неаутентифицированным соединениям с определенных IP-адресов вносить изменения. Обычно, если вы не включаете аутентификацию, сервер работает в режиме только для чтения, чтобы анонимные пользователи не вносили изменений в ваши библиотеки. Эта опция позволяет любому пользователю подключаться к серверу calibre для внесения изменений. Должен быть список адресов или спецификаций сетей через запятую. Небезопасно запускать сервер без проверки подлинности, с использованием calibredb для внесения изменений в ваши библиотеки. Обратите внимание, что включение этой опции означает, что любой, кто подключается с указанных IP-адресов, может вносить изменения в ваши библиотеки.

--url-prefix

Префикс, добавляемый ко всем URL-ам. Полезно, если вы хотите запустить этот сервер за обратным прокси. Например, используйте /calibre в качестве префикса URL.

--userdb

Путь к пользовательской базе данных, используемой для аутентификации. База данных представляет собой SQLite-файл. Для его создания используйте --manage-users. Вы можете узнать больше об управлении пользователями по адресу: https://manual.calibre-ebook.com/ru/server.html#managing-user-accounts-from-the-command-line-only

--version

показать версию программы и выйти

--worker-count

Количество рабочих потоков для обработки запросов.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Командная строка »
- calibre-smtp

calibre-smtp

calibre-smtp [опции] [от_кого кому текст]

Отправить почту по протоколу SMTP. В calibre-smtp два режима работы. В режиме сочинения вы указываете «от_кого», «кому» и «текст» — всё это используется для создания и отправки почтового сообщения. В режиме фильтра calibre-smtp считывает полное сообщение электронной почты из STDIN и отправляет его.

«Текст» — это содержимое сообщения электронной почты.
Если «текст» не задан, полное сообщение электронной почты считывается из STDIN.
«От_кого» — адрес отправителя, а «кому» — адрес электронной почты получателя.
Когда полное сообщение читается из STDIN, «от_кого» и «кому» используются только в согласовании SMTP, заголовки сообщения не меняются.

Когда вы передаёте аргументы с пробелами в calibre-smtp, заключайте их в кавычки. Например: «/some path/with spaces»

[опции]

--fork, -f

Ветвление и доставка сообщений в фоне. Если вы используете этот вариант, вы должны также использовать --outbox для обработки отказов доставки.

--help, -h

показать это сообщение и выйти

--localhost, -l

Имя локального хоста. Испольуется при подключении к серверу SMTP.

--outbox, -o

Путь к папке почты для хранения недоставленных сообщений.

--timeout, -t

Таймаут для соединения

--verbose, -v

Быть многословнее

--version

показать версию программы и выйти

COMPOSE MAIL

Параметры для составления письма. Игнорируются, если текст не задан

--attachment, -a

Файл для прикрепления к электронному письму

--subject, -s

Тема письма

SMTP RELAY

Параметры для отправки почты через SMTP релей сервер. calibre попытается отправить письмо напрямую, если –relay не задан.

--cafile

Путь к файлу конкатенированных сертификатов CA в формате PEM, используемому для проверки сертификата сервера при использовании TLS. По умолчанию используются системные CA сертификаты.

--dont-verify-server-certificate

Не проверять сертификат сервера при подключении с помощью TLS. Раньше это было поведение по умолчанию в calibre до 3.27. Если вы используется релей с самоподписанным или иным образом недействительным сертификатом, вы можете использовать этот параметр для восстановления поведения до 3.27

--encryption-method, -e

Метод шифрования при подключении к релею. Выбор из TLS, SSL и NONE. По-умолчанию TLS. ВНИМАНИЕ: опция NONE крайне небезопасна

--password, -p

Пароль для релея

--port

Порт подключения к почтовому релей серверу. По-умолчанию 465, если используется SSL шифрование, иначе 25.

--relay, -r

SMTP релей сервер для отправки почты.

--username, -u

Имя для релея

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Командная строка »
- calibredb

calibredb

calibredb команда [опции] [аргументы]

calibredb - это интерфейс командной строки к базе данных calibre. Оно имеет несколько подкоманд, описанных ниже.

calibredb может использоваться для управления любой базой данных calibre указывается путь или calibre Content server, работающий на на локальном компьютере или через Интернет. Вы можете запустить calibre Content server с помощью команды calibre-server или в главной программе calibre нажав Подключить/поделиться  → Запустить Content server. Так как calibredb может вносить изменения в ваш calibre, вы должны сначала настроить аутентификацию на сервере.
Есть два способа сделать это:

- Если вы планируете подключаться только к серверу, работающему на том же компьютере,

вы можете просто использовать параметр –enable-local-write в контент-сервере, чтобы любая программа, включая calibredb, работала на локальный компьютер, чтобы внести изменения в данные calibre. При запуске сервера из главной программы calibre, эта опция есть в Preferences-Sharing over the net-Advanced.

- Если вы хотите разрешить доступ через Интернет, вам следует настроить

учетные записи пользователей на сервере и использовать --username и :option:` –password` options to calibredb, чтобы предоставить доступ. Вы можете настроить аутентификацию пользователей для calibre-server с помощью` –enable-auth` вариант и используя --manage-users для создания учетных записей пользователей.
Если вы запускаете сервер из главной программы calibre, используйте Preferences-Sharing over the net-Require username / password.

Чтобы подключиться к работающему серверу содержимого, передайте URL-адрес сервера в параметр: option: –with-library, см. документацию по этому параметру.

- ГЛОБАЛЬНЫЕ НАСТРОЙКИ

- list

- add

- ДОБАВЛЕНИЕ ИЗ ПАПОК

- remove

- add_format

- remove_format

- show_metadata

- set_metadata

- export

- catalog

- ОПЦИИ EPUB

- saved_searches

- add_custom_column

- custom_columns

- remove_custom_column

- set_custom

- restore_database

- check_library

- list_categories

- backup_metadata

- clone

- embed_metadata

- search

- fts_index

- fts_search

ГЛОБАЛЬНЫЕ НАСТРОЙКИ

--help, -h

показать это сообщение и выйти

--library-path, --with-library

Путь к библиотеке calibre. По умолчанию используется путь, сохранённый в настройках. Также можно подключиться к контент-серверу calibre для выполнения действий в удалённых библиотеках. Для этого используйте URL-адрес: http://hostname:port/#library_id, например, http://localhost:8080/#mylibrary. Library_id - это идентификатор библиотеки библиотеки, к которой вы хотите подключиться, на контент-сервере. Можно использовать специальное значение library_id для получения списка идентификаторов библиотеки, доступных на сервере. Подробнее о настройке доступа через контент-сервер см. в разделе https://manual.calibre-ebook.com/ru/generated/ru/calibredb.html.

--password

Пароль для подключения к контент-серверу calibre. Чтобы прочитать пароль со стандартного ввода, используйте специальное значение: <stdin>. Чтобы прочитать пароль из файла, используйте: <f:/path/to/file> (т.е. <f: за ним следует полный путь к файлу и закрывающий >). Требуются угловые скобки в приведённом выше разделе, не забудьте экранировать их или использовать кавычки в вашем шелле.

--timeout

Тайм-аут в секундах при подключении к библиотеке calibre по сети. По умолчанию - две минуты.

--username

Имя пользователя для соединения с контент-сервером calibre

--version

показать версию программы и выйти

list

calibredb список [options]

Список книг в базе данных calibre.

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

--ascending

Сортировать результаты по возрастанию

--fields, -f

Поля, отображаемые при перечислении книг в базе данных. Должен быть разделённый запятыми список полей. Доступные поля: author_sort, authors, comments, cover, formats, identifiers, isbn, languages, last_modified, pubdate, publisher, rating, series, series_index, size, tags, template, timestamp, title, uuid По умолчанию: title,authors. Специальное поле "all" можно использовать для выбора всех полей. В дополнение к встроенным полям, пользовательские поля также доступны как *field_name, например, для настраиваемого поля #rating, используйте имя: *rating

--for-machine

Генерирует вывод в формате JSON, более подходящем для машинного разбора. Игнорирует настройки ширины строки и разделителя.

--limit

Максимальное число результатов для отображения. По умолчанию: все

--line-width, -w

Максимальная ширина строки вывода. По умолчанию - размер экрана.

--prefix

Префикс для всех путей. По умолчанию - абсолютный путь к директории библиотеки.

--search, -s

Фильтровать результаты запроса. С форматом запроса можно ознакомиться в соответствующей документации в руководстве пользователя. По умолчанию не фильтровать.

--separator

Разделитель полей. По умолчанию это пробел.

--sort-by

Поле, по которому сортируются результаты. Вы можете указать несколько полей, разделив их запятыми. Доступные поля: author_sort, authors, comments, cover, formats, identifiers, isbn, languages, last_modified, pubdate, publisher, rating, series, series_index, size, tags, template, timestamp, title, uuid По умолчанию: id

--template

Шаблон, который нужно запустить, если в списке полей есть "template". Обратите внимание, что шаблоны игнорируются при подключении к серверу calibre. По умолчанию: Нет

--template_file, -t

Путь к файлу, содержащему шаблон для запуска, если в списке полей есть 'template'. По умолчанию: Нет

--template_heading

Заголовок столбца шаблона. По умолчанию: template. Этот параметр игнорируется, если установлен параметр --for-machine

add

calibredb добавить [параметры] файл1 файл2 файл3 ...

Добавить указанные файлы как книги в базу данных. Вы также можете указать папки, см.
параметры, относящиеся к папке, ниже.

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

--authors, -a

Указать авторов добавляемых книг

--automerge, -m

Если будут найдены книги с похожими названиями и авторами, автоматически объединить входящие форматы (файлы) с существующими книжными записями. Значение "ignore" - повторяющиеся форматы отбрасываются. Значение "overwrite" - повторяющиеся форматы в библиотеке перезаписываются вновь добавленными файлами. Значение "new_record" - повторяющиеся форматы помещаются в новую запись книги.

--cover, -c

Путь к обложке для использования в добавленной книге

--duplicates, -d

Добавлять книги в базу данных, даже если они уже есть. Сравнение - по названиям книг и авторам. Параметр --automerge имеет приоритет.

--empty, -e

Добавить пустую книгу (книгу без форматов)

--identifier, -I

Задать идентификаторы книги, напр. -I asin:XXX -I isbn:YYY

--isbn, -i

Указать ISBN добавляемых книг

--languages, -l

Список языков через запятую (желательно использовать языкововую кодировку ISO639, хотя могут быть распознаны и некоторые обычные названия языков)

--series, -s

Указать серию добавляемых книг

--series-index, -S

Указать номер в серии у добавляемых книг

--tags, -T

Указать теги добавляемых книг

--title, -t

Установить название добавляемой книги (книг)

ДОБАВЛЕНИЕ ИЗ ПАПОК

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

--add

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

--ignore

Шаблон имени файла (glob), файлы, соответствующие этому шаблону, будут игнорироваться при сканировании папок на наличие файлов. Можно указать несколько раз для нескольких шаблонов. Например: *.pdf игнорирует все файлы PDF.

--one-book-per-directory, -1

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

--recurse, -r

Рекурсивно обрабатывать папки

remove

calibredb remove ids

Удалить книги, с определёнными идентификаторами из базы данных. Id должен быть разделенным запятыми списком идентификационных номеров (вы можете получить номера идентификаторов с помощью команды поиска). Например, 23,34,57-85 (при указании диапазона последнее число в диапазоне не включено).

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

--permanent

Не использовать Корзина

add_format

calibredb add_format [опции] id файл_электронной_книги

Добавьте файл_электронной_книги в доступные форматы логической книги, определённой по id. Идентификатор можно получить с помощью команды search (искать). Если формат уже существует, он заменяется, если не указана опция –dont-replace (не заменять).

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

--as-extra-data-file

Добавьте файл как дополнительный файл данных в книгу, а не как электронную книгу.

--dont-replace

Не заменять формат, если он уже существует

remove_format

calibredb remove_format [параметры] id fmt

Удалить формат fmt из логической книги с идентификатором id. Получить id - команда search. fmt должен быть расширением файла, например LRF, или TXT, или EPUB. Если логическая книга не имеет доступного fmt не делать ничего.

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

show_metadata

calibredb show_metadata [параметры] id

Показывает метаданные, хранящиеся в базе данных calibre для книги, заданной идентификатором id.
id - числовой идентификатор из команды search.

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

--as-opf

Напечатать метаданные в OPF из (XML)

set_metadata

calibredb set_metadata [options] book_id [/path/to/metadata.opf]

Установить метаданные, хранящиеся в базе данных calibre для книги, указанной book_id из файла metadata.opf OPF. book_id — это идентификационный номер книги из команды поиска. Можно получить представление о формате OPF, используя
–as-opf переключившись на команду show_metadata. Также можно установить метаданные отдельных полей опцией –field. Если вы используете опцию –field, нет необходимости указывать файл OPF.

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

--field, -f

Поле для установки. Формат название_поля:значение. Например: --field tags:tag1,tag2. Используйте --list-fields чтобы получить список названий полей. Вы можете указать эту настройку несколько раз для установки нескольких полей. Помните: для языков вы должны использовать ISO639 языковую кодировку (то есть en для English, fr для French и т.д.). Для идентификаторов, синтаксис --field identifiers:isbn:XXXX,doi:YYYYY. Для булевых (да/нет) полей используйте true и false или yes и no.

--list-fields, -l

Список названий полей метаданных, которые могут быть использованы с опцией --field.

export

calibredb export [options] ids

Экспортировать книги, указанные идентификаторами (список, разделённый запятыми), в файловую систему.
Операция экспорта сохраняет все форматы книги, её обложку и метаданные (в
OPF-файл). Любые дополнительные файлы данных, связанные с книгой, также сохраняются.
Вы можете получить номера идентификаторов с помощью команды поиска (search).

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

--all

Экспортировать все книги в базу данных, игнорируя список id.

--dont-asciiize

calibre преобразует все неанглийские символы в английские эквиваленты имен файлов. Это полезно при сохранении в устаревшую файловую систему без полной поддержки имен файлов Unicode. Указание этого переключателя отключит данный режим.

--dont-save-cover

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

--dont-save-extra-files

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

--dont-update-metadata

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

--dont-write-opf

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

--formats

Список форматов, разделенных запятыми, для сохранения для каждой книги. По умолчанию все доступные форматы сохраняются.

--progress

Прогресс отчёта

--replace-whitespace

Заменить пробел символом подчёркивания.

--single-dir

Экспорт всех книг в одну папку

--template

Шаблон для управления именем файла и структурой папок сохраненных файлов. По умолчанию это "{author_sort}/{title}/{title} - {authors}", при котором книги будут сохраняться в подпапку для каждого автора с именами файлов, содержащими название и автора. Доступные элементы управления: {author_sort, authors, id, isbn, languages, last_modified, pubdate, publisher, rating, series, series_index, tags, timestamp, title}

--timefmt

Формат отображения дат. %d - день, %b - месяц, %m - номер месяца, %Y - год. По умолчанию: %b, %Y

--to-dir

Экспорт книг в указанную папку. По умолчанию .

--to-lowercase

Преобразовать пути в нижний регистр.

catalog

%pкаталог rog /path/to/destination.(csv|epub|mobi|xml ...) [параметры]

Экспортировать каталог в формате, указанном расширением путь/к/назначению.
Параметры управляют тем, как записи отображаются в сгенерированном выводе каталога.
Обратите внимание, что разные форматы каталогов поддерживают разные наборы параметров. Чтобы просмотреть различные параметры, укажите имя выходного файла, а затем
–help опцию.

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

--ids, -i

Разделённый запятыми список идентификаторов из базы данных для каталога. Если указано, --search игнорируется. По умолчанию: all

--search, -s

Фильтровать результаты по поисковому запросу. О формате поискового запроса читайте в связанной с поиском документации в руководстве пользователя. По умолчанию: no filtering (не фильтровать)

--verbose, -v

Показать детальную информацию. Используется для отладки.

ОПЦИИ EPUB

--catalog-title

Название сгенерированного каталога, используемого как название в метаданных. По умолчанию: 'My Books' Применяется к: AZW3, EPUB, MOBI форматам вывода

--cross-reference-authors

Создание перекрестных ссылок в разделе «Авторы» для книг с несколькими авторами. По умолчанию: 'False' Применяется к: AZW3, EPUB, MOBI форматам вывода

--debug-pipeline

Сохранить вывод из разных этапов конвейера преобразования в указанную папку. Полезно, если вы не уверены, на каком этапе процесса преобразования возникает ошибка. По умолчанию: 'None' Применимо к: выходным форматам AZW3, EPUB, MOBI

--exclude-genre

Regex, описывающий теги для исключения в качестве жанров. По умолчанию: '[.+]|^+$' исключает теги в квадратных скобках, напр. '[Project Gutenberg]', и '+', умолчальный тег для чтения книг. Применяется к: AZW3, EPUB, MOBI форматам вывода

--exclusion-rules

Задать правила, используемые для исключения книг из сгенерированного каталога. Модель для правила исключения: ('<rule name>','Tags','<comma-separated list of tags>') или ('<rule name>','<custom column>','<pattern>'). Например: (('Archived books','#status','Archived'),) исключит книгу со значением 'Archived' в польз. столбце 'status'. Когда определено несколько правил, все они применятся. По умолчанию: '(('Catalogs','Tags','Catalog'),)' Применяется к: AZW3, EPUB, MOBI форматам вывода

--generate-authors

Включить раздел «Авторы» в каталог. По умолчанию: 'False' Применяется к: AZW3, EPUB, MOBI форматам вывода

--generate-descriptions

Включить раздел «Примечания» в каталог. По умолчанию: 'False' Применяется к: AZW3, EPUB, MOBI форматам вывода

--generate-genres

Включить раздел «Жанры» в каталог. По умолчанию: 'False' Применяется к: AZW3, EPUB, MOBI форматам вывода

--generate-recently-added

Включить раздел «Недавно добавленные» в каталог. По умолчанию: 'False' Применяется к: AZW3, EPUB, MOBI форматам вывода

--generate-series

Включить раздел «Серии» в каталог. По умолчанию: 'False' Применяется к: AZW3, EPUB, MOBI форматам вывода

--generate-titles

Включить раздел «Названия» в каталог. По умолчанию: 'False' Применяется к: AZW3, EPUB, MOBI форматам вывода

--genre-source-field

Исходное поле для секции 'Жанры'. Умолчание: 'Теги' Применяется к: AZW3, EPUB, MOBI форматам вывода

--header-note-source-field

Пользовательское поле, содержащее текст примечания для вставки в заголовок описания. Умолчание: '' Применяется к: AZW3, EPUB, MOBI форматам вывода

--merge-comments-rule

#<custom field>: [before|after]:[True|False] с указанием: <custom field> Пользовательское поле, содержащее примечания для объединения с комментариями [before|after] Размещение примечаний относительно комментариев [True|False] - между примечаниями и комментариями вставляется горизонтальная линия. По умолчанию: '::' Применимо к выходным форматам AZW3, EPUB, MOBI

--output-profile

Определить профиль вывода. В некоторых случаях для оптимизации каталога для устройства требуется профиль вывода. Например 'kindle' or 'kindle_dx' создаст структурированное Оглавление с Разделами и Авторами. Умолчание: 'None' Применяется к: AZW3, EPUB, MOBI форматам вывода

--prefix-rules

Задать правила включения префиксов, указывающих чтение книг, элементы списка пожеланий и др. польз-х префиксов. Модель для префиксного правила ('<rule name>','<source field>','<pattern>','<prefix>'). Когда определено несколько правил, будет использоваться первое правило сопоставления. Умолчание: '(('Read books','tags','+','✓'),('Wishlist item','tags','Wishlist','×'))' Применяется к: AZW3, EPUB, MOBI форматам вывода

--preset

Использовать именованный пресет, созданный в GUI конструкторе каталогов Пресет задает все настройки для создания каталога. Умолчание: 'None' Применяется к: AZW3, EPUB, MOBI форматам вывода

--thumb-width

Размер подсказки (в дюймах) для обложки книг в каталоге. Диапазон: 1.0 - 2.0 Умолчание: '1.0' Применяется к: AZW3, EPUB, MOBI форматам вывода

--use-existing-cover

Заменить существующую обложку при генерации каталога. Умолчание: 'False' Применяется к: AZW3, EPUB, MOBI форматам вывода

saved_searches

calibredb saved_searches [options] (list|add|remove)

Управление сохраненными в этой базе данных поисками.
При добавлении запроса с уже существующим именем, он будет заменён.

Синтаксис для добавления:

calibredb saved_searches add search_name search_expression

Синтаксис для удаления:

calibredb saved_searches remove search_name

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

add_custom_column

calibredb add_custom_column [опции] ярлык имя тип_данных

Создаёт пользовательский столбец, где «ярлык» — удобное для компьютера название столбца, без пробелов и двоеточий; «имя» — удобное для человека название столбца;
«тип_данных» один из: bool, comments, composite, datetime, enumeration, float, int, rating, series, text

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

--display

Словарь настроек для указания того, как данные в столбцах будут интерпретированы. Это JSON строка. Для перечисления столбцов используйте --display"{\ "enum_values\ ":[\ "val1\ ", \ "val2\ "]}" Есть много настроек, которые можно использовать в переменной отображения. Эти настройки по типу столбца бывают: составными: composite_template, composite_sort, make_category,contains_html, use_decorations даты и времени: date_format перечисление: enum_values, enum_colors, use_decorations целочисленные или с плавающей точкой: number_format текстовые: is_names, use_decorations Для поиска правильных сочетаний лучше всего создать пользовательский столбец соответствующего типа в GUI и посмотреть резервный OPF книги (убедитесь что новый OPF был создан после добавления столбца). Вы увидите JSON для "отображения" для нового столбца в OPF.

--is-multiple

Этот столбец хранит теги как данные (например, несколько значений, разделенных запятыми). Применяется только, если тип данных текст.

custom_columns

calibredb custom_columns [options]

Список доступных пользовательских столбцов. Показ меток столбцов и идентификаторы.

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

--details, -d

Показать подробности для каждой колонки.

remove_custom_column

calibredb remove_custom_column [options] labelУдалить пользовательский столбец, идентифицированный меткой. Вы можете увидеть доступные столбцы с командой custom_columns.

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

--force, -f

Не запрашивать подтверждение

set_custom

calibredb set_custom [options] column id value

Задать значение настраиваемого столбца для книги определяемой по id.
Cписок идентификаторов получается командой поиска.
Список пользовательских имен столбцов получается командой custom_columns

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

--append, -a

Если столбец содержит несколько значений, добавить указанное значение к существующим, а не заменять их.

restore_database

calibredb restore_database [параметры]

Восстановить эту базу данных из метаданных, хранящихся в файлах OPF в каждом папка библиотеки calibre. Это полезно, если ваш файл metadata.db был поврежден.

ВНИМАНИЕ: Эта команда полностью регенерирует вашу базу данных. Теряются все сохраненные поисковые запросы, категории пользователей, плагины, сохраненные конвертации для каждой книги настройки и пользовательские рецепты. Восстановленные метаданные будут иметь точность как в файлах OPF.

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

--really-do-it, -r

Действительно выполнить восстановление. Команда не запустится пока эта опция не определена.

check_library

calibredb check_library [настройки]

Выполняет некоторые проверки файловой системы представляющей библиотеку. Отчёты invalid_titles, extra_titles, invalid_authors, extra_authors, missing_formats, extra_formats, extra_files, missing_covers, extra_covers, failed_folders

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

--csv, -c

Вывод в CSV

--ignore_extensions, -e

Разделённый запятыми список игнорируемых расширений. По умолчанию: все (all).

--ignore_names, -n

Разделённый запятыми список игнорируемых названий. По умолчанию: все (all).

--report, -r

Разделённый запятыми список отчётов. По умолчанию: все (all).

--vacuum-fts-db

Сжать (командой Vacuum) базу данных полнотекстового поиска. Если база данных велика, это может занять много времени и памяти.

list_categories

calibredb list_categories [настройки]

Генерирует отчёт по категориям в базе данных.
Информация соответствует той, что отображается в Браузере тегов.

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

--categories, -r

Разделённый запятыми список имён поиска категорий. По умолчанию: all

--csv, -c

Вывод в CSV

--dialect

Тип файла CSV для создания. Варианты: excel, excel-tab, unix

--item_count, -i

Выводить только общее число записей в категории вместо поэлементного подсчёта.

--width, -w

Максимальная ширина строки вывода. По умолчанию - размер экрана.

backup_metadata

calibredb backup_metadata [параметры]

Резервное копирование метаданных, хранящихся в базе данных, в отдельные файлы OPF в каждой папке с книгами. Обычно это происходит автоматически, но вы можете запустить эту команду для принудительного повторного создания файлов OPF с параметром –all.

Обратите внимание, что обычно в этом нет необходимости, так как файлы OPF сохраняются.
автоматически, каждый раз при изменении метаданных.

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

--all

Обычно команда работает только с книгами, имеющими устаревшие OPF файлы. Эта опция заставляет её работать со всеми книгами.

clone

calibredb clone path/to/new/library

Создать клон текущей библиотеки. Создаётся новая пустая библиотека, содержащая все пользовательские столбцы, виртуальные библиотеки и прочие параметры, аналогично текущей библиотеке.

Клонированная библиотека не содержит книг. Чтобы создать полную копию со всеми книгами просто скопируйте папку библиотеки.

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

embed_metadata

calibredb embed_metadata [параметры] book_id

Обновление метаданных в файлах текущей книги, хранящейся в библиотеке calibre, из метаданных в базе данных calibre. Обычно метаданные обновляются только во время экспорта файлов из calibre, эта команда полезна, если вы хотите, чтобы файлы были обновлены на месте. Заметьте, что различные форматы файлов поддерживают разное количество метаданных. Вы можете использовать специальное значение „all“ для book_id, чтобы обновить метаданные во всех книгах. Вы можете также задать несколько идентификаторов книг, разделённых пробелами, и диапазон идентификаторов, разделённых дефисами. Например: calibredb embed_metadata 1 2 10-15 23

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

--only-formats, -f

Обновлять только метаданные в файлах заданного формата. Задайте несколько раз для нескольких форматов. По умолчанию, обновляются все форматы.

search

calibredb search [опции] поисковое выражение

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

Выражение поиска может быть любым из языка поисковых запросов calibre, например: calibredb search author:asimov „title:»i robot»“

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

--limit, -l

Максимальное число возвращаемых результатов. По умолчанию - все результаты.

fts_index

calibredb fts_index [параметры] enable/disable/status/reindex

Управление индексом для полнотекстового поиска.

enable

Включить индексирование для данной библиотеки

disable

Включить индексирование для данной библиотеки

status

Показать состояние индексирования

reindex

Переиндексировать отдельные книги или библиотеку целиком. Для отдельных книг, укажите их идентификаторы после команды reindex.
Если идентификаторы не указаны, то переиндексируется вся библиотека.

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

--indexing-speed

Скорость индексации. Быстрое индексирование использует максимум ресурсов компьютера, медленное - менее ресурсоёмко. Скорость сбрасывается на медленную после каждого вызова.

--wait-for-completion

Ждать, пока все книги будут проиндексированы, периодически показывая ход индексации.

fts_search

calibredb fts_search [параметры] выражение поиска

Полнотекстовый поиск в библиотеке или ее фрагменте.

Когда вы передаёте аргументы с пробелами в calibredb, заключайте их в кавычки. Например: «/some path/with spaces»

--do-not-match-on-related-words

Точное совпадение вместо родственных слов, напр. correction не совпадает с correcting.

--include-snippets

Выводить фрагменты текста вокруг совпадений. Это существенно замедлит поиск.

--indexing-threshold

Сколько процентов библиотеки должно быть проиндексировано для включения полнотекстового поиска. По умолчанию 90

--match-end-marker

Маркер конца совпавшего слова во фрагменте

--match-start-marker

Маркер начала совпавшего слова во фрагменте

--output-format

Формат вывода результатов: "text" для обычного текста или "json".

--restrict-to

Ограничьте искомые книги, используя поисковое выражение или идентификаторы. Например: ids:1,2,3 для ограничения по идентификаторам или search:tag:foo для ограничения книг с тегом foo.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Командная строка »
- ebook-convert

ebook-convert

ebook-convert input_file output_file [параметры]

Преобразовать электронную книгу из одного формата в другой.

input_file - это вход, а output_file - это выход. Оба должны быть указаны в качестве первых двух аргументов команды.

Выходной формат электронной книги определяется расширением файла output_file. Выходной_файл также может иметь специальный формат .EXT, где EXT - это расширение выходного файла. В этом случае имя выходного файла происходит от имени входного файла. Обратите внимание, что имена файлов не должны начинаться с дефиса. Наконец, если output_file не имеет расширения, он рассматривается как папка, и в эту папку записывается «открытая электронная книга» (OEB), состоящая из файлов HTML. Эти файлы - это файлы, которые обычно передаются в плагин вывода.

После указания файла ввода и вывода вы можете настроить конвертацию, указав различные параметры. Доступные параметры зависят от типов входных и выходных файлов. Чтобы получить справку по ним, укажите входной и выходной файл, а затем используйте параметр -h.

Полную документацию по системе конвертации см. Конвертирование книг

Когда вы передаёте аргументы с пробелами в ebook-convert, заключайте их в кавычки. Например: «/some path/with spaces»

Параметры и значения по умолчанию для свойств/ параметров изменяются в зависимости и их от входного и выходного форматов, так что Вы всегда должны выполнять проверку ::
ebook-convert myfile.input_format myfile.output_format -h
Ниже приведены параметры общие для всех преобразований, а за ними следуют параметры, специфичные для каждого входного и выходного формата.

- ВХОДНЫЕ НАСТРОЙКИ

- ВЫХОДНЫЕ НАСТРОЙКИ

- ВИД И ФУНКЦИИ

- ЭВРИСТИЧЕСКАЯ ОБРАБОТКА

- НАЙТИ И ЗАМЕНИТЬ

- ОПРЕДЕЛЕНИЕ СТРУКТУРЫ

- ОГЛАВЛЕНИЕ

- МЕТАДАННЫЕ

- ОТЛАДКА

--help, -h

показать это сообщение и выйти

--input-profile

Указать профиль ввода. Профиль ввода указывает конвертеру, как интерпретировать различную информацию внутри исходного документа. Например, длина зависит от разрешения (длина в пикселях). Варианты: cybookg3, cybook_opus, default, hanlinv3, hanlinv5, illiad, irexdr1000, irexdr800, kindle, msreader, mobipocket, nook, sony, sony300, sony900

--list-recipes

Список встроенных имен рецептов. Вы можете создать электронную книгу из встроенного рецепта, подобного этому: ebook-convert "Recipe Name.recipe" output.epub

--output-profile

Выбор профиля вывода. Профиль вывода определяет параметры оптимизации преобразуемого документа для выбранного типа устройств. В некоторых случаях (редко), профиль вывода определяет параметры оптимизации для конкретного устройства. Доступные профили:cybookg3, cybook_opus, default, hanlinv3, hanlinv5, illiad, ipad, ipad3, irexdr1000, irexdr800, jetbook5, kindle, kindle_dx, kindle_fire, kindle_oasis, kindle_pw, kindle_pw3, kindle_scribe, kindle_voyage, kobo, msreader, mobipocket, nook, nook_color, nook_hd_plus, pocketbook_inkpad3, pocketbook_lux, pocketbook_hd, pocketbook_900, pocketbook_pro_912, galaxy, sony, sony300, sony900, sony-landscape, sonyt3, tablet, generic_eink, generic_eink_hd, generic_eink_large

--version

показать версию программы и выйти

ВХОДНЫЕ НАСТРОЙКИ

Опции для контроля обработки входного файла mobi

--input-encoding

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

ВЫХОДНЫЕ НАСТРОЙКИ

Настройки для контроля обработки выходного файла epub

--dont-split-on-page-breaks

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

--epub-flatten

Этот параметр нужен только если EPUB-файл будет использован с FBReaderJ. Файловая структура внутри EPUB файла будет сплющена, все файлы разместятся в корне.

--epub-inline-toc

Добавить встроенное оглавление, которое появится как часть основного содержания книги.

--epub-max-image-size

Максимальный размер изображения (ширина x высота). Значение none означает использование размера экрана из выходного профиля. Значение profile - максимальный размер не указан. Например, значение 100x200 - изменение размера всех изображений так чтобы их ширина была не больше 100 пикселей, а высота - не больше 200 пикселей. Обратите внимание, что это влияет только на размер самих файлов изображений. Любое данное изображение может отображаться в другом размере в зависимости от стиля, примененного к нему в документе.

--epub-toc-at-end

Поместить встроенное оглавление в конце книги, а не в начале.

--epub-version

Версия файла EPUB для генерации. EPUB 2 - наиболее широко совместим, используйте EPUB 3, только если вы знаете, что он вам действительно нужен.

--extract-to

Распакуйте содержимое сгенерированной книги в указанную папку. Содержимое папки сначала удаляется, так что будьте осторожны.

--flow-size

Разбить все HTML файлы, размер которых превышает заданный (в КБ). Это необходимо сделать, поскольку большинство читалок EPUB не могут обрабатывать большие файлы. По умолчанию 260KB определяется как размер, необходимый для Adobe Digital Editions. Установите параметр в 0, чтобы отключить разбиение файлов.

--no-default-epub-cover

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

--no-svg-cover

Не использовать файл SVG в качестве обложки. Используйте эту опцию только, если файл EPUB будет использоваться на устройстве не поддерживающем SVG, таком как iPhone или JetBook Lite. Без включенной опции, подобные устройства будут отображать обложку в виде пустой страницы.

--preserve-cover-aspect-ratio

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

--pretty-print

Если отмечено, модуль вывода попытается форматировать вывод в удобном для чтения виде. Может не работать на некоторых модулях вывода.

--toc-title

Заголовок любого сгенерированного встроенного оглавления.

ВИД И ФУНКЦИИ

Опции для контроля за внешним видом выходного файла

--asciiize

Транслитерировать символы Unicode в ASCII. Используйте с осторожностью, символы Unicode будут заменены на ASCII. Например, 'Pelé' заменится на 'Pele'. Также обратите внимание, что в случаях, когда существует несколько представлений символа (например, символов, используемых в китайском и японском языках), будет использоваться представление, основанное на текущем языке интерфейса calibre.

--base-font-size

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

--change-justification

Изменить выравнивание текста. Значение "Влево" преобразует все выравнивание текста из источника в выравнивание по левому краю (то есть не выравненому). Значение "Выровнять" преобразует весь не выровненный текст в выровненный. Значение "оригинал" (по умолчанию) не изменяет выравнивание в исходном файле. Обратите внимание, что только некоторые выходные форматы поддерживают выравнивание.

--disable-font-rescaling

Запретить масштабирование шрифтов.

--embed-all-fonts

Встроить каждый шрифт, который содержится в исходном документе, но ещё не встроен. Используемые шрифты - искать в вашей системе и встроить их, если найдены. Встраивание будет работать только если формат, в который вы конвертируете, поддерживает встраиваемые шрифты. Например EPUB, AZW3, DOCX и PDF. Убедитесь, что у вас есть лицензия на встраиваемые шрифты, используемые в этом документе.

--embed-font-family

Встроить определённое семейство шрифтов в книгу. Устанавливает «основной» шрифт книги. Если в исходном документе заданы собственные шрифты, они, вероятно, переопределят основной шрифт. Вы можете использовать опцию фильтрации стилей для удаления шрифтов исходного документа. Заметьте, что встраивание шрифтов работает только с некоторыми выходными форматами, преимущественно EPUB, AZW3 и DOCX.

--expand-css

По умолчанию calibre будет использовать сокращенную форму для различных свойств CSS, таких как margin, padding, border и т.п. Этот параметр включает использование полной расширенной формы. Обратите внимание, что CSS всегда расширяется при создании файлов EPUB с профилем вывода на Nook, поскольку Nook не может обрабатывать сокращенный CSS.

--extra-css

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

--filter-css

Разделённый запятой список CSS-свойств, которые должны быть удалены изо всех правил CSS. Это полезно, если настройка одного из стилей мешает ему быть изменённым на вашем устройстве. Например: font-family, color, margin-left, margin-right

--font-size-mapping

Сопоставления имён шрифтов и размеров (в пунктах) из CSS. Например, строка настроек выглядит так: 12,12,14,16,18,20,22,24. Это сопоставления для размеров от xx-small до xx-large, с последним размером для огромных шрифтов. Алгоритм масштабирования шрифтов использует эти размеры для правильного масштабирования. По умолчанию используются настройки выбранного профиля.

--insert-blank-line

Вставить пустую строку между абзацами. Не будет работать, если в исходном документе абзацы (теги <p> или <div>) не используются.

--insert-blank-line-size

Установить высоту вставляемых пустых строк (в em). Высота строк между абзацами будет в два раза превышать заданное значение.

--keep-ligatures

Сохраните лигатуры, присутствующие во входном документе. Лигатура — это комбинированный символ пары символов, таких как ff, fi, fl и т. д. Большинство программ чтения не поддерживают лигатуры в шрифтах по умолчанию, поэтому они вряд ли будут отображаться правильно. По умолчанию calibre превратит лигатуру в соответствующую пару обычных символов. Обратите внимание, что под лигатурами здесь подразумеваются только лигатуры Юникода, а не лигатуры, созданные с помощью CSS или стилей шрифта. Вместо этого эта опция сохранит их.

--line-height

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

--linearize-tables

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

--margin-bottom

Установить нижнюю границу в pt (пунктах). Умолчание - 5.0. Установка значения меньше нуля приведет к отсутствию границы (настройка полей в исходном документе будет сохранена). Примечание. Ориентированные на страницы форматы, такие как PDF и DOCX, имеют свои собственные настройки полей.

--margin-left

Установить левую границу в pt (пунктах). Умолчание - 5.0. Установка значения меньше нуля приведет к отсутствию границы (настройка полей в исходном документе будет сохранена). Примечание. Ориентированные на страницы форматы, такие как PDF и DOCX, имеют свои собственные настройки полей.

--margin-right

Установить правую границу в pt (пунктах). Умолчание - 5.0. Установка значения меньше нуля приведет к отсутствию границы (настройка полей в исходном документе будет сохранена). Примечание. Ориентированные на страницы форматы, такие как PDF и DOCX, имеют свои собственные настройки полей.

--margin-top

Установить верхнюю границу в pt (пунктах). Умолчание - 5.0. Установка значения меньше нуля приведет к отсутствию границы (настройка полей в исходном документе будет сохранена). Примечание. Ориентированные на страницы форматы, такие как PDF и DOCX, имеют свои собственные настройки полей.

--minimum-line-height

Минимальная высота строки в процентах от вычисленного размера шрифта элемента. Calibre позаботится о том, чтобы каждый элемент имел высоту строки не менее заданной, независимо от того, что указано во входящем документе. Для отключения задайте ноль. По умолчанию — 120%. Использование данной настройки предпочтительнее, чем прямое указание высоты строки, кроме случая, когда Вы точно понимаете свои действия. Например, «двойной интервал» получается при значении 240.

--remove-paragraph-spacing

Удалить промежутки между абзацами и установить отступ абзаца в 1.5em. Удаление промежутков не будет работать, если в исходном документе абзацы (теги <p> или <div>) не используются.

--remove-paragraph-spacing-indent-size

Когда Calibre удаляет пустые строки между абзацами, то автоматически расставляет красные строки, чтобы абзацы визуально выделялись. Эта опция устанавливает величину отступа (в em). Если установлена отрицательная величина, используется размер отступа, указанный в документе.

--smarten-punctuation

Преобразовать обычные кавычки, тире и троеточия к их типографическим эквивалентам. Более подробно, см. https://daringfireball.net/projects/smartypants

--subset-embedded-fonts

Сократить все внедрённые шрифты. Каждый внедрённый шрифт будет содержать только используемые в документе символы. Это уменьшает размер файла шрифтов. Полезно если вы внедряете объёмный шрифт с большим количеством неиспользуемых символов.

--transform-css-rules

Путь к файлу, содержащему правила для преобразования стилей CSS в этой книге. Самый простой способ создать такой файл - использовать мастер для создания правил в графическом интерфейсе calibre. Получите доступ к нему в разделе Оформление->Стили трансформации из диалогового окна преобразования. После создания правил вы можете использовать кнопку «Экспорт», чтобы сохранить их в файле.

--transform-html-rules

Путь к файлу, содержащему правила трансформации HTML в этой книге. Простейший способ создать такой файл - использовать мастер для создания правил в графическом интерфейсе calibre. Доступ к нему можно получить в разделе «Внешний вид - Преобразование HTML» диалогового окна конвертации. После создания правил вы можете использовать кнопку «Экспорт», чтобы сохранить их в файл.

--unsmarten-punctuation

Преобразовать нестандартные кавычки, тире и многоточия в их обычные эквиваленты.

ЭВРИСТИЧЕСКАЯ ОБРАБОТКА

Изменить текст документа и структуру используя общие шаблоны. По умолчанию выключено. Используйте –enable-heuristics чтобы включить. Отдельные действия могут быть отключены с помощью настройки –disable-*.

--disable-dehyphenate

Анализировать переносы слов в документе. В качестве словаря для определения, оставить или убрать перенос, используется сам документ.

--disable-delete-blank-paragraphs

Убрать пустые абзацы, когда они встречаются между другими абзацами

--disable-fix-indents

Преобразовать отступы, состоящие из повторяющихся неразрывных пробелов, в CSS-отступы.

--disable-format-scene-breaks

Выравненные по левому краю разрывы сцены выравниваются по центру. Заменяет мягкий разрыв сцены из нескольких пустых линий на горизонтальные линии.

--disable-italicize-common-cases

Искать общие слова и шаблоны, обозначающие курсив, и преобразовывать в курсивное написание.

--disable-markup-chapter-headings

Определять неформатированные заголовки разделов и подзаголовки. Менять их на теги h2 и h3. Эта настройка не создаёт оглавление, но может быть использована совместно с функцией определения структуры для его создания.

--disable-renumber-headings

Искать последовательные вхождения тегов <h1> или <h2>. Теги перенумеровываются, чтобы предотвратить разбиение заголовков разделов посередине.

--disable-unwrap-lines

Отмена переноса строк с учётом знаков препинания и другой информации о форматировании.

--enable-heuristics

Разрешить эвристическую обработку. Эту опцию нужно включить для выполнения любого вида эвристической обработки.

--html-unwrap-factor

Коэффициент для определения длины, при которой строка должна быть соединена. Допускаются десятичные значения между 0 и 1. Значение по умолчанию составляет 0,4, чуть меньше средней длины строки. Если только несколько строк в документе требуют соединения, то это значение должно быть уменьшено.

--replace-scene-breaks

Заменить разрывы сцен определённым текстом. По умолчанию используется текст из входного документа.

НАЙТИ И ЗАМЕНИТЬ

Изменить содержание и структуру документа используя пользовательский шаблон.

--search-replace

Путь к файлу, содержащему регулярные выражения для поиска и замены. Файл должен содержать чередующиеся строки регулярного выражения, за которым следует шаблон замены (который может быть пустой строкой). Регулярное выражение должно быть в синтаксисе regex Python, и файл должен быть в кодировке UTF-8.

--sr1-replace

Текст, на который будут заменяться найденные тексты, соответствующие первому выражению поиска.

--sr1-search

Шаблон поиска (регулярное выражение), заменяемый на первый текст замены.

--sr2-replace

Текст, на который будут заменяться найденные тексты, соответствующие второму выражению поиска.

--sr2-search

Шаблон поиска (регулярное выражение), заменяемый на второй текст замены.

--sr3-replace

Текст, на который будут заменяться найденные тексты, соответствующие третьему выражению поиска.

--sr3-search

Шаблон поиска (регулярное выражение), заменяемый на третий текст замены.

ОПРЕДЕЛЕНИЕ СТРУКТУРЫ

Управление автоопределением структуры документа.

--add-alt-text-to-img

Если у <img> тега нет атрибута alt, проверьте связанный файл изображения на наличие метаданных, определяющих альтернативный текст, и используйте их для заполнения атрибута alt. Атрибут alt улучшает доступность, предоставляя текстовые описания для пользователей, которые не могут видеть или полностью интерпретировать визуальный контент.

--chapter

Выражение XPath для определения названий глав. По умолчанию следует учитывать теги <h1> или <h2>, содержащие слова "chapter", "book", "section", "prologue", "epilogue" или "part". В качестве заголовков глав, а также любых тегов, которые имеют класс="chapter". Используемое выражение должно оценивать список элементов. Чтобы отключить обнаружение глав, используйте выражение "/". Более подробную информацию об использовании этой функции см. В учебнике по XPath в руководстве пользователя calibre.

--chapter-mark

Указать, как помечать найденные главы. Значение «pagebreak» будет вставлять разрывы страниц после глав. Значение «rule» будет вставлять горизонтальную линию перед главами. Значение «none» отключит разметку глав, а значение «both» будет вставлять и разрывы страниц, и пустые строки.

--disable-remove-fake-margins

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

--insert-metadata

Вставить метаданные книги в начале книги. Это полезно, если ваша читалка не поддерживает отображение/поиск метаданных напрямую.

--page-breaks-before

XPath-выражение. Разрывы страниц вставляются перед указанными элементами. Для отключения выражения используйте : /

--prefer-metadata-cover

Использовать обложку, полученную из исходного файла, вместо заданной.

--remove-first-image

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

--start-reading-at

Выражение XPath для определения местоположения в документе, с которого следует начать чтение. Некоторые читалки (в первую очередь Kindle) используют это местоположение в качестве позиции для открытия книги. Дополнительную информацию об этой функции см. в учебнике по XPath в руководстве пользователя calibre.

ОГЛАВЛЕНИЕ

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

--duplicate-links-in-toc

Разрешить дублирующиеся записи при создании оглавления из ссылок в исходном документе, т. е. разрешить записи с одинаковым текстом, если они указывают на разные места.

--level1-toc

XPath-выражение, которое определяет все теги, которые должны быть добавлены в оглавление на первом уровне. Если эта опция определена, то она имеет преимущество над другими видами автоматического обнаружения. Смотрите инструкцию к XPath в руководстве пользователя Calibre, чтобы увидеть примеры.

--level2-toc

XPath-выражение, которое определяет все теги, которые должны быть добавлены в оглавление на втором уровне. Каждая запись добавляется под записью предыдущего уровня. Смотрите инструкцию к XPath в руководстве пользователя Calibre, чтобы увидеть примеры.

--level3-toc

XPath-выражение, которое определяет все теги, которые должны быть добавлены в оглавление на третьем уровне. Каждая запись добавляется под записью предыдущего уровня. Смотрите инструкцию к XPath в руководстве пользователя Calibre, чтобы увидеть примеры.

--max-toc-links

Максимальное количество ссылок, вносимых в оглавление. Поставьте 0 для отключения. По умолчанию 50. Ссылки добавляются в оглавление только если количество найденных глав меньше порогового.

--no-chapters-in-toc

Не добавлять автоматически определённые главы в содержание.

--toc-filter

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

--toc-threshold

Если обнаружено меньшее число глав, чем указано, то ссылки добавляются в содержание. По умолчанию: 6

--use-auto-toc

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

МЕТАДАННЫЕ

Настройки для установки метаданных выходного файла

--author-sort

Строка, используемая при сортировке по авторам.

--authors

Указать авторов. Несколько авторов должны быть разделены амперсандами (&).

--book-producer

Установить создателя книги.

--comments

Установить описание электронной книги.

--cover

Установить обложку для указанного файла или URL

--isbn

Указать ISBN книги.

--language

Установить язык.

--pubdate

Установить дату публикации (предполагается, что дата соответствует местному часовому поясу, если не задано)

--publisher

Установить издателя электронной книги.

--rating

Поставьте оценку. Должна быть числом от 1 до 5.

--read-metadata-from-opf, --from-opf, -m

Прочитать метаданные из указанного OPF-файла. Метаданные, прочитанные таким образом, заменят все метаданные исходного документа

--series

Установить серию этой электронной книги.

--series-index

Указать номер книги в серии.

--tags

Указать теги для книги. Должны быть разделены запятыми.

--timestamp

Установить дату книги (больше нигде не используется)

--title

Указать название.

--title-sort

Версия названия, используемая для сортировки.

ОТЛАДКА

Настройки для облегчения отладки преобразования

--debug-pipeline, -d

Сохранить вывод из разных этапов конвейера конвертации в указанную папку. Полезно, если вы не уверены, на каком этапе процесса конвертации возникает ошибка.

--verbose, -v

Уровень детальности. Используйте несколько раз для большей детальности. Использование дважды приведёт к максимальной детальности, единожды - к средней и ни разу - к наименьшей детальности.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Командная строка »
- ebook-edit

ebook-edit

ebook-edit [опции] [путь_к_электронной_книге] [имя_файла_внутри_книги …]

Запускает инструмент редактирования книги calibre. Также можно задать имена файлов внутри книги, которые автоматически откроются для редактирования.

Когда вы передаёте аргументы с пробелами в ebook-edit, заключайте их в кавычки. Например: «/some path/with spaces»

[опции]

--detach

Отвязаться от управляющего терминала (только Linux)

--help, -h

показать это сообщение и выйти

--select-text

Текст, который нужно выделить в книге, когда она открыта для редактирования.

--version

показать версию программы и выйти

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Командная строка »
- ebook-meta

ebook-meta

ebook-meta файл_электронной_книги [опции]

Читать / писать метаданные из / в файлы электронных книг.

Поддерживаемые форматы для чтения метаданных: azw, azw1, azw3, azw4, cb7, cbc, cbr, cbz, chm, docx, epub, fb2, fbz, html, htmlz, imp, kepub, lit, lrf, lrx, mobi, odt, oebzip, opf, pdb, pdf, pml, pmlz, pobi, prc, rar, rb, rtf, snb, tpz, txt, txtz, updb, zip

Поддерживаемые форматы для записи метаданных: azw, azw1, azw3, azw4, docx, epub, fb2, fbz, htmlz, kepub, lrf, mobi, odt, pdb, pdf, prc, rtf, tpz, txtz

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

Когда вы передаёте аргументы с пробелами в ebook-meta, заключайте их в кавычки. Например: «/some path/with spaces»

[опции]

--author-sort

Строка для сортировки по автору. Если не указана, а авторы указаны, будет авто-сгенерирована из списка авторов.

--authors, -a

Вписать авторов. Несколько авторов разделяются символом &. Имена должны быть указаны в порядке: Имя Фамилия.

--book-producer, -k

Установить создателя книги.

--category

Установить категорию книги

--comments, -c

Установить описание электронной книги.

--cover

Указать файл обложки.

--date, -d

Установить дату публикации

--disallow-rendered-cover

Для таких форматов, как EPUB, которые используют 'обложку по умолчанию' для первой отображаемой страницы, отключите такие обложки по умолчанию

--from-opf

Прочитать метаданные из указанного файла OPF и использовать его для установки метаданных электронной книги. Метаданные, указанные в командной строке, будут переопределять метаданные, считанные из файла OPF

--get-cover

Получить обложку из электронной книги и сохранить её как указанный файл.

--help, -h

показать это сообщение и выйти

--identifier

Установить идентификаторы книги, могут быть указаны несколько раз. Например: «--identifier uri:https://acme.com --identifier isbn:12345». Чтобы удалить идентификатор, не указывайте значение: «--identifier isbn:». Обратите внимание, что для файлов EPUB идентификатор, помеченный как идентификатор пакета, не может быть удалён.

--index, -i

Указать номер книги в серии.

--isbn

Указать ISBN книги.

--language, -l

Установить язык.

--lrf-bookid

Указать BookID в файлах LRF

--publisher, -p

Установить издателя электронной книги.

--rating, -r

Поставьте оценку. Должна быть числом от 1 до 5.

--series, -s

Установить серию этой электронной книги.

--tags

Указать теги для книги. Должны быть разделены запятыми.

--title, -t

Указать название.

--title-sort

Версия имени для сортировки. Если отсутствует, а имя указано, то будет автоматически сгенерировано из имени.

--to-opf

Укажите имя файла OPF. Метаданные будут сохранены в этом OPF-файле.

--version

показать версию программы и выйти

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Командная строка »
- ebook-polish

ebook-polish

ebook-polish [параметры] входной_файл [выходной_файл]

Полировка книг - все о том, чтобы придать блеск совершенства

Вашим тщательно обработанным электронным книгам.

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

Вы должны использовать этот инструмент в качестве последнего шага в процессе создания электронной книги.

Обратите внимание, что полировка работает только с файлами в форматах AZW3 или EPUB или KEPUB.

Когда вы передаёте аргументы с пробелами в ebook-polish, заключайте их в кавычки. Например: «/some path/with spaces»

[опции]

--add-soft-hyphens, -H

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

--compress-images, -i

Сжать иллюстрации для уменьшения объёма, без ухудшения качества изображения.

--cover, -c

Путь к обложке. Изменяет обложку, указанную в электронной книге. Если обложки нет или она неправильно идентифицирована, вставляется новая обложка.

--download-external-resources, -d

Загрузите внешние ресурсы, такие как изображения, таблицы стилей и т. д., которые указывают на URL-адреса, а не на файлы в книге. Все такие ресурсы будут загружены и добавлены в книгу, чтобы в книге больше не было ссылок на внешние ресурсы.

--embed-fonts, -e

Внедрить шрифты, указанные в документе, но ещё не внедрённые. Будет осуществлен поиск шрифтов, доступных на компьютере; если шрифты будут найдены, то они будут внедрены в документ. Убедитесь в наличии соответствующих прав на внедрение шрифтов, используемых в документе.

--help, -h

показать это сообщение и выйти

--jacket, -j

Вставить в начале книги страницу обложки, которая будет содержать все метаданные: название, теги, авторов, серии, комментарии, и т.д. Любые предыдущие обложки будут заменены.

--opf, -o

Путь к OPF файлу. Метаданные в книге обновлены из OPF файла.

--remove-jacket

Удалить предыдущую добавленную обложку.

--remove-soft-hyphens

Удаляет мягкие переносы из всего текста книги.

--remove-unused-css, -u

Удалить все неиспользуемые CSS правила из таблицы стилей и тегов <style>. Некоторые книги, созданные с помощью технических шаблонов, могут иметь множество дополнительных CSS правил, не совпадающих с реальным содержанием. Это замедляет работу читалок при разборе правил.

--smarten-punctuation, -p

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

--subset-fonts, -f

Сокращение шрифтов означает уменьшение внедрённых шрифтов до необходимого набора символов, используемых в книге. Это значительно уменьшает размер файлов шрифтов (примерно вдвое). Например, если книга использует специальный шрифт для заголовков, то сокращение шрифтов уменьшит этот шрифт до символов содержащихся только в этих заголовках. Или если в книгу включена курсивная версия шрифта, но текст с ним встречается редко или совсем отсутствует, то курсивный шрифт может быть сокращён всего до нескольких символов или полностью удалён. Единственный недостаток сокращения шрифтов в том, если позже вы пожелаете добавить больше текста в ваши книги, то добавленный текст может быть не охвачен сокращённым шрифтом.

--upgrade-book, -U

Обновление внутренней структуры книги, по возможности. К примеру, обновление книг EPUB 2 до EPUB 3.

--verbose

Более подробный вывод обработки. Полезно при отладке.

--version

показать версию программы и выйти

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Командная строка »
- ebook-viewer

ebook-viewer

ebook-viewer [опции] файл

Читать электронную книгу.

Когда вы передаёте аргументы с пробелами в ebook-viewer, заключайте их в кавычки. Например: «/some path/with spaces»

[опции]

--continue

Продолжить чтение последней открытой книги

--detach

Отвязаться от управляющего терминала (только Linux)

--force-reload

Принудительная перезагрузка всех открытых книг

--full-screen, --fullscreen, -f

Если указано, окно читалки при запуске попытается открыться во весь экран.

--help, -h

показать это сообщение и выйти

--new-instance

Открыть новое окно просмотрщика, даже если установлена опция использования только одного окна просмотрщика

--open-at

Место открытия указанной книги. Местоположение или позицию можно получить, выполнив действия «Перейти->Место» в элементах управления читалкой. Можно использовать форму toc:нечто, и она откроется в месте первой записи оглавления, содержащей строку «нечто». Форма toc-href:нечто будет соответствовать href (внутренней ссылке на место назначения) элементов оглавления. Соответствие точное, если же хотите сопоставить подстроку, воспользуйтесь формой toc-href-contains:нечто. Форма ref:нечто будет использовать Ссылочный режим cсылок. Форма regex:something будет искать регулярное выражение something после открытия книги.

--raise-window

Если указано, окно читалки при запуске попытается выйти на передний план.

--version

показать версию программы и выйти

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Командная строка »
- fetch-ebook-metadata

fetch-ebook-metadata

fetch-ebook-metadata [параметры]

Получить метаданные книги из онлайн-источников. Вы должны задать по крайней мере название, авторов или ISBN.

Когда вы передаёте аргументы с пробелами в fetch-ebook-metadata, заключайте их в кавычки. Например: «/some path/with spaces»

[опции]

--allowed-plugin, -p

Укажите имя используемого плагина загрузки метаданных. По умолчанию будут использоваться все плагины метаданных. Может быть указано несколько раз для нескольких плагинов. Все имена плагинов: Google, Google Images, Amazon.com, Edelweiss, Open Library, Big Book Search

--authors, -a

Автор(ы) книги

--cover, -c

Указать имя файла. Обложка, если доступна, будет сохранена. Без этого параметра обложка не будет загружена.

--help, -h

показать это сообщение и выйти

--identifier, -I

Идентификаторы, такие как ASIN/Goodreads id и т. д. М.б. указаны несколько раз для нескольких идентификаторов. Например:--identifier asin:B0082BAJA0

--isbn, -i

ISBN книги

--opf, -o

Вывести метаданные в OPF формат вместо читаемого текста.

--timeout, -d

Таймаут в секундах. По умолчанию 30

--title, -t

Название книги

--verbose, -v

Вывести лог в консоль (stderr)

--version

показать версию программы и выйти

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Командная строка »
- lrf2lrs

lrf2lrs

lrf2lrs книга.lrf

Преобразовать файл LRF в файл LRS (XML, кодировка UTF-8)

Когда вы передаёте аргументы с пробелами в lrf2lrs, заключайте их в кавычки. Например: «/some path/with spaces»

[опции]

--dont-output-resources

Не сохранять вложенные картинки и шрифты на диск

--help, -h

показать это сообщение и выйти

--output, -o

Выходной файл LRS

--verbose

Быть многословнее

--version

показать версию программы и выйти

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Командная строка »
- lrfviewer

lrfviewer

lrfviewer [опции] книга.lrf

Читать электронную книгу в формате LRF

Когда вы передаёте аргументы с пробелами в lrfviewer, заключайте их в кавычки. Например: «/some path/with spaces»

[опции]

--disable-hyphenation

Отключить переносы. Это должно значительно ускорить визуализацию

--help, -h

показать это сообщение и выйти

--profile

Описать визуализатор LRF

--verbose

Вывести больше информации о процессе рендера

--version

показать версию программы и выйти

--visual-debug

Переключиться на визуальные подсказки для отладки движка визуализации

--white-background

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

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Командная строка »
- lrs2lrf

lrs2lrf

lrs2lrf [опции] файл.lrs

Собрать файл LRS в файл LRF.

Когда вы передаёте аргументы с пробелами в lrs2lrf, заключайте их в кавычки. Например: «/some path/with spaces»

[опции]

--help, -h

показать это сообщение и выйти

--lrs

Преобразование LRS в LRS, необходимо для отладки.

--output, -o

Путь для файла результата

--verbose

Детализация процесса

--version

показать версию программы и выйти

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Командная строка »
- web2disk

web2disk

web2disk URL

Где URL, например: https://google.com

Когда вы передаёте аргументы с пробелами в web2disk, заключайте их в кавычки. Например: «/some path/with spaces»

[опции]

--base-dir, -d

Базовая папка, в которой сохраняется URL. По умолчанию .

--delay

Минимальный интервал в секундах между последовательными вызовами. По умолчанию: 0 с

--dont-download-stylesheets

Не скачивать файлы стилей CSS.

--encoding

Кодировка вебсайтов, которые вы собираетесь скачивать. По умолчанию делается попытка определения кодировки.

--filter-regexp

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

--help, -h

показать это сообщение и выйти

--match-regexp

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

--max-files, -n

Максимальное количество файлов для скачивания. Применимо только к файлам из тегов <a href>. По умолчанию: 9223372036854775807

--max-recursions, -r

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

--timeout, -t

Максимальное время ожидания ответа от сервера. По умолчанию: 10.0 с

--verbose

Показать детальную информацию. Используется для отладки.

--version

показать версию программы и выйти

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Настройка среды разработки calibre

Настройка среды разработки calibre

У calibre полностью открытый исходный код, лицензированный GNU GPL v3 [https://www.gnu.org/licenses/gpl.html]. То есть вы можете загружать и изменять программу по собственному желанию. В этом разделе вы узнаете, как настроить среду разработки calibre на выбранной операционной системе. В основном calibre написана на Python [https://www.python.org] со вставками кода на C/C++ для повышения скорости и взаимодействия с системой. Обратите внимание, что calibre требует, как минимум, Python 3.8.

Содержание

- Философия разработки

- Макет кода

- Получение кода

- Отправка ваших изменений для включения

- Среда разработки под Windows

- Среда разработки под macOS

- Среда разработки под Linux

- Раздельная установка «обычной» и «разрабатываемой» версий calibre на одном компьютере

- Советы по отладке

- Использование print statements

- Использование интерактивного интерпретатора Python

- Использовать отладчик Python как удаленный отладчик

- Использование отладчика в вашей любимой Python IDE

- Выполнение произвольных скриптов в окружении Python calibre

- Использование calibre в собственных проектах

- Установка исполняемого файла calibre

- Установка из исходного кода в Linux

- Документация API различных частей calibre

Философия разработки

Истоки calibre лежат в мире Unix, что означает высокую модульность её дизайна. Модули взаимодействуют друг с другом через чётко определённые интерфейсы. Это заметно облегчает добавление новых возможностей и исправление ошибок, вызывая бурный темп развития. Из-за своих корней calibre имеет полноценный интерфейс командной строки для всех функций, задокументированный в разделе «Командная строка».

Модульная конструкция calibre выражается через «Плагины». Существует tutorial по написанию плагинов calibre. Например, добавление поддержки нового устройства в calibre обычно включает в себя написание менее 100 строк кода в виде плагина драйвера устройства. Вы можете просмотреть встроенные драйверы [https://github.com/kovidgoyal/calibre/tree/master/src/calibre/devices]. Точно так же добавление поддержки новых форматов преобразования включает в себя написание плагинов формата ввода/вывода. Другим примером модульного дизайна является recipe system для получения новостей. Дополнительные примеры плагинов, предназначенных для добавления функций в calibre, см. В разделе «Индекс плагинов <https://www.mobileread.com/forums/showthread.php?p=1362767#post1362767>`_.

Макет кода

Весь Python код calibre находится в пакете calibre. Этот пакет содержит следующие основные подпакеты

- devices — все драйверы устройств. Просто просмотрите некоторые встроенные драйверы, чтобы понять, как они работают.

- Подробнее см.: devices.interface, определяющий интерфейс, поддерживаемый драйверами устройств, и devices.usbms, определяющий универсальный драйвер, который подключается к устройству USBMS. Все драйверы calibre на базе USBMS наследуются от него.

- e-books — весь код преобразования электронных книг/метаданных. Хорошая отправная точка — calibre.ebooks.conversion.cli, который является модулем, включающим команду ebook-convert. Процесс преобразования контролируется через translation.plumber. Код, не зависящий от формата, находится в ebooks.oeb, а код, зависящий от формата, — в ebooks.format_name.

- Чтение, запись и загрузка метаданных — всё это в ebooks.metadata

- Преобразование происходит в конвейере (о структуре конвейера см. Предисловие). Конвейер состоит из входного плагина, различных трансформаций и выходного плагина. Код, который создаёт и управляет конвейером, находится в plumber.py. Конвейер работает над представлением электронной книги, похожей на разархивированный epub, с манифестом, spine, toc, guide, html-содержимым и т. д. Класс, который управляет этим представлением, — OEBBook в ebooks.oeb.base, различные трансформации, применяемые к книге во время конвертации, находятся в oeb/transforms/*.py. А входные и выходные плагины — в translation/plugins/*.py.

- Редактирование электронной книги происходит с использованием другого объекта контейнера. Это задокументировано в Документация API инструмента редактирования электронных книг.

- db — серверная часть базы данных. См.: Документация API интерфейса базы данных об интерфейсе библиотеки calibre.

- Контент-сервер: srv — это контент-сервер calibre.

- gui2 — графический интерфейс пользователя. Инициализация GUI происходит в gui2.main и gui2.ui. Программа просмотра электронных книг находится в gui2.viewer. Редактор электронных книг находится в gui2.tweak_book.

Если вы хотите найти точки входа для всех исполняемых файлов calibre различного уровня, посмотрите на структуру entry_points в linux.py [https://github.com/kovidgoyal/calibre/blob/master/src/calibre/linux.py].

Если вам нужна помощь в понимании кода, отправьте сообщение на форум разработчиков [https://www.mobileread.com/forums/forumdisplay.php?f=240], и вы, скорее всего, получите помощь от одного из многих разработчиков calibre.

Получение кода

Получить исходный код calibre можно двумя способами: при помощи системы управления версиями или прямой загрузкой архива tarball [https://calibre-ebook.com/dist/src].

calibre использует Git [https://www.git-scm.com/], распределенную систему контроля версий. Git доступен на всех платформах, поддерживаемых calibre. После установки Git вы можете получить исходный код calibre с помощью команды:

git clone https://github.com/kovidgoyal/calibre.git

В Windows вам потребуется полный путь, который будет выглядеть примерно так C:\Program Files\Git\git.exe.

calibre — это очень большой проект с очень длинной историей управления исходным кодом, поэтому описанное выше может занять некоторое время (от 10 минут до часа в зависимости от скорости вашего интернета).

Хотите получить код быстрее - исходный код последней версии всегда доступен в виде archive [https://calibre-ebook.com/dist/src].

Чтобы обновить ветку до последнего кода, используйте команду:

git pull --no-edit

Вы также можете просмотреть код на GitHub [https://github.com/kovidgoyal/calibre]

Отправка ваших изменений для включения

Если вы планируете внести только несколько небольших изменений, вы можете внести свои изменения и создать «merge directive», которую затем можете прикрепить к заявке в calibre bug tracker [https://bugs.launchpad.net/calibre]. Для этого внесите свои изменения, затем запустите:

git commit -am "Comment describing your changes"
git format-patch origin/master --stdout > my-changes

Это создаст файл my-changes в текущей папке, просто прикрепите его к тикету на трекере ошибок calibre <https://bugs.launchpad.net/calibre>`_. Обратите внимание, что сюда будут включены все сделанные вами коммиты. Если вы хотите отправить только несколько коммитов, вам нужно изменить origin/master выше. Чтобы отправить только последний коммит, используйте:

git format-patch HEAD~1 --stdout > my-changes

Чтобы отправить последние n коммитов, замените 1 на n, например, для последних 3 коммитов:

git format-patch HEAD~3 --stdout > my-changes

Будьте осторожны, чтобы не включать слияния при использовании HEAD~n.

Если вы планируете много заниматься разработкой, то лучше всего создать учетную запись GitHub [https://github.com]. Ниже приведено основное руководство по настройке собственного форка calibre таким образом, чтобы вы могли отправлять пулл-реквесты для включения в основной репозиторий calibre:

- Настройте git на своём компьютере, как описано в статье: Setup Git [https://help.github.com/articles/set-up-git]

- Настройте ssh-ключи для аутентификации на GitHub, как описано здесь: Генерация SSH-ключей [https://help.github.com/articles/generating-ssh-keys]

- Перейдите на https://github.com/kovidgoyal/calibre и нажмите кнопку Fork.

- В Терминале делай:

git clone git@github.com:<username>/calibre.git
git remote add upstream https://github.com/kovidgoyal/calibre.git

Замените <username> выше своим именем пользователя GitHub. Это позволит проверить ваш форк локально.

- Вы можете вносить изменения и коммитить их в любое время. Когда вы будете готовы объединить свою работу, сделайте:

git push

и перейдите к https://github.com/<username>/calibre и нажмите кнопку Pull Request, чтобы сгенерировать пул-реквест, который можно объединить.

- Вы можете в любое время обновить свою локальную копию кодом из основного репозитория, выполнив:

git pull upstream

Вам также следует следить за форумом по разработке calibre [https://www.mobileread.com/forums/forumdisplay.php?f=240]. Прежде чем вносить серьезные изменения, вы должны обсудить их на форуме или напрямую связаться с Ковидом (его адрес электронной почты указан по всему исходному коду).

Среда разработки под Windows

Примечание

Вы также должны получить исходный код calibre отдельно, как описано выше.

Установите calibre обычным образом, используя Windows installer [https://calibre-ebook.com/download_windows]. Затем откройте командную строку и перейдите в ранее проверенную папку кода calibre. Например:

cd C:\Users\kovid\work\calibre

calibre - это папка, содержащая подпапки src и resources.

Следующий шаг - установка переменной среды CALIBRE_DEVELOP_FROM = абсолютный путь к папке src. Итак, следуя приведенному выше примеру, это будет C:\Users\kovid\work\calibre\src. Вот краткое руководство [https://docs.python.org/using/windows.html#excursus-setting-environment-variables] по настройке переменных среды в Windows.

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

echo %CALIBRE_DEVELOP_FROM%

Установка этой переменной окружения означает, что calibre теперь будет загружать весь свой код Python из указанного места.

Это всё! Теперь вы готовы взломать код calibre. Например, откройте файл src\calibre\__init__.py в вашем любимом редакторе и добавьте строку:

print("Hello, world!")

в верхней части файла. Теперь запустите команду calibredb. Самая первая строка вывода должна быть Hello, world!.

Вы также можете настроить окужение разработки calibre в бесплатной Microsoft Visual Studio, если хотите, следуя инструкциям здесь [https://www.mobileread.com/forums/showthread.php?t=251201].

Среда разработки под macOS

Примечание

Вы также должны получить исходный код calibre отдельно, как описано выше.

Установите calibre обычным образом, используя предоставленный .dmg [https://calibre-ebook.com/download_osx]. Затем откройте Terminal и перейдите в ранее проверенную папку кода calibre. Например:

cd /Users/kovid/work/calibre

calibre - это папка, содержащая подпапки src и resources. Инструменты командной строки calibre находятся внутри пакета приложений calibre, в /Applications/calibre.app/Contents/MacOS, вы должны добавить эту папку в переменную среды PATH, если вы хотите легко запускать инструменты командной строки.

Следующий шаг - создание сценария bash, который установит для переменной среды CALIBRE_DEVELOP_FROM абсолютный путь к папке src при запуске calibre в режиме отладки.

Создайте простой текстовый файл:

#!/bin/sh
export CALIBRE_DEVELOP_FROM="/Users/kovid/work/calibre/src"
calibre-debug -g

Сохраните этот файл как /usr/local/bin/calibre-develop, затем установите ему права доступа так, чтобы он мог выполняться:

chmod +x /usr/local/bin/calibre-develop

Как только вы это сделали, запустите:

calibre-develop

При запуске calibre вы должны увидеть диагностическую информацию в окне терминала, а в окне графического интерфейса вы увидите звездочку после номера версии, указывающую, что вы запустились из исходников.

Среда разработки под Linux

Примечание

Вы также должны получить исходный код calibre отдельно, как описано выше.

calibre в основном разработан на Linux. У вас есть два варианта настройки среды разработки. Вы можете установить бинарный файл calibre как обычно и использовать его в качестве среды выполнения для разработки. Этот подход аналогичен тому, который используется в Windows и macOS. Кроме того, вы можете установить calibre из исходных кодов. Инструкции по настройке среды разработки из исходного кода находятся в файле INSTALL в дереве исходного кода. Здесь мы рассмотрим использование двоичного файла в качестве среды выполнения, что является рекомендуемым методом.

Установите calibre обычным образом, используя binary installer [https://calibre-ebook.com/download_linux]. Затем откройте терминал и перейдите в ранее проверенную папку кода calibre. Например:

cd /home/kovid/work/calibre

calibre - это папка, содержащая подпапки src и resources.

Следующий шаг - установка переменной среды CALIBRE_DEVELOP_FROM = абсолютный путь к папке src. Итак, следуя приведенному выше примеру, это будет /home/kovid/work/calibre/src. Как установить переменные среды, зависит от вашего дистрибутива Linux и используемой оболочки.

Примечание

It is recommended to use the binary installer provided from upstream.
Should you insist on using a package provided by your distribution, use the CALIBRE_PYTHON_PATH and CALIBRE_RESOURCES_PATH variables instead.
You can get these by running calibre-debug --paths. But note that distribution calibre packages are often terminally broken and are completely unsupported.

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

echo $CALIBRE_DEVELOP_FROM

Установка этой переменной окружения означает, что calibre теперь будет загружать весь свой код Python из указанного места.

Это всё! Теперь вы готовы взломать код calibre. Например, откройте файл src\calibre\__init__.py в вашем любимом редакторе и добавьте строку:

print("Hello, world!")

в верхней части файла. Теперь запустите команду calibredb. Самая первая строка вывода должна быть Hello, world!.

Раздельная установка «обычной» и «разрабатываемой» версий calibre на одном компьютере

Исходное дерево кодов calibre очень стабильно и редко ломается, но если вы чувствуете необходимость запуска из источника в отдельной тестовой библиотеке и запускаете выпущенную версию calibre с вашей повседневной библиотекой, вы можете легко добиться этого, используя файлы .bat или сценарии оболочки для запуска calibre. В приведенном ниже примере показано, как сделать это в Windows с использованием файлов .bat (инструкции для других платформ такие же, просто используйте сценарий оболочки вместо файла .bat)

Чтобы запустить релизную версию calibre с вашей повседневной библиотекой:

calibre-normal.bat:

calibre.exe "--with-library=C:\path\to\everyday\library folder"

calibre-dev.bat:

set CALIBRE_DEVELOP_FROM=C:\path\to\calibre\checkout\src
calibre.exe "--with-library=C:\path\to\test\library folder"

Советы по отладке

Python — это динамически типизированный язык с отличными возможностями для самоанализа. Ковид написал код ядра calibre без единого использования отладчика. Существует много стратегий для отладки кода calibre:

Использование print statements

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

calibre-debug -g

Точно так же вы можете запустить просмотрщик электронных книг как:

calibre-debug -w /path/to/file/to/be/viewed

Редактор электронных книг может быть запущен так:

calibre-debug --edit-book /path/to/be/edited

Использование интерактивного интерпретатора Python

Вы можете вставить следующие две строки кода, чтобы начать интерактивный сеанс Python в этой точке:

from calibre import ipython
ipython(locals())

При запуске из командной строки запускается интерактивный интерпретатор Python с доступом ко всем локально определенным переменным (переменным в локальной области). Интерактивная подсказка даже имеет завершение по Tab для свойств объекта, и вы можете использовать различные возможности Python для интроспекции, такие как dir(), type(), repr() и т. д.

Использовать отладчик Python как удаленный отладчик

Вы можете использовать встроенный отладчик Python (pdb) в качестве удаленного отладчика из командной строки. Сначала запустите удаленный отладчик в интересующем вас месте кода calibre, например:

from calibre.rpdb import set_trace
set_trace()

Затем запустите calibre, как обычно, или с помощью одной из команд отладки calibre, описанных в предыдущем разделе. Как только вышеуказанная точка в коде будет достигнута, calibre остановится, ожидая подключения отладчика.

Теперь откройте терминал или командную строку и используйте следующую команду для запуска сеанса отладки:

calibre-debug -c "from calibre.rpdb import cli; cli()"

Прочитать о том, как использовать отладчик Python, можно в Документации Python stdlib для модуля pdb (англ.) [https://docs.python.org/library/pdb.html#debugger-commands].

Примечание

По умолчанию удаленный отладчик будет пытаться подключиться к порту 4444. Вы можете изменить его, передав параметр порта обеим функциям set_trace() и cli(), описанным выше, например: set_trace(port=1234) и cli(port=1234).

Примечание

Отладчик Python не может обрабатывать несколько потоков, поэтому вы должны вызывать set_trace один раз для потока, каждый раз с другим номером порта.

Использование отладчика в вашей любимой Python IDE

Можно использовать встроенный отладчик в вашей любимой среде разработки Python, если он поддерживает удаленную отладку. Первый шаг - добавить проверку calibre src в PYTHONPATH в вашей среде IDE. Другими словами, папка, которую вы установили как CALIBRE_DEVELOP_FROM выше, также должна находиться в PYTHONPATH вашей IDE.

Затем поместите модуль удаленного отладчика IDE в подпапку src проверки исходного кода calibre. Добавьте любой код, необходимый для запуска удаленного отладчика до нужного уровня, например, в основной функции. Затем запустите calibre как обычно. Теперь ваша IDE должна иметь возможность подключаться к удаленному отладчику, работающему внутри calibre.

Выполнение произвольных скриптов в окружении Python calibre

Команда calibre-debug предоставляет несколько удобных ключей для выполнения собственного кода с доступом к модулям calibre:

calibre-debug -c "some Python code"

отлично подходит для тестирования небольшого фрагмента кода в командной строке. Он работает так же, как ключ -c для интерпретатора Python:

calibre-debug myscript.py

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

calibre-debug myscript.py -- --option1 arg1

-- заставляет все последующие аргументы быть переданными в ваш скрипт.

Использование calibre в собственных проектах

Можно напрямую использовать функции/код calibre в вашем проекте Python. Существует два способа сделать это:

Установка исполняемого файла calibre

Если у вас есть бинарная установка calibre, вы можете использовать интерпретатор Python в комплекте с calibre, например:

calibre-debug /path/to/your/python/script.py -- arguments to your script

Установка из исходного кода в Linux

Помимо использования описанной выше техники, если вы выполняете установку из исходного кода в Linux, вы также можете напрямую импортировать calibre, как показано ниже:

import init_calibre
import calibre

print(calibre.__version__)

Важно, чтобы вы импортировали модуль init_calibre перед любыми другими модулями/пакетами calibre, поскольку он настраивает интерпретатор для запуска кода calibre.

Документация API различных частей calibre

- Документация API рецептов
- Документация по API для плагинов
- Документация API интерфейса базы данных

- Документация API инструмента редактирования электронных книг

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Настройка среды разработки calibre »
- Документация API интерфейса базы данных

Документация API интерфейса базы данных

Это потокобезопасный API (используется схема блокировки чтения-записи). Получить доступ к API можно так:

from calibre.library import db
db = db('Path to calibre library folder').new_api

Из плагина, являющегося частью основного графического интерфейса calibre, доступ осуществляется так:

db = self.gui.current_db.new_api

class calibre.db.cache.Cache(backend, library_database_instance=None)[исходный код]

Кэш в памяти файла metadata.db из библиотеки calibre. Этот класс также служит потокобезопасным API для доступа к базе данных. Кэш в памяти поддерживается в нормальной форме для максимальной производительности.

SQLITE используется просто как способ надежного чтения и записи из metadata.db. Переработана вся логика чтения/сортировки/поиска/кеширования таблиц. Это было необходимо для максимальной производительности и гибкости.

class EventType(*values)

book_created = 4

Когда в базе данных создается новая запись книги с идентификатором книги в качестве единственного аргумента

book_edited = 8

При редактировании формата книги с аргументами: (book_id, fmt)

books_removed = 5

Когда книги удаляются из базы данных со списком идентификаторов книг в качестве единственного аргумента

format_added = 2

Когда в книгу добавляется формат с аргументами: (book_id, формат)

formats_removed = 3

Когда форматы удаляются из книги, с аргументами: (сопоставление идентификатора книги с набором форматов, удаленных из книги)

indexing_progress_changed = 9

Когда меняется прогресс индексации

items_removed = 7

Когда из некоторых книг удаляются такие элементы, как теги или авторы. Аргументы: (имя_поля, идентификаторы затронутых книг, идентификаторы удаленных элементов)

items_renamed = 6

Когда элементы, такие как теги или авторы, переименовываются в некоторых или во всех книгах. Аргументы: (field_name, затронутые идентификаторы книг, сопоставление старого идентификатора элемента с новым идентификатором элемента)

links_changed = 11

При изменении ссылок, связанных с элементами, с аргументами: (field_name, item_ids)

metadata_changed = 1

Когда некоторые метаданные изменяются для некоторых книг, с аргументами: (имя измененного поля, набор затронутых идентификаторов книг)

notes_changed = 10

Когда примечания, связанные с элементом (ами), изменяются с аргументами: (field_name, item_ids)

add_books(books, add_duplicates=True, apply_import_tags=True, preserve_uuid=False, run_hooks=True, dbapi=None)[исходный код]

Добавьте указанные книги в библиотеку. Книги должны быть повторяемыми из 2-х кортежей, каждый из 2-х кортежей в форме (mi, format_map), где mi - объект метаданных, а format_map - словарь в форме {fmt: path_or_stream}, например: {'EPUB': '/path/to/file.epub'}.

Возвращает пару списков: ids, duplicates. „ids“ содержит идентификаторы всех новосозданных книг в базе данных. duplicates содержит (mi, format_map) для книг уже существующих в базе данных, руководствуясь простой эвристикой обнаружения дубликатов, используемой has_book().

add_custom_book_data(name, val_map, delete_first=False)[исходный код]

Добавить данные для имени, где val_map - это отображение идентификаторов книг в значения. Если delete_first имеет значение True, все ранее сохраненные данные для имени будут удалены.

add_extra_files(book_id, map_of_relpath_to_stream_or_path, replace=True, auto_rename=False)[исходный код]

Добавить дополнительные файлы данных

add_format(book_id, fmt, stream_or_path, replace=True, run_hooks=True, dbapi=None)[исходный код]

Добавить формат в указанную книгу. Вернёт True, если формат был добавлен успешно.

Параметры:

- replace – If True replace existing format, otherwise if the format already exists, return False.

- run_hooks – If True, file type plugins are run on the format before and after being added.

- dbapi – Для внутреннего пользования

add_listener(event_callback_function, check_already_added=False)[исходный код]

Регистрирует функцию обратного вызова, которая будет вызываться после выполнения определённых действий с этой базой данных. Функция должна принимать три аргумента: (EventType, library_id, event_type_specific_data)

add_notes_resource(path_or_stream_or_data, name: str, mtime: float = None) → int[исходный код]

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

all_annotation_types()[исходный код]

Возвращает кортеж из всех типов аннотаций в базе данных.

all_annotation_users()[исходный код]

Возвращает кортеж из всех (user_type, имя пользователя), которые имеют аннотации.

all_annotations(restrict_to_user=None, limit=None, annotation_type=None, ignore_removed=False, restrict_to_book_ids=None)[исходный код]

Возвращает кортеж всех аннотаций, соответствующих указанным критериям. ignore_removed определяет, будут ли также возвращаться удалённые аннотации. Удалённые аннотации - это просто каркас, используемый для объединения аннотаций.

all_annotations_for_book(book_id)[исходный код]

Возвращает кортеж, содержащий все аннотации для указанного book_id в виде dict с ключами: format, user_type, user, annotation. Здесь annotation - это данные аннотации.

all_book_ids(type=<class 'frozenset'>)[исходный код]

Замороженный набор всех известных идентификаторов книг.

all_field_for(field, book_ids, default_value=None)[исходный код]

То же, что field_for, за исключением того, что работает с несколькими книгами одновременно

all_field_ids(name)[исходный код]

Замороженный набор идентификаторов для всех значений в поле name.

all_field_names(field)[исходный код]

Замороженный набор имен всех полей (следует использовать только для полей „много-один“ и „много-много“)

annotation_count_for_book(book_id)[исходный код]

Возвращает количество аннотаций для указанной книги, доступных в базе данных.

annotations_map_for_book(book_id, fmt, user_type='local', user='viewer')[исходный код]

Возвращает сопоставление тип аннотации -> данные аннотации для указанных book_id, format, user и user_type.

author_data(author_ids=None)[исходный код]

Вернуть данные об авторе в виде словаря с ключами: имя, сортировка, ссылка

Если авторов с указанными идентификаторами не найдено, возвращается пустой словарь. Если author_ids - None, возвращаются данные для всех авторов.

author_sort_from_authors(authors, key_func=<function make_change_case_func.<locals>.change_case>)[исходный код]

Учитывая список авторов, вернуть строку author_sort для авторов, предпочитая сортировку по авторам, связанную с автором, а не вычисленную строку.

books_for_field(name, item_id)[исходный код]

Вернуть все книги, связанные с элементом, идентифицированным с помощью item_id, где элемент принадлежит полю name.

Возвращаемое значение - это набор идентификаторов книг или пустой набор, если элемент или поле не существует.

books_in_virtual_library(vl, search_restriction=None, virtual_fields=None)[исходный код]

Вернуть набор книг из указанной виртуальной библиотеки

compress_covers(book_ids, jpeg_quality=100, progress_callback=None)[исходный код]

Сжать изображения обложек для указанных книг. Качество сжатия 100 - сжатие без потерь, иначе - сжатие с потерями.

Коллбэк прогресса будет вызываться с book_id и старым и новым размерами для каждой обработанной книги. При возникновении ошибки новый размер - строка с подробными сведениями об ошибке.

copy_cover_to(book_id, dest, use_hardlink=False, report_file_size=None)[исходный код]

Копировать обложку в файл как объект dest. Возвращает False, если обложки не существует или если dest - тот же файл, что и текущая обложка. dest также может быть путем, и в этом случае обложка копируется по нему тогда и только тогда, когда путь отличается от текущего пути (с учетом чувствительности к регистру).

copy_format_to(book_id, fmt, dest, use_hardlink=False, report_file_size=None)[исходный код]

Копировать формат fmt в файл как объект dest. Если указанный формат не существует, вызывает ошибку NoSuchFormat. dest также может быть путем (к файлу), и в этом случае формат копируется по нему, если путь отличается от текущего пути (с учетом чувствительности к регистру).

cover(book_id, as_file=False, as_image=False, as_path=False, as_pixmap=False)[исходный код]

Вернуть обложку или None. По умолчанию возвращает обложку в виде байтовой строки.

ВНИМАНИЕ: использование as_path скопирует обложку во временный файл и вернет путь к временному файлу. Вы должны удалить временный файл, когда закончите работать с ним.

Параметры:

- as_file – Если True, вернуть изображение как объект открытого файла (SpooledTemporaryFile)

- as_image – Если True вернуть изображение как объект QImage

- as_pixmap – Если True вернуть изображение как объект QPixmap

- as_path – Если True, вернуть изображение как путь, указывающий на временный файл

data_for_find_identical_books()[исходный код]

Вернуть данные, которые можно использовать для реализации find_identical_books() в рабочем процессе без доступа к базе данных. См. реализацию в db.utils.

data_for_has_book()[исходный код]

Возвращает данные, подходящие для использования в has_book(). Это можно использовать для реализации has_book() в рабочем процессе без доступа к базе данных.

delete_annotations(annot_ids)[исходный код]

Удалить аннотации с указанными идентификаторами.

delete_custom_book_data(name, book_ids=())[исходный код]

Удалить данные для имени. По умолчанию удаляются все данные. Если вы хотите удалить данные только для некоторых идентификаторов книг, передайте список идентификаторов книг.

delete_trash_entry(book_id, category)[исходный код]

Удалить запись из корзины. Здесь категория „b“ для книг и „f“ для форматов.

embed_metadata(book_ids, only_fmts=None, report_error=None, report_progress=None)[исходный код]

Обновить метаданные во всех форматах указанных book_ids до текущих метаданных в базе данных.

expire_old_trash()[исходный код]

Записи из корзины слишком старые

export_note(field, item_id) → str[исходный код]

Экспортировать примечание в виде одного HTML-документа с встроенными изображениями в виде URL-адресов данных

fast_field_for(field_obj, book_id, default_value=None)[исходный код]

То же, что field_for, только исключает дополнительный поиск для получения объекта поля

field_for(name, book_id, default_value=None)[исходный код]

Вернуть значение поля name для книги, идентифицированной с помощью book_id. Если такой книги не существует, или она не имеет определенного значения для поля name или такое поле не существует, то возвращается default_value.

default_value не используется для заголовка, title_sort, авторов, author_sort и series_index. Это потому, что они всегда имеют значения в базе данных. default_value используется для всех настраиваемых столбцов.

Возвращаемое значение для полей is_multiple всегда является кортежем, даже если значения не найдены (другими словами, default_value игнорируется). Исключение составляют идентификаторы, для которых возвращаемое значение всегда dict. Возвращаемые кортежи всегда находятся в ссылочном порядке (в порядке их создания).

field_ids_for(name, book_id)[исходный код]

Вернуть идентификаторы (в виде кортежа) для значений, которые поле name имеет в книге, идентифицированной с помощью book_id. Если значений нет, или такой книги, или такого поля нет, возвращается пустой кортеж.

field_supports_notes(field=None) → bool[исходный код]

Вернуть True, если указанное поле поддерживает примечания. Если поле равно None, вернуть неизменяемое множество всех полей, поддерживающих примечания.

find_identical_books(mi, search_restriction='', book_ids=None)[исходный код]

Находит книги, у которых есть надмножество авторов в mi и одинаковое название (нечеткое совпадение названия). См. Также data_for_find_identical_books().

format(book_id, fmt, as_file=False, as_path=False, preserve_filename=False)[исходный код]

Вернуть формат электронной книги в виде байтовой строки или None, если формат не существует или у нас нет разрешения на запись в файл электронной книги.

Параметры:

- as_file – If True the e-book format is returned as a file object. Note that the file object is a SpooledTemporaryFile, so if what you want to do is copy the format to another file, use copy_format_to() instead for performance.

- as_path – Копирует файл формата во временный файл и возвращает путь к временному файлу.

- preserve_filename – Если True и возвращает путь, имя файла такое же, как и в библиотеке. Обратите внимание, что использование этого означает, что повторные вызовы дают один и тот же временный файл (который каждый раз создается заново)

format_abspath(book_id, fmt)[исходный код]

Вернуть абсолютный путь к файлу электронной книги формата format. Вам почти никогда не следует использовать это, так как это нарушает обещание потоковой безопасности этого API. Вместо этого используйте copy_format_to().

В настоящее время используется только в calibredb list, viewer, edit book, compare_format to original format, open with, массовое редактирование метаданных и каталоги (через get_data_as_dict ()).

Я не верю, что кто-либо, кроме средства просмотра, открывающего и редактирующего книгу, выполняет какие-либо операции ввода-вывода записи файлов с результатами этого вызова.

format_hash(book_id, fmt)[исходный код]

Вернуть хэш указанного формата для указанной книги. Тип хэша зависит от серверной части, но обычно это SHA-256.

format_metadata(book_id, fmt, allow_cache=True, update_db=False)[исходный код]

Вернуть путь, размер и время для указанного формата для указанной книги. Вы не должны использовать path, если в этом нет крайней необходимости, поскольку прямой доступ к нему нарушает гарантии потоковой безопасности этого API. Вместо этого используйте метод copy_format_to().

Параметры:

- allow_cache – Если используются кэшированные значения True, в противном случае выполняется медленный доступ к файловой системе. Значения кеша могли быть устаревшими, если доступ к файловой системе выполнялся вне этого API.

- update_db – Если True поле max_size базы данных обновляется для этой книги.

formats(book_id, verify_formats=True)[исходный код]

Вернуть кортеж всех форматов для указанной книги. Если verify_formats имеет значение True, проверяет, существуют ли файлы на диске.

get_all_items_that_have_notes(field_name=None) → set[int] | dict[str, set[int]][исходный код]

Вернуть все item_ids для элементов, у которых есть примечания в указанном поле или во всех полях, если field_name равно None

get_all_link_maps_for_book(book_id)[исходный код]

Возвращает все ссылки для всех полей, на которые ссылается книга, идентифицированная book_id. Если book_id не существует, метод возвращает {}.

Пример. Предположим, что у автора A есть ссылка X, у автора B есть ссылка Y, у тега S есть ссылка F, а у тега T есть ссылка G. Если у книги 1 есть автор A и тег T, этот метод возвращает {„authors“:{„A“:“X“}, „теги“:{„T“, „G“}}. Если автор книги 2 не является ни A, ни B и не имеет тегов, этот метод возвращает {}.

Параметры:

book_id – book id в вопросе.

Результат:

{field: {field_value, link_value}, … для всех полей с field_value, имеющим непустое значение ссылки для этой книги

get_categories(sort='name', book_ids=None, already_fixed=None, first_letter_sort=False, uncollapsed_categories=None)[исходный код]

Используется внутри для реализации браузера тегов

get_custom_book_data(name, book_ids=(), default=None)[исходный код]

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

get_id_map(field)[исходный код]

Вернуть сопоставление номеров идентификаторов со значениями для указанного поля. Поле должно быть полем «многие-один» или «многие-многие», в противном случае возникает ошибка ValueError.

get_ids_for_custom_book_data(name)[исходный код]

Вернуть набор идентификаторов книг, в названии которых есть данные.

get_item_id(field, item_name, case_sensitive=False)[исходный код]

Возвращает идентификатор элемента для item_name или None, если не найден. Эта функция работает очень медленно, если при поиске нескольких имен используется либо get_item_ids(), либо get_item_name_map(). Аналогично, поиск с учетом регистра выполняется быстрее, чем без учета регистра.

get_item_ids(field, item_names, case_sensitive=False)[исходный код]

Возвращает значение dict , сопоставляющее item_name с идентификатором элемента , или None

get_item_name(field, item_id)[исходный код]

Вернуть имя элемента для элемента, указанного в item_id в указанном поле. См. также get_id_map().

get_item_name_map(field, normalize_func=None)[исходный код]

Вернуть отображение значений элементов в идентификаторы

get_link_map(for_field)[исходный код]

Возвращает словарь ссылок для предоставленного поля.

Параметры:

for_field – имя поиска поля, для которого требуется карта ссылок

Результат:

{field_value:link_value, …} для непустых ссылок

get_metadata(book_id, get_cover=False, get_user_categories=True, cover_as_data=False)[исходный код]

Возвращает метаданные для книги, идентифицированной book_id как объект calibre.ebooks.metadata.book.base.Metadata. Обратите внимание, что список форматов не проверяется. Если get_cover - True, обложка возвращается, либо путь к временному файлу как mi.cover, либо если cover_as_data имеет значение True, то как mi.cover_data.

get_next_series_num_for(series, field='series', current_indices=False)[исходный код]

Возвращает индекс следующей серии для указанной серии с учетом различных предпочтений, управляющих генерацией номера следующей серии.

Параметры:

- field – Поле типа серии (по умолчанию - встроенный столбец серии)

- current_indices – If True, returns a mapping of book_id to current series_index value instead.

get_notes_resource(resource_hash) → dict | None[исходный код]

Вернуть словарь, содержащий данные ресурса и имя или None, если ресурс с указанным хэшем не найден

get_proxy_metadata(book_id)[исходный код]

Например get_metadata(), за исключением того, что он возвращает объект ProxyMetadata, который только считывает значения из базы данных по запросу. Это намного быстрее, чем get_metadata, когда необходимо получить доступ только к небольшому количеству полей из возвращенного объекта метаданных.

get_usage_count_by_id(field)[исходный код]

Вернуть отображение идентификатора в счетчик использования для всех значений указанного поля, которое должно быть полем «много-один» или «много-много»

has_book(mi)[исходный код]

Верните True, если база данных содержит запись с тем же заголовком, что и переданный объект Metadata. При сравнении регистр не учитывается. См. также data_for_has_book().

has_format(book_id, fmt)[исходный код]

Вернуть True, если формат существует на диске

has_id(book_id)[исходный код]

Вернёт True, если указанный book_id существует в БД

import_note(field, item_id, path_to_html_file, path_is_data=False)[исходный код]

Импортировать ранее экспортированное примечание или произвольный HTML-файл в качестве примечания для указанного элемента

init()[исходный код]

Инициализировать этот кеш данными из серверной части.

items_with_notes_in_book(book_id: int) → dict[str, dict[int, str]][исходный код]

Вернуть словарь поля для элементов, у которых есть примечания для этого поля для указанной книги

link_for(field, item_id)[исходный код]

Вернуть ссылку, если она есть, для указанного элемента или None, если ссылка не найдена

list_extra_files(book_id, use_cache=False, pattern='') → tuple[ExtraFile, ...][исходный код]

Получить информацию о дополнительных файлах в каталоге книги.

Параметры:

- book_id – идентификатор книги в базе данных для книги

- pattern – паттерн имен файлов для поиска. Пустой паттерн соответствует всем дополнительным файлам. Паттерны должны использовать / в качестве разделителя. Используйте константу DATA_FILE_PATTERN для сопоставления файлов внутри каталога данных.

Результат:

Кортеж всех дополнительных файлов, соответствующих указанному шаблону. Каждый элемент кортежа — это ExtraFile(relpath, file_path, stat_result). Где relpath — это относительный путь файла к каталогу книги с использованием / в качестве разделителя. stat_result — это результат вызова os.stat() для файла.

merge_annotations_for_book(book_id, fmt, annots_list, user_type='local', user='viewer')[исходный код]

Объединяет указанные аннотации с существующими аннотациями для book_id, fm, user_type и user.

merge_extra_files(dest_id, src_ids, replace=False)[исходный код]

Объединить дополнительные файлы из src_ids в dest_id. Конфликтующие файлы автоматически переименовываются, если только replace=True не заменяет их.

move_book_from_trash(book_id)[исходный код]

Восстановить книгу из каталога корзины

move_format_from_trash(book_id, fmt)[исходный код]

Восстановить формат из каталога корзины

multisort(fields, ids_to_sort=None, virtual_fields=None)[исходный код]

Вернуть список отсортированных идентификаторов книг. Если ids_to_sort - None, возвращаются идентификаторы всех книг.

поля должны быть списком из двух кортежей формы (field_name, ascending = True или False). Наиболее значимое поле - первое в кортеже из двух элементов.

notes_data_for(field, item_id) → str[исходный код]

Вернуть все данные примечаний в виде словаря или None, если примечание не существует

notes_for(field, item_id) → str[исходный код]

Вернуть документ с примечаниями или пустую строку, если не найдено

notes_resources_used_by(field, item_id)[исходный код]

Вернуть набор хэшей ресурсов всех ресурсов, используемых примечанием для указанного элемента

pref(name, default=None, namespace=None)[исходный код]

Вернуть значение для указанного предпочтения или значение, указанное как по умолчанию, если предпочтение не установлено.

read_backup(book_id)[исходный код]

Вернуть резервную копию метаданных OPF для книги в виде байтовой строки или None, если такой резервной копии не существует

remove_books(book_ids, permanent=False)[исходный код]

Удалить записи книг, указанных в book_ids, из базы данных и удалить их файлы. Если для параметра permanent установлено значение False, файлы помещаются в корзину для библиотеки.

remove_extra_files(book_id: int, relpaths: Iterable[str], permanent=False) → dict[str, Exception | None][исходный код]

Удалить указанные дополнительные файлы либо в корзину, либо навсегда.

remove_formats(formats_map, db_only=False)[исходный код]

Remove the specified formats from the specified books.

Параметры:

- formats_map – Сопоставить book_id со списком форматов, которые нужно удалить из книги.

- db_only – Если True, удалить только запись для формата из базы данных, не удалять файл формата из файловой системы.

Результат:

Сопоставление идентификатора книги с набором форматов, фактически удаленных из файловой системы для этой книги

remove_items(field, item_ids, restrict_to_book_ids=None)[исходный код]

Удалить все элементы в указанном поле с указанными идентификаторами. Возвращает набор идентификаторов затронутых книг. restrict_to_book_ids - необязательный набор идентификаторов книг. Если указано, элементы будут удалены только из этих книг.

rename_extra_files(book_id, map_of_relpath_to_new_relpath, replace=False)[исходный код]

Переименовать дополнительные файлы данных

rename_items(field, item_id_to_new_name_map, change_index=True, restrict_to_book_ids=None)[исходный код]

Переименовать элементы из полей «многие-один» или «многие-многие», например тегов или серий.

Параметры:

- change_index – При переименовании в поле типа серии также изменить значения series_index.

- restrict_to_book_ids – Необязательный набор идентификаторов книг, для которых должно быть выполнено переименование, по умолчанию для всех книг.

restore_book(book_id, mi, last_modified, path, formats, annotations=())[исходный код]

Восстановить запись книги в базе данных для книги, которая уже существует в файловой системе

restore_original_format(book_id, original_fmt)[исходный код]

Восстановить указанный формат из ранее сохраненного ORIGINAL_FORMAT, если таковой имеется. Вернуть True в случае успеха. ORIGINAL_FORMAT удаляется после успешного восстановления.

property safe_read_lock

Безопасная блокировка чтения - это блокировка, которая ничего не делает, если поток уже имеет блокировку записи, в противном случае он получает блокировку чтения. Это необходимо для предотвращения ошибок DowngradeLockErrors, которые могут возникнуть при обновлении кеша поиска при наличии составных столбцов. Обновление кеша поиска содержит эксклюзивную блокировку, но поиск в составном столбце включает чтение значений полей через ProxyMetadata, который пытается получить общую блокировку. Могут быть и другие сценарии, которые также вызывают это.

Это свойство возвращает новый объект блокировки при каждом доступе. Этот объект блокировки не является рекурсивным (для повышения производительности) и должен использоваться только в операторе with как with cache.safe_read_lock:, иначе произойдут плохие вещи.

save_original_format(book_id, fmt)[исходный код]

Сохранить копию указанного формата как ORIGINAL_FORMAT, перезаписав любой существующий ORIGINAL_FORMAT.

search(query, restriction='', virtual_fields=None, book_ids=None)[исходный код]

Искать в базе данных указанный запрос, возвращая набор совпадающих идентификаторов книг.

Параметры:

- restriction – Ограничение, добавляемое (AND) к указанному запросу. Обратите внимание, что ограничения кэшируются, поэтому поиск «a» AND «b» будет медленнее, чем «a» с ограничением «b».

- virtual_fields – Используется внутри (виртуальные поля, такие как on_device, для поиска).

- book_ids – Если не None, то набор идентификаторов книг, по которым будет выполняться поиск книг вместо поиска по всем книгам.

search_annotations(fts_engine_query, use_stemming=True, highlight_start=None, highlight_end=None, snippet_size=None, annotation_type=None, restrict_to_book_ids=None, restrict_to_user=None, ignore_removed=False)[исходный код]

Возвращает кортеж аннотаций, соответствующий указанному полнотекстовому запросу.

search_notes(fts_engine_query='', use_stemming=True, highlight_start=None, highlight_end=None, snippet_size=None, restrict_to_fields=(), return_text=True, result_type=<class 'tuple'>, process_each_result=None, limit=None)[исходный код]

Искать текст примечаний с использованием индекса FTS. Если запрос пустой, вернуть все примечания.

set_annotations_for_book(book_id, fmt, annots_list, user_type='local', user='viewer')[исходный код]

Установить все аннотации для указанных book_id, fmt, user_type и user.

set_conversion_options(options, fmt='PIPE')[исходный код]

параметры должны быть картой в форме {book_id: conversion_options}

set_cover(book_id_data_map)[исходный код]

Установить обложку для этой книги. Данные могут быть QImage, QPixmap, файловым объектом или байтовой строкой. Также может быть None - любая существующая обложка будет удалена

set_field(name, book_id_to_val_map, allow_case_change=True, do_path_update=True)[исходный код]

Установить значения поля, указанного в name. Возвращает набор всех идентификаторов книг, на которые повлияло изменение.

Параметры:

- book_id_to_val_map – Сопоставление book_ids значениям, которые следует применить.

- allow_case_change – Если True, регистр полей много-один или много-много будет изменен. Например, если у книги есть тег tag1, а вы установили тег для другой книги как Tag1, тогда обе книги будут иметь тег Tag1, если allow_case_change - True, иначе они оба будут иметь тег tag1.

- do_path_update – Используется внутри, вы никогда не должны его менять.

set_link_map(field, value_to_link_map, only_set_if_no_existing_link=False)[исходный код]

Устанавливает ссылки для значений элемента в поле. Примечание: этот метод не изменяет значения не в value_to_link_map.

Параметры:

- field – имя поиска

- value_to_link_map – dict(field_value:ссылка, …). Обратите внимание, что это значения, а не идентификаторы полей.

Результат:

книги изменены путем установки ссылки

set_metadata(book_id, mi, ignore_errors=False, force_changes=False, set_title=True, set_authors=True, allow_case_change=False)[исходный код]

Установить метаданные для книги id из объекта Metadata mi

Установка force_changes = True заставит set_metadata обновлять поля, даже если mi содержит пустые значения. В этом случае None отличается от empty. Если mi.XXX равно „None“, XXX не заменяется, в противном случае - нет. Теги, идентификаторы и атрибуты обложки - это особые случаи. Теги и идентификаторы не могут быть установлены на „None“, поэтому они всегда будут заменены, если force_changes истинно. Вы должны убедиться, что mi содержит те значения, которые вы хотите, чтобы книга имела. Обложки всегда меняются, если предоставляется новая обложка, но никогда не удаляются. Также обратите внимание, что force_changes не влияет на установку заголовка или авторов.

set_notes_for(field, item_id, doc: str, searchable_text: str = '', resource_hashes=(), remove_unused_resources=False) → int[исходный код]

Установить документ с примечаниями. Если искомый текст отличается от документа, указать его как searchable_text. Если документ ссылается на ресурсы, их хэши должны присутствовать в resource_hashes. Установить remove_unused_resources в True, чтобы очистить неиспользуемые ресурсы, обратите внимание, что обновление примечания автоматически очищает ресурсы, относящиеся к этому примечанию.

set_pref(name, val, namespace=None)[исходный код]

Установите указанное предпочтение на указанное значение. См. Также: meth:pref.

split_if_is_multiple_composite(f, val)[исходный код]

Если f является составным ключом поиска столбца, а столбец is_multiple, то разделите v на уникальные непустые значения. Сравнение чувствительно к регистру. Порядок не сохраняется. Возвращает list() для совместимости с получателями полей метаданных прокси, например тегами.

tags_older_than(tag, delta=None, must_have_tag=None, must_have_authors=None)[исходный код]

Возвратить идентификаторы всех книг с тегом tag, которые старше указанного времени. сравнение тегов производится без учета регистра.

Параметры:

- delta – Объект timedelta или None. Если None, то возвращаются все идентификаторы с тегом.

- must_have_tag – Если нет, то список совпадений будет ограничен книгами с этим тегом.

- must_have_authors – Список авторов. Если нет, то список совпадений будет ограничен книгами, имеющими этих авторов (без учета регистра).

unretire_note_for(field, item_id) → int[исходный код]

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

update_annotations(annot_id_map)[исходный код]

Обновить аннотации.

user_categories_for_books(book_ids, proxy_metadata_map=None)[исходный код]

Вернуть категории пользователей для указанных книг. proxy_metadata_map не является обязательным и полезен для повышения производительности в контекстах, где уже существует объект ProxyMetadata для книг. Это должно быть отображение book_ids на соответствующие им объекты ProxyMetadata.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »
- Настройка среды разработки calibre »

- Документация API инструмента редактирования электронных книг

Документация API инструмента редактирования электронных книг

Инструменты редактирования электронных книг состоят из объекта calibre.ebooks.oeb.polish.container.Container, который представляет книгу как набор файлов ресурсов HTML +, и различных инструментов, которые можно использовать для выполнения операций с контейнером. Все инструменты представлены в виде функций уровня модуля в различных модулях calibre.ebooks.oeb.polish.*.

Вы получаете объект-контейнер для книги по следующему пути:

from calibre.ebooks.oeb.polish.container import get_container
container = get_container('Path to book file', tweak_mode=True)

If you are writing a plugin for the E-book editor, you get the current container for the book being edited like this:

from calibre.gui2.tweak_book import current_container
container = current_container()
if container is None:
report_error # No book has been opened yet

Объект контейнера
class calibre.ebooks.oeb.polish.container.Container(rootpath=None, opfpath=None, log=<calibre.utils.logging.Log object>, clone_data=None)[исходный код]

Контейнер представляет собой открытую электронную книгу в виде папки, полной файлов и файла OPF. Есть два важных понятия:

- Корневая папка. Это основа электронной книги. Все файлы электронных книг находятся внутри этой папки или в ее подпапках.

- Имена: это пути к файлам книг относительно корневой папки. Они всегда содержат разделители POSIX и не заключаются в кавычки. Их можно рассматривать как канонические идентификаторы файлов в книге. Большинство методов объекта-контейнера работают с именами. Имена всегда в нормальной форме NFC Unicode.

- Клоны: объект-контейнер поддерживает эффективное клонирование на диске, которое используется для реализации контрольных точек в редакторе электронных книг. Чтобы это работало, вы никогда не должны обращаться к файлам в файловой системе напрямую. Вместо этого используйте raw_data() или open() для чтения/записи файлов компонентов в книге.

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

abspath_to_name(fullpath, root=None)[исходный код]

Преобразование абсолютного пути в каноническое имя относительно root

Параметры:

root – Базовая папка. По умолчанию используется корень для этого объекта-контейнера.

add_file(name, data=b'', media_type=None, spine_index=None, modify_name_if_needed=False, process_manifest_item=None, suggested_id='')[исходный код]

Add a file to this container. Entries for the file are automatically created in the OPF manifest and spine
(if the file is a text document)

add_name_to_manifest(name, process_manifest_item=None, suggested_id='')[исходный код]

Добавить в манифест запись для файла с указанным именем. Возвращает идентификатор манифеста.

add_properties(name, *properties)[исходный код]

Добавить указанные свойства в элемент манифеста, идентифицированный по имени.

apply_unique_properties(name, *properties)[исходный код]

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

book_type = 'oeb'

Тип книги epub для файлов EPUB и azw3 для файлов AZW3

commit(outpath=None, keep_parsed=False)[исходный код]

Зафиксировать все загрязненные проанализированные объекты в файловой системе и записать файл электронной книги по пути outpath.

Параметры:

- output – Путь для записи сохраненного файла электронной книги. Если None, используется путь к исходному файлу книги.

- keep_parsed – Если True, проанализированные представления зафиксированных элементов хранятся в кеше.

commit_item(name, keep_parsed=False)[исходный код]

Зафиксировать проанализированный объект на диске (он сериализуется и записывается в базовый файл). Если keep_parsed - True, проанализированное представление сохраняется в кеше. См. также :parsed()

dirty(name)[исходный код]

Отметить проанализированный объект, соответствующий имени, как грязный. См. Также :parsed().

exists(name)[исходный код]

Истинно, только если существует файл/папка, соответствующие каноническому имени. Обратите внимание, что эта функция страдает от ограничений базовой файловой системы ОС, в частности из-за (не) чувствительности. Таким образом, в файловой системе, нечувствительной к регистру, это вернет True, даже если регистр имени отличается от регистра файла базовой файловой системы. См. Также has_name()

filesize(name)[исходный код]

Вернуть размер в байтах файла, представленного указанным каноническим именем. Автоматически обрабатывает загрязненные проанализированные объекты. См. также :parsed()

generate_item(name, id_prefix=None, media_type=None, unique_href=True)[исходный код]

Добавить в манифест элемент с href, производным от заданного имени. Автоматически обеспечивает уникальность href и id. Возвращает сгенерированный элемент.

get_file_path_for_processing(name, allow_modification=True)[исходный код]

Подобно open(), за исключением того, что он возвращает путь к файлу вместо объекта открытого файла.

property guide_type_map

Сопоставление guide типа с каноническим именем

has_name(name)[исходный код]

Вернуть True, если существует файл с тем же каноническим именем, что и указанное. В отличие от exists() этот метод всегда чувствителен к регистру.

href_to_name(href, base=None)[исходный код]

Преобразование href (относительно base) в имя. base должно быть именем или None, в этом случае используется self.root.

insert_into_xml(parent, item, index=None)[исходный код]

Вставить элемент в родительский (или добавить, если индекс равен None), исправив отступ. Работает только с самозакрывающимися элементами.

is_dir = False

Если этот контейнер представляет собой распакованную книгу (каталог)

iterlinks(name, get_line_numbers=True)[исходный код]

Перебрать все ссылки по имени. Если get_line_numbers имеет значение True, выдает результаты формы (ссылка, номер_строки, смещение). Где line_number - это номер line_number, в котором находится ссылка, а смещение - это количество символов от начала строки. Обратите внимание, что смещение может охватывать несколько строк, если не ноль.

make_name_unique(name)[исходный код]

Убедится, что name уже не существует в этой книге. Если это так, вернуть измененную версию, которая не существует.

manifest_has_name(name)[исходный код]

Вернуть True, если в манифесте есть запись, соответствующая имени

property manifest_id_map

Сопоставление идентификатора манифеста с каноническими именами

manifest_items_of_type(predicate)[исходный код]

Имена всех элементов манифеста, тип носителя которых соответствует предикату. predicate может быть набором, списком, строкой или функцией, принимающей один аргумент, которая будет вызываться с медиа-типом.

manifest_items_with_property(property_name)[исходный код]

Все элементы манифеста, имеющие указанное свойство

property manifest_type_map

Сопоставление манифестного медиа-типа со списком канонических имен этого медиа-типа

property mi

Метаданные этой книги как объект метаданных. Обратите внимание, что этот объект создается «на лету» каждый раз, когда это свойство запрашивается, поэтому используйте его с осторожностью.

name_to_abspath(name)[исходный код]

Преобразование канонического имени в путь, полностью зависящий от ОС

name_to_href(name, base=None)[исходный код]

Convert a name to a href relative to base, which must be a name or
None in which case self.root is used as the base

property names_that_must_not_be_changed

Набор имен, которые нельзя переименовывать. Зависит от формата файла электронной книги.

property names_that_must_not_be_removed

Набор имен, которые нельзя удалять из контейнера. Зависит от формата файла электронной книги.

property names_that_need_not_be_manifested

Набор имен, которые могут отсутствовать в манифесте. Зависит от формата файла электронной книги.

open(name, mode='rb')[исходный код]

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

property opf

Разобранный файл OPF

opf_get_or_create(name)[исходный код]

Удобный метод, позволяющий либо вернуть первый элемент XML с указанным именем, либо создать его в элементе opf:package, а затем вернуть его, если он ещё не существует.

property opf_version

Версия, установленная на <package> элементе OPF

property opf_version_parsed

Версия, заданная в <package> элементе OPF как кортеж целых чисел

opf_xpath(expr)[исходный код]

Удобный метод для оценки выражения XPath в файле OPF имеет предопределенные префиксы пространства имен opf: и dc:.

parsed(name)[исходный код]

Вернуть проанализированное представление файла, указанного по имени. Для файлов HTML и XML возвращается дерево lxml. Для файлов CSS возвращается таблица стилей css_parser. Обратите внимание, что проанализированные объекты кэшируются для повышения производительности. Если вы вносите какие-либо изменения в анализируемый объект, вы должны вызвать dirty(), чтобы контейнер знал, что нужно обновить кеш. См. Также replace().

raw_data(name, decode=True, normalize_to_nfc=True)[исходный код]

Вернуть необработанные данные, соответствующие файлу, указанному по имени

Параметры:

- decode – Если True и файл имеет текстовый MIME-тип, декодировать его и вернуть объект Unicode вместо необработанных байтов.

- normalize_to_nfc – Если True, возвращаемый объект unicode нормализуется до нормальной формы NFC, как это требуется для форматов файлов EPUB и AZW3.

relpath(path, base=None)[исходный код]

Преобразование абсолютного пути (с разделителями os) в путь относительно базы (по умолчанию self.root). Относительный путь - не имя. Для этого используйте abspath_to_name().

remove_from_spine(spine_items, remove_if_no_longer_in_spine=True)[исходный код]

Удалить указанные пункты (по каноническому имени) из корешка. Если remove_if_no_longer_in_spine имеет значение True, элементы также удаляются из книги, а не только из корешка.

remove_from_xml(item)[исходный код]

Удалить элемент из родительского, исправляя отступ (работает только с самозакрывающимися элементами)

remove_item(name, remove_from_guide=True)[исходный код]

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

rename(current_name, new_name)[исходный код]

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

replace(name, obj)[исходный код]

Заменить проанализированный объект, соответствующий имени, на obj, который должен быть аналогичным объектом, то есть деревом lxml для HTML/XML или таблицей стилей css_parser для файла CSS.

replace_links(name, replace_func)[исходный код]

Заменить все ссылки в имени с помощью replace_func, которая должна быть вызываемой, которая принимает URL-адрес и возвращает замененный URL-адрес. Он также должен иметь атрибут replace, который имеет значение True, если выполняется какая-либо фактическая замена. Удобные способы создания таких вызываемых объектов - это использование классов LinkReplacer и LinkRebaser.

serialize_item(name)[исходный код]

Преобразовать проанализированный объект (идентифицируемый каноническим именем) в байтовую строку. Смотрите parsed().

set_spine(spine_items)[исходный код]

Задать для spine значение spine_items, где spine_items - это итерабельность формы (имя, линейная). Вызовет ошибку, если одно из имен отсутствует в манифесте.

property spine_items

Итератор, определяющий путь для каждого элемента корешка книги. См. также :spine_iter и spine_items.

property spine_iter

Итератор, который выдает элемент с именем is_linear для каждого элемента в корешке книги. item - это элемент lxml, name - каноническое имя файла, а is_linear - True, если элемент является линейным. См. также :spine_names и spine_items.

property spine_names

Итератор, дающий имя и is_linear для каждого элемента в корешке книги. См. также :spine_iter и spine_items.

Управление файлами компонентов в контейнере

calibre.ebooks.oeb.polish.replace.replace_links(container, link_map, frag_map=<function <lambda>>, replace_in_opf=False)[исходный код]

Заменить ссылки на файлы в контейнере. Будет перебирать все файлы в контейнере и изменять в них указанные ссылки.

Параметры:

- link_map – Сопоставление старого канонического имени с новым каноническим именем. Например: {'images/old.png': 'images/new.png'}

- frag_map – Вызываемый объект, который принимает два аргумента (имя, привязка) и возвращает новую привязку. Это полезно, если вам нужно изменить привязки в файлах HTML. По умолчанию ничего не делает.

- replace_in_opf – Если false, ссылки не заменяются в файле OPF.

calibre.ebooks.oeb.polish.replace.rename_files(container, file_map)[исходный код]

Переименовывать файлы в контейнере, автоматически обновляя все ссылки на них.

Параметры:

file_map – Сопоставление старого канонического имени с новым каноническим именем, например: {'text/chapter1.html': 'chapter1.html'}.

calibre.ebooks.oeb.polish.replace.get_recommended_folders(container, names)[исходный код]

Вернуть папки, рекомендованные для данных имен файлов. Рекомендация основана на том, где в контейнере находится большинство файлов одного типа. Если файлы определенного типа отсутствуют, рекомендуемая папка считается папкой, содержащей файл OPF.

Хорошая печать и автоматическое исправление ошибок синтаксического анализа

calibre.ebooks.oeb.polish.pretty.fix_html(container, raw)[исходный код]

Исправить любые ошибки синтаксического анализа в HTML, представленном в виде строки в необработанном виде. Исправление производится с помощью алгоритма парсинга HTML5.

calibre.ebooks.oeb.polish.pretty.fix_all_html(container)[исходный код]

Исправить все ошибки синтаксического анализа во всех файлах HTML в контейнере. Исправление производится с помощью алгоритма парсинга HTML5.

calibre.ebooks.oeb.polish.pretty.pretty_html(container, name, raw)[исходный код]

Хорошая печать HTML в представленного виде строки в необработанном виде

calibre.ebooks.oeb.polish.pretty.pretty_css(container, name, raw)[исходный код]

Хорошая печать CSS, представленной в виде строки в необработанном виде

calibre.ebooks.oeb.polish.pretty.pretty_xml(container, name, raw)[исходный код]

Хорошая печать XML, представленного в виде строки в необработанном виде. Если name - это имя OPF, выполняется дополнительная обработка, специфичная для OPF.

calibre.ebooks.oeb.polish.pretty.pretty_all(container)[исходный код]

Хорошая печать всех файлов HTML/CSS/XML в контейнере

Управление обложками книг

calibre.ebooks.oeb.polish.jacket.remove_jacket(container)[исходный код]

Удалить существующую обложку, если таковая имеется. Возвращает False, если существующая обложка не найдена.

calibre.ebooks.oeb.polish.jacket.add_or_replace_jacket(container)[исходный код]

Либо создать новую обложку из метаданных книги, либо заменить существующую обложку. Возвращает True, если существующая обложка была заменена.

Разделение и слияние файлов

calibre.ebooks.oeb.polish.split.split(container, name, loc_or_xpath, before=True, totals=None)[исходный код]

Разделить файл, указанный по имени, в позиции, указанной параметром loc_or_xpath. При разделении автоматически переносятся все ссылки и ссылки на затронутые файлы.

Параметры:

- loc_or_xpath – Должно быть выражением XPath, например //h:div[@id=“split_here“]. Также может быть loc, который используется внутри для реализации разделения на панели предварительного просмотра.

- before – Если True, разделение происходит до идентифицированного элемента, иначе после него.

- totals – Используется внутри

calibre.ebooks.oeb.polish.split.multisplit(container, name, xpath, before=True)[исходный код]

Разделить указанный файл в нескольких местах (все теги, соответствующие указанному выражению XPath). См. также: split(). При разделении автоматически переносятся все ссылки и ссылки на затронутые файлы.

Параметры:

before – Если True, разбиение происходит до идентифицированного элемента, иначе после него.

calibre.ebooks.oeb.polish.split.merge(container, category, names, master)[исходный код]

Объединить указанные файлы в один файл, автоматически перенеся все ссылки и ссылки на затронутые файлы. Все файлы должны быть либо HTML, либо CSS.

Параметры:

- category – Должен быть либо text для файлов HTML, либо styles для файлов CSS.

- names – Список файлов для слияния

- master – Какой из объединенных файлов является главным файлом, то есть файлом, который останется после объединения.

Управление обложками

calibre.ebooks.oeb.polish.cover.set_cover(container, cover_path, report=None, options=None)[исходный код]

Установить обложку книги на изображение, на которое указывает cover_path

Параметры:

- cover_path – Либо абсолютный путь к файлу изображения, либо каноническое имя изображения в книге. При использовании изображения в книге вы также должны установить параметры, см. ниже.

- report – Необязательный вызываемый объект, который принимает единственный аргумент. Он будет вызываться с информацией об обрабатываемых задачах

- options – None или словарь, который контролирует установку обложки. В словаре могут быть записи: keep_aspect: True или False (сохранить соотношение сторон обложек в EPUB) no_svg: True или False (использовать обертку обложки SVG на титульной странице EPUB) existing: True или False (cover_path относится к существующему изображению в книге)

calibre.ebooks.oeb.polish.cover.mark_as_cover(container, name)[исходный код]

Отметить указанное изображение как изображение обложки.

calibre.ebooks.oeb.polish.cover.mark_as_titlepage(container, name, move_to_start=True)[исходный код]

Отметить указанный HTML-файл как титульную страницу EPUB.

Параметры:

move_to_start – Если True, HTML-файл перемещается в начало корешка.

Работа с CSS

calibre.ebooks.oeb.polish.fonts.change_font(container, old_name, new_name=None)[исходный код]

Изменить семейство шрифтов с old_name на new_name. Изменяет все вхождения семейства шрифтов в таблицах стилей, тегах стилей и атрибутах стиля. Если old_name относится к встроенному шрифту, он удаляется. Вы можете установить для new_name значение None, чтобы удалить семейство шрифтов вместо его изменения.

calibre.ebooks.oeb.polish.css.remove_unused_css(container, report=None, remove_unused_classes=False, merge_rules=False, merge_rules_with_identical_properties=False, remove_unreferenced_sheets=False)[исходный код]

Удалить из книги все неиспользуемые правила CSS. Неиспользуемое правило CSS - это правило, не соответствующее фактическому содержанию.

Параметры:

- report – Необязательный вызываемый объект, который принимает единственный аргумент. Вызывается с информацией о выполняемых операциях.

- remove_unused_classes – Если True, атрибуты класса в HTML, которые не соответствуют никаким правилам CSS, также удаляются.

- merge_rules – Если True, правила с идентичными селекторами объединяются.

- merge_rules_with_identical_properties – Если True, правила с одинаковыми свойствами объединяются.

- remove_unreferenced_sheets – Если True, таблицы стилей, на которые не ссылается какой-либо контент, удаляются.

calibre.ebooks.oeb.polish.css.filter_css(container, properties, names=())[исходный код]

Удалить указанные свойства CSS из всех правил CSS в книге.

Параметры:

- properties – Набор свойств для удаления. Например: {'font-family', 'color'}.

- names – Файлы, из которых нужно удалить свойства. По умолчанию используются все файлы HTML и CSS в книге.

Работа с оглавлением

calibre.ebooks.oeb.polish.toc.from_xpaths(container, xpaths, prefer_title=False)[исходный код]

Создать оглавление из списка выражений XPath. Каждое выражение в списке соответствует уровню создания ToC. Например: ['//h:h1','//h:h2','//h:h3'] сгенерирует трехуровневую таблицу содержания из <h1>, <h2> и <h3> тегов.

calibre.ebooks.oeb.polish.toc.from_links(container)[исходный код]

Создать оглавление из ссылок в книге.

calibre.ebooks.oeb.polish.toc.from_files(container)[исходный код]

Создать оглавление из файлов в книге.

calibre.ebooks.oeb.polish.toc.create_inline_toc(container, title=None)[исходный код]

Создать встроенное (HTML) оглавление из существующего оглавления NCX.

Параметры:

title – Заголовок этого оглавления.

Средство редактирования книги
class calibre.gui2.tweak_book.plugin.Tool[исходный код]

Базовые классы: object

Базовый класс для отдельных инструментов в плагине Edit Book. Среди полезных членов:

- self.plugin: ссылка на объект calibre.customize.Plugin, которому принадлежит этот инструмент.

- self. boss

- self. gui

Методы, которые необходимо переопределить в подклассах:

- create_action()

- register_shortcut()

name = None

Установить уникальное имя, которое будет использоваться как ключ

allowed_in_toolbar = True

Если True, пользователь может разместить этот инструмент на панели инструментов плагинов.

allowed_in_menu = True

Если True, пользователь может выбрать размещение этого инструмента в меню плагинов.

toolbar_button_popup_mode = 'delayed'

Всплывающий режим для меню (если есть) кнопки панели инструментов. Возможные значения: „delayed“, „instant“, „button“.

property boss

Объект calibre.gui2.tweak_book.boss.Boss. Используется для управления пользовательским интерфейсом.

property gui

Главное окно пользовательского интерфейса

property current_container

Вернуть текущий объект calibre.ebooks.oeb.polish.container.Container, представляющий редактируемую книгу.

register_shortcut(qaction, unique_name, default_keys=(), short_text=None, description=None, **extra_data)[исходный код]

Зарегистрируйте сочетание клавиш, которое будет запускать указанное действие qaction. Это сочетание клавиш станет автоматически настраиваемым пользователем в разделе «Сочетания клавиш» настроек редактора.

Параметры:

- qaction – Объект QAction, он будет срабатывать, когда пользователь нажимает настроенную комбинацию клавиш.

- unique_name – Уникальное имя для этого ярлыка/действия. Он будет использоваться для внутренних целей, он не должен использоваться другими действиями в этом плагине.

- default_keys – Список сочетаний клавиш по умолчанию. Если не указано иное, ярлыки по умолчанию не будут установлены. Если указанные здесь ярлыки конфликтуют со встроенными ярлыками или ярлыками из пользовательской конфигурации/других плагинов, они будут проигнорированы. В этом случае пользователям придется настраивать ярлыки вручную в настройках. Например: default_keys=('Ctrl+J', 'F9').

- short_text – Необязательное краткое описание этого действия. Если не указано иное, будет использован текст из QAction.

- description – Необязательное более подробное описание этого действия, оно будет использовано в записи настроек для этого ярлыка.

create_action(for_toolbar=True)[исходный код]

Создать QAction, который будет добавлен либо на панель инструментов плагинов, либо в меню плагинов в зависимости от for_toolbar. Например:

def create_action(self, for_toolbar=True):
ac = QAction(get_icons('myicon.png'), 'Do something')
if for_toolbar:
# We want the toolbar button to have a popup menu
menu = QMenu()
ac.setMenu(menu)
menu.addAction('Do something else')
subaction = menu.addAction('And another')

# Register a keyboard shortcut for this toolbar action be
# careful to do this for only one of the toolbar action or
# the menu action, not both.
self.register_shortcut(ac, 'some-unique-name', default_keys=('Ctrl+K',))
return ac

См. также

Метод register_shortcut().

Управление пользовательским интерфейсом редактора

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

class calibre.gui2.tweak_book.boss.Boss(parent, notify=None)[исходный код]

add_savepoint(msg)[исходный код]

Создать контрольную точку восстановления с именем, указываемым как msg

apply_container_update_to_gui(mark_as_modified=True)[исходный код]

Обновить все компоненты пользовательского интерфейса, чтобы отразить последние данные в текущем контейнере книги.

Параметры:

mark_as_modified – Если True, книга будет помечена как изменённая, поэтому пользователю будет предложено сохранить её при выходе.

close_editor(name)[исходный код]

Закрыть редактор, который редактирует файл, указанный в name.

commit_all_editors_to_container()[исходный код]

Зафиксировать любые изменения, внесённые пользователем в файлы, открытые в редакторах, в контейнер. Вы должны вызвать этот метод перед выполнением каких-либо действий с текущим контейнером.

property currently_editing

Вернуть имя редактируемого файла или None, если файл не редактируется.

edit_file(name, syntax=None, use_template=None)[исходный код]

Открыть файл, указанный по имени, в редакторе

Параметры:

- syntax – Медиа-тип файла, например, text/html. Если не указано, это определяется по расширению файла.

- use_template – Шаблон для инициализации открытого редактора с помощью

open_book(path=None, edit_file=None, clear_notify_data=True, open_folder=False, search_text=None)[исходный код]

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

Параметры:

edit_file – Имя файла во вновь открытой книге, чтобы начать редактирование. Также может быть список имен.

rewind_savepoint()[исходный код]

Отменить предыдущее создание контрольной точки восстановления, полезно, если вы создаете контрольную точку, а затем прерываете операцию без изменений

save_book()[исходный код]

Сохранить книгу. Сохранение происходит в фоновом режиме

set_modified()[исходный код]

Отметить книгу как изменённую

show_current_diff(allow_revert=True, to_container=None)[исходный код]

Показать изменения в книге по сравнению с последней контрольной точкой

Параметры:

- allow_revert – Если True, в диалоговом окне сравнения будет кнопка, позволяющая пользователю отменить все изменения.

- to_container – Объект контейнера, с которым сравнивается текущий контейнер. Если None, используется контейнер с ранее установленной контрольной точкой.

show_editor(name)[исходный код]

Показать редактор, редактирующий файл, указанный в name

sync_preview_to_editor()[исходный код]

Синхронизировать положение панели предварительного просмотра с текущей позицией курсора в текущем редакторе

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- вперёд |
- назад |
- Старт »

- Технические средства защиты авторских прав (DRM — digital rights management)

Технические средства защиты авторских прав (DRM — digital rights management)

Управление цифровыми правами (DRM) - это общий термин для технологий контроля доступа, который может использоваться производителями оборудования, издателями, владельцами авторских прав и частными лицами, чтобы попытаться наложить ограничения на использование цифрового контента и устройств. Это также, иногда, пренебрежительно описывается как Управление цифровыми ограничениями. Этот термин используется для описания любой технологии, которая запрещает использование (законного или иного) цифрового контента, который не был желателен или предвиден поставщиком контента. Термин обычно не относится к другим формам защиты от копирования, которые можно обойти без изменения файла или устройства, например, серийные номера или файлы ключей. Это может также относиться к ограничениям, связанным с конкретными экземплярами цифровых произведений или устройств. Технологии DRM пытаются контролировать использование цифровых медиа, предотвращая доступ, копирование или преобразование в другие форматы конечными пользователями. Смотрите wikipedia <https://en.wikipedia.org/wiki/Digital_rights_management> _.

Что несёт DRM лично для меня?

Когда вы покупаете электронную книгу с помощью DRM, вы на самом деле не владеете ею, но приобрели разрешение на её использование в соответствии с требованиями продавца. DRM ограничивает то, что вы можете делать с электронными книгами, которые вы «купили». Часто люди, которые покупают книги с DRM, не знают о степени этих ограничений. Эти ограничения не позволяют вам переформатировать электронную книгу по своему вкусу, в том числе вносить стилистические изменения, такие как корректировка размеров шрифта, хотя есть программное обеспечение, которое позволяет вам делать такие вещи для не DRM книг. Люди часто удивляются, что электронная книга, которую они купили в определенном формате, не может быть преобразована в другой формат, если электронная книга имеет DRM. Поэтому, если у вас есть Amazon Kindle и вы купили книгу, проданную Barnes and Nobles, вы должны знать, что если эта электронная книга имеет DRM, вы не сможете прочитать её на своем Kindle. Обратите внимание, что я говорю о книге, которую вы покупаете, не украсть или спиратить, а купить.

Что DRM даёт авторам?

Издатели электронных книг с DRM утверждают, что DRM - это все ради авторов и для защиты их художественной целостности и предотвращения пиратства. Но DRM НЕ предотвращает пиратство. Люди, которые хотят пиратского контента или используют пиратский контент, все ещё делают это и добиваются успеха. В настоящее время Amazon, Adobe, Barnes и Noble управляют тремя основными схемами DRM для электронных книг, и все три схемы DRM были взломаны. Все, что делает DRM - это неудобство для законных пользователей. Можно утверждать, что это на самом деле вредит авторам, поскольку люди, которые купили бы книгу, решили найти пиратскую версию, поскольку они не готовы мириться с DRM. Те, кто будет пиратствовать в отсутствие DRM, делают это и в его присутствии. Повторюсь, ключевой момент заключается в том, что DRM не предотвращает пиратство. Таким образом, DRM не только бессмысленен и вреден для покупателей электронных книг, но и по сути бессмысленная трата денег.

DRM и свобода

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

Почему calibre не поддерживает DRM?

calibre является программным обеспечением с открытым исходным кодом, в то время как DRM по своей природе закрыт. Если бы calibre поддерживал открытие или просмотр файлов DRM, его можно было бы тривиально изменить, чтобы использовать в качестве инструмента для удаления DRM, а это сейчас незаконно. Программное обеспечение с открытым исходным кодом и DRM - это столкновение принципов. В то время как DRM - это всё, что касается управления пользователем, программное обеспечение с открытым исходным кодом - это расширение возможностей пользователя. Они просто не могут сосуществовать.

Каково мнение calibre о поставщиках информации?

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

Как я могу помочь бороться с DRM?

Как человек, который читает и покупает электронные книги, вы можете помочь бороться с DRM. Не покупайте электронные книги с DRM. Отдельные издатели публикуют электронные книги и без DRM. Поищите у них на сайте желаемую электронную книгу. Если вам нравятся книги определённых независимых авторов продающих электронные книги без DRM сделайте им пожертвования (если вы можете себе это позволить). Это деньги будут потрачены не зря, поскольку их электронные книги, как правило, дешевле (но могут быть исключения), приобретаемых у издателей с DRM, и, вероятно, будут работать на всех ваших устройствах в будущем, что позволит вам снова сэкономить на покупке электронных книг. Не препятствуйте своим пиратством издателям и авторам электронных книг без DRM. Контент-провайдеры заслуживают компенсации за свои усилия. Не наказывайте их за попытки сделать доступнее электронные книги без DRM. В конечном итоге это вредит и вам. Если вы купили DRM-отягощённые книги у книготорговцев и не знали, что они содержат DRM, то оставляйте комментарий или отзыв на веб-сайте, информируя будущих покупателей о наличии DRM. Многие книготорговцы не считают важным дать чёткое указание своим покупателям на то, содержит ли электронная книга DRM или нет. Здесь <https://www.defectivebydesign.org/guide/ebooks> вы найдете руководство по жизни без DRM.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- назад |
- Старт »
- Глоссарий

Глоссарий

RSS

RSS (Really Simple Syndication) - формат веб-фида, используемый для публикации часто обновляемого контента, например новостей, сообщений в блогах и т.д. Это формат, который особенно подходит для чтения компьютерами и поэтому он - предпочтительный способ получения контента из Интернета для электронной книги. В Интернете есть много других форматов фидов, и большинство из них понимается в calibre. В частности, имеется хорошая поддержка формата * ATOM *, который обычно используется для блогов.

рецепт

Рецепт - это набор инструкций, в которых рассказывается о том, как преобразовать онлайн-источник новостей, например журнал или блог, в электронную книгу. Рецепт - это по существу код Python [https://www.python.org]. Таким образом, он способен преобразовывать произвольно сложные источники новостей в электронные книги. На самом простом уровне это всего лишь набор переменных, таких как URL-адреса, которые дают достаточную информацию для выхода в Интернет и загрузки новостей.

HTML

HTML (язык разметки гипертекста), поднабор стандартного обобщенного языка разметки (SGML) для электронной публикации, является стандартом, используемым для Всемирной паутины.

CSS

CSS (Cascading Style Sheets) — это язык, описывающий, как должен отображаться документ HTML (визуальный стиль).

API

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

LRF

LRF — формат электронных книг в читалках SONY.

URL

URL (Uniform Resource Locator), например: http://example.com

regexp

Регулярные выражения предоставляют ёмкие и гибкие возможности по распознаванию интересующих строк текста, например, конкретных символов, слов или наборов символов. Смотрите the tutorial для знакомства с регулярными выражениями.

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Содержание модулей Python

Содержание модулей Python
c

| c |
| calibre |
| calibre.customize | Defines various abstract base classes that can be subclassed to create plugins.
| calibre.customize.conversion |
| calibre.db.cache | The API accessing and manipulating a calibre library.
| calibre.devices.interface |
| calibre.ebooks.metadata.book.base |
| calibre.ebooks.metadata.sources.base |
| calibre.ebooks.oeb.polish.container | The container object used to represent a book as a collection of its constituent HTML files.
| calibre.ebooks.oeb.polish.cover |
| calibre.ebooks.oeb.polish.css |
| calibre.ebooks.oeb.polish.jacket |
| calibre.ebooks.oeb.polish.pretty |
| calibre.ebooks.oeb.polish.replace |
| calibre.ebooks.oeb.polish.split |
| calibre.ebooks.oeb.polish.toc |
| calibre.gui2.tweak_book.boss |
| calibre.utils.formatter_functions |
| calibre.web.feeds.news | The API for writing recipes is defined by the :class:`BasicNewsRecipe`

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Обзор: исходный код модуля

Все модули, в которых есть код

- PyQt6.QtCore
- PyQt6.QtWidgets
- calibre.customize

- calibre.customize.conversion

- calibre.db.cache
- calibre.devices.interface
- calibre.devices.usbms.cli
- calibre.devices.usbms.device
- calibre.devices.usbms.driver
- calibre.ebooks.metadata.book.base
- calibre.ebooks.metadata.sources.base
- calibre.ebooks.oeb.polish.container
- calibre.ebooks.oeb.polish.cover
- calibre.ebooks.oeb.polish.css
- calibre.ebooks.oeb.polish.fonts
- calibre.ebooks.oeb.polish.jacket
- calibre.ebooks.oeb.polish.pretty
- calibre.ebooks.oeb.polish.replace
- calibre.ebooks.oeb.polish.split
- calibre.ebooks.oeb.polish.toc
- calibre.gui2.actions
- calibre.gui2.preferences
- calibre.gui2.tweak_book.boss
- calibre.gui2.tweak_book.plugin
- calibre.utils.formatter_functions
- calibre.web.feeds.news

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.customize

Исходный код calibre.customize

__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'

import enum import importlib import os import sys import zipfile from calibre.constants import ismacos, iswindows, numeric_version from calibre.ptempfile import PersistentTemporaryFile if iswindows:
platform = 'windows'
elif ismacos:
platform = 'osx'
else:
platform = 'linux'

class PluginNotFound(ValueError):
pass class InvalidPlugin(ValueError):
pass class PluginInstallationType(enum.IntEnum):
EXTERNAL = 1
SYSTEM = 2
BUILTIN = 3

[документация]
class Plugin: # {{{
'''
A calibre plugin. Useful members include:

* ``self.installation_type``: Stores how the plugin was installed.
* ``self.plugin_path``: Stores path to the ZIP file that contains this plugin or None if it is a builtin plugin
* ``self.site_customization``: Stores a customization string entered by the user.

Methods that should be overridden in sub classes:

* :meth:`initialize`
* :meth:`customization_help`

Useful methods:

* :meth:`temporary_file`
* :meth:`__enter__`
* :meth:`load_resources`

'''
#: List of platforms this plugin works on.
#: For example: ``['windows', 'osx', 'linux']`` supported_platforms = []

#: The name of this plugin. You must set it something other
#: than Trivial Plugin for it to work.
name = 'Trivial Plugin'

#: The version of this plugin as a 3-tuple (major, minor, revision)
version = (1, 0, 0)

#: A short string describing what this plugin does description = _('Does absolutely nothing')

#: The author of this plugin author = _('Unknown')

#: When more than one plugin exists for a filetype,
#: the plugins are run in order of decreasing priority.
#: Plugins with higher priority will be run first.
#: The highest possible priority is ``sys.maxsize``.
#: Default priority is 1.
priority = 1

#: The earliest version of calibre this plugin requires minimum_calibre_version = (0, 4, 118)

#: The way this plugin is installed installation_type = None

#: If False, the user will not be able to disable this plugin. Use with
#: care.
can_be_disabled = True

#: The type of this plugin. Used for categorizing plugins in the
#: GUI type = _('Base')

def __init__(self, plugin_path):
self.plugin_path = plugin_path self.site_customization = None

[документация]
def initialize(self):
'''
Called once when calibre plugins are initialized. Plugins are re-initialized every time a new plugin is added. Also note that if the plugin is run in a worker process, such as for adding books, then the plugin will be initialized for every new worker process.

Perform any plugin specific initialization here, such as extracting resources from the plugin ZIP file. The path to the ZIP file is available as ``self.plugin_path``.

Note that ``self.site_customization`` is **not** available at this point.
'''
pass

[документация]
def config_widget(self):
'''
Implement this method and :meth:`save_settings` in your plugin to use a custom configuration dialog, rather then relying on the simple string based default customization.

This method, if implemented, must return a QWidget. The widget can have an optional method validate() that takes no arguments and is called immediately after the user clicks OK. Changes are applied if and only if the method returns True.

If for some reason you cannot perform the configuration at this time, return a tuple of two strings (message, details), these will be displayed as a warning dialog to the user and the process will be aborted.
'''
raise NotImplementedError()

[документация]
def save_settings(self, config_widget):
'''
Save the settings specified by the user with config_widget.

:param config_widget: The widget returned by :meth:`config_widget`.

'''
raise NotImplementedError()

[документация]
def do_user_config(self, parent=None):
'''
This method shows a configuration dialog for this plugin. It returns
True if the user clicks OK, False otherwise. The changes are automatically applied.
'''
from qt.core import QApplication, QDialog, QDialogButtonBox, QLabel, QLineEdit, QScrollArea, QSize, Qt, QVBoxLayout from calibre.gui2 import gprefs class ConfigDialog(QDialog):

def __init__(self, parent, config_widget):
super().__init__(parent)
self.config_widget = config_widget def accept(self):
if ((validate := getattr(self.config_widget, 'validate', None)) and getattr(self.config_widget, 'validate_before_accept', False)):
if not validate():
return super().accept()

try:
config_widget = self.config_widget()
except NotImplementedError:
config_widget = None prefname = 'plugin config dialog:'+self.type + ':' + self.name config_dialog = ConfigDialog(parent, config_widget)
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
v = QVBoxLayout(config_dialog)
button_box.accepted.connect(config_dialog.accept)
button_box.rejected.connect(config_dialog.reject)
config_dialog.setWindowTitle(_('Customize') + ' ' + self.name)

if isinstance(config_widget, tuple):
from calibre.gui2 import warning_dialog warning_dialog(parent, _('Cannot configure'), config_widget[0], det_msg=config_widget[1], show=True)
return False if config_widget is not None:
class SA(QScrollArea):
def sizeHint(self):
sz = self.widget().sizeHint()
fw = 2 * self.frameWidth()
return QSize(sz.width() + self.verticalScrollBar().sizeHint().width() + fw, sz.height() + fw)
sa = SA(config_dialog)
sa.setWidget(config_widget)
sa.setWidgetResizable(True)
v.addWidget(sa)
v.addWidget(button_box)
if not config_dialog.restore_geometry(gprefs, prefname):
QApplication.instance().ensure_window_on_screen(config_dialog)
config_dialog.exec()

if config_dialog.result() == QDialog.DialogCode.Accepted:
if hasattr(config_widget, 'validate'):
if config_widget.validate():
self.save_settings(config_widget)
else:
self.save_settings(config_widget)
else:
from calibre.customize.ui import customize_plugin, plugin_customization help_text = self.customization_help(gui=True)
help_text = QLabel(help_text, config_dialog)
help_text.setWordWrap(True)
help_text.setTextInteractionFlags(Qt.TextInteractionFlag.LinksAccessibleByMouse | Qt.TextInteractionFlag.LinksAccessibleByKeyboard)
help_text.setOpenExternalLinks(True)
v.addWidget(help_text)
sc = plugin_customization(self)
if not sc:
sc = ''
sc = sc.strip()
sc = QLineEdit(sc, config_dialog)
v.addWidget(sc)
v.addWidget(button_box)
config_dialog.restore_geometry(gprefs, prefname)
config_dialog.exec()

if config_dialog.result() == QDialog.DialogCode.Accepted:
sc = str(sc.text()).strip()
customize_plugin(self, sc)

config_dialog.save_geometry(gprefs, prefname)
return config_dialog.result()

[документация]
def load_resources(self, names):
'''
If this plugin comes in a ZIP file (user added plugin), this method will allow you to load resources from the ZIP file.

For example to load an image::

pixmap = QPixmap()
pixmap.loadFromData(self.load_resources(['images/icon.png'])['images/icon.png'])
icon = QIcon(pixmap)

:param names: List of paths to resources in the ZIP file using / as separator

:return: A dictionary of the form ``{name: file_contents}``. Any names that were not found in the ZIP file will not be present in the dictionary.

'''
if self.plugin_path is None:
raise ValueError('This plugin was not loaded from a ZIP file')
ans = {}
with zipfile.ZipFile(self.plugin_path, 'r') as zf:
for candidate in zf.namelist():
if candidate in names:
ans[candidate] = zf.read(candidate)
return ans

[документация]
def customization_help(self, gui=False):
'''
Return a string giving help on how to customize this plugin.
By default raise a :class:`NotImplementedError`, which indicates that the plugin does not require customization.

If you re-implement this method in your subclass, the user will be asked to enter a string as customization for this plugin.
The customization string will be available as
``self.site_customization``.

Site customization could be anything, for example, the path to a needed binary on the user's computer.

:param gui: If True return HTML help, otherwise return plain text help.

'''
raise NotImplementedError()

[документация]
def temporary_file(self, suffix):
'''
Return a file-like object that is a temporary file on the file system.
This file will remain available even after being closed and will only be removed on interpreter shutdown. Use the ``name`` member of the returned object to access the full path to the created temporary file.

:param suffix: The suffix that the temporary file will have.
'''
return PersistentTemporaryFile(suffix)

def is_customizable(self):
try:
self.customization_help()
return True except NotImplementedError:
return False def __enter__(self, *args):
'''
Add this plugin to the python path so that it's contents become directly importable.
Useful when bundling large python libraries into the plugin. Use it like this::
with plugin:
import something
'''
if self.plugin_path is not None:
from importlib.machinery import EXTENSION_SUFFIXES from calibre.utils.zipfile import ZipFile with ZipFile(self.plugin_path) as zf:
extensions = {x.lower() for x in EXTENSION_SUFFIXES}
zip_safe = True for name in zf.namelist():
for q in extensions:
if name.endswith(q):
zip_safe = False break if not zip_safe:
break if zip_safe:
sys.path.append(self.plugin_path)
self.sys_insertion_path = self.plugin_path else:
from calibre.ptempfile import TemporaryDirectory self._sys_insertion_tdir = TemporaryDirectory('plugin_unzip')
self.sys_insertion_path = self._sys_insertion_tdir.__enter__(*args)
zf.extractall(self.sys_insertion_path)
sys.path.append(self.sys_insertion_path)

def __exit__(self, *args):
ip, it = getattr(self, 'sys_insertion_path', None), getattr(self,
'_sys_insertion_tdir', None)
if ip in sys.path:
sys.path.remove(ip)
if hasattr(it, '__exit__'):
it.__exit__(*args)

[документация]
def cli_main(self, args):
'''
This method is the main entry point for your plugins command line interface. It is called when the user does: calibre-debug -r "Plugin
Name". Any arguments passed are present in the args variable.
'''
raise NotImplementedError(f'The {self.name} plugin has no command line interface')

# }}}

[документация]
class FileTypePlugin(Plugin): # {{{
'''
A plugin that is associated with a particular set of file types.
'''

#: Set of file types for which this plugin should be run.
#: Use '*' for all file types.
#: For example: ``{'lit', 'mobi', 'prc'}`` file_types = set()

#: If True, this plugin is run when books are added
#: to the database on_import = False

#: If True, this plugin is run after books are added
#: to the database. In this case the postimport and postadd
#: methods of the plugin are called.
on_postimport = False

#: If True, this plugin is run after a book is converted.
#: In this case the postconvert method of the plugin is called.
on_postconvert = False

#: If True, this plugin is run after a book file is deleted
#: from the database. In this case the postdelete method of
#: the plugin is called.
on_postdelete = False

#: If True, this plugin is run just before a conversion on_preprocess = False

#: If True, this plugin is run after conversion
#: on the final file produced by the conversion output plugin.
on_postprocess = False type = _('File type')

[документация]
def run(self, path_to_ebook):
'''
Run the plugin. Must be implemented in subclasses.
It should perform whatever modifications are required on the e-book and return the absolute path to the modified e-book. If no modifications are needed, it should return the path to the original e-book. If an error is encountered it should raise an Exception. The default implementation simply return the path to the original e-book. Note that the path to the original file (before any file type plugins are run, is available as self.original_path_to_file).

The modified e-book file should be created with the
:meth:`temporary_file` method.

:param path_to_ebook: Absolute path to the e-book.

:return: Absolute path to the modified e-book.
'''
# Default implementation does nothing return path_to_ebook

[документация]
def postimport(self, book_id, book_format, db):
'''
Called post import, i.e., after the book file has been added to the database. Note that this is different from :meth:`postadd` which is called when the book record is created for the first time. This method is called whenever a new file is added to a book record. It is useful for modifying the book record based on the contents of the newly added file.

:param book_id: Database id of the added book.
:param book_format: The file type of the book that was added.
:param db: Library database.
'''
pass # Default implementation does nothing

[документация]
def postconvert(self, book_id, book_format, db):
'''
Called post conversion, i.e., after the conversion output book file has been added to the database.
Note that it is run after a conversion only, not after a book is added. It is useful for modifying the book record based on the contents of the newly added file.

:param book_id: Database id of the added book.
:param book_format: The file type of the book that was added.
:param db: Library database.
'''
pass # Default implementation does nothing

[документация]
def postdelete(self, book_id, book_format, db):
'''
Called post deletion, i.e., after the book file has been deleted from the database. Note that it is not run when a book record is deleted, only when one or more formats from the book are deleted. It is useful for modifying the book record based on the format of the deleted file.

:param book_id: Database id of the added book.
:param book_format: The file type of the book that was added.
:param db: Library database.
'''
pass # Default implementation does nothing

[документация]
def postadd(self, book_id, fmt_map, db):
'''
Called post add, i.e. after a book has been added to the db. Note that this is different from :meth:`postimport`, which is called after a single book file has been added to a book. postadd() is called only when an entire book record with possibly more than one book file has been created for the first time.
This is useful if you wish to modify the book record in the database when the book is first added to calibre.

:param book_id: Database id of the added book.
:param fmt_map: Map of file format to path from which the file format was added. Note that this might or might not point to an actual existing file, as sometimes files are added as streams. In which case it might be a dummy value or a non-existent path.
:param db: Library database
'''
pass # Default implementation does nothing

# }}}

[документация]
class MetadataReaderPlugin(Plugin): # {{{
'''
A plugin that implements reading metadata from a set of file types.
'''
#: Set of file types for which this plugin should be run.
#: For example: ``set(['lit', 'mobi', 'prc'])`` file_types = set()

supported_platforms = ['windows', 'osx', 'linux']
version = numeric_version author = 'Kovid Goyal'

type = _('Metadata reader')

def __init__(self, *args, **kwargs):
Plugin.__init__(self, *args, **kwargs)
self.quick = False

[документация]
def get_metadata(self, stream, type):
'''
Return metadata for the file represented by stream (a file like object that supports reading). Raise an exception when there is an error with the input data.

:param type: The type of file. Guaranteed to be one of the entries in :attr:`file_types`.
:return: A :class:`calibre.ebooks.metadata.book.Metadata` object
'''
return

# }}}

[документация]
class MetadataWriterPlugin(Plugin): # {{{
'''
A plugin that implements reading metadata from a set of file types.
'''
#: Set of file types for which this plugin should be run.
#: For example: ``set(['lit', 'mobi', 'prc'])`` file_types = set()

supported_platforms = ['windows', 'osx', 'linux']
version = numeric_version author = 'Kovid Goyal'

type = _('Metadata writer')

def __init__(self, *args, **kwargs):
Plugin.__init__(self, *args, **kwargs)
self.apply_null = False

[документация]
def set_metadata(self, stream, mi, type):
'''
Set metadata for the file represented by stream (a file like object that supports reading). Raise an exception when there is an error with the input data.

:param type: The type of file. Guaranteed to be one of the entries in :attr:`file_types`.
:param mi: A :class:`calibre.ebooks.metadata.book.Metadata` object
'''
pass

# }}}

[документация]
class CatalogPlugin(Plugin): # {{{
'''
A plugin that implements a catalog generator.
'''

resources_path = None

#: Output file type for which this plugin should be run.
#: For example: 'epub' or 'xml'
file_types = set()

type = _('Catalog generator')

#: CLI parser options specific to this plugin, declared as `namedtuple` `Option`:
#:
#: from collections import namedtuple
#: Option = namedtuple('Option', 'option, default, dest, help')
#: cli_options = [Option('--catalog-title', default = 'My Catalog',
#: dest = 'catalog_title', help = (_('Title of generated catalog. \nDefault:') + " '" + '%default' + "'"))]
#: cli_options parsed in calibre.db.cli.cmd_catalog:option_parser()
#:
cli_options = []

def _field_sorter(self, key):
'''
Custom fields sort after standard fields
'''
if key.startswith('#'):
return f'~{key[1:]}'
else:
return key def search_sort_db(self, db, opts):

db.search(opts.search_text)

if getattr(opts, 'sort_by', None):
# 2nd arg = ascending db.sort(opts.sort_by, True)
return db.get_data_as_dict(ids=opts.ids)

def get_output_fields(self, db, opts):
# Return a list of requested fields all_std_fields = {'author_sort','authors','comments','cover','formats',
'id','isbn','library_name','ondevice','pubdate','publisher',
'rating','series_index','series','size','tags','timestamp',
'title_sort','title','uuid','languages','identifiers'}
all_custom_fields = set(db.custom_field_keys())
for field in list(all_custom_fields):
fm = db.field_metadata[field]
if fm['datatype'] == 'series':
all_custom_fields.add(field+'_index')
all_fields = all_std_fields.union(all_custom_fields)

if getattr(opts, 'fields', 'all') != 'all':
# Make a list from opts.fields of = [x.strip() for x in opts.fields.split(',')]
requested_fields = set(of)

# Validate requested_fields if requested_fields - all_fields:
from calibre.library import current_library_name invalid_fields = sorted(requested_fields - all_fields)
print('invalid --fields specified: {}'.format(', '.join(invalid_fields)))
print("available fields in '{}': {}".format(current_library_name(), ', '.join(sorted(all_fields))))
raise ValueError('unable to generate catalog with specified fields')

fields = [x for x in of if x in all_fields]
else:
fields = sorted(all_fields, key=self._field_sorter)

if not opts.connected_device['is_device_connected'] and 'ondevice' in fields:
fields.pop(int(fields.index('ondevice')))

return fields

[документация]
def initialize(self):
'''
If plugin is not a built-in, copy the plugin's .ui and .py files from the ZIP file to $TMPDIR.
Tab will be dynamically generated and added to the Catalog Options dialog in calibre.gui2.dialogs.catalog.py:Catalog
'''
from calibre.customize.builtins import plugins as builtin_plugins from calibre.customize.ui import config from calibre.ptempfile import PersistentTemporaryDirectory if type(self) not in builtin_plugins and self.name not in config['disabled_plugins']:
files_to_copy = [f'{self.name.lower()}.{ext}' for ext in ['ui','py']]
resources = zipfile.ZipFile(self.plugin_path,'r')

if self.resources_path is None:
self.resources_path = PersistentTemporaryDirectory('_plugin_resources', prefix='')

for file in files_to_copy:
try:
resources.extract(file, self.resources_path)
except Exception:
print(f' customize:__init__.initialize(): {file} not found in {os.path.basename(self.plugin_path)}')
continue resources.close()

[документация]
def run(self, path_to_output, opts, db, ids, notification=None):
'''
Run the plugin. Must be implemented in subclasses.
It should generate the catalog in the format specified in file_types, returning the absolute path to the generated catalog file. If an error is encountered it should raise an Exception.

The generated catalog file should be created with the
:meth:`temporary_file` method.

:param path_to_output: Absolute path to the generated catalog file.
:param opts: A dictionary of keyword arguments
:param db: A LibraryDatabase2 object
'''
# Default implementation does nothing raise NotImplementedError('CatalogPlugin.generate_catalog() default '
'method, should be overridden in subclass')

# }}}

[документация]
class InterfaceActionBase(Plugin): # {{{ supported_platforms = ['windows', 'osx', 'linux']
author = 'Kovid Goyal'
type = _('User interface action')
can_be_disabled = False actual_plugin = None def __init__(self, *args, **kwargs):
Plugin.__init__(self, *args, **kwargs)
self.actual_plugin_ = None

[документация]
def load_actual_plugin(self, gui):
'''
This method must return the actual interface action plugin object.
'''
ac = self.actual_plugin_ if ac is None:
mod, cls = self.actual_plugin.split(':')
ac = getattr(importlib.import_module(mod), cls)(gui, self.site_customization)
self.actual_plugin_ = ac return ac

# }}}

[документация]
class PreferencesPlugin(Plugin): # {{{

'''
A plugin representing a widget displayed in the Preferences dialog.

This plugin has only one important method :meth:`create_widget`. The various fields of the plugin control how it is categorized in the UI.
'''

supported_platforms = ['windows', 'osx', 'linux']
author = 'Kovid Goyal'
type = _('Preferences')
can_be_disabled = False

#: Import path to module that contains a class named ConfigWidget
#: which implements the ConfigWidgetInterface. Used by
#: :meth:`create_widget`.
config_widget = None

#: Where in the list of categories the :attr:`category` of this plugin should be.
category_order = 100

#: Where in the list of names in a category, the :attr:`gui_name` of this
#: plugin should be name_order = 100

#: The category this plugin should be in category = None

#: The category name displayed to the user for this plugin gui_category = None

#: The name displayed to the user for this plugin gui_name = None

#: The icon for this plugin, should be an absolute path icon = None

#: The description used for tooltips and the like description = None

[документация]
def create_widget(self, parent=None):
'''
Create and return the actual Qt widget used for setting this group of preferences. The widget must implement the
:class:`calibre.gui2.preferences.ConfigWidgetInterface`.

The default implementation uses :attr:`config_widget` to instantiate the widget.
'''
base, _, wc = self.config_widget.partition(':')
if not wc:
wc = 'ConfigWidget'
base = importlib.import_module(base)
widget = getattr(base, wc)
return widget(parent)

# }}}

class StoreBase(Plugin): # {{{ supported_platforms = ['windows', 'osx', 'linux']
author = 'John Schember'
type = _('Store')
# Information about the store. Should be in the primary language
# of the store. This should not be translatable when set by
# a subclass.
description = _('An e-book store.')
minimum_calibre_version = (0, 8, 0)
version = (1, 0, 1)

actual_plugin = None

# Does the store only distribute e-books without DRM.
drm_free_only = False
# This is the 2 letter country code for the corporate
# headquarters of the store.
headquarters = ''
# All formats the store distributes e-books in.
formats = []
# Is this store on an affiliate program?
affiliate = False def load_actual_plugin(self, gui):
'''
This method must return the actual interface action plugin object.
'''
mod, cls = self.actual_plugin.split(':')
self.actual_plugin_object = getattr(importlib.import_module(mod), cls)(gui, self.name)
return self.actual_plugin_object def customization_help(self, gui=False):
if getattr(self, 'actual_plugin_object', None) is not None:
return self.actual_plugin_object.customization_help(gui)
raise NotImplementedError()

def config_widget(self):
if getattr(self, 'actual_plugin_object', None) is not None:
return self.actual_plugin_object.config_widget()
raise NotImplementedError()

def save_settings(self, config_widget):
if getattr(self, 'actual_plugin_object', None) is not None:
return self.actual_plugin_object.save_settings(config_widget)
raise NotImplementedError()

# }}}

class EditBookToolPlugin(Plugin): # {{{ type = _('Edit book tool')
minimum_calibre_version = (1, 46, 0)

# }}}

class LibraryClosedPlugin(Plugin): # {{{
'''
LibraryClosedPlugins are run when a library is closed, either at shutdown, when the library is changed, or when a library is used in some other way.
At the moment these plugins won't be called by the CLI functions.
'''
type = _('Library closed')

# minimum version 2.54 because that is when support was added minimum_calibre_version = (2, 54, 0)

def run(self, db):
'''
The db will be a reference to the new_api (db.cache.py).

The plugin must run to completion. It must not use the GUI, threads, or any signals.
'''
raise NotImplementedError('LibraryClosedPlugin '
'run method must be overridden in subclass')
# }}}

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.customize »
- calibre.customize.conversion

Исходный код calibre.customize.conversion

'''
Defines the plugin system for conversions.
'''
import numbers import os import re import shutil from calibre import CurrentDir from calibre.customize import Plugin class ConversionOption:
'''
Class representing conversion options
'''

def __init__(self, name=None, help=None, long_switch=None, short_switch=None, choices=None):
self.name = name self.help = help self.long_switch = long_switch self.short_switch = short_switch self.choices = choices if self.long_switch is None:
self.long_switch = self.name.replace('_', '-')

self.validate_parameters()

def validate_parameters(self):
'''
Validate the parameters passed to :meth:`__init__`.
'''
if re.match(r'[a-zA-Z_]([a-zA-Z0-9_])*', self.name) is None:
raise ValueError(self.name + ' is not a valid Python identifier')
if not self.help:
raise ValueError('You must set the help text')

def __hash__(self):
return hash(self.name)

def __eq__(self, other):
return self.name == getattr(other, 'name', other)

def clone(self):
return ConversionOption(name=self.name, help=self.help, long_switch=self.long_switch, short_switch=self.short_switch, choices=self.choices)

class OptionRecommendation:
LOW = 1
MED = 2
HIGH = 3 def __init__(self, recommended_value=None, level=LOW, **kwargs):
'''
An option recommendation. That is, an option as well as its recommended value and the level of the recommendation.
'''
self.level = level self.recommended_value = recommended_value self.option = kwargs.pop('option', None)
if self.option is None:
self.option = ConversionOption(**kwargs)

self.validate_parameters()

@property def help(self):
return self.option.help def clone(self):
return OptionRecommendation(recommended_value=self.recommended_value, level=self.level, option=self.option.clone())

def validate_parameters(self):
if self.option.choices and self.recommended_value not in \ self.option.choices:
raise ValueError(f'OpRec: {self.option.name}: Recommended value not in choices')
if not (isinstance(self.recommended_value, (numbers.Number, bytes, str)) or self.recommended_value is None):
raise ValueError(f'OpRec: {self.option.name}:' + repr( self.recommended_value) + ' is not a string or a number')

class DummyReporter:

def __init__(self):
self.cancel_requested = False def __call__(self, percent, msg=''):
pass def gui_configuration_widget(name, parent, get_option_by_name, get_option_help, db, book_id, for_output=True):
import importlib def widget_factory(cls):
return cls(parent, get_option_by_name, get_option_help, db, book_id)

if for_output:
try:
output_widget = importlib.import_module(
'calibre.gui2.convert.'+name)
pw = output_widget.PluginWidget pw.ICON = 'back.png'
pw.HELP = _('Options specific to the output format.')
return widget_factory(pw)
except ImportError:
pass else:
try:
input_widget = importlib.import_module(
'calibre.gui2.convert.'+name)
pw = input_widget.PluginWidget pw.ICON = 'forward.png'
pw.HELP = _('Options specific to the input format.')
return widget_factory(pw)
except ImportError:
pass return None

[документация]
class InputFormatPlugin(Plugin):
'''
InputFormatPlugins are responsible for converting a document into
HTML+OPF+CSS+etc.
The results of the conversion *must* be encoded in UTF-8.
The main action happens in :meth:`convert`.
'''

type = _('Conversion input')
can_be_disabled = False supported_platforms = ['windows', 'osx', 'linux']
commit_name = None # unique name under which options for this plugin are saved ui_data = None

#: Set of file types for which this plugin should be run
#: For example: ``set(['azw', 'mobi', 'prc'])`` file_types = set()

#: If True, this input plugin generates a collection of images,
#: one per HTML file. This can be set dynamically, in the convert method
#: if the input files can be both image collections and non-image collections.
#: If you set this to True, you must implement the get_images() method that returns
#: a list of images.
is_image_collection = False

#: Number of CPU cores used by this plugin.
#: A value of -1 means that it uses all available cores core_usage = 1

#: If set to True, the input plugin will perform special processing
#: to make its output suitable for viewing for_viewer = False

#: The encoding that this input plugin creates files in. A value of
#: None means that the encoding is undefined and must be
#: detected individually output_encoding = 'utf-8'

#: Options shared by all Input format plugins. Do not override
#: in sub-classes. Use :attr:`options` instead. Every option must be an
#: instance of :class:`OptionRecommendation`.
common_options = {
OptionRecommendation(name='input_encoding', recommended_value=None, level=OptionRecommendation.LOW, help=_('Specify the character encoding of the input document. If '
'set this option will override any encoding declared by the '
'document itself. Particularly useful for documents that '
'do not declare an encoding or that have erroneous '
'encoding declarations.')
)}

#: Options to customize the behavior of this plugin. Every option must be an
#: instance of :class:`OptionRecommendation`.
options = set()

#: A set of 3-tuples of the form
#: (option_name, recommended_value, recommendation_level)
recommendations = set()

def __init__(self, *args):
Plugin.__init__(self, *args)
self.report_progress = DummyReporter()

[документация]
def get_images(self):
'''
Return a list of absolute paths to the images, if this input plugin represents an image collection. The list of images is in the same order as the spine and the TOC.
'''
raise NotImplementedError()

[документация]
def convert(self, stream, options, file_ext, log, accelerators):
'''
This method must be implemented in sub-classes. It must return the path to the created OPF file or an :class:`OEBBook` instance.
All output should be contained in the current folder.
If this plugin creates files outside the current folder they must be deleted/marked for deletion before this method returns.

:param stream: A file like object that contains the input file.
:param options: Options to customize the conversion process.
Guaranteed to have attributes corresponding to all the options declared by this plugin. In addition, it will have a verbose attribute that takes integral values from zero upwards. Higher numbers mean be more verbose. Another useful attribute is
``input_profile`` that is an instance of
:class:`calibre.customize.profiles.InputProfile`.
:param file_ext: The extension (without the .) of the input file. It is guaranteed to be one of the `file_types` supported by this plugin.
:param log: A :class:`calibre.utils.logging.Log` object. All output should use this object.
:param accelerators: A dictionary of various information that the input plugin can get easily that would speed up the subsequent stages of the conversion.

'''
raise NotImplementedError()

def __call__(self, stream, options, file_ext, log, accelerators, output_dir):
try:
log(f'InputFormatPlugin: {self.name} running')
if hasattr(stream, 'name'):
log('on', stream.name)
except Exception:
# In case stdout is broken pass with CurrentDir(output_dir):
for x in os.listdir('.'):
shutil.rmtree(x) if os.path.isdir(x) else os.remove(x)

ret = self.convert(stream, options, file_ext, log, accelerators)

return ret

[документация]
def postprocess_book(self, oeb, opts, log):
'''
Called to allow the input plugin to perform postprocessing after the book has been parsed.
'''
pass

[документация]
def specialize(self, oeb, opts, log, output_fmt):
'''
Called to allow the input plugin to specialize the parsed book for a particular output format. Called after postprocess_book and before any transforms are performed on the parsed book.
'''
pass

[документация]
def gui_configuration_widget(self, parent, get_option_by_name, get_option_help, db, book_id=None):
'''
Called to create the widget used for configuring this plugin in the calibre GUI. The widget must be an instance of the PluginWidget class.
See the builtin input plugins for examples.
'''
name = self.name.lower().replace(' ', '_')
return gui_configuration_widget(name, parent, get_option_by_name, get_option_help, db, book_id, for_output=False)

[документация]
class OutputFormatPlugin(Plugin):
'''
OutputFormatPlugins are responsible for converting an OEB document
(OPF+HTML) into an output e-book.

The OEB document can be assumed to be encoded in UTF-8.
The main action happens in :meth:`convert`.
'''

type = _('Conversion output')
can_be_disabled = False supported_platforms = ['windows', 'osx', 'linux']
commit_name = None # unique name under which options for this plugin are saved ui_data = None

#: The file type (extension without leading period) that this
#: plugin outputs file_type = None

#: Options shared by all Input format plugins. Do not override
#: in sub-classes. Use :attr:`options` instead. Every option must be an
#: instance of :class:`OptionRecommendation`.
common_options = {
OptionRecommendation(name='pretty_print', recommended_value=False, level=OptionRecommendation.LOW, help=_('If specified, the output plugin will try to create output '
'that is as human readable as possible. May not have any effect '
'for some output plugins.')
)}

#: Options to customize the behavior of this plugin. Every option must be an
#: instance of :class:`OptionRecommendation`.
options = set()

#: A set of 3-tuples of the form
#: (option_name, recommended_value, recommendation_level)
recommendations = set()

@property def description(self):
return _('Convert e-books to the %s format')%self.file_type.upper()

def __init__(self, *args):
Plugin.__init__(self, *args)
self.report_progress = DummyReporter()

[документация]
def convert(self, oeb_book, output, input_plugin, opts, log):
'''
Render the contents of `oeb_book` (which is an instance of
:class:`calibre.ebooks.oeb.OEBBook`) to the file specified by output.

:param output: Either a file like object or a string. If it is a string it is the path to a folder that may or may not exist. The output plugin should write its output into that folder. If it is a file like object, the output plugin should write its output into the file.
:param input_plugin: The input plugin that was used at the beginning of the conversion pipeline.
:param opts: Conversion options. Guaranteed to have attributes corresponding to the OptionRecommendations of this plugin.
:param log: The logger. Print debug/info messages etc. using this.

'''
raise NotImplementedError()

@property def is_periodical(self):
return self.oeb.metadata.publication_type and \ str(self.oeb.metadata.publication_type[0]).startswith('periodical:')

[документация]
def specialize_options(self, log, opts, input_fmt):
'''
Can be used to change the values of conversion options, as used by the conversion pipeline.
'''
pass

[документация]
def specialize_css_for_output(self, log, opts, item, stylizer):
'''
Can be used to make changes to the CSS during the CSS flattening process.

:param item: The item (HTML file) being processed
:param stylizer: A Stylizer object containing the flattened styles for item. You can get the style for any element by stylizer.style(element).

'''
pass

[документация]
def gui_configuration_widget(self, parent, get_option_by_name, get_option_help, db, book_id=None):
'''
Called to create the widget used for configuring this plugin in the calibre GUI. The widget must be an instance of the PluginWidget class.
See the builtin output plugins for examples.
'''
name = self.name.lower().replace(' ', '_')
return gui_configuration_widget(name, parent, get_option_by_name, get_option_help, db, book_id, for_output=True)

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.db.cache

Исходный код calibre.db.cache

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import hashlib import operator import os import random import shutil import sys import traceback import weakref from collections import defaultdict from collections.abc import Iterable, MutableSet, Set from functools import partial, wraps from io import DEFAULT_BUFFER_SIZE, BytesIO from queue import Queue from threading import Lock from time import mktime, monotonic, sleep, time from typing import NamedTuple from calibre import as_unicode, detect_ncpus, isbytestring from calibre.constants import iswindows, preferred_encoding from calibre.customize.ui import run_plugins_on_import, run_plugins_on_postadd, run_plugins_on_postdelete, run_plugins_on_postimport from calibre.db import SPOOL_SIZE, _get_next_series_num_for_list from calibre.db.annotations import merge_annotations from calibre.db.categories import get_categories from calibre.db.constants import COVER_FILE_NAME, DATA_DIR_NAME, NOTES_DIR_NAME from calibre.db.errors import NoSuchBook, NoSuchFormat from calibre.db.fields import IDENTITY, InvalidLinkTable, create_field from calibre.db.lazy import FormatMetadata, FormatsList, ProxyMetadata from calibre.db.listeners import EventDispatcher, EventType from calibre.db.locking import DowngradeLockError, LockingError, SafeReadLock, create_locks, try_lock from calibre.db.notes.connect import copy_marked_up_text from calibre.db.search import Search from calibre.db.tables import VirtualTable from calibre.db.utils import type_safe_sort_key_function from calibre.db.write import get_series_values, uniq from calibre.ebooks import check_ebook_format from calibre.ebooks.metadata import author_to_author_sort, string_to_authors, title_sort from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.opf2 import metadata_to_opf from calibre.ptempfile import PersistentTemporaryFile, SpooledTemporaryFile, base_dir from calibre.utils.config import prefs, tweaks from calibre.utils.date import UNDEFINED_DATE, is_date_undefined, timestampfromdt, utcnow from calibre.utils.date import now as nowf from calibre.utils.filenames import make_long_path_useable from calibre.utils.icu import lower as icu_lower from calibre.utils.icu import sort_key from calibre.utils.localization import canonicalize_lang from polyglot.builtins import cmp, iteritems, itervalues, string_or_bytes class ExtraFile(NamedTuple):
relpath: str file_path: str stat_result: os.stat_result def api(f):
f.is_cache_api = True return f def read_api(f):
f = api(f)
f.is_read_api = True return f def write_api(f):
f = api(f)
f.is_read_api = False return f def wrap_simple(lock, func):
@wraps(func)
def call_func_with_lock(*args, **kwargs):
try:
with lock:
return func(*args, **kwargs)
except DowngradeLockError:
# We already have an exclusive lock, no need to acquire a shared
# lock. See the safe_read_lock properties' documentation for why
# this is necessary.
return func(*args, **kwargs)
return call_func_with_lock def run_import_plugins(path_or_stream, fmt):
fmt = fmt.lower()
if hasattr(path_or_stream, 'seek'):
path_or_stream.seek(0)
pt = PersistentTemporaryFile('_import_plugin.'+fmt)
shutil.copyfileobj(path_or_stream, pt, 1024**2)
pt.close()
path = pt.name else:
path = path_or_stream return run_plugins_on_import(path, fmt)

def _add_newbook_tag(mi):
tags = prefs['new_book_tags']
if tags:
for tag in [t.strip() for t in tags]:
if tag:
if not mi.tags:
mi.tags = [tag]
elif tag not in mi.tags:
mi.tags.append(tag)

def _add_default_custom_column_values(mi, fm):
cols = fm.custom_field_metadata(include_composites=False)
for cc,col in iteritems(cols):
dv = col['display'].get('default_value', None)
try:
if dv is not None:
if not mi.get_user_metadata(cc, make_copy=False):
mi.set_user_metadata(cc, col)
dt = col['datatype']
if dt == 'datetime' and icu_lower(dv) == 'now':
dv = nowf()
mi.set(cc, dv)
except Exception:
traceback.print_exc()

dynamic_category_preferences = frozenset({'grouped_search_make_user_categories', 'grouped_search_terms', 'user_categories'})

[документация]
class Cache:
'''
An in-memory cache of the metadata.db file from a calibre library.
This class also serves as a threadsafe API for accessing the database.
The in-memory cache is maintained in normal form for maximum performance.

SQLITE is simply used as a way to read and write from metadata.db robustly.
All table reading/sorting/searching/caching logic is re-implemented. This was necessary for maximum performance and flexibility.
'''
EventType = EventType fts_indexing_sleep_time = 4 # seconds def __init__(self, backend, library_database_instance=None):
self.shutting_down = False self.is_doing_rebuild_or_vacuum = False self.backend = backend
# We want templates to have access to LibraryDatabase if we have it,
# otherwise this instance (Cache)
self.database_instance = (weakref.ref(self) if library_database_instance is None else weakref.ref(library_database_instance))
self.event_dispatcher = EventDispatcher()
self.fields = {}
self.composites = {}
self.read_lock, self.write_lock = create_locks()
self.format_metadata_cache = defaultdict(dict)
self.formatter_template_cache = {}
self.dirtied_cache = {}
self.link_maps_cache = {}
self.extra_files_cache = {}
self.vls_for_books_cache = None self.vls_for_books_lib_in_process = None self.vls_cache_lock = Lock()
self.dirtied_sequence = 0 self.cover_caches = set()
self.clear_search_cache_count = 0

# Implement locking for all simple read/write API methods
# An unlocked version of the method is stored with the name starting
# with a leading underscore. Use the unlocked versions when the lock
# has already been acquired.
for name in dir(self):
func = getattr(self, name)
ira = getattr(func, 'is_read_api', None)
if ira is not None:
# Save original function setattr(self, '_'+name, func)
# Wrap it in a lock lock = self.read_lock if ira else self.write_lock setattr(self, name, wrap_simple(lock, func))

self._search_api = Search(self, 'saved_searches', self.field_metadata.get_search_terms())
self.initialize_dynamic()
self.initialize_fts()

@property def new_api(self):
return self

@property def library_id(self):
return self.backend.library_id

@property def dbpath(self):
return self.backend.dbpath

@property def is_fat_filesystem(self):
return self.backend.is_fat_filesystem

@property def safe_read_lock(self):
''' A safe read lock is a lock that does nothing if the thread already has a write lock, otherwise it acquires a read lock. This is necessary to prevent DowngradeLockErrors, which can happen when updating the search cache in the presence of composite columns. Updating the search cache holds an exclusive lock, but searching a composite column involves reading field values via ProxyMetadata which tries to get a shared lock. There may be other scenarios that trigger this as well.

This property returns a new lock object on every access. This lock object is not recursive (for performance) and must only be used in a with statement as ``with cache.safe_read_lock:`` otherwise bad things will happen.'''
return SafeReadLock(self.read_lock)

@write_api def ensure_has_search_category(self, fail_on_existing=True):
if len(self._search_api.saved_searches.names()) > 0:
self.field_metadata.add_search_category(label='search', name=_('Saved searches'), fail_on_existing=fail_on_existing)

def _initialize_dynamic_categories(self):
# Reconstruct the user categories, putting them into field_metadata fm = self.field_metadata fm.remove_dynamic_categories()
for user_cat in sorted(self._pref('user_categories', {}), key=sort_key):
cat_name = '@' + user_cat # add the '@' to avoid name collision while cat_name:
try:
fm.add_user_category(label=cat_name, name=user_cat)
except ValueError:
break # Can happen since we are removing dots and adding parent categories ourselves cat_name = cat_name.rpartition('.')[0]

# add grouped search term user categories muc = frozenset(self._pref('grouped_search_make_user_categories', []))
for cat in sorted(self._pref('grouped_search_terms', {}), key=sort_key):
if cat in muc:
# There is a chance that these can be duplicates of an existing
# user category. Print the exception and continue.
try:
self.field_metadata.add_user_category(label='@' + cat, name=cat)
except ValueError:
traceback.print_exc()
self._ensure_has_search_category()

self.field_metadata.add_grouped_search_terms( self._pref('grouped_search_terms', {}))
self._refresh_search_locations()

@write_api def initialize_dynamic(self):
self.backend.dirty_books_with_dirtied_annotations()
self.dirtied_cache = {x:i for i, x in enumerate(self.backend.dirtied_books())}
if self.dirtied_cache:
self.dirtied_sequence = max(itervalues(self.dirtied_cache))+1 self._initialize_dynamic_categories()

@write_api def initialize_template_cache(self):
self.formatter_template_cache = {}

@write_api def set_user_template_functions(self, user_template_functions):
self.backend.set_user_template_functions(user_template_functions)

@write_api def clear_composite_caches(self, book_ids=None):
for field in itervalues(self.composites):
field.clear_caches(book_ids=book_ids)

@write_api def clear_search_caches(self, book_ids=None):
self.clear_search_cache_count += 1 self._search_api.update_or_clear(self, book_ids)
self.vls_for_books_cache = None self.vls_for_books_lib_in_process = None

@write_api def clear_extra_files_cache(self, book_id=None):
if book_id is None:
self.extra_files_cache = {}
else:
self.extra_files_cache.pop(book_id, None)

@read_api def last_modified(self):
return self.backend.last_modified()

def __enter__(self):
self.backend.__enter__()

def __exit__(self, exc_type, exc_value, tb):
self.backend.__exit__(exc_type, exc_value, tb)

@write_api def clear_caches(self, book_ids=None, template_cache=True, search_cache=True):
if template_cache:
self._initialize_template_cache() # Clear the formatter template cache for field in itervalues(self.fields):
if hasattr(field, 'clear_caches'):
field.clear_caches(book_ids=book_ids) # Clear the composite cache and ondevice caches if book_ids:
for book_id in book_ids:
self.format_metadata_cache.pop(book_id, None)
else:
self.format_metadata_cache.clear()
if search_cache:
self._clear_search_caches(book_ids)
self._clear_link_map_cache(book_ids)

@write_api def clear_link_map_cache(self, book_ids=None):
if book_ids is None:
self.link_maps_cache = {}
else:
for book in book_ids:
self.link_maps_cache.pop(book, None)

@write_api def reload_from_db(self, clear_caches=True):
if clear_caches:
self._clear_caches()
with self.backend.conn: # Prevent other processes, such as calibredb from interrupting the reload by locking the db self.backend.prefs.load_from_db()
self._search_api.saved_searches.load_from_db()
for field in itervalues(self.fields):
if hasattr(field, 'table'):
field.table.read(self.backend) # Reread data from metadata.db

@property def field_metadata(self):
return self.backend.field_metadata def _get_metadata(self, book_id, get_user_categories=True): # {{{ mi = Metadata(None, template_cache=self.formatter_template_cache)

mi._proxy_metadata = ProxyMetadata(self, book_id, formatter=mi.formatter)

author_ids = self._field_ids_for('authors', book_id)
adata = self._author_data(author_ids)
aut_list = [adata[i] for i in author_ids]
aum = []
aus = {}
for rec in aut_list:
aut = rec['name']
aum.append(aut)
aus[aut] = rec['sort']
mi.title = self._field_for('title', book_id, default_value=_('Unknown'))
mi.authors = aum mi.author_sort = self._field_for('author_sort', book_id, default_value=_('Unknown'))
mi.author_sort_map = aus mi.comments = self._field_for('comments', book_id)
mi.publisher = self._field_for('publisher', book_id)
n = utcnow()
mi.timestamp = self._field_for('timestamp', book_id, default_value=n)
mi.pubdate = self._field_for('pubdate', book_id, default_value=n)
mi.uuid = self._field_for('uuid', book_id, default_value='dummy')
mi.title_sort = self._field_for('sort', book_id, default_value=_('Unknown'))
mi.last_modified = self._field_for('last_modified', book_id, default_value=n)
formats = self._field_for('formats', book_id)
mi.format_metadata = {}
mi.languages = list(self._field_for('languages', book_id))
if not formats:
good_formats = None else:
mi.format_metadata = FormatMetadata(self, book_id, formats)
good_formats = FormatsList(sorted(formats), mi.format_metadata)
# These three attributes are returned by the db2 get_metadata(),
# however, we don't actually use them anywhere other than templates, so
# they have been removed, to avoid unnecessary overhead. The templates
# all use _proxy_metadata.
# mi.book_size = self._field_for('size', book_id, default_value=0)
# mi.ondevice_col = self._field_for('ondevice', book_id, default_value='')
# mi.db_approx_formats = formats mi.formats = good_formats mi.has_cover = _('Yes') if self._field_for('cover', book_id, default_value=False) else ''
mi.tags = list(self._field_for('tags', book_id, default_value=()))
mi.series = self._field_for('series', book_id)
if mi.series:
mi.series_index = self._field_for('series_index', book_id, default_value=1.0)
mi.rating = self._field_for('rating', book_id)
mi.set_identifiers(self._field_for('identifiers', book_id, default_value={}))
mi.application_id = book_id mi.id = book_id for key, meta in self.field_metadata.custom_iteritems():
mi.set_user_metadata(key, meta)
if meta['datatype'] != 'composite':
# composites are evaluated on demand in metadata.book.base
# because their value is None val = self._field_for(key, book_id)
if isinstance(val, tuple):
val = list(val)
extra = self._field_for(key+'_index', book_id)
mi.set(key, val=val, extra=extra)

mi.link_maps = self._get_all_link_maps_for_book(book_id)

user_cat_vals = {}
if get_user_categories:
user_cats = self._pref('user_categories', {})
for ucat in user_cats:
res = []
for name,cat,ign in user_cats[ucat]:
v = mi.get(cat, None)
if isinstance(v, list):
if name in v:
res.append([name,cat])
elif name == v:
res.append([name,cat])
user_cat_vals[ucat] = res mi.user_categories = user_cat_vals return mi
# }}}

[документация]
@api def init(self):
'''
Initialize this cache with data from the backend.
'''
with self.write_lock:
self.backend.read_tables()
bools_are_tristate = self.backend.prefs['bools_are_tristate']

for field, table in iteritems(self.backend.tables):
self.fields[field] = create_field(field, table, bools_are_tristate, self.backend.get_template_functions, self.database_instance)
if table.metadata['datatype'] == 'composite':
self.composites[field] = self.fields[field]

self.fields['ondevice'] = create_field('ondevice', VirtualTable('ondevice'), bools_are_tristate, self.backend.get_template_functions, self.database_instance)

for name, field in iteritems(self.fields):
if name[0] == '#' and name.endswith('_index'):
field.series_field = self.fields[name[:-len('_index')]]
self.fields[name[:-len('_index')]].index_field = field elif name == 'series_index':
field.series_field = self.fields['series']
self.fields['series'].index_field = field elif name == 'authors':
field.author_sort_field = self.fields['author_sort']
elif name == 'title':
field.title_sort_field = self.fields['sort']
if self.backend.prefs['update_all_last_mod_dates_on_start']:
self.update_last_modified(self.all_book_ids())
self.backend.prefs.set('update_all_last_mod_dates_on_start', False)# FTS API {{{ def initialize_fts(self):
self.fts_queue_thread = None self.fts_measuring_rate = None self.fts_num_done_since_start = 0 self.fts_job_queue = Queue()
self.fts_indexing_left = self.fts_indexing_total = 0 fts = self.backend.initialize_fts(weakref.ref(self))
if self.is_fts_enabled():
self.start_fts_pool()
return fts def start_fts_pool(self):
from threading import Event, Thread self.fts_dispatch_stop_event = Event()
self.fts_queue_thread = Thread(name='FTSQueue', target=Cache.dispatch_fts_jobs, args=( self.fts_job_queue, self.fts_dispatch_stop_event, weakref.ref(self)), daemon=True)
self.fts_queue_thread.start()
self.backend.fts.pool.initialize()
self.backend.fts.pool.initialized.wait()
self.queue_next_fts_job()

@read_api def is_fts_enabled(self):
return self.backend.fts_enabled

@write_api def fts_start_measuring_rate(self, measure=True):
self.fts_measuring_rate = monotonic() if measure else None self.fts_num_done_since_start = 0 def _update_fts_indexing_numbers(self, job_time=None):
# this is called when new formats are added and when a format is
# indexed, but NOT when books or formats are deleted, so total may not
# be up to date.
nl = self.backend.fts.number_dirtied()
nt = self.backend.get('SELECT COUNT(*) FROM main.data')[0][0] or 0 if not nl:
self._fts_start_measuring_rate(measure=False)
if job_time is not None and self.fts_measuring_rate is not None:
self.fts_num_done_since_start += 1 if (self.fts_indexing_left, self.fts_indexing_total) != (nl, nt) or job_time is not None:
self.fts_indexing_left = nl self.fts_indexing_total = nt self.event_dispatcher(EventType.indexing_progress_changed, *self._fts_indexing_progress())

@read_api def fts_indexing_progress(self):
rate = None if self.fts_measuring_rate is not None and self.fts_num_done_since_start > 4:
rate = self.fts_num_done_since_start / (monotonic() - self.fts_measuring_rate)
return self.fts_indexing_left, self.fts_indexing_total, rate

@write_api def enable_fts(self, enabled=True, start_pool=True):
fts = self.backend.enable_fts(weakref.ref(self) if enabled else None)
if fts and start_pool: # used in the tests self.start_fts_pool()
if not fts and self.fts_queue_thread:
self.fts_job_queue.put(None)
self.fts_queue_thread = None self.fts_job_queue = Queue()
if fts:
self._update_fts_indexing_numbers()
return fts

@write_api def fts_unindex(self, book_id, fmt=None):
self.backend.fts_unindex(book_id, fmt=fmt)

@staticmethod def dispatch_fts_jobs(queue, stop_dispatch, dbref):
from .fts.text import is_fmt_extractable def do_one():
self = dbref()
if self is None:
return False start_time = monotonic()
with self.read_lock:
if not self.backend.fts_enabled:
return False book_id, fmt = self.backend.get_next_fts_job()
if book_id is None:
return False path = self._format_abspath(book_id, fmt)
if not path or not is_fmt_extractable(fmt):
with self.write_lock:
self.backend.remove_dirty_fts(book_id, fmt)
self._update_fts_indexing_numbers()
return True with self.read_lock, open(path, 'rb') as src, PersistentTemporaryFile(suffix=f'.{fmt.lower()}') as pt:
sz = 0 h = hashlib.sha1()
while True:
chunk = src.read(DEFAULT_BUFFER_SIZE)
if not chunk:
break sz += len(chunk)
h.update(chunk)
pt.write(chunk)
with self.write_lock:
queued = self.backend.queue_fts_job(book_id, fmt, pt.name, sz, h.hexdigest(), start_time)
if not queued: # means a dirtied book was removed from the dirty list because the text has not changed self._update_fts_indexing_numbers(monotonic() - start_time)
return self.backend.fts_has_idle_workers def loop_while_more_available():
self = dbref()
if not self or not self.backend.fts_enabled:
return has_more = True while has_more and not self.shutting_down and self.backend.fts_enabled and not stop_dispatch.is_set():
try:
has_more = do_one()
except Exception:
if self.backend.fts_enabled:
traceback.print_exc()
sleep(self.fts_indexing_sleep_time)

while not getattr(dbref(), 'shutting_down', True):
x = queue.get()
if x is None:
break loop_while_more_available()

@write_api def queue_next_fts_job(self):
if not self.backend.fts_enabled:
return self.fts_job_queue.put(True)
self._update_fts_indexing_numbers()

@write_api def commit_fts_result(self, book_id, fmt, fmt_size, fmt_hash, text, err_msg, start_time):
ans = self.backend.commit_fts_result(book_id, fmt, fmt_size, fmt_hash, text, err_msg)
self._update_fts_indexing_numbers(monotonic() - start_time)
return ans

@write_api def reindex_fts_book(self, book_id, *fmts):
if not self.is_fts_enabled():
return if not fmts:
fmts = self._formats(book_id)
self.backend.reindex_fts_book(book_id, *fmts)
self._queue_next_fts_job()

@api def reindex_fts(self):
if not self.is_fts_enabled():
return with self.write_lock:
self._shutdown_fts()
self._shutdown_fts(stage=2)
with self.write_lock:
self.backend.reindex_fts()
fts = self.initialize_fts()
fts.initialize(self.backend.conn) # ensure fts is pre-initialized needed for the tests self._queue_next_fts_job()
return fts

@write_api def set_fts_num_of_workers(self, num):
existing = self.backend.fts_num_of_workers if num != existing:
self.backend.fts_num_of_workers = num if num > existing:
self._queue_next_fts_job()
return True return False

@write_api def set_fts_speed(self, slow=True):
orig = self.fts_indexing_sleep_time if slow:
self.fts_indexing_sleep_time = Cache.fts_indexing_sleep_time changed = self._set_fts_num_of_workers(1)
else:
self.fts_indexing_sleep_time = 0.1 changed = self._set_fts_num_of_workers(max(1, detect_ncpus()))
changed = changed or orig != self.fts_indexing_sleep_time if changed and self.fts_measuring_rate is not None:
self._fts_start_measuring_rate()
return changed

@write_api # we need to use write locking as SQLITE gives a locked table error if multiple FTS queries are made at the same time def fts_search( self, fts_engine_query, use_stemming=True, highlight_start=None, highlight_end=None, snippet_size=None, restrict_to_book_ids=None, return_text=True, result_type=tuple, process_each_result=None,
):
return result_type(self.backend.fts_search( fts_engine_query, use_stemming=use_stemming, highlight_start=highlight_start, highlight_end=highlight_end, snippet_size=snippet_size, return_text=return_text, restrict_to_book_ids=restrict_to_book_ids, process_each_result=process_each_result,
))

# }}}

# Notes API {{{

[документация]
@read_api def notes_for(self, field, item_id) -> str:
' Return the notes document or an empty string if not found '
return self.backend.notes_for(field, item_id)

[документация]
@read_api def notes_data_for(self, field, item_id) -> str:
' Return all notes data as a dict or None if note does not exist '
return self.backend.notes_data_for(field, item_id)

[документация]
@read_api def get_all_items_that_have_notes(self, field_name=None) -> set[int] | dict[str, set[int]]:
' Return all item_ids for items that have notes in the specified field or all fields if field_name is None '
return self.backend.get_all_items_that_have_notes(field_name)

[документация]
@read_api def field_supports_notes(self, field=None) -> bool:
' Return True iff the specified field supports notes. If field is None return frozenset of all fields that support notes. '
if field is None:
return self.backend.notes.allowed_fields return field in self.backend.notes.allowed_fields

[документация]
@read_api def items_with_notes_in_book(self, book_id: int) -> dict[str, dict[int, str]]:
' Return a dict of field to items that have associated notes for that field for the specified book '
ans = {}
for k in self.backend.notes.allowed_fields:
try:
field = self.fields[k]
except KeyError:
continue v = {}
for item_id in field.ids_for_book(book_id):
if self.backend.notes_for(k, item_id):
v[item_id] = field.table.id_map[item_id]
if v:
ans[k] = v return ans

[документация]
@write_api def set_notes_for(self, field, item_id, doc: str, searchable_text: str = copy_marked_up_text, resource_hashes=(), remove_unused_resources=False) -> int:
'''
Set the notes document. If the searchable text is different from the document, specify it as searchable_text. If the document references resources their hashes must be present in resource_hashes. Set remove_unused_resources to True to cleanup unused resources, note that updating a note automatically cleans up resources pertaining to that note anyway.
'''
ans = self.backend.set_notes_for(field, item_id, doc, searchable_text, resource_hashes, remove_unused_resources)
self.event_dispatcher(EventType.notes_changed, field, frozenset({item_id}))
return ans

[документация]
@write_api def add_notes_resource(self, path_or_stream_or_data, name: str, mtime: float = None) -> int:
' Add the specified resource so it can be referenced by notes and return its content hash '
return self.backend.add_notes_resource(path_or_stream_or_data, name, mtime)

[документация]
@read_api def get_notes_resource(self, resource_hash) -> dict | None:
' Return a dict containing the resource data and name or None if no resource with the specified hash is found '
return self.backend.get_notes_resource(resource_hash)

[документация]
@read_api def notes_resources_used_by(self, field, item_id):
' Return the set of resource hashes of all resources used by the note for the specified item '
return frozenset(self.backend.notes_resources_used_by(field, item_id))

[документация]
@write_api def unretire_note_for(self, field, item_id) -> int:
' Unretire a previously retired note for the specified item. Notes are retired when an item is removed from the database '
ans = self.backend.unretire_note_for(field, item_id)
self.event_dispatcher(EventType.notes_changed, field, frozenset({item_id}))
return ans

[документация]
@read_api def export_note(self, field, item_id) -> str:
' Export the note as a single HTML document with embedded images as data: URLs '
return self.backend.export_note(field, item_id)

[документация]
@write_api def import_note(self, field, item_id, path_to_html_file, path_is_data=False):
' Import a previously exported note or an arbitrary HTML file as the note for the specified item '
if path_is_data:
html = path_to_html_file ctime = mtime = time()
basedir = base_dir()
else:
with open(path_to_html_file, 'rb') as f:
html = f.read()
st = os.stat(f.fileno())
ctime, mtime = st.st_ctime, st.st_mtime basedir = os.path.dirname(os.path.abspath(path_to_html_file))
ans = self.backend.import_note(field, item_id, html, basedir, ctime, mtime)
self.event_dispatcher(EventType.notes_changed, field, frozenset({item_id}))
return ans

[документация]
@write_api # we need to use write locking as SQLITE gives a locked table error if multiple FTS queries are made at the same time def search_notes( self, fts_engine_query='', use_stemming=True, highlight_start=None, highlight_end=None, snippet_size=None, restrict_to_fields=(), return_text=True, result_type=tuple, process_each_result=None, limit=None,
):
' Search the text of notes using an FTS index. If the query is empty return all notes. '
return result_type(self.backend.search_notes( fts_engine_query, use_stemming=use_stemming, highlight_start=highlight_start, highlight_end=highlight_end, snippet_size=snippet_size, return_text=return_text, restrict_to_fields=restrict_to_fields, process_each_result=process_each_result, limit=limit,
))

# }}}

# Cache Layer API {{{

[документация]
@write_api def add_listener(self, event_callback_function, check_already_added=False):
'''
Register a callback function that will be called after certain actions are taken on this database. The function must take three arguments:
(:class:`EventType`, library_id, event_type_specific_data)
'''
self.event_dispatcher.library_id = getattr(self, 'server_library_id', self.library_id)
if check_already_added and event_callback_function in self.event_dispatcher:
return False self.event_dispatcher.add_listener(event_callback_function)
return True

@write_api def remove_listener(self, event_callback_function):
self.event_dispatcher.remove_listener(event_callback_function)

[документация]
@read_api def field_for(self, name, book_id, default_value=None):
'''
Return the value of the field ``name`` for the book identified by ``book_id``. If no such book exists or it has no defined value for the field ``name`` or no such field exists, then
``default_value`` is returned.

``default_value`` is not used for title, title_sort, authors, author_sort and series_index. This is because these always have values in the db.
``default_value`` is used for all custom columns.

The returned value for is_multiple fields are always tuples, even when no values are found (in other words, default_value is ignored). The exception is identifiers for which the returned value is always a dictionary.
The returned tuples are always in link order, that is, the order in which they were created.
'''
if self.composites and name in self.composites:
return self.composite_for(name, book_id, default_value=default_value)
try:
field = self.fields[name]
except KeyError:
return default_value if field.is_multiple:
default_value = field.default_value try:
return field.for_book(book_id, default_value=default_value)
except (KeyError, IndexError):
return default_value

[документация]
@read_api def fast_field_for(self, field_obj, book_id, default_value=None):
' Same as field_for, except that it avoids the extra lookup to get the field object '
if field_obj.is_composite:
return field_obj.get_value_with_cache(book_id, self._get_proxy_metadata)
if field_obj.is_multiple:
default_value = field_obj.default_value try:
return field_obj.for_book(book_id, default_value=default_value)
except (KeyError, IndexError):
return default_value

[документация]
@read_api def all_field_for(self, field, book_ids, default_value=None):
' Same as field_for, except that it operates on multiple books at once '
field_obj = self.fields[field]
return {book_id:self._fast_field_for(field_obj, book_id, default_value=default_value) for book_id in book_ids}

@read_api def composite_for(self, name, book_id, mi=None, default_value=''):
try:
f = self.fields[name]
except KeyError:
return default_value if mi is None:
return f.get_value_with_cache(book_id, self._get_proxy_metadata)
else:
return f._render_composite_with_cache(book_id, mi, mi.formatter, mi.template_cache)

[документация]
@read_api def field_ids_for(self, name, book_id):
'''
Return the ids (as a tuple) for the values that the field ``name`` has on the book identified by ``book_id``. If there are no values, or no such book, or no such field, an empty tuple is returned.
'''
try:
return self.fields[name].ids_for_book(book_id)
except (KeyError, IndexError):
return ()

[документация]
@read_api def books_for_field(self, name, item_id):
'''
Return all the books associated with the item identified by
``item_id``, where the item belongs to the field ``name``.

Returned value is a set of book ids, or the empty set if the item or the field does not exist.
'''
try:
return self.fields[name].books_for(item_id)
except (KeyError, IndexError):
return set()

[документация]
@read_api def all_book_ids(self, type=frozenset):
'''
Frozen set of all known book ids.
'''
return type(self.fields['uuid'].table.book_col_map)

[документация]
@read_api def all_field_ids(self, name):
'''
Frozen set of ids for all values in the field ``name``.
'''
return frozenset(iter(self.fields[name]))

[документация]
@read_api def all_field_names(self, field):
''' Frozen set of all fields names (should only be used for many-one and many-many fields) '''
if field == 'formats':
return frozenset(self.fields[field].table.col_book_map)

try:
return frozenset(self.fields[field].table.id_map.values())
except AttributeError:
raise ValueError(f'{field} is not a many-one or many-many field')

[документация]
@read_api def get_usage_count_by_id(self, field):
''' Return a mapping of id to usage count for all values of the specified field, which must be a many-one or many-many field. '''
try:
return {k:len(v) for k, v in iteritems(self.fields[field].table.col_book_map)}
except AttributeError:
raise ValueError(f'{field} is not a many-one or many-many field')

[документация]
@read_api def get_id_map(self, field):
''' Return a mapping of id numbers to values for the specified field.
The field must be a many-one or many-many field, otherwise a ValueError is raised. '''
try:
return self.fields[field].table.id_map.copy()
except AttributeError:
if field == 'title':
return self.fields[field].table.book_col_map.copy()
raise ValueError(f'{field} is not a many-one or many-many field')

[документация]
@read_api def get_item_name(self, field, item_id):
''' Return the item name for the item specified by item_id in the specified field. See also :meth:`get_id_map`.'''
return self.fields[field].table.id_map[item_id]

[документация]
@read_api def get_item_id(self, field, item_name, case_sensitive=False):
''' Return the item id for item_name or None if not found.
This function is very slow if doing lookups for multiple names use either get_item_ids() or get_item_name_map().
Similarly, case sensitive lookups are faster than case insensitive ones. '''
field = self.fields[field]
if hasattr(field, 'item_ids_for_names'):
d = field.item_ids_for_names(self.backend, (item_name,), case_sensitive)
for v in d.values():
return v

[документация]
@read_api def get_item_ids(self, field, item_names, case_sensitive=False):
' Return a dict mapping item_name to the item id or None '
field = self.fields[field]
if hasattr(field, 'item_ids_for_names'):
return field.item_ids_for_names(self.backend, item_names, case_sensitive)
return dict.fromkeys(item_names)

[документация]
@read_api def get_item_name_map(self, field, normalize_func=None):
' Return mapping of item values to ids '
if normalize_func is None:
return {v:k for k, v in self.fields[field].table.id_map.items()}
return {normalize_func(v):k for k, v in self.fields[field].table.id_map.items()}

[документация]
@read_api def author_data(self, author_ids=None):
'''
Return author data as a dictionary with keys: name, sort, link

If no authors with the specified ids are found an empty dictionary is returned. If author_ids is None, data for all authors is returned.
'''
af = self.fields['authors']
if author_ids is None:
return {aid:af.author_data(aid) for aid in af.table.id_map}
return {aid:af.author_data(aid) for aid in author_ids if aid in af.table.id_map}

[документация]
@read_api def format_hash(self, book_id, fmt):
''' Return the hash of the specified format for the specified book. The kind of hash is backend dependent, but is usually SHA-256. '''
try:
name = self.fields['formats'].format_fname(book_id, fmt)
path = self._field_for('path', book_id).replace('/', os.sep)
except Exception:
raise NoSuchFormat(f'Record {book_id} has no fmt: {fmt}')
return self.backend.format_hash(book_id, fmt, name, path)

[документация]
@api def format_metadata(self, book_id, fmt, allow_cache=True, update_db=False):
'''
Return the path, size and mtime for the specified format for the specified book.
You should not use path unless you absolutely have to, since accessing it directly breaks the threadsafe guarantees of this API. Instead use the :meth:`copy_format_to` method.

:param allow_cache: If ``True`` cached values are used, otherwise a slow filesystem access is done. The cache values could be out of date if access was performed to the filesystem outside of this API.

:param update_db: If ``True`` The max_size field of the database is updated for this book.
'''
if not fmt:
return {}
fmt = fmt.upper()
# allow_cache and update_db are mutually exclusive. Give priority to update_db if allow_cache and not update_db:
x = self.format_metadata_cache[book_id].get(fmt, None)
if x is not None:
return x with self.safe_read_lock:
try:
name = self.fields['formats'].format_fname(book_id, fmt)
path = self._field_for('path', book_id).replace('/', os.sep)
except Exception:
return {}

ans = {}
if path and name:
ans = self.backend.format_metadata(book_id, fmt, name, path)
self.format_metadata_cache[book_id][fmt] = ans if update_db and 'size' in ans:
with self.write_lock:
max_size = self.fields['formats'].table.update_fmt(book_id, fmt, name, ans['size'], self.backend)
self.fields['size'].table.update_sizes({book_id: max_size})

return ans@read_api def format_files(self, book_id):
field = self.fields['formats']
fmts = field.table.book_col_map.get(book_id, ())
return {fmt:field.format_fname(book_id, fmt) for fmt in fmts}

@read_api def format_db_size(self, book_id, fmt):
field = self.fields['formats']
return field.format_size(book_id, fmt)

[документация]
@read_api def pref(self, name, default=None, namespace=None):
' Return the value for the specified preference or the value specified as ``default`` if the preference is not set. '
if namespace is not None:
return self.backend.prefs.get_namespaced(namespace, name, default)
return self.backend.prefs.get(name, default)

[документация]
@write_api def set_pref(self, name, val, namespace=None):
' Set the specified preference to the specified value. See also :meth:`pref`. '
if namespace is not None:
self.backend.prefs.set_namespaced(namespace, name, val)
return self.backend.prefs.set(name, val)
if name in ('grouped_search_terms', 'virtual_libraries'):
self._clear_search_caches()
if name in dynamic_category_preferences:
self._initialize_dynamic_categories()

[документация]
@api def get_metadata(self, book_id, get_cover=False, get_user_categories=True, cover_as_data=False):
'''
Return metadata for the book identified by book_id as a :class:`calibre.ebooks.metadata.book.base.Metadata` object.
Note that the list of formats is not verified. If get_cover is True, the cover is returned, either a path to temp file as mi.cover or if cover_as_data is True then as mi.cover_data.
'''

# Check if virtual_libraries_for_books rebuilt its cache. If it did then
# we must clear the composite caches so the new data can be taken into
# account. Clearing the caches requires getting a write lock, so it must
# be done outside of the closure of _get_metadata().
composite_cache_needs_to_be_cleared = False with self.safe_read_lock:
vl_cache_was_none = self.vls_for_books_cache is None mi = self._get_metadata(book_id, get_user_categories=get_user_categories)
if vl_cache_was_none and self.vls_for_books_cache is not None:
composite_cache_needs_to_be_cleared = True if composite_cache_needs_to_be_cleared:
try:
self.clear_composite_caches()
except LockingError:
# We can't clear the composite caches because a read lock is set.
# As a consequence the value of a composite column that calls
# virtual_libraries() might be wrong. Oh well. Log and keep running.
print("Couldn't get write lock after vls_for_books_cache was loaded", file=sys.stderr)
traceback.print_exc()

if get_cover:
if cover_as_data:
cdata = self.cover(book_id)
if cdata:
mi.cover_data = ('jpeg', cdata)
else:
mi.cover = self.cover(book_id, as_path=True)

return mi

[документация]
@read_api def get_proxy_metadata(self, book_id):
''' Like :meth:`get_metadata` except that it returns a ProxyMetadata object that only reads values from the database on demand. This is much faster than get_metadata when only a small number of fields need to be accessed from the returned metadata object. '''
return ProxyMetadata(self, book_id)

[документация]
@api def cover(self, book_id, as_file=False, as_image=False, as_path=False, as_pixmap=False):
'''
Return the cover image or None. By default, returns the cover as a bytestring.

WARNING: Using as_path will copy the cover to a temp file and return the path to the temp file. You should delete the temp file when you are done with it.

:param as_file: If True return the image as an open file object (a SpooledTemporaryFile)
:param as_image: If True return the image as a QImage object
:param as_pixmap: If True return the image as a QPixmap object
:param as_path: If True return the image as a path pointing to a temporary file
'''
if as_file:
ret = SpooledTemporaryFile(SPOOL_SIZE)
if not self.copy_cover_to(book_id, ret):
ret.close()
return ret.seek(0)
elif as_path:
pt = PersistentTemporaryFile('_dbcover.jpg')
with pt:
if not self.copy_cover_to(book_id, pt):
return ret = pt.name elif as_pixmap or as_image:
from qt.core import QImage, QPixmap ret = QImage() if as_image else QPixmap()
with self.safe_read_lock:
path = self._format_abspath(book_id, '__COVER_INTERNAL__')
if path:
ret.load(path)
else:
buf = BytesIO()
if not self.copy_cover_to(book_id, buf):
return ret = buf.getvalue()
return ret

@read_api def cover_or_cache(self, book_id, timestamp, as_what='bytes'):
try:
path = self._field_for('path', book_id).replace('/', os.sep)
except AttributeError:
return False, None, None return self.backend.cover_or_cache(path, timestamp, as_what)

@read_api def cover_last_modified(self, book_id):
try:
path = self._field_for('path', book_id).replace('/', os.sep)
except AttributeError:
return return self.backend.cover_last_modified(path)

[документация]
@read_api def copy_cover_to(self, book_id, dest, use_hardlink=False, report_file_size=None):
'''
Copy the cover to the file like object ``dest``. Returns False if no cover exists or dest is the same file as the current cover.
dest can also be a path in which case the cover is copied to it if and only if the path is different from the current path (taking case sensitivity into account).
'''
try:
path = self._field_for('path', book_id).replace('/', os.sep)
except AttributeError:
return False return self.backend.copy_cover_to(path, dest, use_hardlink=use_hardlink, report_file_size=report_file_size)

[документация]
@write_api def compress_covers(self, book_ids, jpeg_quality=100, progress_callback=None):
'''
Compress the cover images for the specified books. A compression quality of 100 will perform lossless compression, otherwise lossy compression.

The progress callback will be called with the book_id and the old and new sizes for each book that has been processed. If an error occurs, the new size will be a string with the error details.
'''
jpeg_quality = max(10, min(jpeg_quality, 100))
path_map = {}
for book_id in book_ids:
try:
path_map[book_id] = self._field_for('path', book_id).replace('/', os.sep)
except AttributeError:
continue self.backend.compress_covers(path_map, jpeg_quality, progress_callback)

[документация]
@read_api def copy_format_to(self, book_id, fmt, dest, use_hardlink=False, report_file_size=None):
'''
Copy the format ``fmt`` to the file like object ``dest``. If the specified format does not exist, raises :class:`NoSuchFormat` error.
dest can also be a path (to a file), in which case the format is copied to it, iff the path is different from the current path (taking case sensitivity into account).
'''
fmt = (fmt or '').upper()
try:
name = self.fields['formats'].format_fname(book_id, fmt)
path = self._field_for('path', book_id).replace('/', os.sep)
except (KeyError, AttributeError):
raise NoSuchFormat(f'Record {book_id} has no {fmt} file')

return self.backend.copy_format_to(book_id, fmt, name, path, dest, use_hardlink=use_hardlink, report_file_size=report_file_size)

[документация]
@read_api def format_abspath(self, book_id, fmt):
'''
Return absolute path to the e-book file of format `format`. You should almost never use this, as it breaks the threadsafe promise of this API.
Instead use, :meth:`copy_format_to`.

Currently used only in calibredb list, the viewer, edit book, compare_format to original format, open with, bulk metadata edit and the catalogs (via get_data_as_dict()).

Apart from the viewer, open with and edit book, I don't believe any of the others do any file write I/O with the results of this call.
'''
fmt = (fmt or '').upper()
try:
path = self._field_for('path', book_id).replace('/', os.sep)
except Exception:
return None if path:
if fmt == '__COVER_INTERNAL__':
return self.backend.cover_abspath(book_id, path)
else:
try:
name = self.fields['formats'].format_fname(book_id, fmt)
except Exception:
return None if name:
return self.backend.format_abspath(book_id, fmt, name, path)

[документация]
@read_api def has_format(self, book_id, fmt):
'Return True iff the format exists on disk'
fmt = (fmt or '').upper()
try:
name = self.fields['formats'].format_fname(book_id, fmt)
path = self._field_for('path', book_id).replace('/', os.sep)
except Exception:
return False return self.backend.has_format(book_id, fmt, name, path)

[документация]
@api def save_original_format(self, book_id, fmt):
' Save a copy of the specified format as ORIGINAL_FORMAT, overwriting any existing ORIGINAL_FORMAT. '
fmt = fmt.upper()
if 'ORIGINAL' in fmt:
raise ValueError('Cannot save original of an original fmt')
fmtfile = self.format(book_id, fmt, as_file=True)
if fmtfile is None:
return False with fmtfile:
nfmt = 'ORIGINAL_'+fmt return self.add_format(book_id, nfmt, fmtfile, run_hooks=False)

[документация]
@write_api def restore_original_format(self, book_id, original_fmt):
''' Restore the specified format from the previously saved
ORIGINAL_FORMAT, if any. Return True on success. The ORIGINAL_FORMAT is deleted after a successful restore. '''
original_fmt = original_fmt.upper()
fmt = original_fmt.partition('_')[2]
try:
ofmt_name = self.fields['formats'].format_fname(book_id, original_fmt)
path = self._field_for('path', book_id).replace('/', os.sep)
except Exception:
return False if self.backend.is_format_accessible(book_id, original_fmt, ofmt_name, path):
self.add_format(book_id, fmt, BytesIO(), run_hooks=False)
fmt_name = self.fields['formats'].format_fname(book_id, fmt)
file_size = self.backend.rename_format_file(book_id, ofmt_name, original_fmt, fmt_name, fmt, path)
self.fields['formats'].table.update_fmt(book_id, fmt, fmt_name, file_size, self.backend)
self._remove_formats({book_id:(original_fmt,)})
return True return False

[документация]
@read_api def formats(self, book_id, verify_formats=True):
'''
Return tuple of all formats for the specified book. If verify_formats is True, verifies that the files exist on disk.
'''
ans = self.field_for('formats', book_id)
if verify_formats and ans:
try:
path = self._field_for('path', book_id).replace('/', os.sep)
except Exception:
return ()

def verify(fmt):
try:
name = self.fields['formats'].format_fname(book_id, fmt)
except Exception:
return False return self.backend.has_format(book_id, fmt, name, path)

ans = tuple(x for x in ans if verify(x))
return ans

[документация]
@api def format(self, book_id, fmt, as_file=False, as_path=False, preserve_filename=False):
'''
Return the e-book format as a bytestring or `None` if the format doesn't exist, or we don't have permission to write to the e-book file.

:param as_file: If True the e-book format is returned as a file object. Note that the file object is a SpooledTemporaryFile, so if what you want to do is copy the format to another file, use :meth:`copy_format_to` instead for performance.
:param as_path: Copies the format file to a temp file and returns the path to the temp file
:param preserve_filename: If True and returning a path the filename is the same as that used in the library. Note that using this means that repeated calls yield the same temp file (which is re-created each time)
'''
fmt = (fmt or '').upper()
ext = ('.'+fmt.lower()) if fmt else ''
if as_path:
if preserve_filename:
with self.safe_read_lock:
try:
fname = self.fields['formats'].format_fname(book_id, fmt)
except Exception:
return None fname += ext bd = base_dir()
d = os.path.join(bd, 'format_abspath')
try:
os.makedirs(d)
except Exception:
pass ret = os.path.join(d, fname)
try:
self.copy_format_to(book_id, fmt, ret)
except NoSuchFormat:
return None else:
with PersistentTemporaryFile(ext) as pt:
try:
self.copy_format_to(book_id, fmt, pt)
except NoSuchFormat:
return None ret = pt.name elif as_file:
with self.safe_read_lock:
try:
fname = self.fields['formats'].format_fname(book_id, fmt)
except Exception:
return None fname += ext ret = SpooledTemporaryFile(SPOOL_SIZE)
try:
self.copy_format_to(book_id, fmt, ret)
except NoSuchFormat:
ret.close()
return None ret.seek(0)
# Various bits of code try to use the name as the default
# title when reading metadata, so set it ret.name = fname else:
buf = BytesIO()
try:
self.copy_format_to(book_id, fmt, buf)
except NoSuchFormat:
return None ret = buf.getvalue()

return ret@read_api def newly_added_book_ids(self, count=5, book_ids=None) -> list[int]:
ids_to_sort = self._all_book_ids(list) if book_ids is None else list(book_ids)
ids_to_sort.sort(reverse=True)
return ids_to_sort[:count]

@read_api def size_stats(self) -> dict[str, int]:
return self.backend.size_stats()

[документация]
@read_api def multisort(self, fields, ids_to_sort=None, virtual_fields=None):
'''
Return a list of sorted book ids. If ids_to_sort is None, all book ids are returned.

fields must be a list of 2-tuples of the form (field_name, ascending=True or False). The most significant field is the first
2-tuple.
'''
ids_to_sort = self._all_book_ids() if ids_to_sort is None else ids_to_sort get_metadata = self._get_proxy_metadata lang_map = self.fields['languages'].book_value_map virtual_fields = virtual_fields or {}

fm = {'title':'sort', 'authors':'author_sort'}

def sort_key_func(field):
'Handle series type fields, virtual fields and the id field'
idx = field + '_index'
is_series = idx in self.fields try:
func = self.fields[fm.get(field, field)].sort_keys_for_books(get_metadata, lang_map)
except KeyError:
if field == 'id':
return IDENTITY else:
return virtual_fields[fm.get(field, field)].sort_keys_for_books(get_metadata, lang_map)
if is_series:
idx_func = self.fields[idx].sort_keys_for_books(get_metadata, lang_map)

def skf(book_id):
return (func(book_id), idx_func(book_id))
return skf return func

# Sort only once on any given field fields = uniq(fields, operator.itemgetter(0))

if len(fields) == 1:
keyfunc = sort_key_func(fields[0][0])
reverse = not fields[0][1]
try:
return sorted(ids_to_sort, key=keyfunc, reverse=reverse)
except Exception as err:
print('Failed to sort database on field:', fields[0][0], 'with error:', err, file=sys.stderr)
try:
return sorted(ids_to_sort, key=type_safe_sort_key_function(keyfunc), reverse=reverse)
except Exception as err:
print('Failed to type-safe sort database on field:', fields[0][0], 'with error:', err, file=sys.stderr)
return sorted(ids_to_sort, reverse=reverse)
sort_key_funcs = tuple(sort_key_func(field) for field, order in fields)
orders = tuple(1 if order else -1 for _, order in fields)
Lazy = object() # Lazy load the sort keys for sub-sort fields class SortKey:

__slots__ = 'book_id', 'sort_key'

def __init__(self, book_id):
self.book_id = book_id
# Calculate only the first sub-sort key since that will always be used self.sort_key = [key(book_id) if i == 0 else Lazy for i, key in enumerate(sort_key_funcs)]

def compare_to_other(self, other):
for i, (order, self_key, other_key) in enumerate(zip(orders, self.sort_key, other.sort_key)):
if self_key is Lazy:
self_key = self.sort_key[i] = sort_key_funcs[i](self.book_id)
if other_key is Lazy:
other_key = other.sort_key[i] = sort_key_funcs[i](other.book_id)
ans = cmp(self_key, other_key)
if ans != 0:
return ans * order return 0 def __eq__(self, other):
return self.compare_to_other(other) == 0 def __ne__(self, other):
return self.compare_to_other(other) != 0 def __lt__(self, other):
return self.compare_to_other(other) < 0 def __le__(self, other):
return self.compare_to_other(other) <= 0 def __gt__(self, other):
return self.compare_to_other(other) > 0 def __ge__(self, other):
return self.compare_to_other(other) >= 0 return sorted(ids_to_sort, key=SortKey)

[документация]
@read_api def search(self, query, restriction='', virtual_fields=None, book_ids=None):
'''
Search the database for the specified query, returning a set of matched book ids.

:param restriction: A restriction that is ANDed to the specified query. Note that restrictions are cached, therefore the search for a AND b will be slower than a with restriction b.

:param virtual_fields: Used internally (virtual fields such as on_device to search over).

:param book_ids: If not None, a set of book ids for which books will be searched instead of searching all books.
'''
return self._search_api(self, query, restriction, virtual_fields=virtual_fields, book_ids=book_ids)

[документация]
@read_api def books_in_virtual_library(self, vl, search_restriction=None, virtual_fields=None):
' Return the set of books in the specified virtual library '
vl = self._pref('virtual_libraries', {}).get(vl) if vl else None if not vl and not search_restriction:
return self.all_book_ids()
# We utilize the search restriction cache to speed this up srch = partial(self._search, virtual_fields=virtual_fields)
if vl:
if search_restriction:
return frozenset(srch('', vl) & srch('', search_restriction))
return frozenset(srch('', vl))
return frozenset(srch('', search_restriction))

@read_api def number_of_books_in_virtual_library(self, vl=None, search_restriction=None):
if not vl and not search_restriction:
return len(self.fields['uuid'].table.book_col_map)
return len(self.books_in_virtual_library(vl, search_restriction))

[документация]
@api def get_categories(self, sort='name', book_ids=None, already_fixed=None, first_letter_sort=False, uncollapsed_categories=None):
' Used internally to implement the Tag Browser '
try:
with self.safe_read_lock:
return get_categories(self, sort=sort, book_ids=book_ids, first_letter_sort=first_letter_sort, uncollapsed_categories=uncollapsed_categories)
except InvalidLinkTable as err:
bad_field = err.field_name if bad_field == already_fixed:
raise with self.write_lock:
self.fields[bad_field].table.fix_link_table(self.backend)
return self.get_categories(sort=sort, book_ids=book_ids, already_fixed=bad_field)

@write_api def update_last_modified(self, book_ids, now=None):
if book_ids:
if now is None:
now = nowf()
f = self.fields['last_modified']
f.writer.set_books({book_id:now for book_id in book_ids}, self.backend)
if self.composites:
self._clear_composite_caches(book_ids)
self._clear_search_caches(book_ids)

@write_api def mark_as_dirty(self, book_ids):
self._update_last_modified(book_ids)
already_dirtied = set(self.dirtied_cache).intersection(book_ids)
new_dirtied = book_ids - already_dirtied already_dirtied = {book_id:self.dirtied_sequence+i for i, book_id in enumerate(already_dirtied)}
if already_dirtied:
self.dirtied_sequence = max(itervalues(already_dirtied)) + 1 self.dirtied_cache.update(already_dirtied)
if new_dirtied:
self.backend.dirty_books(new_dirtied)
new_dirtied = {book_id:self.dirtied_sequence+i for i, book_id in enumerate(new_dirtied)}
self.dirtied_sequence = max(itervalues(new_dirtied)) + 1 self.dirtied_cache.update(new_dirtied)

@write_api def commit_dirty_cache(self):
if self.dirtied_cache:
self.backend.dirty_books(self.dirtied_cache)

@write_api def check_dirtied_annotations(self):
if not self.backend.dirty_books_with_dirtied_annotations():
return book_ids = set(self.backend.dirtied_books())
new_dirtied = book_ids - set(self.dirtied_cache)
if new_dirtied:
new_dirtied = {book_id:self.dirtied_sequence+i for i, book_id in enumerate(new_dirtied)}
self.dirtied_sequence = max(itervalues(new_dirtied)) + 1 self.dirtied_cache.update(new_dirtied)

[документация]
@write_api def set_field(self, name, book_id_to_val_map, allow_case_change=True, do_path_update=True):
'''
Set the values of the field specified by ``name``. Returns the set of all book ids that were affected by the change.

:param book_id_to_val_map: Mapping of book_ids to values that should be applied.
:param allow_case_change: If True, the case of many-one or many-many fields will be changed.
For example, if a book has the tag ``tag1`` and you set the tag for another book to ``Tag1`` then the both books will have the tag ``Tag1`` if allow_case_change is True, otherwise they will both have the tag ``tag1``.
:param do_path_update: Used internally, you should never change it.
'''
f = self.fields[name]
is_series = f.metadata['datatype'] == 'series'
update_path = name in {'title', 'authors'}
if update_path and iswindows:
paths = (x for x in (self._field_for('path', book_id) for book_id in book_id_to_val_map) if x)
self.backend.windows_check_if_files_in_use(paths)

if is_series:
bimap, simap = {}, {}
sfield = self.fields[name + '_index']
for k, v in iteritems(book_id_to_val_map):
if isinstance(v, string_or_bytes):
v, sid = get_series_values(v)
else:
v = sid = None if sid is None and name.startswith('#'):
sid = self._fast_field_for(sfield, k)
sid = 1.0 if sid is None else sid # The value to be set the db link table bimap[k] = v if sid is not None:
simap[k] = sid book_id_to_val_map = bimap dirtied = f.writer.set_books( book_id_to_val_map, self.backend, allow_case_change=allow_case_change)

if is_series and simap:
sf = self.fields[f.name+'_index']
dirtied |= sf.writer.set_books(simap, self.backend, allow_case_change=False)

if dirtied:
if update_path and do_path_update:
self._update_path(dirtied, mark_as_dirtied=False)
self._mark_as_dirty(dirtied)
self._clear_link_map_cache(dirtied)
self.event_dispatcher(EventType.metadata_changed, name, dirtied)
return dirtied@write_api def update_path(self, book_ids, mark_as_dirtied=True):
for book_id in book_ids:
title = self._field_for('title', book_id, default_value=_('Unknown'))
try:
author = self._field_for('authors', book_id, default_value=(_('Unknown'),))[0]
except IndexError:
author = _('Unknown')
self.backend.update_path(book_id, title, author, self.fields['path'], self.fields['formats'])
self.format_metadata_cache.pop(book_id, None)
if mark_as_dirtied:
self._mark_as_dirty(book_ids)
self._clear_link_map_cache(book_ids)

@read_api def get_a_dirtied_book(self):
if self.dirtied_cache:
return random.choice(tuple(self.dirtied_cache))
return None def _metadata_as_object_for_dump(self, book_id):
mi = self._get_metadata(book_id)
# Always set cover to cover.jpg. Even if cover doesn't exist,
# no harm done. This way no need to call dirtied when
# cover is set/removed mi.cover = 'cover.jpg'
mi.all_annotations = self._all_annotations_for_book(book_id)
return mi

@read_api def get_metadata_for_dump(self, book_id):
mi = None
# get the current sequence number for this book to pass back to the
# backup thread. This will avoid double calls in the case where the
# thread has not done the work between the put and the get_metadata sequence = self.dirtied_cache.get(book_id, None)
if sequence is not None:
try:
# While a book is being created, the path is empty. Don't bother to
# try to write the opf, because it will go to the wrong folder.
if self._field_for('path', book_id):
mi = self._metadata_as_object_for_dump(book_id)
except Exception:
# This almost certainly means that the book has been deleted while
# the backup operation sat in the queue.
traceback.print_exc()
return mi, sequence

@write_api def clear_dirtied(self, book_id, sequence):
# Clear the dirtied indicator for the books. This is used when fetching
# metadata, creating an OPF, and writing a file are separated into steps.
# The last step is clearing the indicator dc_sequence = self.dirtied_cache.get(book_id, None)
if dc_sequence is None or sequence is None or dc_sequence == sequence:
self.backend.mark_book_as_clean(book_id)
self.dirtied_cache.pop(book_id, None)

@write_api def write_backup(self, book_id, raw):
try:
path = self._field_for('path', book_id).replace('/', os.sep)
except Exception:
return self.backend.write_backup(path, raw)

@read_api def dirty_queue_length(self):
return len(self.dirtied_cache)

[документация]
@read_api def read_backup(self, book_id):
''' Return the OPF metadata backup for the book as a bytestring or None if no such backup exists. '''
try:
path = self._field_for('path', book_id).replace('/', os.sep)
except Exception:
return try:
return self.backend.read_backup(path)
except OSError:
return None

@write_api def dump_metadata(self, book_ids=None, remove_from_dirtied=True, callback=None):
# Write metadata for each record to an individual OPF file. If callback
# is not None, it is called once at the start with the number of book_ids
# being processed. And once for every book_id, with arguments (book_id,
# mi, ok).
if book_ids is None:
book_ids = set(self.dirtied_cache)

if callback is not None:
callback(len(book_ids), True, False)

for book_id in book_ids:
if self._field_for('path', book_id) is None:
if callback is not None:
callback(book_id, None, False)
continue mi, sequence = self._get_metadata_for_dump(book_id)
if mi is None:
if callback is not None:
callback(book_id, mi, False)
continue try:
raw = metadata_to_opf(mi)
self._write_backup(book_id, raw)
if remove_from_dirtied:
self._clear_dirtied(book_id, sequence)
except Exception:
pass if callback is not None:
callback(book_id, mi, True)

[документация]
@write_api def set_cover(self, book_id_data_map):
''' Set the cover for this book. The data can be either a QImage,
QPixmap, file object or bytestring. It can also be None, in which case any existing cover is removed. '''

for book_id, data in iteritems(book_id_data_map):
try:
path = self._field_for('path', book_id).replace('/', os.sep)
except AttributeError:
self._update_path((book_id,))
path = self._field_for('path', book_id).replace('/', os.sep)

self.backend.set_cover(book_id, path, data)
for cc in self.cover_caches:
cc.invalidate(book_id_data_map)
return self._set_field('cover', { book_id:(0 if data is None else 1) for book_id, data in iteritems(book_id_data_map)})

@write_api def add_cover_cache(self, cover_cache):
if not callable(cover_cache.invalidate):
raise ValueError('Cover caches must have an invalidate method')
self.cover_caches.add(cover_cache)

@write_api def remove_cover_cache(self, cover_cache):
self.cover_caches.discard(cover_cache)

[документация]
@write_api def set_metadata(self, book_id, mi, ignore_errors=False, force_changes=False, set_title=True, set_authors=True, allow_case_change=False):
'''
Set metadata for the book `id` from the `Metadata` object `mi`

Setting force_changes=True will force set_metadata to update fields even if mi contains empty values. In this case, 'None' is distinguished from
'empty'. If mi.XXX is None, the XXX is not replaced, otherwise it is.
The tags, identifiers, and cover attributes are special cases. Tags and identifiers cannot be set to None so they will always be replaced if force_changes is true. You must ensure that mi contains the values you want the book to have. Covers are always changed if a new cover is provided, but are never deleted. Also note that force_changes has no effect on setting title or authors.
'''
dirtied = set()

try:
# Handle code passing in an OPF object instead of a Metadata object mi = mi.to_book_metadata()
except (AttributeError, TypeError):
pass def set_field(name, val):
dirtied.update(self._set_field(name, {book_id:val}, do_path_update=False, allow_case_change=allow_case_change))

path_changed = False if set_title and mi.title:
path_changed = True set_field('title', mi.title)
authors_changed = False if set_authors:
path_changed = True if not mi.authors:
mi.authors = [_('Unknown')]
authors = []
for a in mi.authors:
authors += string_to_authors(a)
set_field('authors', authors)
authors_changed = True if path_changed:
self._update_path({book_id})

def protected_set_field(name, val):
try:
set_field(name, val)
except Exception:
if ignore_errors:
traceback.print_exc()
else:
raise

# force_changes has no effect on cover manipulation try:
cdata = mi.cover_data[1]
if cdata is None and isinstance(mi.cover, string_or_bytes) and mi.cover and os.access(mi.cover, os.R_OK):
with open(mi.cover, 'rb') as f:
cdata = f.read() or None if cdata is not None:
self._set_cover({book_id: cdata})
except Exception:
if ignore_errors:
traceback.print_exc()
else:
raise try:
with self.backend.conn: # Speed up set_metadata by not operating in autocommit mode for field in ('rating', 'series_index', 'timestamp'):
val = getattr(mi, field)
if val is not None:
protected_set_field(field, val)

val = mi.get('author_sort', None)
if authors_changed and (not val or mi.is_null('author_sort')):
val = self._author_sort_from_authors(mi.authors)
if authors_changed or (force_changes and val is not None) or not mi.is_null('author_sort'):
protected_set_field('author_sort', val)

for field in ('publisher', 'series', 'tags', 'comments',
'languages', 'pubdate'):
val = mi.get(field, None)
if (force_changes and val is not None) or not mi.is_null(field):
protected_set_field(field, val)

val = mi.get('title_sort', None)
if (force_changes and val is not None) or not mi.is_null('title_sort'):

protected_set_field('sort', val)

# identifiers will always be replaced if force_changes is True mi_idents = mi.get_identifiers()
if force_changes:
protected_set_field('identifiers', mi_idents)
elif mi_idents:
identifiers = self._field_for('identifiers', book_id, default_value={})
for key, val in iteritems(mi_idents):
if val and val.strip(): # Don't delete an existing identifier identifiers[icu_lower(key)] = val protected_set_field('identifiers', identifiers)

user_mi = mi.get_all_user_metadata(make_copy=False)
fm = self.field_metadata for key in user_mi:
if (key in fm and user_mi[key]['datatype'] == fm[key]['datatype'] and ( user_mi[key]['datatype'] != 'text' or ( user_mi[key]['is_multiple'] == fm[key]['is_multiple']))):
val = mi.get(key, None)
if force_changes or val is not None:
protected_set_field(key, val)
idx = key + '_index'
if idx in self.fields:
extra = mi.get_extra(key)
if extra is not None or force_changes:
protected_set_field(idx, extra)
except Exception:
# sqlite will rollback the entire transaction, thanks to the with
# statement, so we have to re-read everything form the db to ensure
# the db and Cache are in sync self._reload_from_db()
raise return dirtieddef _do_add_format(self, book_id, fmt, stream, name=None, mtime=None):
path = self._field_for('path', book_id)
if path is None:
# Theoretically, this should never happen, but apparently it
# does: https://www.mobileread.com/forums/showthread.php?t=233353 self._update_path({book_id}, mark_as_dirtied=False)
path = self._field_for('path', book_id)

path = path.replace('/', os.sep)
title = self._field_for('title', book_id, default_value=_('Unknown'))
try:
author = self._field_for('authors', book_id, default_value=(_('Unknown'),))[0]
except IndexError:
author = _('Unknown')

size, fname = self.backend.add_format(book_id, fmt, stream, title, author, path, name, mtime=mtime)
return size, fname

[документация]
@api def add_format(self, book_id, fmt, stream_or_path, replace=True, run_hooks=True, dbapi=None):
'''
Add a format to the specified book. Return True if the format was added successfully.

:param replace: If True replace existing format, otherwise if the format already exists, return False.
:param run_hooks: If True, file type plugins are run on the format before and after being added.
:param dbapi: Internal use only.
'''
needs_close = False if run_hooks:
# Run import plugins, the write lock is not held to cater for
# broken plugins that might spin the event loop by popping up a
# message in the GUI during the processing.
npath = run_import_plugins(stream_or_path, fmt)
fmt = os.path.splitext(npath)[-1].lower().replace('.', '').upper()
stream_or_path = open(make_long_path_useable(npath), 'rb')
needs_close = True fmt = check_ebook_format(stream_or_path, fmt)

with self.write_lock:
if not self._has_id(book_id):
raise NoSuchBook(book_id)
fmt = (fmt or '').upper()
self.format_metadata_cache[book_id].pop(fmt, None)
try:
name = self.fields['formats'].format_fname(book_id, fmt)
except Exception:
name = None if name and not replace:
if needs_close:
stream_or_path.close()
return False if hasattr(stream_or_path, 'read'):
stream = stream_or_path else:
stream = open(make_long_path_useable(stream_or_path), 'rb')
needs_close = True try:
size, fname = self._do_add_format(book_id, fmt, stream, name)
finally:
if needs_close:
stream.close()
del stream max_size = self.fields['formats'].table.update_fmt(book_id, fmt, fname, size, self.backend)
self.fields['size'].table.update_sizes({book_id: max_size})
self._update_last_modified((book_id,))
self.event_dispatcher(EventType.format_added, book_id, fmt)

if run_hooks:
# Run post import plugins, the write lock is released so the plugin
# can call api without a locking violation.
run_plugins_on_postimport(dbapi or self, book_id, fmt)
stream_or_path.close()

self.queue_next_fts_job()
return True[документация]
@write_api def remove_formats(self, formats_map, db_only=False):
'''
Remove the specified formats from the specified books.

:param formats_map: A mapping of book_id to a list of formats to be removed from the book.
:param db_only: If True, only remove the record for the format from the db, do not delete the actual format file from the filesystem.
:return: A map of book id to set of formats actually deleted from the filesystem for that book
'''
table = self.fields['formats'].table formats_map = {book_id:frozenset((f or '').upper() for f in fmts) for book_id, fmts in iteritems(formats_map)}
removed_map = {}

for book_id, fmts in iteritems(formats_map):
for fmt in fmts:
self.format_metadata_cache[book_id].pop(fmt, None)

if not db_only:
removes = defaultdict(set)
metadata_map = {}
for book_id, fmts in iteritems(formats_map):
try:
path = self._field_for('path', book_id).replace('/', os.sep)
except Exception:
continue for fmt in fmts:
try:
name = self.fields['formats'].format_fname(book_id, fmt)
except Exception:
continue if name and path:
removes[book_id].add((fmt, name, path))
if removes[book_id]:
metadata_map[book_id] = {'title': self._field_for('title', book_id), 'authors': self._field_for('authors', book_id)}
if removes:
removed_map = self.backend.remove_formats(removes, metadata_map)

size_map = table.remove_formats(formats_map, self.backend)
self.fields['size'].table.update_sizes(size_map)

for book_id, fmts in iteritems(formats_map):
for fmt in fmts:
run_plugins_on_postdelete(self, book_id, fmt)

self._update_last_modified(tuple(formats_map))
self.event_dispatcher(EventType.formats_removed, formats_map)
return removed_map

[документация]
@read_api def get_next_series_num_for(self, series, field='series', current_indices=False):
'''
Return the next series index for the specified series, taking into account the various preferences that control next series number generation.

:param field: The series-like field (defaults to the builtin series column)
:param current_indices: If True, returns a mapping of book_id to current series_index value instead.
'''
books = ()
sf = self.fields[field]
if series:
q = icu_lower(series)
for val, book_ids in sf.iter_searchable_values(self._get_proxy_metadata, frozenset(self._all_book_ids())):
if q == icu_lower(val):
books = book_ids break idf = sf.index_field index_map = {book_id:self._fast_field_for(idf, book_id, default_value=1.0) for book_id in books}
if current_indices:
return index_map series_indices = sorted(index_map.values(), key=lambda s: s or 0)
return _get_next_series_num_for_list(tuple(series_indices), unwrap=False)

[документация]
@read_api def author_sort_from_authors(self, authors, key_func=icu_lower):
'''Given a list of authors, return the author_sort string for the authors, preferring the author sort associated with the author over the computed string. '''
table = self.fields['authors'].table result = []
rmap = {key_func(v):k for k, v in iteritems(table.id_map)}
for aut in authors:
aid = rmap.get(key_func(aut), None)
result.append(author_to_author_sort(aut) if aid is None else table.asort_map[aid])
return ' & '.join(_f for _f in result if _f)

[документация]
@read_api def data_for_has_book(self):
''' Return data suitable for use in :meth:`has_book`. This can be used for an implementation of :meth:`has_book` in a worker process without access to the db. '''
try:
return {icu_lower(title) for title in itervalues(self.fields['title'].table.book_col_map)}
except TypeError:
# Some non-unicode titles in the db return {icu_lower(as_unicode(title)) for title in itervalues(self.fields['title'].table.book_col_map)}

[документация]
@read_api def has_book(self, mi):
''' Return True iff the database contains an entry with the same title as the passed in Metadata object. The comparison is case-insensitive.
See also :meth:`data_for_has_book`. '''
title = mi.title if title:
if isbytestring(title):
title = title.decode(preferred_encoding, 'replace')
q = icu_lower(title).strip()
for title in itervalues(self.fields['title'].table.book_col_map):
if q == icu_lower(title):
return True return False

[документация]
@read_api def has_id(self, book_id):
' Return True iff the specified book_id exists in the db '
return book_id in self.fields['title'].table.book_col_map

@write_api def create_book_entry(self, mi, cover=None, add_duplicates=True, force_id=None, apply_import_tags=True, preserve_uuid=False):
if mi.tags:
mi.tags = list(mi.tags)
if apply_import_tags:
_add_newbook_tag(mi)
_add_default_custom_column_values(mi, self.field_metadata)
if not add_duplicates and self._has_book(mi):
return series_index = (self._get_next_series_num_for(mi.series) if mi.series_index is None else mi.series_index)
try:
series_index = float(series_index)
except Exception:
try:
series_index = float(self._get_next_series_num_for(mi.series))
except Exception:
series_index = 1.0 if not mi.authors:
mi.authors = (_('Unknown'),)
aus = mi.author_sort if not mi.is_null('author_sort') else self._author_sort_from_authors(mi.authors)
mi.title = mi.title or _('Unknown')
if mi.is_null('title_sort'):
mi.title_sort = title_sort(mi.title, lang=mi.languages[0] if mi.languages else None)
if isbytestring(aus):
aus = aus.decode(preferred_encoding, 'replace')
if isbytestring(mi.title):
mi.title = mi.title.decode(preferred_encoding, 'replace')
if force_id is None:
self.backend.execute('INSERT INTO books(title, series_index, author_sort) VALUES (?, ?, ?)',
(mi.title, series_index, aus))
else:
self.backend.execute('INSERT INTO books(id, title, series_index, author_sort) VALUES (?, ?, ?, ?)',
(force_id, mi.title, series_index, aus))
book_id = self.backend.last_insert_rowid()
self.event_dispatcher(EventType.book_created, book_id)

mi.timestamp = utcnow() if (mi.timestamp is None or is_date_undefined(mi.timestamp)) else mi.timestamp mi.pubdate = UNDEFINED_DATE if mi.pubdate is None else mi.pubdate if cover is not None:
mi.cover, mi.cover_data = None, (None, cover)
self._set_metadata(book_id, mi, ignore_errors=True)
lm = getattr(mi, 'link_maps', None)
if lm:
for field, link_map in lm.items():
if self._has_link_map(field):
self._set_link_map(field, link_map, only_set_if_no_existing_link=True)
if preserve_uuid and mi.uuid:
self._set_field('uuid', {book_id:mi.uuid})
# Update the caches for fields from the books table self.fields['size'].table.book_col_map[book_id] = 0 row = next(self.backend.execute('SELECT sort, series_index, author_sort, uuid, has_cover FROM books WHERE id=?', (book_id,)))
for field, val in zip(('sort', 'series_index', 'author_sort', 'uuid', 'cover'), row):
if field == 'cover':
val = bool(val)
elif field == 'uuid':
self.fields[field].table.uuid_to_id_map[val] = book_id self.fields[field].table.book_col_map[book_id] = val return book_id

[документация]
@api def add_books(self, books, add_duplicates=True, apply_import_tags=True, preserve_uuid=False, run_hooks=True, dbapi=None):
'''
Add the specified books to the library. Books should be an iterable of
2-tuples, each 2-tuple of the form :code:`(mi, format_map)` where mi is a
Metadata object and format_map is a dictionary of the form :code:`{fmt: path_or_stream}`, for example: :code:`{'EPUB': '/path/to/file.epub'}`.

Returns a pair of lists: :code:`ids, duplicates`. ``ids`` contains the book ids for all newly created books in the database. ``duplicates`` contains the :code:`(mi, format_map)` for all books that already exist in the database as per the simple duplicate detection heuristic used by :meth:`has_book`.
'''
duplicates, ids = [], []
for mi, format_map in books:
book_id = self.create_book_entry(mi, add_duplicates=add_duplicates, apply_import_tags=apply_import_tags, preserve_uuid=preserve_uuid)
if book_id is None:
duplicates.append((mi, format_map))
else:
fmt_map = {}
ids.append(book_id)
for fmt, stream_or_path in format_map.items():
if self.add_format(book_id, fmt, stream_or_path, dbapi=dbapi, run_hooks=run_hooks):
fmt_map[fmt.lower()] = getattr(stream_or_path, 'name', stream_or_path) or '<stream>'
run_plugins_on_postadd(dbapi or self, book_id, fmt_map)
return ids, duplicates

[документация]
@write_api def remove_books(self, book_ids, permanent=False):
''' Remove the books specified by the book_ids from the database and delete their format files. If ``permanent`` is False, then the format files are placed in the per-library trash directory. '''
path_map = {}
for book_id in book_ids:
try:
path = self._field_for('path', book_id).replace('/', os.sep)
except Exception:
path = None path_map[book_id] = path if not permanent and path:
# ensure metadata.opf is written and up-to-date so we can restore the book try:
mi = self._metadata_as_object_for_dump(book_id)
raw = metadata_to_opf(mi)
self.backend.write_backup(path, raw)
except Exception:
traceback.print_exc()
self.backend.remove_books(path_map, permanent=permanent)
for field in itervalues(self.fields):
try:
table = field.table except AttributeError:
continue # Some fields like ondevice do not have tables else:
table.remove_books(book_ids, self.backend)
self._search_api.discard_books(book_ids)
self._clear_caches(book_ids=book_ids, template_cache=False, search_cache=False)
for cc in self.cover_caches:
cc.invalidate(book_ids)
self.event_dispatcher(EventType.books_removed, book_ids)@read_api def author_sort_strings_for_books(self, book_ids):
val_map = {}
for book_id in book_ids:
authors = self._field_ids_for('authors', book_id)
adata = self._author_data(authors)
val_map[book_id] = tuple(adata[aid]['sort'] for aid in authors)
return val_map

[документация]
@write_api def rename_items(self, field, item_id_to_new_name_map, change_index=True, restrict_to_book_ids=None):
'''
Rename items from a many-one or many-many field such as tags or series.

:param change_index: When renaming in a series-like field also change the series_index values.
:param restrict_to_book_ids: An optional set of book ids for which the rename is to be performed, defaults to all books.
'''
f = self.fields[field]
affected_books = set()
try:
sv = f.metadata['is_multiple']['ui_to_list']
except (TypeError, KeyError, AttributeError):
sv = None if restrict_to_book_ids is not None:
# We have a VL. Only change the item name for those books if not isinstance(restrict_to_book_ids, (Set, MutableSet)):
restrict_to_book_ids = frozenset(restrict_to_book_ids)
id_map = {}
default_process_map = {}
for old_id, new_name in iteritems(item_id_to_new_name_map):
new_names = tuple(x.strip() for x in new_name.split(sv)) if sv else (new_name,)
# Get a list of books in the VL with the item books_with_id = f.books_for(old_id)
books_to_process = books_with_id & restrict_to_book_ids if len(books_with_id) == len(books_to_process):
# All the books with the ID are in the VL, so we can use
# the normal processing default_process_map[old_id] = new_name elif books_to_process:
affected_books.update(books_to_process)
newvals = {}
for book_id in books_to_process:
# Get the current values, remove the one being renamed, then add
# the new value(s) back.
vals = self._field_for(field, book_id)
# Check for is_multiple if isinstance(vals, tuple):
# We must preserve order.
vals = list(vals)
# Don't need to worry about case here because we
# are fetching its one-true spelling. But lets be
# careful anyway try:
dex = vals.index(self._get_item_name(field, old_id))
# This can put the name back with a different case vals[dex] = new_names[0]
# now add any other items if they aren't already there if len(new_names) > 1:
set_vals = {icu_lower(x) for x in vals}
for v in new_names[1:]:
lv = icu_lower(v)
if lv not in set_vals:
vals.append(v)
set_vals.add(lv)
newvals[book_id] = vals except Exception:
traceback.print_exc()
else:
newvals[book_id] = new_names[0]
# Allow case changes self._set_field(field, newvals)
id_map[old_id] = self._get_item_id(field, new_names[0])
if default_process_map:
ab, idm = self._rename_items(field, default_process_map, change_index=change_index)
affected_books.update(ab)
id_map.update(idm)
self.event_dispatcher(EventType.items_renamed, field, affected_books, id_map)
return affected_books, id_map try:
func = f.table.rename_item except AttributeError:
raise ValueError(f'Cannot rename items for one-one fields: {field}')
moved_books = set()
id_map = {}
for item_id, new_name in item_id_to_new_name_map.items():
new_names = tuple(x.strip() for x in new_name.split(sv)) if sv else (new_name,)
books, new_id = func(item_id, new_names[0], self.backend)
affected_books.update(books)
id_map[item_id] = new_id if new_id != item_id:
moved_books.update(books)
if len(new_names) > 1:
# Add the extra items to the books extra = new_names[1:]
self._set_field(field, {book_id:self._fast_field_for(f, book_id) + extra for book_id in books})

if affected_books:
if field == 'authors':
self._set_field('author_sort',
{k:' & '.join(v) for k, v in iteritems(self._author_sort_strings_for_books(affected_books))})
self._update_path(affected_books, mark_as_dirtied=False)
elif change_index and hasattr(f, 'index_field') and tweaks['series_index_auto_increment'] != 'no_change':
for book_id in moved_books:
self._set_field(f.index_field.name, {book_id:self._get_next_series_num_for(self._fast_field_for(f, book_id), field=field)})
self._mark_as_dirty(affected_books)
self._clear_link_map_cache(affected_books)
self.event_dispatcher(EventType.items_renamed, field, affected_books, id_map)
return affected_books, id_map

[документация]
@write_api def remove_items(self, field, item_ids, restrict_to_book_ids=None):
''' Delete all items in the specified field with the specified ids.
Returns the set of affected book ids. ``restrict_to_book_ids`` is an optional set of books ids. If specified the items will only be removed from those books. '''
field = self.fields[field]
if restrict_to_book_ids is not None and not isinstance(restrict_to_book_ids, (MutableSet, Set)):
restrict_to_book_ids = frozenset(restrict_to_book_ids)
affected_books = field.table.remove_items(item_ids, self.backend, restrict_to_book_ids=restrict_to_book_ids)
if affected_books:
if hasattr(field, 'index_field'):
self._set_field(field.index_field.name, {bid:1.0 for bid in affected_books})
else:
self._mark_as_dirty(affected_books)
self._clear_link_map_cache(affected_books)
self.event_dispatcher(EventType.items_removed, field, affected_books, item_ids)
return affected_books

[документация]
@write_api def add_custom_book_data(self, name, val_map, delete_first=False):
''' Add data for name where val_map is a map of book_ids to values. If delete_first is True, all previously stored data for name will be removed. '''
missing = frozenset(val_map) - self._all_book_ids()
if missing:
raise ValueError(f'add_custom_book_data: no such book_ids: {missing}')
self.backend.add_custom_data(name, val_map, delete_first)

[документация]
@read_api def get_custom_book_data(self, name, book_ids=(), default=None):
''' Get data for name. By default returns data for all book_ids, pass in a list of book ids if you only want some data. Returns a map of book_id to values. If a particular value could not be decoded, uses default for it. '''
return self.backend.get_custom_book_data(name, book_ids, default)

[документация]
@write_api def delete_custom_book_data(self, name, book_ids=()):
''' Delete data for name. By default deletes all data, if you only want to delete data for some book ids, pass in a list of book ids. '''
self.backend.delete_custom_book_data(name, book_ids)

[документация]
@read_api def get_ids_for_custom_book_data(self, name):
''' Return the set of book ids for which name has data. '''
return self.backend.get_ids_for_custom_book_data(name)

@read_api def conversion_options(self, book_id, fmt='PIPE'):
return self.backend.conversion_options(book_id, fmt)

@read_api def has_conversion_options(self, ids, fmt='PIPE'):
return self.backend.has_conversion_options(ids, fmt)

@write_api def delete_conversion_options(self, book_ids, fmt='PIPE'):
return self.backend.delete_conversion_options(book_ids, fmt)

[документация]
@write_api def set_conversion_options(self, options, fmt='PIPE'):
''' options must be a map of the form {book_id:conversion_options} '''
return self.backend.set_conversion_options(options, fmt)

@write_api def refresh_format_cache(self):
self.fields['formats'].table.read(self.backend)
self.format_metadata_cache.clear()

@write_api def refresh_ondevice(self):
self.fields['ondevice'].clear_caches()
self.clear_search_caches()
self.clear_composite_caches()

@read_api def books_matching_device_book(self, lpath):
ans = set()
for book_id, (_, _, _, _, lpaths) in self.fields['ondevice'].cache.items():
if lpath in lpaths:
ans.add(book_id)
return ans

[документация]
@read_api def tags_older_than(self, tag, delta=None, must_have_tag=None, must_have_authors=None):
'''
Return the ids of all books having the tag ``tag`` that are older than the specified time. tag comparison is case insensitive.

:param delta: A timedelta object or None. If None, then all ids with the tag are returned.

:param must_have_tag: If not None the list of matches will be restricted to books that have this tag

:param must_have_authors: A list of authors. If not None the list of matches will be restricted to books that have these authors (case insensitive).

'''
tag_map = {icu_lower(v):k for k, v in iteritems(self._get_id_map('tags'))}
tag = icu_lower(tag.strip())
mht = icu_lower(must_have_tag.strip()) if must_have_tag else None tag_id, mht_id = tag_map.get(tag, None), tag_map.get(mht, None)
ans = set()
if mht_id is None and mht:
return ans if tag_id is not None:
tagged_books = self._books_for_field('tags', tag_id)
if mht_id is not None and tagged_books:
tagged_books = tagged_books.intersection(self._books_for_field('tags', mht_id))
if tagged_books:
if must_have_authors is not None:
amap = {icu_lower(v):k for k, v in iteritems(self._get_id_map('authors'))}
books = None for author in must_have_authors:
abooks = self._books_for_field('authors', amap.get(icu_lower(author), None))
books = abooks if books is None else books.intersection(abooks)
if not books:
break tagged_books = tagged_books.intersection(books or set())
if delta is None:
ans = tagged_books else:
now = nowf()
for book_id in tagged_books:
ts = self._field_for('timestamp', book_id)
if (now - ts) > delta:
ans.add(book_id)
return ans@write_api def set_sort_for_authors(self, author_id_to_sort_map, update_books=True):
sort_map = self.fields['authors'].table.set_sort_names(author_id_to_sort_map, self.backend)
changed_books = set()
if update_books:
val_map = {}
for author_id in sort_map:
books = self._books_for_field('authors', author_id)
changed_books |= books for book_id in books:
authors = self._field_ids_for('authors', book_id)
adata = self._author_data(authors)
sorts = [adata[x]['sort'] for x in authors]
val_map[book_id] = ' & '.join(sorts)
if val_map:
self._set_field('author_sort', val_map)
if changed_books:
self._mark_as_dirty(changed_books)
self._clear_link_map_cache(changed_books)
return changed_books

@write_api def set_link_for_authors(self, author_id_to_link_map):
link_map = self.fields['authors'].table.set_links(author_id_to_link_map, self.backend)
changed_books = set()
for author_id in link_map:
changed_books |= self._books_for_field('authors', author_id)
if changed_books:
self._mark_as_dirty(changed_books)
self._clear_link_map_cache(changed_books)
self.event_dispatcher(EventType.links_changed, 'authors', frozenset(author_id_to_link_map))
return changed_books

@read_api def has_link_map(self, field):
return hasattr(getattr(self.fields.get(field), 'table', None), 'link_map')

[документация]
@read_api def get_link_map(self, for_field):
'''
Return a dictionary of links for the supplied field.

:param for_field: the lookup name of the field for which the link map is desired

:return: {field_value:link_value, ...} for non-empty links
'''
if for_field not in self.fields:
raise ValueError(f'Lookup name {for_field} is not a valid name')
table = self.fields[for_field].table lm = getattr(table, 'link_map', None)
if lm is None:
raise ValueError(f"Lookup name {for_field} doesn't have a link map")
lm = table.link_map vm = table.id_map ans = {vm.get(fid):v for fid,v in lm.items() if v}
ans.pop(None, None)
return ans

[документация]
@read_api def link_for(self, field, item_id):
'''
Return the link, if any, for the specified item or None if no link is found
'''
f = self.fields.get(field)
if f is not None:
table = f.table lm = getattr(table, 'link_map', None)
if lm is not None:
return lm.get(item_id)

[документация]
@read_api def get_all_link_maps_for_book(self, book_id):
'''
Returns all links for all fields referenced by book identified by book_id.
If book_id doesn't exist then the method returns {}.

Example: Assume author A has link X, author B has link Y, tag S has link
F, and tag T has link G. If book 1 has author A and tag T, this method returns {'authors':{'A':'X'}, 'tags':{'T', 'G'}}.
If book 2's author is neither A nor B and has no tags, this method returns {}.

:param book_id: the book id in question.

:return: {field: {field_value, link_value}, ... for all fields with a field_value having a non-empty link value for that book

'''
if not self._has_id(book_id):
# Works for book_id is None.
return {}
cached = self.link_maps_cache.get(book_id)
if cached is not None:
return cached links = {}
def add_links_for_field(f):
field_ids = self._field_ids_for(f, book_id)
if field_ids:
table = self.fields[f].table lm = table.link_map id_link_map = {fid:lm.get(fid) for fid in field_ids}
vm = table.id_map d = {vm.get(fid):v for fid, v in id_link_map.items() if v}
d.pop(None, None)
if d:
links[f] = d for field in ('authors', 'publisher', 'series', 'tags'):
add_links_for_field(field)
for field in self.field_metadata.custom_field_keys(include_composites=False):
if self._has_link_map(field):
add_links_for_field(field)
self.link_maps_cache[book_id] = links return links[документация]
@write_api def set_link_map(self, field, value_to_link_map, only_set_if_no_existing_link=False):
'''
Sets links for item values in field.
Note: this method doesn't change values not in the value_to_link_map

:param field: the lookup name
:param value_to_link_map: dict(field_value:link, ...). Note that these are values, not field ids.

:return: books changed by setting the link

'''
if field not in self.fields:
raise ValueError(f'Lookup name {field} is not a valid name')
table = getattr(self.fields[field], 'table', None)
if table is None:
raise ValueError(f"Lookup name {field} doesn't have a link map")
# Clear the links for book cache as we don't know what will be affected self.link_maps_cache = {}

fids = self._get_item_ids(field, value_to_link_map)
if only_set_if_no_existing_link:
lm = table.link_map id_to_link_map = {fid:value_to_link_map[k] for k, fid in fids.items() if fid is not None and not lm.get(fid)}
else:
id_to_link_map = {fid:value_to_link_map[k] for k, fid in fids.items() if fid is not None}
result_map = table.set_links(id_to_link_map, self.backend)
changed_books = set()
for id_ in result_map:
changed_books |= self._books_for_field(field, id_)
if changed_books:
self._mark_as_dirty(changed_books)
self._clear_link_map_cache(changed_books)
self.event_dispatcher(EventType.links_changed, field, frozenset(id_to_link_map))
return changed_books@read_api def lookup_by_uuid(self, uuid):
return self.fields['uuid'].table.lookup_by_uuid(uuid)

@write_api def delete_custom_column(self, label=None, num=None):
self.backend.delete_custom_column(label, num)

@write_api def create_custom_column(self, label, name, datatype, is_multiple, editable=True, display={}):
return self.backend.create_custom_column(label, name, datatype, is_multiple, editable=editable, display=display)

@write_api def set_custom_column_metadata(self, num, name=None, label=None, is_editable=None, display=None, update_last_modified=False):
changed = self.backend.set_custom_column_metadata(num, name=name, label=label, is_editable=is_editable, display=display)
if changed:
if update_last_modified:
self._update_last_modified(self._all_book_ids())
else:
self.backend.prefs.set('update_all_last_mod_dates_on_start', True)
return changed

@read_api def get_books_for_category(self, category, item_id_or_composite_value):
f = self.fields[category]
if hasattr(f, 'get_books_for_val'):
# Composite field return f.get_books_for_val(item_id_or_composite_value, self._get_proxy_metadata, self._all_book_ids())
return self._books_for_field(f.name, int(item_id_or_composite_value))

[документация]
@read_api def split_if_is_multiple_composite(self, f, val):
'''
If f is a composite column lookup key and the column is is_multiple then split v into unique non-empty values. The comparison is case sensitive.
Order is not preserved. Return a list() for compatibility with proxy metadata field getters, for example tags.
'''
fm = self.field_metadata.get(f, None)
if fm and fm['datatype'] == 'composite' and fm['is_multiple']:
sep = fm['is_multiple'].get('cache_to_list', ',')
return list({v.strip() for v in val.split(sep) if v.strip()})
return val

[документация]
@read_api def data_for_find_identical_books(self):
''' Return data that can be used to implement
:meth:`find_identical_books` in a worker process without access to the db. See db.utils for an implementation. '''
at = self.fields['authors'].table author_map = defaultdict(set)
for aid, author in iteritems(at.id_map):
author_map[icu_lower(author)].add(aid)
return (author_map, at.col_book_map.copy(), self.fields['title'].table.book_col_map.copy(), self.fields['languages'].book_value_map.copy())

@read_api def update_data_for_find_identical_books(self, book_id, data):
author_map, author_book_map, title_map, lang_map = data title_map[book_id] = self._field_for('title', book_id)
lang_map[book_id] = self._field_for('languages', book_id)
at = self.fields['authors'].table for aid in at.book_col_map.get(book_id, ()):
author_map[icu_lower(at.id_map[aid])].add(aid)
try:
author_book_map[aid].add(book_id)
except KeyError:
author_book_map[aid] = {book_id}

[документация]
@read_api def find_identical_books(self, mi, search_restriction='', book_ids=None):
''' Finds books that have a superset of the authors in mi and the same title (title is fuzzy matched). See also :meth:`data_for_find_identical_books`. '''
from calibre.db.utils import fuzzy_title identical_book_ids = set()
langq = tuple(x for x in map(canonicalize_lang, mi.languages or ()) if x and x != 'und')
if mi.authors:
try:
quathors = mi.authors[:20] # Too many authors causes parsing of the search expression to fail query = ' and '.join('authors:"={}"'.format(a.replace('"', '')) for a in quathors)
qauthors = mi.authors[20:]
except ValueError:
return identical_book_ids try:
book_ids = self._search(query, restriction=search_restriction, book_ids=book_ids)
except Exception:
traceback.print_exc()
return identical_book_ids if qauthors and book_ids:
matches = set()
qauthors = {icu_lower(x) for x in qauthors}
for book_id in book_ids:
aut = self._field_for('authors', book_id)
if aut:
aut = {icu_lower(x) for x in aut}
if aut.issuperset(qauthors):
matches.add(book_id)
book_ids = matches for book_id in book_ids:
fbook_title = self._field_for('title', book_id)
fbook_title = fuzzy_title(fbook_title)
mbook_title = fuzzy_title(mi.title)
if fbook_title == mbook_title:
bl = self._field_for('languages', book_id)
if not langq or not bl or bl == langq:
identical_book_ids.add(book_id)
return identical_book_ids@read_api def get_top_level_move_items(self):
all_paths = {self._field_for('path', book_id).partition('/')[0] for book_id in self._all_book_ids()}
return self.backend.get_top_level_move_items(all_paths)

@write_api def move_library_to(self, newloc, progress=None, abort=None):
def progress_callback(item_name, item_count, total):
try:
if progress is not None:
progress(item_name, item_count, total)
except Exception:
traceback.print_exc()

all_paths = {self._field_for('path', book_id).partition('/')[0] for book_id in self._all_book_ids()}
self.backend.move_library_to(all_paths, newloc, progress=progress_callback, abort=abort)

@read_api def saved_search_names(self):
return self._search_api.saved_searches.names()

@read_api def saved_search_lookup(self, name):
return self._search_api.saved_searches.lookup(name)

@write_api def saved_search_set_all(self, smap):
self._search_api.saved_searches.set_all(smap)
self._clear_search_caches()

@write_api def saved_search_delete(self, name):
self._search_api.saved_searches.delete(name)
self._clear_search_caches()

@write_api def saved_search_add(self, name, val):
self._search_api.saved_searches.add(name, val)

@write_api def saved_search_rename(self, old_name, new_name):
self._search_api.saved_searches.rename(old_name, new_name)
self._clear_search_caches()

@write_api def change_search_locations(self, newlocs):
self._search_api.change_locations(newlocs)

@write_api def refresh_search_locations(self):
self._search_api.change_locations(self.field_metadata.get_search_terms())

@write_api def dump_and_restore(self, callback=None, sql=None):
return self.backend.dump_and_restore(callback=callback, sql=sql)

@write_api def vacuum(self, include_fts_db=False, include_notes_db=True):
self.is_doing_rebuild_or_vacuum = True try:
self.backend.vacuum(include_fts_db, include_notes_db)
finally:
self.is_doing_rebuild_or_vacuum = False def __del__(self):
self.close()

def _shutdown_fts(self, stage=1):
if stage == 1:
self.backend.shutdown_fts()
if self.fts_queue_thread is not None:
self.fts_job_queue.put(None)
if hasattr(self, 'fts_dispatch_stop_event'):
self.fts_dispatch_stop_event.set()
return
# the fts supervisor thread could be in the middle of committing a
# result to the db, so holding a lock here will cause a deadlock if self.fts_queue_thread is not None:
self.fts_queue_thread.join()
self.fts_queue_thread = None self.backend.join_fts()

@api def close(self):
with self.write_lock:
if hasattr(self, 'close_called'):
return self.close_called = True self.shutting_down = True self.event_dispatcher.close()
self._shutdown_fts()
try:
from calibre.customize.ui import available_library_closed_plugins except ImportError:
pass # happens during interpreter shutdown else:
for plugin in available_library_closed_plugins():
try:
plugin.run(self)
except Exception:
traceback.print_exc()
self._shutdown_fts(stage=2)
with self.write_lock:
self.backend.close()

@property def is_closed(self):
return self.backend.is_closed

@write_api def clear_trash_bin(self):
self.backend.clear_trash_dir()

@read_api def list_trash_entries(self):
books, formats = self.backend.list_trash_entries()
ff = []
for e in formats:
if self._has_id(e.book_id):
ff.append(e)
e.cover_path = self.format_abspath(e.book_id, '__COVER_INTERNAL__')
return books, formats

@read_api def copy_format_from_trash(self, book_id, fmt, dest):
fmt = fmt.upper()
fpath = self.backend.path_for_trash_format(book_id, fmt)
if not fpath:
raise ValueError(f'No format {fmt} found in book {book_id}')
shutil.copyfile(fpath, dest)

[документация]
@write_api def move_format_from_trash(self, book_id, fmt):
''' Undelete a format from the trash directory '''
if not self._has_id(book_id):
raise ValueError(f'A book with the id {book_id} does not exist')
fmt = fmt.upper()
try:
name = self.fields['formats'].format_fname(book_id, fmt)
except Exception:
name = None fpath = self.backend.path_for_trash_format(book_id, fmt)
if not fpath:
raise ValueError(f'No format {fmt} found in book {book_id}')
size, fname = self._do_add_format(book_id, fmt, fpath, name)
self.format_metadata_cache.pop(book_id, None)
max_size = self.fields['formats'].table.update_fmt(book_id, fmt, fname, size, self.backend)
self.fields['size'].table.update_sizes({book_id: max_size})
self.event_dispatcher(EventType.format_added, book_id, fmt)
self.backend.remove_trash_formats_dir_if_empty(book_id)

@read_api def copy_book_from_trash(self, book_id, dest: str):
self.backend.copy_book_from_trash(book_id, dest)

[документация]
@write_api def move_book_from_trash(self, book_id):
''' Undelete a book from the trash directory '''
if self._has_id(book_id):
raise ValueError(f'A book with the id {book_id} already exists')
mi, annotations, formats = self.backend.get_metadata_for_trash_book(book_id)
mi.cover = None self._create_book_entry(mi, add_duplicates=True, force_id=book_id, apply_import_tags=False, preserve_uuid=True)
path = self._field_for('path', book_id).replace('/', os.sep)
self.backend.move_book_from_trash(book_id, path)
self.format_metadata_cache.pop(book_id, None)
f = self.fields['formats'].table max_size = 0 for (fmt, size, fname) in formats:
max_size = max(max_size, f.update_fmt(book_id, fmt, fname, size, self.backend))
self.fields['size'].table.update_sizes({book_id: max_size})
cover = self.backend.cover_abspath(book_id, path)
if cover and os.path.exists(cover):
self._set_field('cover', {book_id:1})
if annotations:
self._restore_annotations(book_id, annotations)

[документация]
@write_api def delete_trash_entry(self, book_id, category):
" Delete an entry from the trash. Here category is 'b' for books and 'f' for formats. "
self.backend.delete_trash_entry(book_id, category)

[документация]
@write_api def expire_old_trash(self):
' Expire entries from the trash that are too old '
self.backend.expire_old_trash()

[документация]
@write_api def restore_book(self, book_id, mi, last_modified, path, formats, annotations=()):
''' Restore the book entry in the database for a book that already exists on the filesystem '''
cover, mi.cover = mi.cover, None self._create_book_entry(mi, add_duplicates=True, force_id=book_id, apply_import_tags=False, preserve_uuid=True)
self._update_last_modified((book_id,), last_modified)
if cover and os.path.exists(cover):
self._set_field('cover', {book_id:1})
f = self.fields['formats'].table for (fmt, size, fname) in formats:
f.update_fmt(book_id, fmt, fname, size, self.backend)
self.fields['path'].table.set_path(book_id, path, self.backend)
if annotations:
self._restore_annotations(book_id, annotations)

@read_api def virtual_libraries_for_books(self, book_ids, virtual_fields=None):
# use a primitive lock to ensure that only one thread is updating
# the cache and that recursive calls don't do the update. This
# method can recurse via self._search()
with try_lock(self.vls_cache_lock) as got_lock:
# Using a list is slightly faster than a set.
c = defaultdict(list)
if not got_lock:
# We get here if resolving the books in a VL triggers another VL
# cache calculation. This can be 'real' recursion, for example a
# VL expression using a template that calls virtual_libraries(),
# or a search using a location of 'all' that causes evaluation
# of a composite that uses virtual_libraries(). The first case
# is an error and the exception message should appear somewhere.
# However, the error can seem nondeterministic. It might not be
# raised if the use is via a composite and that composite is
# evaluated before it is used in the search. The second case is
# also an error but if the composite isn't used in a VL then the
# eventual answer will be correct because get_metadata() will
# clear the caches.
raise ValueError(_('Recursion detected while processing Virtual library "%s"')
% self.vls_for_books_lib_in_process)
if self.vls_for_books_cache is None:
libraries = self._pref('virtual_libraries', {})
for lib, expr in libraries.items():
book = None self.vls_for_books_lib_in_process = lib try:
for book in self._search(expr, virtual_fields=virtual_fields):
c[book].append(lib)
except Exception as e:
if book:
c[book].append(_('[Error in Virtual library {0}: {1}]').format(lib, str(e)))
self.vls_for_books_cache = {b:tuple(sorted(libs, key=sort_key)) for b, libs in c.items()}
if not book_ids:
book_ids = self._all_book_ids()
# book_ids is usually 1 long. The loop will be faster than a comprehension r = {}
default = ()
for b in book_ids:
r[b] = self.vls_for_books_cache.get(b, default)
return r

[документация]
@read_api def user_categories_for_books(self, book_ids, proxy_metadata_map=None):
''' Return the user categories for the specified books.
proxy_metadata_map is optional and is useful for a performance boost, in contexts where a ProxyMetadata object for the books already exists.
It should be a mapping of book_ids to their corresponding ProxyMetadata objects.
'''
user_cats = self._pref('user_categories', {})
pmm = proxy_metadata_map or {}
ans = {}

for book_id in book_ids:
proxy_metadata = pmm.get(book_id) or self._get_proxy_metadata(book_id)
user_cat_vals = ans[book_id] = {}
for ucat, categories in iteritems(user_cats):
user_cat_vals[ucat] = res = []
for name, cat, ign in categories:
try:
field_obj = self.fields[cat]
except KeyError:
continue if field_obj.is_composite:
v = field_obj.get_value_with_cache(book_id, lambda x:proxy_metadata)
else:
v = self._fast_field_for(field_obj, book_id)

if isinstance(v, (list, tuple)):
if name in v:
res.append([name, cat])
elif name == v:
res.append([name, cat])
return ans

[документация]
@write_api def embed_metadata(self, book_ids, only_fmts=None, report_error=None, report_progress=None):
''' Update metadata in all formats of the specified book_ids to current metadata in the database. '''
field = self.fields['formats']
from calibre.customize.ui import apply_null_metadata from calibre.ebooks.metadata.meta import set_metadata from calibre.ebooks.metadata.opf2 import pretty_print if only_fmts:
only_fmts = {f.lower() for f in only_fmts}

def doit(fmt, mi, stream):
with apply_null_metadata, pretty_print:
set_metadata(stream, mi, stream_type=fmt, report_error=report_error)
stream.seek(0, os.SEEK_END)
return stream.tell()

for i, book_id in enumerate(book_ids):
fmts = field.table.book_col_map.get(book_id, ())
if not fmts:
continue mi = self._get_metadata(book_id)
buf = BytesIO()
if not self._copy_cover_to(book_id, buf):
return cdata = buf.getvalue()
if cdata:
mi.cover_data = ('jpeg', cdata)
try:
path = self._field_for('path', book_id).replace('/', os.sep)
except Exception:
continue for fmt in fmts:
if only_fmts is not None and fmt.lower() not in only_fmts:
continue try:
name = self.fields['formats'].format_fname(book_id, fmt)
except Exception:
continue if name and path:
try:
new_size = self.backend.apply_to_format(book_id, path, name, fmt, partial(doit, fmt, mi))
except Exception as e:
if report_error is not None:
tb = traceback.format_exc()
if iswindows and isinstance(e, PermissionError) and e.filename and isinstance(e.filename, str):
from calibre_extensions import winutil try:
p = winutil.get_processes_using_files(e.filename)
except OSError:
pass else:
path_map = {x['path']: x for x in p}
tb = _('Could not open the file: "{}". It is already opened in the following programs:').format(e.filename)
for path, x in path_map.items():
tb += '\n' + f'{x["app_name"]}: {path}'
report_error(mi, fmt, tb)
new_size = None else:
raise if new_size is not None:
self.format_metadata_cache[book_id].get(fmt, {})['size'] = new_size max_size = self.fields['formats'].table.update_fmt(book_id, fmt, name, new_size, self.backend)
self.fields['size'].table.update_sizes({book_id: max_size})
if report_progress is not None:
report_progress(i+1, len(book_ids), mi)@read_api def get_last_read_positions(self, book_id, fmt, user):
fmt = fmt.upper()
ans = []
for device, cfi, epoch, pos_frac in self.backend.execute(
'SELECT device,cfi,epoch,pos_frac FROM last_read_positions WHERE book=? AND format=? AND user=?',
(book_id, fmt, user)):
ans.append({'device':device, 'cfi': cfi, 'epoch':epoch, 'pos_frac':pos_frac})
return ans

@write_api def set_last_read_position(self, book_id, fmt, user='_', device='_', cfi=None, epoch=None, pos_frac=0):
fmt = fmt.upper()
device = device or '_'
user = user or '_'
if not cfi:
self.backend.execute(
'DELETE FROM last_read_positions WHERE book=? AND format=? AND user=? AND device=?',
(book_id, fmt, user, device))
else:
self.backend.execute(
'INSERT OR REPLACE INTO last_read_positions(book,format,user,device,cfi,epoch,pos_frac) VALUES (?,?,?,?,?,?,?)',
(book_id, fmt, user, device, cfi, epoch or time(), pos_frac))

@read_api def export_library(self, library_key, exporter, progress=None, abort=None):
from polyglot.binary import as_hex_unicode key_prefix = as_hex_unicode(library_key)
book_ids = self._all_book_ids()
total = len(book_ids) + 2 has_fts = self.is_fts_enabled()
if has_fts:
total += 1 poff = 0 def report_progress(fname):
nonlocal poff if progress is not None:
progress(fname, poff, total)
poff += 1 report_progress('metadata.db')
pt = PersistentTemporaryFile('-export.db')
pt.close()
self.backend.backup_database(pt.name)
dbkey = key_prefix + ':::' + 'metadata.db'
with open(pt.name, 'rb') as f:
exporter.add_file(f, dbkey)
os.remove(pt.name)
if has_fts:
report_progress('full-text-search.db')
pt = PersistentTemporaryFile('-export.db')
pt.close()
self.backend.backup_fts_database(pt.name)
ftsdbkey = key_prefix + ':::full-text-search.db'
with open(pt.name, 'rb') as f:
exporter.add_file(f, ftsdbkey)
os.remove(pt.name)
notesdbkey = key_prefix + ':::notes.db'
with PersistentTemporaryFile('-export.db') as pt:
self.backend.export_notes_data(pt)
pt.flush()
pt.seek(0)
report_progress('notes.db')
exporter.add_file(pt, notesdbkey)

format_metadata = {}
extra_files = {}
metadata = {'format_data':format_metadata, 'metadata.db':dbkey, 'notes.db': notesdbkey, 'total':total, 'extra_files': extra_files}
if has_fts:
metadata['full-text-search.db'] = ftsdbkey for i, book_id in enumerate(book_ids):
if abort is not None and abort.is_set():
return if progress is not None:
report_progress(self._field_for('title', book_id))
format_metadata[book_id] = fm = {}
for fmt in self._formats(book_id):
mdata = self.format_metadata(book_id, fmt)
key = f'{key_prefix}:{book_id}:{fmt}'
fm[fmt] = key mtime = mdata.get('mtime')
if mtime is not None:
mtime = timestampfromdt(mtime)
with exporter.start_file(key, mtime=mtime) as dest:
self._copy_format_to(book_id, fmt, dest)
cover_key = '{}:{}:{}'.format(key_prefix, book_id, '.cover')
with exporter.start_file(cover_key) as dest:
if not self.copy_cover_to(book_id, dest):
dest.discard()
else:
fm['.cover'] = cover_key bp = self.field_for('path', book_id)
extra_files[book_id] = ef = {}
if bp:
for (relpath, fobj, stat_result) in self.backend.iter_extra_files(book_id, bp, self.fields['formats']):
key = f'{key_prefix}:{book_id}:.|{relpath}'
with exporter.start_file(key, mtime=stat_result.st_mtime) as dest:
shutil.copyfileobj(fobj, dest)
ef[relpath] = key exporter.set_metadata(library_key, metadata)
if progress is not None:
progress(_('Completed'), total, total)

[документация]
@read_api def annotations_map_for_book(self, book_id, fmt, user_type='local', user='viewer'):
'''
Return a map of annotation type -> annotation data for the specified book_id, format, user and user_type.
'''
ans = {}
for annot in self.backend.annotations_for_book(book_id, fmt, user_type, user):
ans.setdefault(annot['type'], []).append(annot)
return ans

[документация]
@read_api def all_annotations_for_book(self, book_id):
'''
Return a tuple containing all annotations for the specified book_id as a dict with keys:
`format`, `user_type`, `user`, `annotation`. Here, annotation is the annotation data.
'''
return tuple(self.backend.all_annotations_for_book(book_id))

[документация]
@read_api def annotation_count_for_book(self, book_id):
'''
Return the number of annotations for the specified book available in the database.
'''
return self.backend.annotation_count_for_book(book_id)

[документация]
@read_api def all_annotation_users(self):
'''
Return a tuple of all (user_type, user name) that have annotations.
'''
return tuple(self.backend.all_annotation_users())

[документация]
@read_api def all_annotation_types(self):
'''
Return a tuple of all annotation types in the database.
'''
return tuple(self.backend.all_annotation_types())

[документация]
@read_api def all_annotations(self, restrict_to_user=None, limit=None, annotation_type=None, ignore_removed=False, restrict_to_book_ids=None):
'''
Return a tuple of all annotations matching the specified criteria.
`ignore_removed` controls whether removed (deleted) annotations are also returned. Removed annotations are just a skeleton used for merging of annotations.
'''
return tuple(self.backend.all_annotations(restrict_to_user, limit, annotation_type, ignore_removed, restrict_to_book_ids))

[документация]
@read_api def search_annotations( self, fts_engine_query, use_stemming=True, highlight_start=None, highlight_end=None, snippet_size=None, annotation_type=None, restrict_to_book_ids=None, restrict_to_user=None, ignore_removed=False
):
'''
Return of a tuple of annotations matching the specified Full-text query.
'''
return tuple(self.backend.search_annotations( fts_engine_query, use_stemming, highlight_start, highlight_end, snippet_size, annotation_type, restrict_to_book_ids, restrict_to_user, ignore_removed
))

[документация]
@write_api def delete_annotations(self, annot_ids):
'''
Delete annotations with the specified ids.
'''
self.backend.delete_annotations(annot_ids)

[документация]
@write_api def update_annotations(self, annot_id_map):
'''
Update annotations.
'''
self.backend.update_annotations(annot_id_map)

@write_api def restore_annotations(self, book_id, annotations):
from calibre.utils.date import EPOCH from calibre.utils.iso8601 import parse_iso8601 umap = defaultdict(list)
for adata in annotations:
key = adata['user_type'], adata['user'], adata['format']
a = adata['annotation']
ts = (parse_iso8601(a['timestamp']) - EPOCH).total_seconds()
umap[key].append((a, ts))
for (user_type, user, fmt), annots_list in iteritems(umap):
self._set_annotations_for_book(book_id, fmt, annots_list, user_type=user_type, user=user)

[документация]
@write_api def set_annotations_for_book(self, book_id, fmt, annots_list, user_type='local', user='viewer'):
'''
Set all annotations for the specified book_id, fmt, user_type and user.
'''
self.backend.set_annotations_for_book(book_id, fmt, annots_list, user_type, user)

[документация]
@write_api def merge_annotations_for_book(self, book_id, fmt, annots_list, user_type='local', user='viewer'):
'''
Merge the specified annotations into the existing annotations for book_id, fm, user_type, and user.
'''
from calibre.utils.date import EPOCH from calibre.utils.iso8601 import parse_iso8601 amap = self._annotations_map_for_book(book_id, fmt, user_type=user_type, user=user)
merge_annotations(annots_list, amap)
alist = []
for val in itervalues(amap):
for annot in val:
ts = (parse_iso8601(annot['timestamp']) - EPOCH).total_seconds()
alist.append((annot, ts))
self._set_annotations_for_book(book_id, fmt, alist, user_type=user_type, user=user)

@write_api def save_annotations_list(self, book_id: int, book_fmt: str, sync_annots_user: str, alist: list[dict]) -> None:
self.backend.save_annotations_list(book_id, book_fmt, sync_annots_user, alist)

@write_api def reindex_annotations(self):
self.backend.reindex_annotations()

@read_api def are_paths_inside_book_dir(self, book_id, paths, sub_path=''):
try:
path = self._field_for('path', book_id).replace('/', os.sep)
except Exception:
return set()
return {x for x in paths if self.backend.is_path_inside_book_dir(x, path, sub_path)}

[документация]
@write_api def add_extra_files(self, book_id, map_of_relpath_to_stream_or_path, replace=True, auto_rename=False):
' Add extra data files '
path = self._field_for('path', book_id).replace('/', os.sep)
added = {}
for relpath, stream_or_path in map_of_relpath_to_stream_or_path.items():
added[relpath] = bool(self.backend.add_extra_file(relpath, stream_or_path, path, replace, auto_rename))
self._clear_extra_files_cache(book_id)
return added

[документация]
@write_api def rename_extra_files(self, book_id, map_of_relpath_to_new_relpath, replace=False):
' Rename extra data files '
path = self._field_for('path', book_id).replace('/', os.sep)
renamed = set()
for relpath, newrelpath in map_of_relpath_to_new_relpath.items():
if self.backend.rename_extra_file(relpath, newrelpath, path, replace):
renamed.add(relpath)
self._clear_extra_files_cache(book_id)
return renamed

[документация]
@write_api def merge_extra_files(self, dest_id, src_ids, replace=False):
' Merge the extra files from src_ids into dest_id. Conflicting files are auto-renamed unless replace=True in which case they are replaced. '
added = set()
path = self._field_for('path', dest_id)
if path:
path = path.replace('/', os.sep)
for src_id in src_ids:
book_path = self._field_for('path', src_id)
if book_path:
book_path = book_path.replace('/', os.sep)
for (relpath, file_path, stat_result) in self.backend.iter_extra_files( src_id, book_path, self.fields['formats'], yield_paths=True):
added.add(self.backend.add_extra_file(relpath, file_path, path, replace=replace, auto_rename=True))
self._clear_extra_files_cache(dest_id)
return added

[документация]
@write_api def remove_extra_files(self, book_id: int, relpaths: Iterable[str], permanent=False) -> dict[str, Exception | None]:
'''
Delete the specified extra files, either to Recycle Bin or permanently.
'''
path = self._field_for('path', book_id)
if path:
self._clear_extra_files_cache(book_id)
return self.backend.remove_extra_files(path, relpaths, permanent)
return dict.fromkeys(relpaths)

[документация]
@read_api def list_extra_files(self, book_id, use_cache=False, pattern='') -> tuple[ExtraFile, ...]:
'''
Get information about extra files in the book's directory.

:param book_id: the database book id for the book
:param pattern: the pattern of filenames to search for. Empty pattern matches all extra files. Patterns must use / as separator.
Use the DATA_FILE_PATTERN constant to match files inside the data directory.

:return: A tuple of all extra files matching the specified pattern. Each element of the tuple is
ExtraFile(relpath, file_path, stat_result). Where relpath is the relative path of the file to the book directory using / as a separator.
stat_result is the result of calling os.stat() on the file.
'''
ans = self.extra_files_cache.setdefault(book_id, {}).get(pattern)
if ans is None or not use_cache:
ans = []
path = self._field_for('path', book_id)
if path:
for (relpath, file_path, stat_result) in self.backend.iter_extra_files( book_id, path, self.fields['formats'], yield_paths=True, pattern=pattern
):
ans.append(ExtraFile(relpath, file_path, stat_result))
self.extra_files_cache[book_id][pattern] = ans = tuple(ans)
return ans

@read_api def copy_extra_file_to(self, book_id, relpath, stream_or_path):
path = self._field_for('path', book_id).replace('/', os.sep)
self.backend.copy_extra_file_to(book_id, path, relpath, stream_or_path)

@write_api def merge_book_metadata(self, dest_id, src_ids, replace_cover=False, save_alternate_cover=False):
dest_mi = self.get_metadata(dest_id)
merged_identifiers = self._field_for('identifiers', dest_id) or {}
orig_dest_comments = dest_mi.comments dest_cover = orig_dest_cover = self.cover(dest_id)
had_orig_cover = bool(dest_cover)
alternate_covers = []
from calibre.utils.date import is_date_undefined def is_null_date(x):
return x is None or is_date_undefined(x)

for src_id in src_ids:
src_mi = self.get_metadata(src_id)

if src_mi.comments and orig_dest_comments != src_mi.comments:
if not dest_mi.comments:
dest_mi.comments = src_mi.comments else:
dest_mi.comments = str(dest_mi.comments) + '\n\n' + str(src_mi.comments)
if src_mi.title and dest_mi.is_null('title'):
dest_mi.title = src_mi.title dest_mi.title_sort = src_mi.title_sort if (src_mi.authors and src_mi.authors[0] != _('Unknown')) and (not dest_mi.authors or dest_mi.authors[0] == _('Unknown')):
dest_mi.authors = src_mi.authors dest_mi.author_sort = src_mi.author_sort if src_mi.tags:
if not dest_mi.tags:
dest_mi.tags = src_mi.tags else:
dest_mi.tags.extend(src_mi.tags)
if not dest_cover or replace_cover:
src_cover = self.cover(src_id)
if src_cover:
if save_alternate_cover and dest_cover:
alternate_covers.append(dest_cover)
dest_cover = src_cover replace_cover = False elif save_alternate_cover:
src_cover = self.cover(src_id)
if src_cover:
alternate_covers.append(src_cover)
if not dest_mi.publisher:
dest_mi.publisher = src_mi.publisher if not dest_mi.rating:
dest_mi.rating = src_mi.rating if not dest_mi.series:
dest_mi.series = src_mi.series dest_mi.series_index = src_mi.series_index if is_null_date(dest_mi.pubdate) and not is_null_date(src_mi.pubdate):
dest_mi.pubdate = src_mi.pubdate src_identifiers = (src_mi.get_identifiers() or {}).copy()
src_identifiers.update(merged_identifiers)
merged_identifiers = src_identifiers.copy()

if merged_identifiers:
dest_mi.set_identifiers(merged_identifiers)
self._set_metadata(dest_id, dest_mi, ignore_errors=False)

if dest_cover and (not had_orig_cover or dest_cover is not orig_dest_cover):
self._set_cover({dest_id: dest_cover})
if alternate_covers:
existing = {x[0] for x in self._list_extra_files(dest_id)}
h, ext = os.path.splitext(COVER_FILE_NAME)
template = f'{DATA_DIR_NAME}/{h}-{{:03d}}{ext}'
for cdata in alternate_covers:
for i in range(1, 1000):
q = template.format(i)
if q not in existing:
existing.add(q)
self._add_extra_files(dest_id, {q: BytesIO(cdata)}, replace=False, auto_rename=True)
break for key in self.field_metadata: # loop through all defined fields fm = self.field_metadata[key]
if not fm['is_custom']:
continue dt = fm['datatype']
label = fm['label']
try:
field = self.field_metadata.label_to_key(label)
except ValueError:
continue
# Get orig_dest_comments before it gets changed if dt == 'comments':
orig_dest_value = self._field_for(field, dest_id)

for src_id in src_ids:
dest_value = self._field_for(field, dest_id)
src_value = self._field_for(field, src_id)
if (dt == 'comments' and src_value and src_value != orig_dest_value):
if not dest_value:
self._set_field(field, {dest_id: src_value})
else:
dest_value = str(dest_value) + '\n\n' + str(src_value)
self._set_field(field, {dest_id: dest_value})
if (dt in {'bool', 'int', 'float', 'rating', 'datetime'} and dest_value is None):
self._set_field(field, {dest_id: src_value})
if (dt == 'series' and not dest_value and src_value):
src_index = self._field_for(field + '_index', src_id)
self._set_field(field, {dest_id:src_value})
self._set_field(field + '_index', {dest_id:src_index})
if ((dt == 'enumeration' or (dt == 'text' and not fm['is_multiple'])) and not dest_value):
self._set_field(field, {dest_id:src_value})
if (dt == 'text' and fm['is_multiple'] and src_value):
if not dest_value:
dest_value = src_value else:
dest_value = list(dest_value)
dest_value.extend(src_value)
self._set_field(field, {dest_id: dest_value})

@read_api def clone_for_readonly_access(self, dest_dir: str) -> str:
return self.backend.clone_for_readonly_access(dest_dir)def import_library(library_key, importer, library_path, progress=None, abort=None):
from calibre.db.backend import DB metadata = importer.metadata[library_key]
total = metadata['total']
poff = 0 def report_progress(fname):
nonlocal poff if progress is not None:
progress(fname, poff, total)
poff += 1 report_progress('metadata.db')
if abort is not None and abort.is_set():
return importer.save_file(metadata['metadata.db'], 'metadata.db for ' + library_path, os.path.join(library_path, 'metadata.db'))
if 'full-text-search.db' in metadata:
if progress is not None:
progress('full-text-search.db', 1, total)
if abort is not None and abort.is_set():
return poff += 1 importer.save_file(metadata['full-text-search.db'], 'full-text-search.db for ' + library_path, os.path.join(library_path, 'full-text-search.db'))
if abort is not None and abort.is_set():
return if 'notes.db' in metadata:
import zipfile notes_dir = os.path.join(library_path, NOTES_DIR_NAME)
os.makedirs(notes_dir, exist_ok=True)
with importer.start_file(metadata['notes.db'], 'notes.db for ' + library_path) as stream:
stream.check_hash = False with zipfile.ZipFile(stream) as zf:
for zi in zf.infolist():
tpath = zf._extract_member(zi, notes_dir, None)
date_time = mktime(zi.date_time + (0, 0, -1))
os.utime(tpath, (date_time, date_time))
if abort is not None and abort.is_set():
return if importer.corrupted_files:
raise ValueError('Corrupted files:\n' + '\n'.join(importer.corrupted_files))
cache = Cache(DB(library_path, load_user_formatter_functions=False))
cache.init()

format_data = {int(book_id):data for book_id, data in iteritems(metadata['format_data'])}
extra_files = {int(book_id):data for book_id, data in metadata.get('extra_files', {}).items()}
for i, (book_id, fmt_key_map) in enumerate(iteritems(format_data)):
if abort is not None and abort.is_set():
return title = cache._field_for('title', book_id)
if progress is not None:
progress(title, i + poff, total)
cache._update_path((book_id,), mark_as_dirtied=False)
for fmt, fmtkey in fmt_key_map.items():
if fmt == '.cover':
with importer.start_file(fmtkey, _('Cover for %s') % title) as stream:
path = cache._field_for('path', book_id).replace('/', os.sep)
cache.backend.set_cover(book_id, path, stream, no_processing=True)
else:
with importer.start_file(fmtkey, _('{0} format for {1}').format(fmt.upper(), title)) as stream:
size, fname = cache._do_add_format(book_id, fmt, stream, mtime=stream.mtime)
cache.fields['formats'].table.update_fmt(book_id, fmt, fname, size, cache.backend)
for relpath, efkey in extra_files.get(book_id, {}).items():
with importer.start_file(efkey, _('Extra file {0} for book {1}').format(relpath, title)) as stream:
path = cache._field_for('path', book_id).replace('/', os.sep)
cache.backend.add_extra_file(relpath, stream, path)
cache.dump_metadata({book_id})
if importer.corrupted_files:
raise ValueError('Corrupted files:\n' + '\n'.join(importer.corrupted_files))
if progress is not None:
progress(_('Completed'), total, total)
return cache
# }}}

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.devices.interface

Исходный код calibre.devices.interface

__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os from collections import namedtuple from typing import NamedTuple from calibre import prints from calibre.constants import iswindows from calibre.customize import Plugin

FAKE_DEVICE_SERIAL = '__fake_device_for_use_with_connect_to_folder__:'

class ModelMetadata(NamedTuple):
manufacturer_name: str model_name: str vendor_id: int product_id: int bcd: int driver_class: type

@property def settings_key(self) -> str:
return f'{self.manufacturer_name} - {self.model_name}'

def detected_device(self, folder_path):
from calibre.devices.scanner import USBDevice return USBDevice(self.vendor_id, self.product_id, self.bcd, self.manufacturer_name, self.model_name, FAKE_DEVICE_SERIAL + folder_path)

class OpenPopupMessage:

def __init__(self, title='', message='', level='info', skip_dialog_skip_precheck=True):
self.title = title self.message = message self.level = level self.skip_dialog_skip_precheck = skip_dialog_skip_precheck

[документация]
class DevicePlugin(Plugin):
'''
Defines the interface that should be implemented by backends that communicate with an e-book reader.
'''
type = _('Device interface')

#: Ordered list of supported formats
FORMATS = ['lrf', 'rtf', 'pdf', 'txt']
# If True, the config dialog will not show the formats box
HIDE_FORMATS_CONFIG_BOX = False

#: VENDOR_ID can be either an integer, a list of integers or a dictionary
#: If it is a dictionary, it must be a dictionary of dictionaries,
#: of the form::
#:
#: {
#: integer_vendor_id : { product_id : [list of BCDs], ... },
#: ...
#: }
#:
VENDOR_ID = 0x0000

#: An integer or a list of integers
PRODUCT_ID = 0x0000
#: BCD can be either None to not distinguish between devices based on BCD, or
#: it can be a list of the BCD numbers of all devices supported by this driver.
BCD = None

#: Height for thumbnails on the device
THUMBNAIL_HEIGHT = 68

#: Compression quality for thumbnails. Set this closer to 100 to have better
#: quality thumbnails with fewer compression artifacts. Of course, the
#: thumbnails get larger as well.
THUMBNAIL_COMPRESSION_QUALITY = 75

#: Set this to True if the device supports updating cover thumbnails during
#: sync_booklists. Setting it to true will ask device.py to refresh the
#: cover thumbnails during book matching
WANTS_UPDATED_THUMBNAILS = False

#: Whether the metadata on books can be set via the GUI.
CAN_SET_METADATA = ['title', 'authors', 'collections']

#: Whether the device can handle device_db metadata plugboards
CAN_DO_DEVICE_DB_PLUGBOARD = False

# Set this to None if the books on the device are files that the GUI can
# access in order to add the books from the device to the library
BACKLOADING_ERROR_MESSAGE = _('Cannot get files from this device')

#: Path separator for paths to books on device path_sep = os.sep

#: Icon for this device icon = 'reader.png'

# Encapsulates an annotation fetched from the device
UserAnnotation = namedtuple('Annotation','type, value')

#: GUI displays this as a message if not None in the status bar. Useful if opening can take a
#: long time
OPEN_FEEDBACK_MESSAGE = None

#: Set of extensions that are "virtual books" on the device
#: and therefore cannot be viewed/saved/added to library.
#: For example: ``frozenset(['kobo'])``
VIRTUAL_BOOK_EXTENSIONS = frozenset()

#: Message to display to user for virtual book extensions.
VIRTUAL_BOOK_EXTENSION_MESSAGE = None

#: Whether to nuke comments in the copy of the book sent to the device. If
#: not None this should be short string that the comments will be replaced
#: by.
NUKE_COMMENTS = None

#: If True indicates that this driver completely manages device detection,
#: ejecting and so forth. If you set this to True, you *must* implement the
#: detect_managed_devices and debug_managed_device_detection methods.
#: A driver with this set to true is responsible for detection of devices,
#: managing a blacklist of devices, a list of ejected devices and so forth.
#: calibre will periodically call the detect_managed_devices() method and
#: if it returns a detected device, calibre will call open(). open() will
#: be called every time a device is returned even if previous calls to open()
#: failed, therefore the driver must maintain its own blacklist of failed
#: devices. Similarly, when ejecting, calibre will call eject() and then
#: assuming the next call to detect_managed_devices() returns None, it will
#: call post_yank_cleanup().
MANAGES_DEVICE_PRESENCE = False

#: If set the True, calibre will call the :meth:`get_driveinfo()` method
#: after the books lists have been loaded to get the driveinfo.
SLOW_DRIVEINFO = False

#: If set to True, calibre will ask the user if they want to manage the
#: device with calibre, the first time it is detected. If you set this to
#: True you must implement :meth:`get_device_uid()` and
#: :meth:`ignore_connected_device()` and
#: :meth:`get_user_blacklisted_devices` and
#: :meth:`set_user_blacklisted_devices`
ASK_TO_ALLOW_CONNECT = False

#: Set this to a dictionary of the form {'title':title, 'msg':msg, 'det_msg':detailed_msg} to have calibre popup
#: a message to the user after some callbacks are run (currently only upload_books).
#: Be careful to not spam the user with too many messages. This variable is checked after *every* callback,
#: so only set it when you really need to.
user_feedback_after_callback = None

@classmethod def get_gui_name(cls):
if hasattr(cls, 'gui_name'):
return cls.gui_name if hasattr(cls, '__name__'):
return cls.__name__ return cls.name

[документация]
@classmethod def get_open_popup_message(self):
' GUI displays this as a non-modal popup. Should be an instance of OpenPopupMessage '
return

[документация]
@classmethod def model_metadata(self) -> tuple[ModelMetadata, ...]:
' Metadata about all the actual device models this driver supports '
return ()

# Device detection {{{ def test_bcd(self, bcdDevice, bcd):
if bcd is None or len(bcd) == 0:
return True for c in bcd:
if c == bcdDevice:
return True return False

[документация]
def is_usb_connected(self, devices_on_system, debug=False, only_presence=False):
'''
Return True, device_info if a device handled by this plugin is currently connected.

:param devices_on_system: List of devices currently connected

'''
vendors_on_system = {x[0] for x in devices_on_system}
vendors = set(self.VENDOR_ID) if hasattr(self.VENDOR_ID, '__len__') else {self.VENDOR_ID}
if hasattr(self.VENDOR_ID, 'keys'):
products = []
for ven in self.VENDOR_ID:
products.extend(self.VENDOR_ID[ven].keys())
else:
products = self.PRODUCT_ID if hasattr(self.PRODUCT_ID, '__len__') else [self.PRODUCT_ID]

ch = self.can_handle_windows if iswindows else self.can_handle for vid in vendors_on_system.intersection(vendors):
for dev in devices_on_system:
cvid, pid, bcd = dev[:3]
if cvid == vid:
if pid in products:
if hasattr(self.VENDOR_ID, 'keys'):
try:
cbcd = self.VENDOR_ID[vid][pid]
except KeyError:
# Vendor vid does not have product pid, pid
# exists for some other vendor in this
# device continue else:
cbcd = self.BCD if self.test_bcd(bcd, cbcd):
if debug:
prints(dev)
if ch(dev, debug=debug):
return True, dev return False, None

[документация]
def detect_managed_devices(self, devices_on_system, force_refresh=False):
'''
Called only if MANAGES_DEVICE_PRESENCE is True.

Scan for devices that this driver can handle. Should return a device object if a device is found. This object will be passed to the open()
method as the connected_device. If no device is found, return None. The returned object can be anything, calibre does not use it, it is only passed to open().

This method is called periodically by the GUI, so make sure it is not too resource intensive. Use a cache to avoid repeatedly scanning the system.

:param devices_on_system: Set of USB devices found on the system.

:param force_refresh: If True and the driver uses a cache to prevent repeated scanning, the cache must be flushed.

'''
raise NotImplementedError()

[документация]
def debug_managed_device_detection(self, devices_on_system, output):
'''
Called only if MANAGES_DEVICE_PRESENCE is True.

Should write information about the devices detected on the system to output, which is a file like object.

Should return True if a device was detected and successfully opened, otherwise False.
'''
raise NotImplementedError()

# }}}

[документация]
def reset(self, key='-1', log_packets=False, report_progress=None, detected_device=None):
'''
:param key: The key to unlock the device
:param log_packets: If true the packet stream to/from the device is logged
:param report_progress: Function that is called with a % progress
(number between 0 and 100) for various tasks.
If it is called with -1 that means that the task does not have any progress information
:param detected_device: Device information from the device scanner

'''
raise NotImplementedError()

[документация]
def can_handle_windows(self, usbdevice, debug=False):
'''
Optional method to perform further checks on a device to see if this driver is capable of handling it. If it is not it should return False. This method is only called after the vendor, product ids and the bcd have matched, so it can do some relatively time intensive checks. The default implementation returns True. This method is called only on Windows. See also
:meth:`can_handle`.

Note that for devices based on USBMS this method by default delegates to :meth:`can_handle`. So you only need to override :meth:`can_handle` in your subclass of USBMS.

:param usbdevice: A usbdevice as returned by :func:`calibre.devices.winusb.scan_usb_devices`
'''
return True

[документация]
def can_handle(self, device_info, debug=False):
'''
Unix version of :meth:`can_handle_windows`.

:param device_info: Is a tuple of (vid, pid, bcd, manufacturer, product, serial number)

'''

return True
can_handle.is_base_class_implementation = True

[документация]
def open(self, connected_device, library_uuid):
'''
Perform any device specific initialization. Called after the device is detected but before any other functions that communicate with the device.
For example: For devices that present themselves as USB Mass storage devices, this method would be responsible for mounting the device or if the device has been automounted, for finding out where it has been mounted. The method :meth:`calibre.devices.usbms.device.Device.open` has an implementation of this function that should serve as a good example for USB Mass storage devices.

This method can raise an OpenFeedback exception to display a message to the user.

:param connected_device: The device that we are trying to open. It is a tuple of (vendor id, product id, bcd, manufacturer name, product name, device serial number). However, some devices have no serial number and on Windows only the first three fields are present, the rest are None.

:param library_uuid: The UUID of the current calibre library. Can be
None if there is no library (for example when used from the command line).

'''
raise NotImplementedError()

[документация]
def eject(self):
'''
Un-mount / eject the device from the OS. This does not check if there are pending GUI jobs that need to communicate with the device.

NOTE: That this method may not be called on the same thread as the rest of the device methods.
'''
raise NotImplementedError()

[документация]
def post_yank_cleanup(self):
'''
Called if the user yanks the device without ejecting it first.
'''
raise NotImplementedError()

[документация]
def set_progress_reporter(self, report_progress):
'''
Set a function to report progress information.

:param report_progress: Function that is called with a % progress
(number between 0 and 100) for various tasks.
If it is called with -1 that means that the task does not have any progress information

'''
raise NotImplementedError()

[документация]
def get_device_information(self, end_session=True):
'''
Ask device for device information. See L{DeviceInfoQuery}.

:return: (device name, device version, software version on device, MIME type)
The tuple can optionally have a fifth element, which is a drive information dictionary. See usbms.driver for an example.

'''
raise NotImplementedError()

[документация]
def get_driveinfo(self):
'''
Return the driveinfo dictionary. Usually called from get_device_information(), but if loading the driveinfo is slow for this driver, then it should set SLOW_DRIVEINFO. In this case, this method will be called by calibre after the book lists have been loaded. Note that it is not called on the device thread, so the driver should cache the drive info in the books() method and this function should return the cached data.
'''
return {}

[документация]
def card_prefix(self, end_session=True):
'''
Return a 2 element list of the prefix to paths on the cards.
If no card is present None is set for the card's prefix.
E.G.
('/place', '/place2')
(None, 'place2')
('place', None)
(None, None)
'''
raise NotImplementedError()

[документация]
def total_space(self, end_session=True):
'''
Get total space available on the mountpoints:
1. Main memory
2. Memory Card A
3. Memory Card B

:return: A 3 element list with total space in bytes of (1, 2, 3). If a particular device doesn't have any of these locations it should return 0.

'''
raise NotImplementedError()

[документация]
def free_space(self, end_session=True):
'''
Get free space available on the mountpoints:
1. Main memory
2. Card A
3. Card B

:return: A 3 element list with free space in bytes of (1, 2, 3). If a particular device doesn't have any of these locations it should return -1.

'''
raise NotImplementedError()

[документация]
def books(self, oncard=None, end_session=True):
'''
Return a list of e-books on the device.

:param oncard: If 'carda' or 'cardb' return a list of e-books on the specific storage card, otherwise return list of e-books in main memory of device. If a card is specified and no books are on the card return empty list.

:return: A BookList.

'''
raise NotImplementedError()

[документация]
def upload_books(self, files, names, on_card=None, end_session=True, metadata=None):
'''
Upload a list of books to the device. If a file already exists on the device, it should be replaced.
This method should raise a :class:`FreeSpaceError` if there is not enough free space on the device. The text of the FreeSpaceError must contain the word "card" if ``on_card`` is not None otherwise it must contain the word "memory".

:param files: A list of paths
:param names: A list of file names that the books should have once uploaded to the device. len(names) == len(files)
:param metadata: If not None, it is a list of :class:`Metadata` objects.
The idea is to use the metadata to determine where on the device to put the book. len(metadata) == len(files). Apart from the regular cover (path to cover), there may also be a thumbnail attribute, which should be used in preference. The thumbnail attribute is of the form
(width, height, cover_data as jpeg).

:return: A list of 3-element tuples. The list is meant to be passed to :meth:`add_books_to_metadata`.
'''
raise NotImplementedError()

[документация]
@classmethod def add_books_to_metadata(cls, locations, metadata, booklists):
'''
Add locations to the booklists. This function must not communicate with the device.

:param locations: Result of a call to L{upload_books}
:param metadata: List of :class:`Metadata` objects, same as for
:meth:`upload_books`.
:param booklists: A tuple containing the result of calls to
(:meth:`books(oncard=None)`,
:meth:`books(oncard='carda')`,
:meth`books(oncard='cardb')`).

'''
raise NotImplementedError()

[документация]
def delete_books(self, paths, end_session=True):
'''
Delete books at paths on device.
'''
raise NotImplementedError()

[документация]
@classmethod def remove_books_from_metadata(cls, paths, booklists):
'''
Remove books from the metadata list. This function must not communicate with the device.

:param paths: paths to books on the device.
:param booklists: A tuple containing the result of calls to
(:meth:`books(oncard=None)`,
:meth:`books(oncard='carda')`,
:meth`books(oncard='cardb')`).

'''
raise NotImplementedError()

[документация]
def sync_booklists(self, booklists, end_session=True):
'''
Update metadata on device.

:param booklists: A tuple containing the result of calls to
(:meth:`books(oncard=None)`,
:meth:`books(oncard='carda')`,
:meth`books(oncard='cardb')`).

'''
raise NotImplementedError()

[документация]
def get_file(self, path, outfile, end_session=True):
'''
Read the file at ``path`` on the device and write it to outfile.

:param outfile: file object like ``sys.stdout`` or the result of an
:func:`open` call.

'''
raise NotImplementedError()

[документация]
@classmethod def config_widget(cls):
'''
Should return a QWidget. The QWidget contains the settings for the device interface
'''
raise NotImplementedError()

[документация]
@classmethod def save_settings(cls, settings_widget):
'''
Should save settings to disk. Takes the widget created in
:meth:`config_widget` and saves all settings to disk.
'''
raise NotImplementedError()

[документация]
@classmethod def settings(cls):
'''
Should return an opts object. The opts object should have at least one attribute `format_map` which is an ordered list of formats for the device.
'''
raise NotImplementedError()

[документация]
def set_plugboards(self, plugboards, pb_func):
'''
provide the driver the current set of plugboards and a function to select a specific plugboard. This method is called immediately before add_books and sync_booklists.

pb_func is a callable with the following signature::
def pb_func(device_name, format, plugboards)

You give it the current device name (either the class name or
DEVICE_PLUGBOARD_NAME), the format you are interested in (a 'real'
format or 'device_db'), and the plugboards (you were given those by set_plugboards, the same place you got this method).

:return: None or a single plugboard instance.

'''
pass

[документация]
def set_driveinfo_name(self, location_code, name):
'''
Set the device name in the driveinfo file to 'name'. This setting will persist until the file is re-created or the name is changed again.

Non-disk devices should implement this method based on the location codes returned by the get_device_information() method.
'''
pass

[документация]
def prepare_addable_books(self, paths):
'''
Given a list of paths, returns another list of paths. These paths point to addable versions of the books.

If there is an error preparing a book, then instead of a path, the position in the returned list for that book should be a three tuple:
(original_path, the exception instance, traceback)
'''
return paths

[документация]
def startup(self):
'''
Called when calibre is starting the device. Do any initialization required. Note that multiple instances of the class can be instantiated, and thus __init__ can be called multiple times, but only one instance will have this method called. This method is called on the device thread, not the GUI thread.
'''
pass

[документация]
def shutdown(self):
'''
Called when calibre is shutting down, either for good or in preparation to restart. Do any cleanup required. This method is called on the device thread, not the GUI thread.
'''
pass

[документация]
def get_device_uid(self):
'''
Must return a unique id for the currently connected device (this is called immediately after a successful call to open()). You must implement this method if you set ASK_TO_ALLOW_CONNECT = True
'''
raise NotImplementedError()

[документация]
def ignore_connected_device(self, uid):
'''
Should ignore the device identified by uid (the result of a call to get_device_uid()) in the future. You must implement this method if you set ASK_TO_ALLOW_CONNECT = True. Note that this function is called immediately after open(), so if open() caches some state, the driver should reset that state.
'''
raise NotImplementedError()

[документация]
def get_user_blacklisted_devices(self):
'''
Return map of device uid to friendly name for all devices that the user has asked to be ignored.
'''
return {}

[документация]
def set_user_blacklisted_devices(self, devices):
'''
Set the list of device uids that should be ignored by this driver.
'''
pass

[документация]
def specialize_global_preferences(self, device_prefs):
'''
Implement this method if your device wants to override a particular preference. You must ensure that all call sites that want a preference that can be overridden use device_prefs['something'] instead of prefs['something']. Your method should call device_prefs.set_overrides(pref=val, pref=val, ...).
Currently used for:
metadata management (prefs['manage_device_metadata'])
'''
device_prefs.set_overrides()

[документация]
def set_library_info(self, library_name, library_uuid, field_metadata):
'''
Implement this method if you want information about the current calibre library. This method is called at startup and when the calibre library changes while connected.
'''
pass

# Dynamic control interface.
# The following methods are probably called on the GUI thread. Any driver
# that implements these methods must take pains to be thread safe, because
# the device_manager might be using the driver at the same time that one of
# these methods is called.

[документация]
def is_dynamically_controllable(self):
'''
Called by the device manager when starting plugins. If this method returns a string, then a) it supports the device manager's dynamic control interface, and b) that name is to be used when talking to the plugin.

This method can be called on the GUI thread. A driver that implements this method must be thread safe.
'''
return

[документация]
def start_plugin(self):
'''
This method is called to start the plugin. The plugin should begin to accept device connections however it does that. If the plugin is already accepting connections, then do nothing.

This method can be called on the GUI thread. A driver that implements this method must be thread safe.
'''
pass

[документация]
def stop_plugin(self):
'''
This method is called to stop the plugin. The plugin should no longer accept connections, and should cleanup behind itself. It is likely that this method should call shutdown. If the plugin is already not accepting connections, then do nothing.

This method can be called on the GUI thread. A driver that implements this method must be thread safe.
'''
pass

[документация]
def get_option(self, opt_string, default=None):
'''
Return the value of the option indicated by opt_string. This method can be called when the plugin is not started. Return None if the option does not exist.

This method can be called on the GUI thread. A driver that implements this method must be thread safe.
'''
return default

[документация]
def set_option(self, opt_string, opt_value):
'''
Set the value of the option indicated by opt_string. This method can be called when the plugin is not started.

This method can be called on the GUI thread. A driver that implements this method must be thread safe.
'''
pass

[документация]
def is_running(self):
'''
Return True if the plugin is started, otherwise false

This method can be called on the GUI thread. A driver that implements this method must be thread safe.
'''
return False

[документация]
def synchronize_with_db(self, db, book_id, book_metadata, first_call):
'''
Called during book matching when a book on the device is matched with a book in calibre's db. The method is responsible for synchronizing data from the device to calibre's db (if needed).

The method must return a two-value tuple. The first value is a set of calibre book ids changed if calibre's database was changed or None if the database was not changed. If the first value is an empty set then the metadata for the book on the device is updated with calibre's metadata and given back to the device, but no GUI refresh of that book is done.
This is useful when the calibre data is correct but must be sent to the device.

The second value is itself a 2-value tuple. The first value in the tuple specifies whether a book format should be sent to the device. The intent is to permit verifying that the book on the device is the same as the book in calibre. This value must be None if no book is to be sent, otherwise return the base file name on the device (a string like foobar.epub). Be sure to include the extension in the name. The device subsystem will construct a send_books job for all books with not- None returned values. Note: other than to later retrieve the extension, the name is ignored in cases where the device uses a template to generate the file name, which most do. The second value in the returned tuple indicated whether the format is future-dated. Return True if it is, otherwise return False. calibre will display a dialog to the user listing all future dated books.

Extremely important: this method is called on the GUI thread. It must be threadsafe with respect to the device manager's thread.

book_id: the calibre id for the book in the database.
book_metadata: the Metadata object for the book coming from the device.
first_call: True if this is the first call during a sync, False otherwise
'''
return (None, (None, False))

[документация]
class BookList(list):
'''
A list of books. Each Book object must have the fields

#. title
#. authors
#. size (file size of the book)
#. datetime (a UTC time tuple)
#. path (path on the device to the book)
#. thumbnail (can be None) thumbnail is either a str/bytes object with the image data or it should have an attribute image_path that stores an absolute (platform native) path to the image
#. tags (a list of strings, can be empty).

'''

__getslice__ = None
__setslice__ = None def __init__(self, oncard, prefix, settings):
pass

[документация]
def supports_collections(self):
''' Return True if the device supports collections for this book list. '''
raise NotImplementedError()

[документация]
def add_book(self, book, replace_metadata):
'''
Add the book to the booklist. Intent is to maintain any device-internal metadata. Return True if booklists must be sync'ed
'''
raise NotImplementedError()

[документация]
def remove_book(self, book):
'''
Remove a book from the booklist. Correct any device metadata at the same time
'''
raise NotImplementedError()

[документация]
def get_collections(self, collection_attributes):
'''
Return a dictionary of collections created from collection_attributes.
Each entry in the dictionary is of the form collection name:[list of books]

The list of books is sorted by book title, except for collections created from series, in which case series_index is used.

:param collection_attributes: A list of attributes of the Book object

'''
raise NotImplementedError()

class CurrentlyConnectedDevice:

def __init__(self):
self._device = None

@property def device(self):
return self._device

# A device driver can check if a device is currently connected to calibre using
# the following code::
# from calibre.device.interface import currently_connected_device
# if currently_connected_device.device is None:
# # no device connected
# The device attribute will be either None or the device driver object
# (DevicePlugin instance) for the currently connected device.
currently_connected_device = CurrentlyConnectedDevice()

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.devices.usbms.cli

Исходный код calibre.devices.usbms.cli

__license__ = 'GPL v3'
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'

import os import shutil import time from calibre import fsync from calibre.devices.errors import PathError from calibre.utils.filenames import case_preserving_open_file class File:

def __init__(self, path):
stats = os.stat(path)
self.is_dir = os.path.isdir(path)
self.is_readonly = not os.access(path, os.W_OK)
self.ctime = stats.st_ctime self.wtime = stats.st_mtime self.size = stats.st_size if path.endswith(os.sep):
path = path[:-1]
self.path = path self.name = os.path.basename(path)

def check_transfer(infile, dest):
infile.seek(0)
dest.seek(0)
return infile.read() == dest.read()

[документация]
class CLI:

def get_file(self, path, outfile, end_session=True):
path = self.munge_path(path)
with open(path, 'rb') as src:
shutil.copyfileobj(src, outfile)

def put_file(self, infile, path, replace_file=False, end_session=True):
path = self.munge_path(path)
close = False if not hasattr(infile, 'read'):
infile, close = open(infile, 'rb'), True infile.seek(0)
if os.path.isdir(path):
path = os.path.join(path, infile.name)
if not replace_file and os.path.exists(path):
raise PathError('File already exists: ' + path)
dest, actual_path = case_preserving_open_file(path)
with dest:
try:
shutil.copyfileobj(infile, dest)
except OSError:
print('WARNING: First attempt to send file to device failed')
time.sleep(0.2)
infile.seek(0)
dest.seek(0)
dest.truncate()
shutil.copyfileobj(infile, dest)
fsync(dest)
# if not check_transfer(infile, dest): raise Exception('Transfer failed')
if close:
infile.close()
return actual_path def munge_path(self, path):
if path.startswith('/') and not (path.startswith(self._main_prefix) or
(self._card_a_prefix and path.startswith(self._card_a_prefix)) or
(self._card_b_prefix and path.startswith(self._card_b_prefix))):
path = self._main_prefix + path[1:]
elif path.startswith('carda:'):
path = path.replace('carda:', self._card_a_prefix[:-1])
elif path.startswith('cardb:'):
path = path.replace('cardb:', self._card_b_prefix[:-1])
return path def list(self, path, recurse=False, end_session=True, munge=True):
if munge:
path = self.munge_path(path)
if os.path.isfile(path):
return [(os.path.dirname(path), [File(path)])]
entries = [File(os.path.join(path, f)) for f in os.listdir(path)]
dirs = [(path, entries)]
for _file in entries:
if recurse and _file.is_dir:
dirs[len(dirs):] = self.list(_file.path, recurse=True, munge=False)
return dirs def mkdir(self, path, end_session=True):
if self.SUPPORTS_SUB_DIRS:
path = self.munge_path(path)
os.mkdir(path)

def rm(self, path, end_session=True):
path = self.munge_path(path)
self.delete_books([path])

def touch(self, path, end_session=True):
path = self.munge_path(path)
if not os.path.exists(path):
open(path, 'wb').close()
if not os.path.isdir(path):
os.utime(path, None)

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.devices.usbms.device

Исходный код calibre.devices.usbms.device

__license__ = 'GPL v3'
__copyright__ = ('2009, John Schember <john at nachtimwald.com> '
'2009, Kovid Goyal <kovid@kovidgoyal.net>')
__docformat__ = 'restructuredtext en'

'''
Generic device driver. This is not a complete stand alone driver. It is intended to be subclassed with the relevant parts implemented for a particular device. This class handles device detection.
'''

import glob import os import re import subprocess import sys import time from collections import namedtuple from contextlib import suppress from itertools import repeat from calibre import prints from calibre.constants import is_debugging, isfreebsd, islinux, ismacos, iswindows from calibre.devices.errors import DeviceError from calibre.devices.interface import FAKE_DEVICE_SERIAL, DevicePlugin, ModelMetadata from calibre.devices.usbms.deviceconfig import DeviceConfig from calibre.utils.filenames import ascii_filename as sanitize from polyglot.builtins import iteritems, string_or_bytes if ismacos:
osx_sanitize_name_pat = re.compile(r'[.-]')

if iswindows:
usb_info_cache = {}

def eject_exe():
base = sys.extensions_location if hasattr(sys, 'new_app_layout') else os.path.dirname(sys.executable)
return os.path.join(base, 'calibre-eject.exe')

class USBDevice:

def __init__(self, dev):
self.idVendor = dev[0]
self.idProduct = dev[1]
self.bcdDevice = dev[2]
if iswindows:
# Getting this information requires communicating with the device
# we only do that in the can_handle_windows() method, if needed.
self.manufacturer = self.serial = self.product = None else:
self.manufacturer = dev[3]
self.product = dev[4]
self.serial = dev[5]

def match_serial(self, serial):
return self.serial and self.serial == serial def match_numbers(self, vid, pid, bcd):
return self.idVendor == vid and self.idProduct == pid and self.bcdDevice == bcd def match_strings(self, vid, pid, bcd, man, prod):
if not self.match_numbers(vid, pid, bcd):
return False if man == self.manufacturer and prod == self.product:
return True
# As of macOS 10.11.4 Apple started mangling the names returned via the
# IOKit registry. See
# https://www.mobileread.com/forums/showthread.php?t=273213 m = osx_sanitize_name_pat.sub('_', (self.manufacturer or ''))
p = osx_sanitize_name_pat.sub('_', (self.product or ''))
return m == man and p == prod

[документация]
class Device(DeviceConfig, DevicePlugin):
'''
This class provides logic common to all drivers for devices that export themselves as USB Mass Storage devices. Provides implementations for mounting/ejecting of USBMS devices on all platforms.
'''

VENDOR_ID = 0x0
PRODUCT_ID = 0x0
BCD = None

VENDOR_NAME = None

#: String identifying the main memory of the device in the Windows PnP id
#: strings
#: This can be None, string, list of strings or compiled regex
WINDOWS_MAIN_MEM = None

#: String identifying the first card of the device in the Windows PnP id
#: strings
#: This can be None, string, list of strings or compiled regex
WINDOWS_CARD_A_MEM = None

#: String identifying the second card of the device in the Windows PnP id
#: strings
#: This can be None, string, list of strings or compiled regex
WINDOWS_CARD_B_MEM = None

#: Used by the new driver detection to disambiguate main memory from
#: storage cards. Should be a regular expression that matches the
#: main memory mount point assigned by macOS
OSX_MAIN_MEM_VOL_PAT = None
OSX_EJECT_COMMAND = ['diskutil', 'eject']

MAIN_MEMORY_VOLUME_LABEL = ''
STORAGE_CARD_VOLUME_LABEL = ''
STORAGE_CARD2_VOLUME_LABEL = None

EBOOK_DIR_MAIN = ''
EBOOK_DIR_CARD_A = ''
EBOOK_DIR_CARD_B = ''
DELETE_EXTS = []

# USB disk-based devices can see the book files on the device, so can
# copy these back to the library
BACKLOADING_ERROR_MESSAGE = None

#: The maximum length of paths created on the device
MAX_PATH_LEN = 250

#: Put news in its own folder
NEWS_IN_FOLDER = True connected_folder_path = '' # used internally for fake folder device eject_connected_folder = False

[документация]
@classmethod def model_metadata(cls) -> tuple[ModelMetadata, ...]:
def get_representative_ids() -> tuple[int, int, int]:
vid = pid = bcd = 0 if isinstance(cls.VENDOR_ID, dict):
for vid, pid_map in cls.VENDOR_ID.items():
for pid, bcds in pid_map.items():
if isinstance(bcds, int):
bcds = (bcds,)
for bcd in bcds:
return vid or 0, pid or 0, bcd or 0 elif isinstance(cls.VENDOR_ID, (list, tuple)):
vid = cls.VENDOR_ID[-1]
else:
vid = cls.VENDOR_ID if isinstance(cls.PRODUCT_ID, (list, tuple)):
pid = cls.PRODUCT_ID[-1]
else:
pid = cls.PRODUCT_ID if isinstance(cls.BCD, (list, tuple)):
bcd = cls.BCD[-1]
else:
bcd = cls.BCD return vid or 0, pid or 0, bcd or 0 vid, pid, bcd = get_representative_ids()
try:
model_name = cls.get_gui_name()
except TypeError: # The WAYTEQ driver implements this as non classmethod return ()
parts = model_name.split(' ', 1)
manufacturer = ''
if len(parts) > 1:
manufacturer, model_name = parts else:
manufacturer = _('Miscellaneous')
return (
ModelMetadata(manufacturer, model_name, vid, pid, bcd, cls),
)

[документация]
def reset(self, key='-1', log_packets=False, report_progress=None, detected_device=None):
self._main_prefix = self._card_a_prefix = self._card_b_prefix = None self.detected_device = None if detected_device is None else USBDevice(detected_device)
self.set_progress_reporter(report_progress)

[документация]
def set_progress_reporter(self, report_progress):
self.report_progress = report_progress self.report_progress = report_progress if self.report_progress is None:
self.report_progress = lambda x, y: x

[документация]
def card_prefix(self, end_session=True):
return (self._card_a_prefix, self._card_b_prefix)

@classmethod def _windows_space(cls, prefix):
if not prefix:
return 0, 0 prefix = prefix[:-1]
from calibre_extensions import winutil try:
available_space, total_space, free_space = winutil.get_disk_free_space(prefix)
except OSError as err:
if err.winerror == winutil.ERROR_NOT_READY:
# Disk not ready time.sleep(3)
available_space, total_space, free_space = winutil.get_disk_free_space(prefix)
else:
raise return total_space, available_space

[документация]
def total_space(self, end_session=True):
msz = casz = cbsz = 0 if not iswindows:
if self._main_prefix is not None:
stats = os.statvfs(self._main_prefix)
msz = stats.f_frsize * (stats.f_blocks + stats.f_bavail - stats.f_bfree)
if self._card_a_prefix is not None:
stats = os.statvfs(self._card_a_prefix)
casz = stats.f_frsize * (stats.f_blocks + stats.f_bavail - stats.f_bfree)
if self._card_b_prefix is not None:
stats = os.statvfs(self._card_b_prefix)
cbsz = stats.f_frsize * (stats.f_blocks + stats.f_bavail - stats.f_bfree)
else:
msz = self._windows_space(self._main_prefix)[0]
casz = self._windows_space(self._card_a_prefix)[0]
cbsz = self._windows_space(self._card_b_prefix)[0]

return msz, casz, cbsz

[документация]
def free_space(self, end_session=True):
msz = casz = cbsz = 0 if not iswindows:
if self._main_prefix is not None:
stats = os.statvfs(self._main_prefix)
msz = stats.f_frsize * stats.f_bavail if self._card_a_prefix is not None:
stats = os.statvfs(self._card_a_prefix)
casz = stats.f_frsize * stats.f_bavail if self._card_b_prefix is not None:
stats = os.statvfs(self._card_b_prefix)
cbsz = stats.f_frsize * stats.f_bavail else:
msz = self._windows_space(self._main_prefix)[1]
casz = self._windows_space(self._card_a_prefix)[1]
cbsz = self._windows_space(self._card_b_prefix)[1]

return msz, casz, cbsz
def windows_filter_pnp_id(self, pnp_id):
return False

[документация]
def windows_sort_drives(self, drives):
'''
Called to disambiguate main memory and storage card for devices that do not distinguish between them on the basis of `WINDOWS_CARD_NAME`.
For example: The EB600
'''
return drives

[документация]
def can_handle_windows(self, usbdevice, debug=False):
if hasattr(self.can_handle, 'is_base_class_implementation'):
# No custom can_handle implementation return True
# Delegate to the unix can_handle function, creating a unix like
# USBDevice object from calibre.devices.winusb import get_usb_info dev = usb_info_cache.get(usbdevice)
if dev is None:
try:
data = get_usb_info(usbdevice, debug=debug)
except Exception:
time.sleep(0.1)
try:
data = get_usb_info(usbdevice, debug=debug)
except Exception:
data = {}
dev = usb_info_cache[usbdevice] = namedtuple(
'USBDevice', 'vendor_id product_id bcd manufacturer product serial')( usbdevice.vendor_id, usbdevice.product_id, usbdevice.bcd, data.get('manufacturer') or '', data.get('product') or '', data.get('serial_number') or '')
if debug:
prints(f'USB Info for device: {dev}')
return self.can_handle(dev, debug=debug)

def open_windows(self):
from calibre.devices.scanner import drive_is_ok from calibre.devices.winusb import get_drive_letters_for_device usbdev = self.device_being_opened debug = is_debugging() or getattr(self, 'do_device_debug', False)
try:
dlmap = get_drive_letters_for_device(usbdev, debug=debug)
except Exception:
dlmap = {}

if not dlmap.get('drive_letters'):
time.sleep(7)
dlmap = get_drive_letters_for_device(usbdev, debug=debug)

if debug:
from pprint import pformat prints(f'Drive letters for {usbdev}')
prints(pformat(dlmap))

filtered = set()
for dl in dlmap['drive_letters']:
pnp_id = dlmap['pnp_id_map'][dl].upper()
if dl in dlmap['readonly_drives']:
filtered.add(dl)
if debug:
prints(f'Ignoring the drive {dl} as it is readonly')
elif self.windows_filter_pnp_id(pnp_id):
filtered.add(dl)
if debug:
prints(f'Ignoring the drive {dl} because of a PNP filter on {pnp_id}')
elif not drive_is_ok(dl, debug=debug):
filtered.add(dl)
if debug:
prints(f'Ignoring the drive {dl} because failed to get free space for it')
dlmap['drive_letters'] = [dl for dl in dlmap['drive_letters'] if dl not in filtered]

if not dlmap['drive_letters']:
raise DeviceError(_('Unable to detect any disk drives for the device: %s. Try rebooting') % self.get_gui_name())

drives = {}

for drive_letter, which in zip(dlmap['drive_letters'], 'main carda cardb'.split()):
drives[which] = drive_letter + ':\\'

drives = self.windows_sort_drives(drives)
self._main_prefix = drives.get('main')
self._card_a_prefix = drives.get('carda', None)
self._card_b_prefix = drives.get('cardb', None)

@classmethod def run_ioreg(cls, raw=None):
if raw is not None:
return raw ioreg = '/usr/sbin/ioreg'
if not os.access(ioreg, os.X_OK):
ioreg = 'ioreg'
cmd = (ioreg+' -w 0 -S -c IOMedia').split()
for i in range(3):
try:
return subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
except OSError: # Probably an interrupted system call if i == 2:
raise time.sleep(2)

def osx_sort_names(self, names):
return names

@classmethod def osx_run_mount(cls):
for i in range(3):
try:
return subprocess.Popen('mount', stdout=subprocess.PIPE).communicate()[0].decode('utf-8', 'replace')
except OSError: # Probably an interrupted system call if i == 2:
raise time.sleep(2)

@classmethod def osx_get_usb_drives(cls):
from calibre_extensions.usbobserver import get_usb_drives return get_usb_drives()

def _osx_bsd_names(self):
drives = self.osx_get_usb_drives()
matches = []
d = self.detected_device if d.serial:
for path, vid, pid, bcd, ven, prod, serial in drives:
if d.match_serial(serial):
matches.append(path)
if not matches and d.manufacturer and d.product:
for path, vid, pid, bcd, man, prod, serial in drives:
if d.match_strings(vid, pid, bcd, man, prod):
matches.append(path)
if not matches:
# Since Apple started mangling the names stored in the IOKit
# registry, we cannot trust match_strings() so fallback to matching
# on just numbers. See http://www.mobileread.com/forums/showthread.php?t=273213 for path, vid, pid, bcd, man, prod, serial in drives:
if d.match_numbers(vid, pid, bcd):
matches.append(path)
if not matches:
from pprint import pformat raise DeviceError( f'Could not detect BSD names for {self.name}. Try rebooting.\nOutput from osx_get_usb_drives():\n{pformat(drives)}')

pat = re.compile(r'(?P<m>\d+)([a-z]+(?P<p>\d+)){0,1}')

def nums(x):
'Return (disk num, partition number)'
m = pat.search(x)
if m is None:
return (10000, -1)
g = m.groupdict()
if g['p'] is None:
g['p'] = 0 return list(map(int, (g.get('m'), g.get('p'))))

def cmp_key(x):
'''
Sorting based on the following scheme:
- disks without partitions are first
- sub sorted based on disk number
- disks with partitions are sorted first on disk number, then on partition number
'''
x = x.rpartition('/')[-1]
disk_num, part_num = nums(x)
has_part = 1 if part_num > 0 else 0 return has_part, disk_num, part_num matches.sort(key=cmp_key)
drives = {'main':matches[0]}
if len(matches) > 1:
drives['carda'] = matches[1]
if len(matches) > 2:
drives['cardb'] = matches[2]

return drives def osx_bsd_names(self):
drives = {}
for i in range(3):
try:
drives = self._osx_bsd_names()
if len(drives) > 1: # wait for device to settle and SD card (if any) to become available return drives except Exception:
if i == 2:
raise time.sleep(3)
return drives def open_osx(self):
from calibre_extensions.usbobserver import get_mounted_filesystems bsd_drives = self.osx_bsd_names()
drives = self.osx_sort_names(bsd_drives.copy())
mount_map = get_mounted_filesystems()
# macOS 13 Ventura uses a weird scheme for mounted FAT devices of the
# form fat://basename_of_bsd_name/basename_of_mountpoint
# see https://www.mobileread.com/forums/showthread.php?t=347294 for dev_node in tuple(mount_map):
if ':' in dev_node and '//' in dev_node:
val = mount_map[dev_node]
dev_node = dev_node.split('/')[-2]
dev_node = f'/dev/{dev_node}'
if dev_node not in mount_map:
mount_map[dev_node] = val drives = {k: mount_map.get(v) for k, v in iteritems(drives)}
if is_debugging():
print()
from pprint import pprint pprint({'bsd_drives': bsd_drives, 'mount_map': mount_map, 'drives': drives})
if drives.get('carda') is None and drives.get('cardb') is not None:
drives['carda'] = drives.pop('cardb')
if drives.get('main') is None and drives.get('carda') is not None:
drives['main'] = drives.pop('carda')
if drives.get('carda') is None and drives.get('cardb') is not None:
drives['carda'] = drives.pop('cardb')
if drives.get('main') is None:
raise DeviceError(_('Unable to detect the %s mount point. Try rebooting.')%self.__class__.__name__)
pat = self.OSX_MAIN_MEM_VOL_PAT if pat is not None and len(drives) > 1 and 'main' in drives:
if pat.search(drives['main']) is None:
main = drives['main']
for x in ('carda', 'cardb'):
if x in drives and pat.search(drives[x]):
drives['main'] = drives.pop(x)
drives[x] = main break self._main_prefix = drives['main']+os.sep def get_card_prefix(c):
ans = drives.get(c, None)
if ans is not None:
ans += os.sep return ans self._card_a_prefix = get_card_prefix('carda')
self._card_b_prefix = get_card_prefix('cardb')

def find_device_nodes(self, detected_device=None):

def walk(base):
base = os.path.abspath(os.path.realpath(base))
for x in os.listdir(base):
p = os.path.join(base, x)
if os.path.islink(p) or not os.access(p, os.R_OK):
continue isfile = os.path.isfile(p)
yield p, isfile if not isfile:
yield from walk(p)

def raw2num(raw):
raw = raw.lower()
if not raw.startswith('0x'):
raw = '0x' + raw return int(raw, 16)

# Find device node based on vendor, product and bcd d, j = os.path.dirname, os.path.join usb_dir = None if detected_device is None:
detected_device = self.detected_device def test(val, attr):
q = getattr(detected_device, attr)
return q == val def getnum(usb_dir):
def rc(q):
with open(j(usb_dir, q), 'rb') as f:
return raw2num(f.read().decode('utf-8'))
return rc for x, isfile in walk('/sys/devices'):
if isfile and x.endswith('idVendor'):
usb_dir = d(x)
for y in ('idProduct', 'idVendor', 'bcdDevice'):
if not os.access(j(usb_dir, y), os.R_OK):
usb_dir = None break if usb_dir is None:
continue ven, prod, bcd = map(getnum(usb_dir), ('idVendor', 'idProduct', 'bcdDevice'))
if not (test(ven, 'idVendor') and test(prod, 'idProduct') and test(bcd, 'bcdDevice')):
usb_dir = None continue else:
break if usb_dir is None:
raise DeviceError(_('Unable to detect the %s disk drive.')
%self.__class__.__name__)

devnodes, ok = [], {}
for x, isfile in walk(usb_dir):
if not isfile and '/block/' in x:
parts = x.split('/')
idx = parts.index('block')
if idx == len(parts)-2:
sz = j(x, 'size')
node = parts[idx+1]
try:
with open(sz, 'rb') as szf:
exists = int(szf.read().decode('utf-8')) > 0 if exists:
node = self.find_largest_partition(x)
ok[node] = True else:
ok[node] = False except Exception:
ok[node] = False if is_debugging() and not ok[node]:
print(f'\nIgnoring the node: {node} as could not read size from: {sz}')

devnodes.append(node)

devnodes += list(repeat(None, 3))
ans = ['/dev/'+x if ok.get(x) else None for x in devnodes]
ans.sort(key=lambda x: x[5:] if x else 'zzzzz')
return self.linux_swap_drives(ans[:3])

def linux_swap_drives(self, drives):
return drives def node_mountpoint(self, node):
from calibre.devices.udisks import node_mountpoint return node_mountpoint(node)

def find_largest_partition(self, path):
node = path.split('/')[-1]
nodes = []
for x in glob.glob(path+'/'+node+'*'):
sz = x + '/size'

if not os.access(sz, os.R_OK):
continue try:
with open(sz, 'rb') as szf:
sz = int(szf.read().decode('utf-8'))
except Exception:
continue if sz > 0:
nodes.append((x.split('/')[-1], sz))

nodes.sort(key=lambda x: x[1])
if not nodes:
return node return nodes[-1][0]

def open_linux(self):

def mount(node, type):
mp = self.node_mountpoint(node)
if mp is not None:
return mp, 0 def do_mount(node):
try:
from calibre.devices.udisks import mount mount(node)
return 0 except Exception:
print('Udisks mount call failed:')
import traceback traceback.print_exc()
return 1 ret = do_mount(node)
if ret != 0:
return None, ret return self.node_mountpoint(node)+'/', 0 main, carda, cardb = self.find_device_nodes()
if main is None:
raise DeviceError(_('Unable to detect the %s disk drive. Either '
'the device has already been ejected, or your '
'kernel is exporting a deprecated version of SYSFS.')
%self.__class__.__name__)
if is_debugging():
print('\nFound device nodes:', main, carda, cardb)

self._linux_mount_map = {}
mp, ret = mount(main, 'main')
if mp is None:
raise DeviceError(
_('Unable to mount main memory (Error code: %d)')%ret)
if not mp.endswith('/'):
mp += '/'
self._linux_mount_map[main] = mp self._main_prefix = mp self._linux_main_device_node = main cards = [(carda, '_card_a_prefix', 'carda'),
(cardb, '_card_b_prefix', 'cardb')]
for card, prefix, typ in cards:
if card is None:
continue mp, ret = mount(card, typ)
if mp is None:
print(f'Unable to mount card (Error code: {ret})', file=sys.stderr)
else:
if not mp.endswith('/'):
mp += '/'
setattr(self, prefix, mp)
self._linux_mount_map[card] = mp self.filter_read_only_mount_points()

def filter_read_only_mount_points(self):

def is_readonly(mp):
if mp is None:
return True path = os.path.join(mp, 'calibre_readonly_test')
ro = True try:
with open(path, 'wb'):
ro = False except Exception:
pass else:
try:
os.remove(path)
except Exception:
pass if is_debugging() and ro:
print('\nThe mountpoint', mp, 'is readonly, ignoring it')
return ro for mp in ('_main_prefix', '_card_a_prefix', '_card_b_prefix'):
if is_readonly(getattr(self, mp, None)):
setattr(self, mp, None)

if self._main_prefix is None:
for p in ('_card_a_prefix', '_card_b_prefix'):
nmp = getattr(self, p, None)
if nmp is not None:
self._main_prefix = nmp setattr(self, p, None)
break if self._main_prefix is None:
raise DeviceError(_('The main memory of %s is read only. '
'This usually happens because of file system errors.')
%self.__class__.__name__)

if self._card_a_prefix is None and self._card_b_prefix is not None:
self._card_a_prefix = self._card_b_prefix self._card_b_prefix = None

# ------------------------------------------------------
#
# open for FreeBSD
# find the device node or nodes that match the S/N we already have from the scanner
# and attempt to mount each one
# 1. get list of devices via DBUS UDisk2 with matching s/n etc.
# 2. get list of volumes associated with each
# 3. attempt to mount each one using UDisks2
# 4. when finished, we have a list of mount points and associated dbus nodes
# def open_freebsd(self):
from calibre.devices.udisks import find_device_vols_by_serial

# There should be some way to access the -v arg...
verbose = False

# this gives us access to the S/N, etc. of the reader that the scanner has found
# and the match routines for some of that data, like s/n, vendor ID, etc.
d=self.detected_device if not d.serial:
raise DeviceError("Device has no S/N. Can't continue")

vols = find_device_vols_by_serial(d.serial)

if verbose:
print('FBSD:\t', vols)

ok, mv = self.freebsd_mount_volumes(vols)
if not ok:
raise DeviceError(_('Unable to mount the device'))
for k, v in mv.items():
setattr(self, k, v)

def freebsd_mount_volumes(self, vols):
def fmount(node):
mp = self.node_mountpoint(node)
if mp is not None:
# Already mounted return mp from calibre.devices.udisks import mount, rescan for i in range(6):
try:
mp = mount(node)
break except Exception:
if i < 5:
rescan(node)
time.sleep(1)
else:
print('Udisks mount call failed:')
import traceback traceback.print_exc()

return mp mp = None mtd = 0 ans = {
'_main_prefix': None, '_main_vol': None,
'_card_a_prefix': None, '_card_a_vol': None,
'_card_b_prefix': None, '_card_b_vol': None,
}
for vol in vols:
try:
mp = fmount(vol['Device'])
except Exception:
print('Failed to mount: ' + vol['Device'])
import traceback traceback.print_exc()

if mp is None:
continue

# Mount Point becomes Mount Path mp += '/'
DEBUG = is_debugging()
if DEBUG:
print('FBSD:\tmounted', vol['Device'], 'on', mp)
if mtd == 0:
ans['_main_prefix'], ans['_main_vol'] = mp, vol['Device']
if DEBUG:
print('FBSD:\tmain = ', mp)
elif mtd == 1:
ans['_card_a_prefix'], ans['_card_a_vol'] = mp, vol['Device']
if DEBUG:
print('FBSD:\tcard a = ', mp)
elif mtd == 2:
ans['_card_b_prefix'], ans['_card_b_vol'] = mp, vol['Device']
if DEBUG:
print('FBSD:\tcard b = ', mp)
break mtd += 1 return mtd > 0, ans

#
# ------------------------------------------------------
#
# this one is pretty simple:
# just umount each of the previously
# mounted filesystems, using the stored volume object
# def eject_freebsd(self):
from calibre.devices.udisks import umount if self._main_prefix:
umount(self._main_vol)
if self._card_a_prefix:
umount(self._card_a_vol)
if self._card_b_prefix:
umount(self._card_b_vol)

self._main_prefix = self._main_vol = None self._card_a_prefix = self._card_a_vol = None self._card_b_prefix = self._card_b_vol = None
# ------------------------------------------------------ def is_folder_still_available(self):
if self.eject_connected_folder:
self.eject_connected_folder = False self.connected_folder_path = ''
with suppress(OSError):
if self.connected_folder_path:
return os.path.isdir(self.connected_folder_path)
return False

[документация]
def open(self, connected_device, library_uuid):
self._main_prefix = self._card_a_prefix = self._card_b_prefix = None self.connected_folder_path = ''
if getattr(connected_device, 'serial', None) and connected_device.serial.startswith(FAKE_DEVICE_SERIAL):
folder_path = connected_device.serial[len(FAKE_DEVICE_SERIAL):]
if not os.path.isdir(folder_path):
raise DeviceError(f'The path {folder_path} is not a folder cannot connect to it')
if not os.access(folder_path, os.R_OK | os.W_OK):
raise DeviceError(f'You do not have permission to read and write to {folder_path} cannot connect to it')
if not folder_path.endswith(os.sep) and not folder_path.endswith('/'):
folder_path += os.sep self._main_prefix = folder_path self.current_library_uuid = library_uuid self.device_being_opened = connected_device try:
self.post_open_callback()
finally:
self.device_being_opened = None self.connected_folder_path = folder_path return time.sleep(5)
self.device_being_opened = connected_device try:
if islinux:
try:
self.open_linux()
except DeviceError:
time.sleep(7)
self.open_linux()
if isfreebsd:
self._main_vol = self._card_a_vol = self._card_b_vol = None self.open_freebsd()
if iswindows:
self.open_windows()
if ismacos:
try:
self.open_osx()
except DeviceError:
time.sleep(7)
self.open_osx()

self.current_library_uuid = library_uuid self.post_open_callback()
finally:
self.device_being_opened = Nonedef post_open_callback(self):
pass def eject_windows(self):
from threading import Thread drives = []
for x in ('_main_prefix', '_card_a_prefix', '_card_b_prefix'):
x = getattr(self, x, None)
if x is not None:
drives.append(x[0].upper())

def do_it(drives):
subprocess.Popen([eject_exe()] + drives, creationflags=subprocess.CREATE_NO_WINDOW).wait()

t = Thread(target=do_it, args=[drives])
t.daemon = True t.start()
self.__save_win_eject_thread = t def eject_osx(self):
for x in ('_main_prefix', '_card_a_prefix', '_card_b_prefix'):
x = getattr(self, x, None)
if x is not None:
try:
subprocess.Popen(self.OSX_EJECT_COMMAND + [x])
except Exception:
pass def eject_linux(self):
from calibre.devices.udisks import eject, umount drives = [d for d in self.find_device_nodes() if d]
for d in drives:
try:
umount(d)
except Exception:
pass for d in drives:
try:
eject(d)
except Exception as e:
print('Udisks eject call for:', d, 'failed:')
print('\t', e)

def on_device_close(self):
pass def unmount_device(self):
if self.connected_folder_path:
self.eject_connected_folder = True

[документация]
def eject(self):
if islinux:
try:
self.eject_linux()
except Exception:
pass if isfreebsd:
try:
self.eject_freebsd()
except Exception:
pass if iswindows:
try:
self.eject_windows()
except Exception:
pass if ismacos:
try:
self.eject_osx()
except Exception:
pass self._main_prefix = self._card_a_prefix = self._card_b_prefix = None self.on_device_close()

def linux_post_yank(self):
self._linux_mount_map = {}

[документация]
def post_yank_cleanup(self):
if islinux:
try:
self.linux_post_yank()
except Exception:
import traceback traceback.print_exc()
self._main_prefix = self._card_a_prefix = self._card_b_prefix = None self.on_device_close()

def get_main_ebook_dir(self, for_upload=False):
return self.EBOOK_DIR_MAIN def get_carda_ebook_dir(self, for_upload=False):
return self.EBOOK_DIR_CARD_A def get_cardb_ebook_dir(self, for_upload=False):
return self.EBOOK_DIR_CARD_B def _sanity_check(self, on_card, files):
from calibre.devices.utils import sanity_check sanity_check(on_card, files, self.card_prefix(), self.free_space())

def get_dest_dir(prefix, candidates):
if isinstance(candidates, string_or_bytes):
candidates = [candidates]
if not candidates:
candidates = ['']
candidates = [
((os.path.join(prefix, *(x.split('/')))) if x else prefix)
for x in candidates]
existing = [x for x in candidates if os.path.exists(x)]
if not existing:
existing = candidates return existing[0]

if on_card == 'carda':
candidates = self.get_carda_ebook_dir(for_upload=True)
path = get_dest_dir(self._card_a_prefix, candidates)
elif on_card == 'cardb':
candidates = self.get_cardb_ebook_dir(for_upload=True)
path = get_dest_dir(self._card_b_prefix, candidates)
else:
candidates = self.get_main_ebook_dir(for_upload=True)
path = get_dest_dir(self._main_prefix, candidates)

return path

[документация]
def sanitize_callback(self, path):
'''
Callback to allow individual device drivers to override the path sanitization used by :meth:`create_upload_path`.
'''
return sanitize(path)

[документация]
def filename_callback(self, default, mi):
'''
Callback to allow drivers to change the default file name set by :meth:`create_upload_path`.
'''
return default

[документация]
def sanitize_path_components(self, components):
'''
Perform any device specific sanitization on the path components for files to be uploaded to the device
'''
return components

[документация]
def get_annotations(self, path_map):
'''
Resolve path_map to annotation_map of files found on the device
'''
return {}

[документация]
def add_annotation_to_library(self, db, db_id, annotation):
'''
Add an annotation to the calibre library
'''
pass
def create_upload_path(self, path, mdata, fname, create_dirs=True):
from calibre.devices.utils import create_upload_path settings = self.settings()
filepath = create_upload_path(mdata, fname, self.save_template(), self.sanitize_callback, prefix_path=os.path.abspath(path), maxlen=self.MAX_PATH_LEN, use_subdirs=self.SUPPORTS_SUB_DIRS and settings.use_subdirs, news_in_folder=self.NEWS_IN_FOLDER, filename_callback=self.filename_callback, sanitize_path_components=self.sanitize_path_components
)
filedir = os.path.dirname(filepath)

if create_dirs and not os.path.exists(filedir):
os.makedirs(filedir)

return filepath def create_annotations_path(self, mdata, device_path=None):
return self.create_upload_path(os.path.abspath('/<storage>'), mdata, 'x.bookmark', create_dirs=False)

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.devices.usbms.driver

Исходный код calibre.devices.usbms.driver

__license__ = 'GPL v3'
__copyright__ = '2009, John Schember <john at nachtimwald.com>'
__docformat__ = 'restructuredtext en'

'''
Generic USB Mass storage device driver. This is not a complete stand alone driver. It is intended to be subclassed with the relevant parts implemented for a particular device.
'''

import json import os import shutil from itertools import cycle from calibre import fsync, isbytestring, prints from calibre.constants import filesystem_encoding, ismacos, numeric_version from calibre.devices.usbms.books import Book, BookList from calibre.devices.usbms.cli import CLI from calibre.devices.usbms.device import Device from calibre.ebooks.metadata.book.json_codec import JsonCodec from calibre.prints import debug_print from polyglot.builtins import itervalues, string_or_bytes def safe_walk(top, topdown=True, onerror=None, followlinks=False, maxdepth=128):
' A replacement for os.walk that does not die when it encounters undecodeable filenames in a linux filesystem'
if maxdepth < 0:
return islink, join, isdir = os.path.islink, os.path.join, os.path.isdir

# We may not have read permission for top, in which case we can't
# get a list of the files the directory contains. os.path.walk
# always suppressed the exception then, rather than blow up for a
# minor reason when (say) a thousand readable directories are still
# left to visit. That logic is copied here.
try:
names = os.listdir(top)
except OSError as err:
if onerror is not None:
onerror(err)
return dirs, nondirs = [], []
for name in names:
if isinstance(name, bytes):
try:
name = name.decode(filesystem_encoding)
except UnicodeDecodeError:
debug_print(f'Skipping undecodeable file: {name!r}')
continue if isdir(join(top, name)):
dirs.append(name)
else:
nondirs.append(name)

if topdown:
yield top, dirs, nondirs for name in dirs:
new_path = join(top, name)
if followlinks or not islink(new_path):
yield from safe_walk(new_path, topdown, onerror, followlinks, maxdepth-1)
if not topdown:
yield top, dirs, nondirs

# CLI must come before Device as it implements the CLI functions that
# are inherited from the device interface in Device.
[документация]
class USBMS(CLI, Device):
'''
The base class for all USBMS devices. Implements the logic for sending/getting/updating metadata/caching metadata/etc.
'''

description = _('Communicate with an e-book reader.')
author = 'John Schember'
supported_platforms = ['windows', 'osx', 'linux']

# Store type instances of BookList and Book. We must do this because
# a) we need to override these classes in some device drivers, and
# b) the classmethods seem only to see real attributes declared in the
# class, not attributes stored in the class booklist_class = BookList book_class = Book

FORMATS = []
CAN_SET_METADATA = []
METADATA_CACHE = 'metadata.calibre'
DRIVEINFO = 'driveinfo.calibre'

SCAN_FROM_ROOT = False def _update_driveinfo_record(self, dinfo, prefix, location_code, name=None):
import uuid from calibre.utils.date import isoformat, now if not isinstance(dinfo, dict):
dinfo = {}
if dinfo.get('device_store_uuid', None) is None:
dinfo['device_store_uuid'] = str(uuid.uuid4())
if dinfo.get('device_name', None) is None:
dinfo['device_name'] = self.get_gui_name()
if name is not None:
dinfo['device_name'] = name dinfo['location_code'] = location_code dinfo['last_library_uuid'] = getattr(self, 'current_library_uuid', None)
dinfo['calibre_version'] = '.'.join([str(i) for i in numeric_version])
dinfo['date_last_connected'] = isoformat(now())
dinfo['prefix'] = prefix.replace('\\', '/')
return dinfo def _update_driveinfo_file(self, prefix, location_code, name=None):
from calibre.utils.config import from_json, to_json if os.path.exists(os.path.join(prefix, self.DRIVEINFO)):
with open(os.path.join(prefix, self.DRIVEINFO), 'rb') as f:
try:
driveinfo = json.loads(f.read(), object_hook=from_json)
except Exception:
driveinfo = None driveinfo = self._update_driveinfo_record(driveinfo, prefix, location_code, name)
data = json.dumps(driveinfo, default=to_json)
if not isinstance(data, bytes):
data = data.encode('utf-8')
with open(os.path.join(prefix, self.DRIVEINFO), 'wb') as f:
f.write(data)
fsync(f)
else:
driveinfo = self._update_driveinfo_record({}, prefix, location_code, name)
data = json.dumps(driveinfo, default=to_json)
if not isinstance(data, bytes):
data = data.encode('utf-8')
with open(os.path.join(prefix, self.DRIVEINFO), 'wb') as f:
f.write(data)
fsync(f)
return driveinfo

[документация]
def get_device_information(self, end_session=True):
self.report_progress(1.0, _('Get device information...'))
self.driveinfo = {}

def raise_os_error(e):
raise OSError(_('Failed to access files in the main memory of'
' your device. You should contact the device'
' manufacturer for support. Common fixes are:'
' try a different USB cable/USB port on your computer.'
' If you device has a "Reset to factory defaults" type'
' of setting somewhere, use it. Underlying error: %s')
% e) from e if self._main_prefix is not None:
try:
self.driveinfo['main'] = self._update_driveinfo_file(self._main_prefix, 'main')
except PermissionError as e:
if ismacos:
raise PermissionError(_(
'Permission was denied by macOS trying to access files in the main memory of'
' your device. You will need to grant permission explicitly by looking under'
' System Preferences > Security and Privacy > Privacy > Files and Folders.'
' Underlying error: %s'
) % e) from e raise_os_error(e)
except OSError as e:
raise_os_error(e)
try:
if self._card_a_prefix is not None:
self.driveinfo['A'] = self._update_driveinfo_file(self._card_a_prefix, 'A')
if self._card_b_prefix is not None:
self.driveinfo['B'] = self._update_driveinfo_file(self._card_b_prefix, 'B')
except OSError as e:
raise OSError(_('Failed to access files on the SD card in your'
' device. This can happen for many reasons. The SD card may be'
' corrupted, it may be too large for your device, it may be'
' write-protected, etc. Try a different SD card, or reformat'
' your SD card using the FAT32 filesystem. Also make sure'
' there are not too many files in the root of your SD card.'
' Underlying error: %s') % e)
return (self.get_gui_name(), '', '', '', self.driveinfo)

[документация]
def set_driveinfo_name(self, location_code, name):
if location_code == 'main':
self._update_driveinfo_file(self._main_prefix, location_code, name)
elif location_code == 'A':
self._update_driveinfo_file(self._card_a_prefix, location_code, name)
elif location_code == 'B':
self._update_driveinfo_file(self._card_b_prefix, location_code, name)

def formats_to_scan_for(self):
return set(self.settings().format_map) | set(self.FORMATS)

def is_allowed_book_file(self, filename, path, prefix):
return True

[документация]
def books(self, oncard=None, end_session=True):
from calibre.ebooks.metadata.meta import path_to_ext debug_print('USBMS: Fetching list of books from device. Device=', self.__class__.__name__,
'oncard=', oncard)

dummy_bl = self.booklist_class(None, None, None)

if oncard == 'carda' and not self._card_a_prefix:
self.report_progress(1.0, _('Getting list of books on device...'))
return dummy_bl elif oncard == 'cardb' and not self._card_b_prefix:
self.report_progress(1.0, _('Getting list of books on device...'))
return dummy_bl elif oncard and oncard != 'carda' and oncard != 'cardb':
self.report_progress(1.0, _('Getting list of books on device...'))
return dummy_bl prefix = self._card_a_prefix if oncard == 'carda' else \ self._card_b_prefix if oncard == 'cardb' \ else self._main_prefix ebook_dirs = self.get_carda_ebook_dir() if oncard == 'carda' else \ self.EBOOK_DIR_CARD_B if oncard == 'cardb' else \ self.get_main_ebook_dir()

debug_print('USBMS: dirs are:', prefix, ebook_dirs)

# get the metadata cache bl = self.booklist_class(oncard, prefix, self.settings)
need_sync = self.parse_metadata_cache(bl, prefix, self.METADATA_CACHE)

# make a dict cache of paths so the lookup in the loop below is faster.
bl_cache = {}
for idx, b in enumerate(bl):
bl_cache[b.lpath] = idx all_formats = self.formats_to_scan_for()

def update_booklist(filename, path, prefix):
changed = False
# Ignore AppleDouble files if filename.startswith('._'):
return False if path_to_ext(filename) in all_formats and self.is_allowed_book_file(filename, path, prefix):
try:
lpath = os.path.join(path, filename).partition(self.normalize_path(prefix))[2]
if lpath.startswith(os.sep):
lpath = lpath[len(os.sep):]
lpath = lpath.replace('\\', '/')
idx = bl_cache.get(lpath, None)
if idx is not None:
bl_cache[lpath] = None if self.update_metadata_item(bl[idx]):
# print('update_metadata_item returned true')
changed = True else:
if bl.add_book(self.book_from_path(prefix, lpath), replace_metadata=False):
changed = True except Exception: # Probably a filename encoding error import traceback traceback.print_exc()
return changed if isinstance(ebook_dirs, string_or_bytes):
ebook_dirs = [ebook_dirs]
for ebook_dir in ebook_dirs:
ebook_dir = self.path_to_unicode(ebook_dir)
if self.SCAN_FROM_ROOT:
ebook_dir = self.normalize_path(prefix)
else:
ebook_dir = self.normalize_path( os.path.join(prefix, *(ebook_dir.split('/')))
if ebook_dir else prefix)
debug_print('USBMS: scan from root', self.SCAN_FROM_ROOT, ebook_dir)
if not os.path.exists(ebook_dir):
continue
# Get all books in the ebook_dir directory if self.SUPPORTS_SUB_DIRS or self.SUPPORTS_SUB_DIRS_FOR_SCAN:
# build a list of files to check, so we can accurately report progress flist = []
for path, dirs, files in safe_walk(ebook_dir):
for filename in files:
if filename != self.METADATA_CACHE:
flist.append({'filename': self.path_to_unicode(filename),
'path':self.path_to_unicode(path)})
for i, f in enumerate(flist):
self.report_progress(i/float(len(flist)), _('Getting list of books on device...'))
changed = update_booklist(f['filename'], f['path'], prefix)
if changed:
need_sync = True else:
paths = os.listdir(ebook_dir)
for i, filename in enumerate(paths):
self.report_progress((i+1) / float(len(paths)), _('Getting list of books on device...'))
changed = update_booklist(self.path_to_unicode(filename), ebook_dir, prefix)
if changed:
need_sync = True

# Remove books that are no longer in the filesystem. Cache contains
# indices into the booklist if book not in filesystem, None otherwise
# Do the operation in reverse order so indices remain valid for idx in sorted(itervalues(bl_cache), reverse=True, key=lambda x: -1 if x is None else x):
if idx is not None:
need_sync = True del bl[idx]

debug_print(f'USBMS: count found in cache: {len(bl_cache)}, count of files in metadata: {len(bl)}, need_sync: {need_sync}')
if need_sync: # self.count_found_in_bl != len(bl) or need_sync:
if oncard == 'cardb':
self.sync_booklists((None, None, bl))
elif oncard == 'carda':
self.sync_booklists((None, bl, None))
else:
self.sync_booklists((bl, None, None))

self.report_progress(1.0, _('Getting list of books on device...'))
debug_print('USBMS: Finished fetching list of books from device. oncard=', oncard)
return bl[документация]
def upload_books(self, files, names, on_card=None, end_session=True, metadata=None):
debug_print(f'USBMS: uploading {len(files)} books')

path = self._sanity_check(on_card, files)

paths = []
names = iter(names)
metadata = iter(metadata)

for i, infile in enumerate(files):
mdata, fname = next(metadata), next(names)
filepath = self.normalize_path(self.create_upload_path(path, mdata, fname))
if not hasattr(infile, 'read'):
infile = self.normalize_path(infile)
filepath = self.put_file(infile, filepath, replace_file=True)
paths.append(filepath)
try:
self.upload_cover(os.path.dirname(filepath), os.path.splitext(os.path.basename(filepath))[0], mdata, filepath)
except Exception: # Failure to upload cover is not catastrophic import traceback traceback.print_exc()

self.report_progress((i+1) / float(len(files)), _('Transferring books to device...'))

self.report_progress(1.0, _('Transferring books to device...'))
debug_print(f'USBMS: finished uploading {len(files)} books')
return list(zip(paths, cycle([on_card])))

[документация]
def upload_cover(self, path, filename, metadata, filepath):
'''
Upload book cover to the device. Default implementation does nothing.

:param path: The full path to the folder where the associated book is located.
:param filename: The name of the book file without the extension.
:param metadata: metadata belonging to the book. Use metadata.thumbnail for cover
:param filepath: The full path to the e-book file

'''
pass

[документация]
def add_books_to_metadata(self, locations, metadata, booklists):
debug_print(f'USBMS: adding metadata for {len(metadata)} books')

metadata = iter(metadata)
locations = tuple(locations)
for i, location in enumerate(locations):
self.report_progress((i+1) / float(len(locations)), _('Adding books to device metadata listing...'))
info = next(metadata)
blist = 2 if location[1] == 'cardb' else 1 if location[1] == 'carda' else 0

# Extract the correct prefix from the pathname. To do this correctly,
# we must ensure that both the prefix and the path are normalized
# so that the comparison will work. Book's __init__ will fix up
# lpath, so we don't need to worry about that here.
path = self.normalize_path(location[0])
if self._main_prefix:
prefix = self._main_prefix if \ path.startswith(self.normalize_path(self._main_prefix)) else None if not prefix and self._card_a_prefix:
prefix = self._card_a_prefix if \ path.startswith(self.normalize_path(self._card_a_prefix)) else None if not prefix and self._card_b_prefix:
prefix = self._card_b_prefix if \ path.startswith(self.normalize_path(self._card_b_prefix)) else None if prefix is None:
prints('in add_books_to_metadata. Prefix is None!', path, self._main_prefix)
continue lpath = path.partition(prefix)[2]
if lpath.startswith(('/', '\\')):
lpath = lpath[1:]
book = self.book_class(prefix, lpath, other=info)
if book.size is None:
book.size = os.stat(self.normalize_path(path)).st_size b = booklists[blist].add_book(book, replace_metadata=True)
if b:
b._new_book = True self.report_progress(1.0, _('Adding books to device metadata listing...'))
debug_print('USBMS: finished adding metadata')def delete_single_book(self, path):
os.unlink(path)

def delete_extra_book_files(self, path):
filepath = os.path.splitext(path)[0]
for ext in self.DELETE_EXTS:
for x in (filepath, path):
x += ext if os.path.exists(x):
if os.path.isdir(x):
shutil.rmtree(x, ignore_errors=True)
else:
os.unlink(x)

if self.SUPPORTS_SUB_DIRS:
try:
os.removedirs(os.path.dirname(path))
except Exception:
pass

[документация]
def delete_books(self, paths, end_session=True):
debug_print(f'USBMS: deleting {len(paths)} books')
for i, path in enumerate(paths):
self.report_progress((i+1) / float(len(paths)), _('Removing books from device...'))
path = self.normalize_path(path)
if os.path.exists(path):
# Delete the ebook self.delete_single_book(path)
self.delete_extra_book_files(path)

self.report_progress(1.0, _('Removing books from device...'))
debug_print(f'USBMS: finished deleting {len(paths)} books')

[документация]
def remove_books_from_metadata(self, paths, booklists):
debug_print(f'USBMS: removing metadata for {len(paths)} books')

for i, path in enumerate(paths):
self.report_progress((i+1) / float(len(paths)), _('Removing books from device metadata listing...'))
for bl in booklists:
for book in bl:
if path.endswith(book.path):
bl.remove_book(book)
self.report_progress(1.0, _('Removing books from device metadata listing...'))
debug_print(f'USBMS: finished removing metadata for {len(paths)} books')

# If you override this method and you use book._new_book, then you must
# complete the processing before you call this method. The flag is cleared
# at the end just before the return
[документация]
def sync_booklists(self, booklists, end_session=True):
debug_print('USBMS: starting sync_booklists')
json_codec = JsonCodec()

if not os.path.exists(self.normalize_path(self._main_prefix)):
os.makedirs(self.normalize_path(self._main_prefix))

def write_prefix(prefix, listid):
if (prefix is not None and len(booklists) > listid and isinstance(booklists[listid], self.booklist_class)):
if not os.path.exists(prefix):
os.makedirs(self.normalize_path(prefix))
with open(self.normalize_path(os.path.join(prefix, self.METADATA_CACHE)), 'wb') as f:
json_codec.encode_to_file(f, booklists[listid])
fsync(f)
write_prefix(self._main_prefix, 0)
write_prefix(self._card_a_prefix, 1)
write_prefix(self._card_b_prefix, 2)

# Clear the _new_book indication, as we are supposed to be done with
# adding books at this point for blist in booklists:
if blist is not None:
for book in blist:
book._new_book = False self.report_progress(1.0, _('Sending metadata to device...'))
debug_print('USBMS: finished sync_booklists')@classmethod def build_template_regexp(cls):
from calibre.devices.utils import build_template_regexp return build_template_regexp(cls.save_template())

@classmethod def path_to_unicode(cls, path):
if isbytestring(path):
path = path.decode(filesystem_encoding)
return path

[документация]
@classmethod def normalize_path(cls, path):
'Return path with platform native path separators'
if path is None:
return None if os.sep == '\\':
path = path.replace('/', '\\')
else:
path = path.replace('\\', '/')
return cls.path_to_unicode(path)

@classmethod def parse_metadata_cache(cls, bl, prefix, name):
json_codec = JsonCodec()
need_sync = False cache_file = cls.normalize_path(os.path.join(prefix, name))
if os.access(cache_file, os.R_OK):
try:
with open(cache_file, 'rb') as f:
json_codec.decode_from_file(f, bl, cls.book_class, prefix)
except Exception:
import traceback traceback.print_exc()
bl = []
need_sync = True else:
need_sync = True return need_sync

@classmethod def update_metadata_item(cls, book):
changed = False size = os.stat(cls.normalize_path(book.path)).st_size if size != book.size:
changed = True mi = cls.metadata_from_path(book.path)
book.smart_update(mi)
book.size = size return changed

@classmethod def metadata_from_path(cls, path):
return cls.metadata_from_formats([path])

@classmethod def metadata_from_formats(cls, fmts):
from calibre.customize.ui import quick_metadata from calibre.ebooks.metadata.meta import metadata_from_formats with quick_metadata:
return metadata_from_formats(fmts, force_read_metadata=True, pattern=cls.build_template_regexp())

@classmethod def book_from_path(cls, prefix, lpath):
from calibre.ebooks.metadata.book.base import Metadata if cls.settings().read_metadata or cls.MUST_READ_METADATA:
mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, lpath)))
else:
from calibre.ebooks.metadata.meta import metadata_from_filename mi = metadata_from_filename(cls.normalize_path(os.path.basename(lpath)), cls.build_template_regexp())
if mi is None:
mi = Metadata(os.path.splitext(os.path.basename(lpath))[0],
[_('Unknown')])
size = os.stat(cls.normalize_path(os.path.join(prefix, lpath))).st_size book = cls.book_class(prefix, lpath, other=mi, size=size)
return book

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.ebooks.metadata.book.base

Исходный код calibre.ebooks.metadata.book.base

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import copy import traceback from contextlib import suppress from calibre import prints from calibre.constants import DEBUG from calibre.ebooks.metadata.book import ALL_METADATA_FIELDS, SC_COPYABLE_FIELDS, SC_FIELDS_COPY_NOT_NULL, STANDARD_METADATA_FIELDS, TOP_LEVEL_IDENTIFIERS from calibre.library.field_metadata import FieldMetadata from calibre.utils.icu import lower as icu_lower from calibre.utils.icu import sort_key from calibre.utils.localization import ngettext from polyglot.builtins import iteritems, string_or_bytes

# Special sets used to optimize the performance of getting and setting
# attributes on Metadata objects
SIMPLE_GET = frozenset(STANDARD_METADATA_FIELDS - TOP_LEVEL_IDENTIFIERS)
SIMPLE_SET = frozenset(SIMPLE_GET - {'identifiers'})

def human_readable(size, precision=2):
''' Convert a size in bytes into megabytes '''
ans = size/(1024*1024)
if ans < 0.1:
return '<0.1 MB'
return ('%.'+str(precision)+'f'+ ' MB') % ans

NULL_VALUES = {
'user_metadata': {},
'cover_data' : (None, None),
'tags' : [],
'identifiers' : {},
'languages' : [],
'device_collections': [],
'author_sort_map': {},
'authors' : [_('Unknown')],
'author_sort' : _('Unknown'),
'title' : _('Unknown'),
'user_categories' : {},
'link_maps' : {},
'language' : 'und'
}

field_metadata = FieldMetadata()

def reset_field_metadata():
global field_metadata field_metadata = FieldMetadata()

def ck(typ):
return icu_lower(typ).strip().replace(':', '').replace(',', '')

def cv(val):
return val.strip().replace(',', '|')

[документация]
class Metadata:
'''
A class representing all the metadata for a book. The various standard metadata fields are available as attributes of this object. You can also stick arbitrary attributes onto this object.

Metadata from custom columns should be accessed via the get() method, passing in the lookup name for the column, for example: "#mytags".

Use the :meth:`is_null` method to test if a field is null.

This object also has functions to format fields into strings.

The list of standard metadata fields grows with time is in
:data:`STANDARD_METADATA_FIELDS`.

Please keep the method based API of this class to a minimum. Every method becomes a reserved field name.
'''
__calibre_serializable__ = True def __init__(self, title, authors=(_('Unknown'),), other=None, template_cache=None, formatter=None):
'''
@param title: title or ``_('Unknown')``
@param authors: List of strings or []
@param other: None or a metadata object
'''
_data = copy.deepcopy(NULL_VALUES)
_data.pop('language')
object.__setattr__(self, '_data', _data)
if other is not None:
self.smart_update(other)
else:
if title:
self.title = title if authors:
# List of strings or []
self.author = list(authors) if authors else [] # Needed for backward compatibility self.authors = list(authors) if authors else []
from calibre.ebooks.metadata.book.formatter import SafeFormat self.formatter = SafeFormat() if formatter is None else formatter self.template_cache = template_cache

[документация]
def is_null(self, field):
'''
Return True if the value of field is null in this object.
'null' means it is unknown or evaluates to False. So a title of
_('Unknown') is null or a language of 'und' is null.

Be careful with numeric fields since this will return True for zero as well as None.

Also returns True if the field does not exist.
'''
try:
null_val = NULL_VALUES.get(field, None)
val = getattr(self, field, None)
return not val or val == null_val except Exception:
return True
def set_null(self, field):
null_val = copy.copy(NULL_VALUES.get(field))
setattr(self, field, null_val)

def __getattribute__(self, field):
_data = object.__getattribute__(self, '_data')
if field in SIMPLE_GET:
return _data.get(field, None)
if field in TOP_LEVEL_IDENTIFIERS:
return _data.get('identifiers').get(field, None)
if field == 'language':
try:
return _data.get('languages', [])[0]
except Exception:
return NULL_VALUES['language']
try:
return object.__getattribute__(self, field)
except AttributeError:
pass if field in _data['user_metadata']:
d = _data['user_metadata'][field]
val = d['#value#']
if val is None and d['datatype'] == 'composite':
d['#value#'] = 'RECURSIVE_COMPOSITE FIELD (Metadata) ' + field val = d['#value#'] = self.formatter.safe_format( d['display']['composite_template'], self,
_('TEMPLATE ERROR'), self, column_name=field, template_cache=self.template_cache).strip()
return val if field.startswith('#') and field.endswith('_index'):
try:
return self.get_extra(field[:-6])
except Exception:
pass raise AttributeError(
'Metadata object has no attribute named: '+ repr(field))

def __setattr__(self, field, val, extra=None):
_data = object.__getattribute__(self, '_data')
if field in SIMPLE_SET:
if val is None:
val = copy.copy(NULL_VALUES.get(field, None))
_data[field] = val elif field in TOP_LEVEL_IDENTIFIERS:
field, val = self._clean_identifier(field, val)
identifiers = _data['identifiers']
identifiers.pop(field, None)
if val:
identifiers[field] = val elif field == 'identifiers':
if not val:
val = copy.copy(NULL_VALUES.get('identifiers', None))
self.set_identifiers(val)
elif field == 'language':
langs = []
if val and val.lower() != 'und':
langs = [val]
_data['languages'] = langs elif field in _data['user_metadata']:
d = _data['user_metadata'][field]
d['#value#'] = val d['#extra#'] = extra else:
# You are allowed to stick arbitrary attributes onto this object as
# long as they don't conflict with global or user metadata names
# Don't abuse this privilege self.__dict__[field] = val def __iter__(self):
return iter(object.__getattribute__(self, '_data'))

def has_key(self, key):
return key in STANDARD_METADATA_FIELDS or key in object.__getattribute__(self, '_data')['user_metadata']

def _evaluate_all_composites(self):
custom_fields = object.__getattribute__(self, '_data')['user_metadata']
for field in custom_fields:
self._evaluate_composite(field)

def _evaluate_composite(self, field):
f = object.__getattribute__(self, '_data')['user_metadata'].get(field, None)
if f is not None:
if f['datatype'] == 'composite' and f['#value#'] is None:
self.get(field)

[документация]
def deepcopy(self, class_generator=lambda: Metadata(None)):
''' Do not use this method unless you know what you are doing, if you want to create a simple clone of this object, use :meth:`deepcopy_metadata` instead. Class_generator must be a function that returns an instance of Metadata or a subclass of it.'''
# We don't need to evaluate all the composites here because we
# are returning a "real" Metadata instance that has __get_attribute__.
m = class_generator()
if not isinstance(m, Metadata):
return None object.__setattr__(m, '__dict__', copy.deepcopy(self.__dict__))
return m
def deepcopy_metadata(self):
# We don't need to evaluate all the composites here because we
# are returning a "real" Metadata instance that has __get_attribute__.
m = Metadata(None)
object.__setattr__(m, '_data', copy.deepcopy(object.__getattribute__(self, '_data')))
# Also copy these two top-level attributes as they can appear in templates.
with suppress(AttributeError):
object.__setattr__(m, 'id', copy.copy(self.__getattribute__('id')))
with suppress(AttributeError):
object.__setattr__(m, 'has_cover', copy.copy(self.__getattribute__('has_cover')))
return m def get(self, field, default=None):
try:
return self.__getattribute__(field)
except AttributeError:
return default def get_extra(self, field, default=None):
# Don't need to evaluate all composites because a composite can't have
# an extra value
_data = object.__getattribute__(self, '_data')
if field in _data['user_metadata']:
try:
return _data['user_metadata'][field]['#extra#']
except Exception:
return default raise AttributeError(
'Metadata object has no attribute named: '+ repr(field))

def set(self, field, val, extra=None):
self.__setattr__(field, val, extra)

[документация]
def get_identifiers(self):
'''
Return a copy of the identifiers dictionary.
The dict is small, and the penalty for using a reference where a copy is needed is large. Also, we don't want any manipulations of the returned dict to show up in the book.
'''
ans = object.__getattribute__(self, '_data')['identifiers']
if not ans:
ans = {}
return copy.deepcopy(ans)

def _clean_identifier(self, typ, val):
if typ:
typ = ck(typ)
if val:
val = cv(val)
return typ, val

[документация]
def set_identifiers(self, identifiers):
'''
Set all identifiers. Note that if you previously set ISBN, calling this method will delete it.
'''
cleaned = {ck(k):cv(v) for k, v in iteritems(identifiers) if k and v}
object.__getattribute__(self, '_data')['identifiers'] = cleaned

[документация]
def set_identifier(self, typ, val):
'If val is empty, deletes identifier of type typ'
typ, val = self._clean_identifier(typ, val)
if not typ:
return identifiers = object.__getattribute__(self, '_data')['identifiers']

identifiers.pop(typ, None)
if val:
identifiers[typ] = val
def has_identifier(self, typ):
identifiers = object.__getattribute__(self, '_data')['identifiers']
return typ in identifiers

# field-oriented interface. Intended to be the same as in LibraryDatabase

[документация]
def standard_field_keys(self):
'''
return a list of all possible keys, even if this book doesn't have them
'''
return STANDARD_METADATA_FIELDS

[документация]
def custom_field_keys(self):
'''
return a list of the custom fields in this book
'''
return iter(object.__getattribute__(self, '_data')['user_metadata'])

[документация]
def all_field_keys(self):
'''
All field keys known by this instance, even if their value is None
'''
_data = object.__getattribute__(self, '_data')
return frozenset(ALL_METADATA_FIELDS.union(frozenset(_data['user_metadata'])))

[документация]
def metadata_for_field(self, key):
'''
return metadata describing a standard or custom field.
'''
if key not in self.custom_field_keys():
return self.get_standard_metadata(key, make_copy=False)
return self.get_user_metadata(key, make_copy=False)

[документация]
def all_non_none_fields(self):
'''
Return a dictionary containing all non-None metadata fields, including the custom ones.
'''
result = {}
_data = object.__getattribute__(self, '_data')
for attr in STANDARD_METADATA_FIELDS:
v = _data.get(attr, None)
if v is not None:
result[attr] = v
# separate these because it uses the self.get(), not _data.get()
for attr in TOP_LEVEL_IDENTIFIERS:
v = self.get(attr, None)
if v is not None:
result[attr] = v for attr in _data['user_metadata']:
v = self.get(attr, None)
if v is not None:
result[attr] = v if _data['user_metadata'][attr]['datatype'] == 'series':
result[attr+'_index'] = _data['user_metadata'][attr]['#extra#']
return result

# End of field-oriented interface

# Extended interfaces. These permit one to get copies of metadata dictionaries, and to
# get and set custom field metadata

[документация]
def get_standard_metadata(self, field, make_copy):
'''
return field metadata from the field if it is there. Otherwise return
None. field is the key name, not the label. Return a copy if requested, just in case the user wants to change values in the dict.
'''
if field in field_metadata and field_metadata[field]['kind'] == 'field':
if make_copy:
return copy.deepcopy(field_metadata[field])
return field_metadata[field]
return None

[документация]
def get_all_standard_metadata(self, make_copy):
'''
return a dict containing all the standard field metadata associated with the book.
'''
if not make_copy:
return field_metadata res = {}
for k in field_metadata:
if field_metadata[k]['kind'] == 'field':
res[k] = copy.deepcopy(field_metadata[k])
return res

[документация]
def get_all_user_metadata(self, make_copy):
'''
return a dict containing all the custom field metadata associated with the book.
'''
# Must evaluate all composites because we are returning a dict, not a
# Metadata instance self._evaluate_all_composites()
_data = object.__getattribute__(self, '_data')
user_metadata = _data['user_metadata']
if not make_copy:
return user_metadata res = {}
for k in user_metadata:
res[k] = copy.deepcopy(user_metadata[k])
return res

[документация]
def get_user_metadata(self, field, make_copy):
'''
return field metadata from the object if it is there. Otherwise return
None. field is the key name, not the label. Return a copy if requested, just in case the user wants to change values in the dict.
'''
_data = object.__getattribute__(self, '_data')['user_metadata']
if field in _data:
# Must evaluate the field because it might be a composite. It won't
# be evaluated on demand because we are returning its dict, not a
# Metadata instance self._evaluate_composite(field)
if make_copy:
return copy.deepcopy(_data[field])
return _data[field]
return None

[документация]
def set_all_user_metadata(self, metadata):
'''
store custom field metadata into the object. Field is the key name not the label
'''
if metadata is None:
traceback.print_stack()
return um = {}
for key, meta in iteritems(metadata):
m = meta.copy()
if '#value#' not in m:
if m['datatype'] == 'text' and m['is_multiple']:
m['#value#'] = []
else:
m['#value#'] = None um[key] = m
_data = object.__getattribute__(self, '_data')
_data['user_metadata'] = um

[документация]
def set_user_metadata(self, field, metadata):
'''
store custom field metadata for one column into the object. Field is the key name not the label
'''
if field is not None:
if not field.startswith('#'):
raise AttributeError( f"Custom field name {field!r} must begin with '#'")
if metadata is None:
traceback.print_stack()
return m = dict(metadata)
# Copying the elements should not be necessary. The objects referenced
# in the dict should not change. Of course, they can be replaced.
# for k,v in iteritems(metadata):
# m[k] = copy.copy(v)
if '#value#' not in m:
if m['datatype'] == 'text' and m['is_multiple']:
m['#value#'] = []
else:
m['#value#'] = None
_data = object.__getattribute__(self, '_data')
_data['user_metadata'][field] = m

[документация]
def remove_stale_user_metadata(self, other_mi):
'''
Remove user metadata keys (custom column keys) if they don't exist in 'other_mi', which must be a metadata object
'''
me = self.get_all_user_metadata(make_copy=False)
other = set(other_mi.custom_field_keys())
new = {}
for k,v in me.items():
if k in other:
new[k] = v self.set_all_user_metadata(new)

[документация]
def template_to_attribute(self, other, ops):
'''
Takes a list [(src,dest), (src,dest)], evaluates the template in the context of other, then copies the result to self[dest]. This is on a best-efforts basis. Some assignments can make no sense.
'''
if not ops:
return from calibre.ebooks.metadata.book.formatter import SafeFormat formatter = SafeFormat()
for op in ops:
try:
src = op[0]
dest = op[1]
val = formatter.safe_format(src, other, 'PLUGBOARD TEMPLATE ERROR', other)
if dest == 'tags':
self.set(dest, [f.strip() for f in val.split(',') if f.strip()])
elif dest == 'authors':
self.set(dest, [f.strip() for f in val.split('&') if f.strip()])
else:
self.set(dest, val)
except Exception:
if DEBUG:
traceback.print_exc()

# Old Metadata API {{{ def print_all_attributes(self):
for x in STANDARD_METADATA_FIELDS:
prints(f'{x}:', getattr(self, x, 'None'))
for x in self.custom_field_keys():
meta = self.get_user_metadata(x, make_copy=False)
if meta is not None:
prints(x, meta)
prints('--------------')

[документация]
def smart_update(self, other, replace_metadata=False):
'''
Merge the information in `other` into self. In case of conflicts, the information in `other` takes precedence, unless the information in `other` is NULL.
'''
def copy_not_none(dest, src, attr):
v = getattr(src, attr, None)
if v is not None and v != NULL_VALUES.get(attr, None):
setattr(dest, attr, copy.deepcopy(v))

unknown = _('Unknown')
if other.title and other.title != unknown:
self.title = other.title if hasattr(other, 'title_sort'):
self.title_sort = other.title_sort if other.authors and ( other.authors[0] != unknown or ( not self.authors or ( len(self.authors) == 1 and self.authors[0] == unknown and getattr(self, 'author_sort', None) == unknown
)
)
):
self.authors = list(other.authors)
if hasattr(other, 'author_sort_map'):
self.author_sort_map = dict(other.author_sort_map)
if hasattr(other, 'author_sort'):
self.author_sort = other.author_sort if replace_metadata:
# SPECIAL_FIELDS = frozenset(['lpath', 'size', 'comments', 'thumbnail'])
for attr in SC_COPYABLE_FIELDS:
setattr(self, attr, getattr(other, attr, 1.0 if attr == 'series_index' else None))
self.tags = other.tags self.cover_data = getattr(other, 'cover_data',
NULL_VALUES['cover_data'])
self.set_all_user_metadata(other.get_all_user_metadata(make_copy=True))
for x in SC_FIELDS_COPY_NOT_NULL:
copy_not_none(self, other, x)
if callable(getattr(other, 'get_identifiers', None)):
self.set_identifiers(other.get_identifiers())
# language is handled below else:
for attr in SC_COPYABLE_FIELDS:
copy_not_none(self, other, attr)
for x in SC_FIELDS_COPY_NOT_NULL:
copy_not_none(self, other, x)

if other.tags:
# Case-insensitive but case preserving merging lotags = [t.lower() for t in other.tags]
lstags = [t.lower() for t in self.tags]
ot, st = map(frozenset, (lotags, lstags))
for t in st.intersection(ot):
sidx = lstags.index(t)
oidx = lotags.index(t)
self.tags[sidx] = other.tags[oidx]
self.tags += [t for t in other.tags if t.lower() in ot-st]

if getattr(other, 'cover_data', False):
other_cover = other.cover_data[-1]
self_cover = self.cover_data[-1] if self.cover_data else b''
if not self_cover:
self_cover = b''
if not other_cover:
other_cover = b''
if len(other_cover) > len(self_cover):
self.cover_data = other.cover_data if callable(getattr(other, 'custom_field_keys', None)):
for x in other.custom_field_keys():
meta = other.get_user_metadata(x, make_copy=True)
if meta is not None:
self_tags = self.get(x, [])
if isinstance(self_tags, string_or_bytes):
self_tags = []
self.set_user_metadata(x, meta) # get... did the deepcopy other_tags = other.get(x, [])
if meta['datatype'] == 'text' and meta['is_multiple']:
# Case-insensitive but case preserving merging lotags = [t.lower() for t in other_tags]
try:
lstags = [t.lower() for t in self_tags]
except TypeError:
# Happens if x is not a text, is_multiple field
# on self lstags = []
self_tags = []
ot, st = map(frozenset, (lotags, lstags))
for t in st.intersection(ot):
sidx = lstags.index(t)
oidx = lotags.index(t)
self_tags[sidx] = other_tags[oidx]
self_tags += [t for t in other_tags if t.lower() in ot-st]
setattr(self, x, self_tags)

my_comments = getattr(self, 'comments', '')
other_comments = getattr(other, 'comments', '')
if not my_comments:
my_comments = ''
if not other_comments:
other_comments = ''
if len(other_comments.strip()) > len(my_comments.strip()):
self.comments = other_comments

# Copy all the non-none identifiers if callable(getattr(other, 'get_identifiers', None)):
d = self.get_identifiers()
s = other.get_identifiers()
d.update([v for v in iteritems(s) if v[1] is not None])
self.set_identifiers(d)
else:
# other structure not Metadata. Copy the top-level identifiers for attr in TOP_LEVEL_IDENTIFIERS:
copy_not_none(self, other, attr)

other_lang = getattr(other, 'languages', [])
if other_lang and other_lang != ['und']:
self.languages = list(other_lang)
if not getattr(self, 'series', None):
self.series_index = Nonedef format_series_index(self, val=None):
from calibre.ebooks.metadata import fmt_sidx v = self.series_index if val is None else val try:
x = float(v)
except Exception:
x = 1 return fmt_sidx(x)

def authors_from_string(self, raw):
from calibre.ebooks.metadata import string_to_authors self.authors = string_to_authors(raw)

def format_authors(self):
from calibre.ebooks.metadata import authors_to_string return authors_to_string(self.authors)

def format_tags(self):
return ', '.join([str(t) for t in sorted(self.tags, key=sort_key)])

def format_rating(self, v=None, divide_by=1):
if v is None:
if self.rating is not None:
return str(self.rating/divide_by)
return 'None'
return str(v/divide_by)

[документация]
def format_field(self, key, series_with_index=True):
'''
Returns the tuple (display_name, formatted_value)
'''
name, val, ign, ign = self.format_field_extended(key, series_with_index)
return name, val
def format_field_extended(self, key, series_with_index=True):
from calibre.ebooks.metadata import authors_to_string
'''
returns the tuple (display_name, formatted_value, original_value, field_metadata)
'''
from calibre.utils.date import format_date

# Handle custom series index if key.startswith('#') and key.endswith('_index'):
tkey = key[:-6] # strip the _index cmeta = self.get_user_metadata(tkey, make_copy=False)
if cmeta and cmeta['datatype'] == 'series':
if self.get(tkey):
res = self.get_extra(tkey)
return (str(cmeta['name']+'_index'), self.format_series_index(res), res, cmeta)
else:
return (str(cmeta['name']+'_index'), '', '', cmeta)

if key in self.custom_field_keys():
res = self.get(key, None) # get evaluates all necessary composites cmeta = self.get_user_metadata(key, make_copy=False)
name = str(cmeta['name'])
if res is None or res == '': # can't check "not res" because of numeric fields return (name, res, None, None)
orig_res = res datatype = cmeta['datatype']
if datatype == 'text' and cmeta['is_multiple']:
res = cmeta['is_multiple']['list_to_ui'].join(res)
elif datatype == 'series' and series_with_index:
if self.get_extra(key) is not None:
res = res + f' [{self.format_series_index(val=self.get_extra(key))}]'
elif datatype == 'datetime':
res = format_date(res, cmeta['display'].get('date_format','dd MMM yyyy'))
elif datatype == 'bool':
res = _('Yes') if res else _('No')
elif datatype == 'rating':
res = f'{res/2:.2g}'
elif datatype in ['int', 'float']:
try:
fmt = cmeta['display'].get('number_format', None)
res = fmt.format(res)
except Exception:
pass return (name, str(res), orig_res, cmeta)

# convert top-level ids into their value if key in TOP_LEVEL_IDENTIFIERS:
fmeta = field_metadata['identifiers']
name = key res = self.get(key, None)
return (name, res, res, fmeta)

# Translate aliases into the standard field name fmkey = field_metadata.search_term_to_field_key(key)
if fmkey in field_metadata and field_metadata[fmkey]['kind'] == 'field':
res = self.get(key, None)
fmeta = field_metadata[fmkey]
name = str(fmeta['name'])
if res is None or res == '':
return (name, res, None, None)
orig_res = res name = str(fmeta['name'])
datatype = fmeta['datatype']
if key == 'authors':
res = authors_to_string(res)
elif key == 'series_index':
res = self.format_series_index(res)
elif datatype == 'text' and fmeta['is_multiple']:
if isinstance(res, dict):
res = [k + ':' + v for k,v in res.items()]
res = fmeta['is_multiple']['list_to_ui'].join(sorted(filter(None, res), key=sort_key))
elif datatype == 'series' and series_with_index:
res = res + f' [{self.format_series_index()}]'
elif datatype == 'datetime':
res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy'))
elif datatype == 'rating':
res = f'{res/2:.2g}'
elif key == 'size':
res = human_readable(res)
return (name, str(res), orig_res, fmeta)

if kv := self.get(key, None):
return (key, str(kv), kv, None)

return (None, None, None, None)

def __unicode__representation__(self):
'''
A string representation of this object, suitable for printing to console
'''
from calibre.ebooks.metadata import authors_to_string from calibre.utils.date import isoformat ans = []

def fmt(x, y):
ans.append(f'{x:<20}: {y}')

fmt('Title', self.title)
if self.title_sort:
fmt('Title sort', self.title_sort)
if self.authors:
fmt('Author(s)', authors_to_string(self.authors) +
((' [' + self.author_sort + ']')
if self.author_sort and self.author_sort != _('Unknown') else ''))
if self.publisher:
fmt('Publisher', self.publisher)
if getattr(self, 'book_producer', False):
fmt('Book Producer', self.book_producer)
if self.tags:
fmt('Tags', ', '.join([str(t) for t in self.tags]))
if self.series:
fmt('Series', self.series + f' #{self.format_series_index()}')
if not self.is_null('languages'):
fmt('Languages', ', '.join(self.languages))
if self.rating is not None:
fmt('Rating', (f'{float(self.rating)/2:.2g}') if self.rating else '')
if self.timestamp is not None:
fmt('Timestamp', isoformat(self.timestamp))
if self.pubdate is not None:
fmt('Published', isoformat(self.pubdate))
if self.rights is not None:
fmt('Rights', str(self.rights))
if self.identifiers:
fmt('Identifiers', ', '.join([f'{k}:{v}' for k, v in iteritems(self.identifiers)]))
if self.comments:
fmt('Comments', self.comments)

for key in self.custom_field_keys():
val = self.get(key, None)
if val:
name, val = self.format_field(key)
fmt(name, str(val))
return '\n'.join(ans)

[документация]
def to_html(self):
'''
A HTML representation of this object.
'''
from calibre.ebooks.metadata import authors_to_string from calibre.utils.date import isoformat ans = [(_('Title'), str(self.title))]
ans += [(_('Author(s)'), (authors_to_string(self.authors) if self.authors else _('Unknown')))]
ans += [(_('Publisher'), str(self.publisher))]
ans += [(_('Producer'), str(self.book_producer))]
ans += [(_('Comments'), str(self.comments))]
ans += [('ISBN', str(self.isbn))]
ans += [(_('Tags'), ', '.join([str(t) for t in self.tags]))]
if self.series:
ans += [(ngettext('Series', 'Series', 1), str(self.series) + f' #{self.format_series_index()}')]
ans += [(_('Languages'), ', '.join(self.languages))]
if self.timestamp is not None:
ans += [(_('Timestamp'), str(isoformat(self.timestamp, as_utc=False, sep=' ')))]
if self.pubdate is not None:
ans += [(_('Published'), str(isoformat(self.pubdate, as_utc=False, sep=' ')))]
if self.rights is not None:
ans += [(_('Rights'), str(self.rights))]
for key in self.custom_field_keys():
val = self.get(key, None)
if val:
name, val = self.format_field(key)
ans += [(name, val)]
for i, x in enumerate(ans):
ans[i] = '<tr><td><b>{}</b></td><td>{}</td></tr>'.format(*x)
return '<table>{}</table>'.format('\n'.join(ans))__str__ = __unicode__representation__ def __nonzero__(self):
return bool(self.title or self.author or self.comments or self.tags)
__bool__ = __nonzero__# }}}

def field_from_string(field, raw, field_metadata):
''' Parse the string raw to return an object that is suitable for calling set() on a Metadata object. '''
dt = field_metadata['datatype']
val = object if dt in {'int', 'float'}:
val = int(raw) if dt == 'int' else float(raw)
elif dt == 'rating':
val = float(raw) * 2 elif dt == 'datetime':
from calibre.utils.iso8601 import parse_iso8601 try:
val = parse_iso8601(raw, require_aware=True)
except Exception:
from calibre.utils.date import parse_only_date val = parse_only_date(raw)
elif dt == 'bool':
if raw.lower() in {'true', 'yes', 'y'}:
val = True elif raw.lower() in {'false', 'no', 'n'}:
val = False else:
raise ValueError(f'Unknown value for {field}: {raw}')
elif dt == 'text':
ism = field_metadata['is_multiple']
if ism:
val = [x.strip() for x in raw.split(ism['ui_to_list'])]
if field == 'identifiers':
val = {x.partition(':')[0]:x.partition(':')[-1] for x in val}
elif field == 'languages':
from calibre.utils.localization import canonicalize_lang val = [canonicalize_lang(x) for x in val]
val = [x for x in val if x]
if val is object:
val = raw return val def get_model_metadata_instance():
'''
Get a metadata instance that contains all the fields in the current database with the fields to a plausible value. This function must only be used in the GUI thread.
'''
from calibre.gui2 import is_gui_thread if not is_gui_thread():
raise ValueError('get_model_metadata_instance() must only be used in the GUI thread')

mi = Metadata(_('Title'), [_('Author')])
mi.author_sort = _('Author Sort')
mi.series = ngettext('Series', 'Series', 1)
mi.series_index = 3 mi.rating = 4.0 mi.tags = [_('Tag 1'), _('Tag 2')]
mi.languages = ['eng']
mi.id = -1 from calibre.gui2.ui import get_gui from calibre.utils.date import DEFAULT_DATE fm = get_gui().current_db.new_api.field_metadata mi.set_all_user_metadata(fm.custom_field_metadata())
for col in mi.get_all_user_metadata(False):
if fm[col]['datatype'] == 'datetime':
mi.set(col, DEFAULT_DATE)
elif fm[col]['datatype'] in ('int', 'float', 'rating'):
mi.set(col, 2)
elif fm[col]['datatype'] == 'bool':
mi.set(col, False)
elif fm[col]['is_multiple']:
mi.set(col, [col])
else:
mi.set(col, col, 1)
return mi

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.ebooks.metadata.sources.base

Исходный код calibre.ebooks.metadata.sources.base

#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai from __future__ import absolute_import, division, print_function, unicode_literals

__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import re import threading from functools import total_ordering from calibre import browser, random_user_agent from calibre.customize import Plugin from calibre.ebooks.metadata import check_isbn from calibre.ebooks.metadata.author_mapper import cap_author_token from calibre.utils.localization import canonicalize_lang, get_lang from polyglot.builtins import cmp, iteritems def create_log(ostream=None):
from calibre.utils.logging import FileStream, ThreadSafeLog log = ThreadSafeLog(level=ThreadSafeLog.DEBUG)
log.outputs = [FileStream(ostream)]
return log

# Comparing Metadata objects for relevance {{{ words = ('the', 'a', 'an', 'of', 'and')
prefix_pat = re.compile(r'^(%s)\s+'%('|'.join(words)))
trailing_paren_pat = re.compile(r'\(.*\)$')
whitespace_pat = re.compile(r'\s+')

def cleanup_title(s):
if not s:
s = _('Unknown')
s = s.strip().lower()
s = prefix_pat.sub(' ', s)
s = trailing_paren_pat.sub('', s)
s = whitespace_pat.sub(' ', s)
return s.strip()

[документация]
@total_ordering class InternalMetadataCompareKeyGen:
'''
Generate a sort key for comparison of the relevance of Metadata objects, given a search query. This is used only to compare results from the same metadata source, not across different sources.

The sort key ensures that an ascending order sort is a sort by order of decreasing relevance.

The algorithm is:

* Prefer results that have at least one identifier the same as for the query
* Prefer results with a cached cover URL
* Prefer results with all available fields filled in
* Prefer results with the same language as the current user interface language
* Prefer results that are an exact title match to the query
* Prefer results with longer comments (greater than 10% longer)
* Use the relevance of the result as reported by the metadata source's search engine
'''

def __init__(self, mi, source_plugin, title, authors, identifiers):
same_identifier = 2 idents = mi.get_identifiers()
for k, v in iteritems(identifiers):
if idents.get(k) == v:
same_identifier = 1 break all_fields = 1 if source_plugin.test_fields(mi) is None else 2 exact_title = 1 if title and \ cleanup_title(title) == cleanup_title(mi.title) else 2 language = 1 if mi.language:
mil = canonicalize_lang(mi.language)
if mil != 'und' and mil != canonicalize_lang(get_lang()):
language = 2 has_cover = 2 if (not source_plugin.cached_cover_url_is_reliable or source_plugin.get_cached_cover_url(mi.identifiers) is None) else 1 self.base = (same_identifier, has_cover, all_fields, language, exact_title)
self.comments_len = len((mi.comments or '').strip())
self.extra = getattr(mi, 'source_relevance', 0)

def compare_to_other(self, other):
a = cmp(self.base, other.base)
if a != 0:
return a cx, cy = self.comments_len, other.comments_len if cx and cy:
t = (cx + cy) / 20 delta = cy - cx if abs(delta) > t:
return -1 if delta < 0 else 1 return cmp(self.extra, other.extra)

def __eq__(self, other):
return self.compare_to_other(other) == 0 def __ne__(self, other):
return self.compare_to_other(other) != 0 def __lt__(self, other):
return self.compare_to_other(other) < 0 def __le__(self, other):
return self.compare_to_other(other) <= 0 def __gt__(self, other):
return self.compare_to_other(other) > 0 def __ge__(self, other):
return self.compare_to_other(other) >= 0# }}}

def get_cached_cover_urls(mi):
from calibre.customize.ui import metadata_plugins plugins = list(metadata_plugins(['identify']))
for p in plugins:
url = p.get_cached_cover_url(mi.identifiers)
if url:
yield (p, url)

def dump_caches():
from calibre.customize.ui import metadata_plugins return {p.name:p.dump_caches() for p in metadata_plugins(['identify'])}

def load_caches(dump):
from calibre.customize.ui import metadata_plugins plugins = list(metadata_plugins(['identify']))
for p in plugins:
cache = dump.get(p.name, None)
if cache:
p.load_caches(cache)

def fixauthors(authors):
if not authors:
return authors ans = []
for x in authors:
ans.append(' '.join(map(cap_author_token, x.split())))
return ans def fixcase(x):
if x:
from calibre.utils.titlecase import titlecase x = titlecase(x)
return x class Option:
__slots__ = ('choices', 'default', 'desc', 'label', 'name', 'type')

def __init__(self, name, type_, default, label, desc, choices=None):
'''
:param name: The name of this option. Must be a valid python identifier
:param type_: The type of this option, one of ('number', 'string',
'bool', 'choices')
:param default: The default value for this option
:param label: A short (few words) description of this option
:param desc: A longer description of this option
:param choices: A dict of possible values, used only if type='choices'.
dict is of the form {key:human readable label, ...}
'''
self.name, self.type, self.default, self.label, self.desc = (name, type_, default, label, desc)
if choices and not isinstance(choices, dict):
choices = {x: x for x in choices}
self.choices = choices

[документация]
class Source(Plugin):

type = _('Metadata source')
author = 'Kovid Goyal'

supported_platforms = ['windows', 'osx', 'linux']

#: Set of capabilities supported by this plugin.
#: Useful capabilities are: 'identify', 'cover'
capabilities = frozenset()

#: List of metadata fields that can potentially be download by this plugin
#: during the identify phase touched_fields = frozenset()

#: Set this to True if your plugin returns HTML formatted comments has_html_comments = False

#: Setting this to True means that the browser object will indicate
#: that it supports gzip transfer encoding. This can speedup downloads
#: but make sure that the source actually supports gzip transfer encoding
#: correctly first supports_gzip_transfer_encoding = False

#: Set this to True to ignore HTTPS certificate errors when connecting
#: to this source.
ignore_ssl_errors = False

#: Cached cover URLs can sometimes be unreliable (i.e. the download could
#: fail or the returned image could be bogus). If that is often the case
#: with this source, set to False cached_cover_url_is_reliable = True

#: A list of :class:`Option` objects. They will be used to automatically
#: construct the configuration widget for this plugin options = ()

#: A string that is displayed at the top of the config widget for this
#: plugin config_help_message = None

#: If True this source can return multiple covers for a given query can_get_multiple_covers = False

#: If set to True covers downloaded by this plugin are automatically trimmed.
auto_trim_covers = False

#: If set to True, and this source returns multiple results for a query,
#: some of which have ISBNs and some of which do not, the results without
#: ISBNs will be ignored prefer_results_with_isbn = True def __init__(self, *args, **kwargs):
Plugin.__init__(self, *args, **kwargs)
self.running_a_test = False # Set to True when using identify_test()
self._isbn_to_identifier_cache = {}
self._identifier_to_cover_url_cache = {}
self.cache_lock = threading.RLock()
self._config_obj = None self._browser = None self.prefs.defaults['ignore_fields'] = []
for opt in self.options:
self.prefs.defaults[opt.name] = opt.default

# Configuration {{{

[документация]
def is_configured(self):
'''
Return False if your plugin needs to be configured before it can be used. For example, it might need a username/password/API key.
'''
return True
def is_customizable(self):
return True

[документация]
def customization_help(self):
return 'This plugin can only be customized using the GUI'

[документация]
def config_widget(self):
from calibre.gui2.metadata.config import ConfigWidget return ConfigWidget(self)

[документация]
def save_settings(self, config_widget):
config_widget.commit()

@property def prefs(self):
if self._config_obj is None:
from calibre.utils.config import JSONConfig self._config_obj = JSONConfig('metadata_sources/%s.json'%self.name)
return self._config_obj
# }}}

# Browser {{{

@property def user_agent(self):
# Pass in an index to random_user_agent() to test with a particular
# user agent return random_user_agent()

@property def browser(self):
if self._browser is None:
self._browser = browser(user_agent=self.user_agent, verify_ssl_certificates=not self.ignore_ssl_errors)
if self.supports_gzip_transfer_encoding:
self._browser.set_handle_gzip(True)
return self._browser.clone_browser()

# }}}

# Caching {{{ def get_related_isbns(self, id_):
with self.cache_lock:
for isbn, q in iteritems(self._isbn_to_identifier_cache):
if q == id_:
yield isbn def cache_isbn_to_identifier(self, isbn, identifier):
with self.cache_lock:
self._isbn_to_identifier_cache[isbn] = identifier def cached_isbn_to_identifier(self, isbn):
with self.cache_lock:
return self._isbn_to_identifier_cache.get(isbn, None)

def cache_identifier_to_cover_url(self, id_, url):
with self.cache_lock:
self._identifier_to_cover_url_cache[id_] = url def cached_identifier_to_cover_url(self, id_):
with self.cache_lock:
return self._identifier_to_cover_url_cache.get(id_, None)

def dump_caches(self):
with self.cache_lock:
return {'isbn_to_identifier':self._isbn_to_identifier_cache.copy(),
'identifier_to_cover':self._identifier_to_cover_url_cache.copy()}

def load_caches(self, dump):
with self.cache_lock:
self._isbn_to_identifier_cache.update(dump['isbn_to_identifier'])
self._identifier_to_cover_url_cache.update(dump['identifier_to_cover'])

# }}}

# Utility functions {{{

[документация]
def get_author_tokens(self, authors, only_first_author=True):
'''
Take a list of authors and return a list of tokens useful for an
AND search query. This function tries to return tokens in first name middle names last name order, by assuming that if a comma is in the author name, the name is in lastname, other names form.
'''

if authors:
# Leave ' in there for Irish names remove_pat = re.compile(r'[!@#$%^&*()（）「」{}`~"\s\[\]/]')
replace_pat = re.compile(r'[-+.:;,，。；：]')
if only_first_author:
authors = authors[:1]
for au in authors:
has_comma = ',' in au au = replace_pat.sub(' ', au)
parts = au.split()
if has_comma:
# au probably in ln, fn form parts = parts[1:] + parts[:1]
for tok in parts:
tok = remove_pat.sub('', tok).strip()
if len(tok) > 2 and tok.lower() not in ('von', 'van',
_('Unknown').lower()):
yield tok

[документация]
def get_title_tokens(self, title, strip_joiners=True, strip_subtitle=False):
'''
Take a title and return a list of tokens useful for an AND search query.
Excludes connectives(optionally) and punctuation.
'''
if title:
# strip sub-titles if strip_subtitle:
subtitle = re.compile(r'([\(\[\{].*?[\)\]\}]|[/:\\].*$)')
if len(subtitle.sub('', title)) > 1:
title = subtitle.sub('', title)

title_patterns = [(re.compile(pat, re.IGNORECASE), repl) for pat, repl in
[
# Remove things like: (2010) (Omnibus) etc.
(r'(?i)[({\[](\d{4}|omnibus|anthology|hardcover|audiobook|audio\scd|paperback|turtleback|mass\s*market|edition|ed\.)[\])}]', ''),
# Remove any strings that contain the substring edition inside
# parentheses
(r'(?i)[({\[].*?(edition|ed.).*?[\]})]', ''),
# Remove commas used a separators in numbers
(r'(\d+),(\d+)', r'\1\2'),
# Remove hyphens only if they have whitespace before them
(r'(\s-)', ' '),
# Replace other special chars with a space
(r'''[:,;!@$%^&*(){}.`~"\s\[\]/]《》「」“”''', ' '),
]]

for pat, repl in title_patterns:
title = pat.sub(repl, title)

tokens = title.split()
for token in tokens:
token = token.strip().strip('"').strip("'")
if token and (not strip_joiners or token.lower() not in ('a',
'and', 'the', '&')):
yield token

[документация]
def split_jobs(self, jobs, num):
'Split a list of jobs into at most num groups, as evenly as possible'
groups = [[] for i in range(num)]
jobs = list(jobs)
while jobs:
for gr in groups:
try:
job = jobs.pop()
except IndexError:
break gr.append(job)
return [g for g in groups if g]

[документация]
def test_fields(self, mi):
'''
Return the first field from self.touched_fields that is null on the mi object
'''
for key in self.touched_fields:
if key.startswith('identifier:'):
key = key.partition(':')[-1]
if not mi.has_identifier(key):
return 'identifier: ' + key elif mi.is_null(key):
return key

[документация]
def clean_downloaded_metadata(self, mi):
'''
Call this method in your plugin's identify method to normalize metadata before putting the Metadata object into result_queue. You can of course, use a custom algorithm suited to your metadata source.
'''
docase = mi.language == 'eng' or mi.is_null('language')
if docase and mi.title:
mi.title = fixcase(mi.title)
mi.authors = fixauthors(mi.authors)
if mi.tags and docase:
mi.tags = list(map(fixcase, mi.tags))
mi.isbn = check_isbn(mi.isbn)

def download_multiple_covers(self, title, authors, urls, get_best_cover, timeout, result_queue, abort, log, prefs_name='max_covers'):
if not urls:
log('No images found for, title: %r and authors: %r'%(title, authors))
return import time from threading import Thread if prefs_name:
urls = urls[:self.prefs[prefs_name]]
if get_best_cover:
urls = urls[:1]
log('Downloading %d covers'%len(urls))
workers = [Thread(target=self.download_image, args=(u, timeout, log, result_queue)) for u in urls]
for w in workers:
w.daemon = True w.start()
alive = True start_time = time.time()
while alive and not abort.is_set() and time.time() - start_time < timeout:
alive = False for w in workers:
if w.is_alive():
alive = True break abort.wait(0.1)

def download_image(self, url, timeout, log, result_queue):
try:
ans = self.browser.open_novisit(url, timeout=timeout).read()
result_queue.put((self, ans))
log('Downloaded cover from: %s'%url)
except Exception:
self.log.exception('Failed to download cover from: %r'%url)

# }}}

# Metadata API {{{

[документация]
def get_book_url(self, identifiers):
'''
Return a 3-tuple or None. The 3-tuple is of the form:
(identifier_type, identifier_value, URL).
The URL is the URL for the book identified by identifiers at this source. identifier_type, identifier_value specify the identifier corresponding to the URL.
This URL must be browsable to by a human using a browser. It is meant to provide a clickable link for the user to easily visit the books page at this source.
If no URL is found, return None. This method must be quick, and consistent, so only implement it if it is possible to construct the URL from a known scheme given identifiers.
'''
return

[документация]
def get_book_url_name(self, idtype, idval, url):
'''
Return a human readable name from the return value of get_book_url().
'''
return self.name

[документация]
def get_book_urls(self, identifiers):
'''
Override this method if you would like to return multiple URLs for this book.
Return a list of 3-tuples. By default this method simply calls :func:`get_book_url`.
'''
data = self.get_book_url(identifiers)
if data is None:
return ()
return (data,)

[документация]
def get_cached_cover_url(self, identifiers):
'''
Return cached cover URL for the book identified by the identifiers dictionary or None if no such URL exists.

Note that this method must only return validated URLs, i.e. not URLS that could result in a generic cover image or a not found error.
'''
return

[документация]
def id_from_url(self, url):
'''
Parse a URL and return a tuple of the form:
(identifier_type, identifier_value).
If the URL does not match the pattern for the metadata source, return None.
'''
return

[документация]
def identify_results_keygen(self, title=None, authors=None, identifiers={}):
'''
Return a function that is used to generate a key that can sort Metadata objects by their relevance given a search query (title, authors, identifiers).

These keys are used to sort the results of a call to :meth:`identify`.

For details on the default algorithm see
:class:`InternalMetadataCompareKeyGen`. Re-implement this function in your plugin if the default algorithm is not suitable.
'''
def keygen(mi):
return InternalMetadataCompareKeyGen(mi, self, title, authors, identifiers)
return keygen

[документация]
def identify(self, log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=30):
'''
Identify a book by its Title/Author/ISBN/etc.

If identifiers(s) are specified and no match is found and this metadata source does not store all related identifiers (for example, all ISBNs of a book), this method should retry with just the title and author
(assuming they were specified).

If this metadata source also provides covers, the URL to the cover should be cached so that a subsequent call to the get covers API with the same ISBN/special identifier does not need to get the cover URL again. Use the caching API for this.

Every Metadata object put into result_queue by this method must have a
`source_relevance` attribute that is an integer indicating the order in which the results were returned by the metadata source for this query.
This integer will be used by :meth:`compare_identify_results`. If the order is unimportant, set it to zero for every result.

Make sure that any cover/ISBN mapping information is cached before the
Metadata object is put into result_queue.

:param log: A log object, use it to output debugging information/errors
:param result_queue: A result Queue, results should be put into it.
Each result is a Metadata object
:param abort: If abort.is_set() returns True, abort further processing and return as soon as possible
:param title: The title of the book, can be None
:param authors: A list of authors of the book, can be None
:param identifiers: A dictionary of other identifiers, most commonly
{'isbn':'1234...'}
:param timeout: Timeout in seconds, no network request should hang for longer than timeout.
:return: None if no errors occurred, otherwise a unicode representation of the error suitable for showing to the user

'''
return

[документация]
def download_cover(self, log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False):
'''
Download a cover and put it into result_queue. The parameters all have the same meaning as for :meth:`identify`. Put (self, cover_data) into result_queue.

This method should use cached cover URLs for efficiency whenever possible. When cached data is not present, most plugins simply call identify and use its results.

If the parameter get_best_cover is True and this plugin can get multiple covers, it should only get the "best" one.
'''
pass

# }}}

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.ebooks.oeb.polish.container

Исходный код calibre.ebooks.oeb.polish.container

#!/usr/bin/env python
# License: GPLv3 Copyright: 2013, Kovid Goyal <kovid at kovidgoyal.net> import errno import hashlib import logging import os import re import shutil import sys import unicodedata import uuid from collections import defaultdict from io import BytesIO from itertools import count from math import floor from css_parser import getUrls, replaceUrls from calibre import CurrentDir, walk from calibre.constants import iswindows from calibre.customize.ui import plugin_for_input_format, plugin_for_output_format from calibre.ebooks import escape_xpath_attr from calibre.ebooks.chardet import xml_to_unicode from calibre.ebooks.conversion.plugins.epub_input import ADOBE_OBFUSCATION, IDPF_OBFUSCATION, decrypt_font_data from calibre.ebooks.conversion.preprocess import CSSPreProcessor as cssp from calibre.ebooks.conversion.preprocess import HTMLPreProcessor from calibre.ebooks.metadata.opf3 import CALIBRE_PREFIX, ensure_prefix, items_with_property, read_prefixes from calibre.ebooks.metadata.utils import parse_opf_version from calibre.ebooks.mobi import MobiError from calibre.ebooks.mobi.reader.headers import MetadataHeader from calibre.ebooks.oeb.base import (
DC11_NS,
OEB_DOCS,
OEB_STYLES,
OPF,
OPF2_NS,
Manifest, itercsslinks, iterlinks, rewrite_links, serialize, urlquote, urlunquote,
)
from calibre.ebooks.oeb.parse_utils import NotHTML, parse_html from calibre.ebooks.oeb.polish.errors import DRMError, InvalidBook from calibre.ebooks.oeb.polish.parsing import decode_xml from calibre.ebooks.oeb.polish.parsing import parse as parse_html_tweak from calibre.ebooks.oeb.polish.utils import OEB_FONTS, CommentFinder, PositionFinder, adjust_mime_for_epub, guess_type, insert_self_closing, parse_css from calibre.ptempfile import PersistentTemporaryDirectory, PersistentTemporaryFile, TemporaryDirectory from calibre.utils.filenames import hardlink_file, make_long_path_useable, nlinks_file, retry_on_fail from calibre.utils.ipc.simple_worker import WorkerError, fork_job from calibre.utils.logging import default_log from calibre.utils.xml_parse import safe_xml_fromstring from calibre.utils.zipfile import ZipFile from polyglot.builtins import iteritems from polyglot.urllib import urlparse exists, join, relpath = os.path.exists, os.path.join, os.path.relpath
OPF_NAMESPACES = {'opf':OPF2_NS, 'dc':DC11_NS}
null = object()
OEB_FONTS # for plugin compat class CSSPreProcessor(cssp):

def __call__(self, data):
return self.MS_PAT.sub(self.ms_sub, data)

def clone_dir(src, dest):
' Clone a folder using hard links for the files, dest must already exist '
for x in os.listdir(src):
dpath = os.path.join(dest, x)
spath = os.path.join(src, x)
if os.path.isdir(spath):
os.mkdir(dpath)
clone_dir(spath, dpath)
else:
try:
hardlink_file(spath, dpath)
except Exception:
shutil.copy2(spath, dpath)

def clone_container(container, dest_dir, container_class=None):
' Efficiently clone a container using hard links '
dest_dir = os.path.abspath(os.path.realpath(dest_dir))
clone_data = container.clone_data(dest_dir)
container_class = container_class or type(container)
return container_class(log=container.log, clone_data=clone_data)

def name_to_abspath(name, root):
return os.path.abspath(join(root, *name.split('/')))

def abspath_to_name(path, root):
return relpath(os.path.abspath(path), root).replace(os.sep, '/')

def name_to_href(name, root, base=None, quote=urlquote):
fullpath = name_to_abspath(name, root)
basepath = root if base is None else os.path.dirname(name_to_abspath(base, root))
path = relpath(fullpath, basepath).replace(os.sep, '/')
return quote(path)

def href_to_name(href, root, base=None):
base = root if base is None else os.path.dirname(name_to_abspath(base, root))
try:
purl = urlparse(href)
except ValueError:
return None if purl.scheme or not purl.path:
return None href = urlunquote(purl.path)
if iswindows and ':' in href:
# path manipulations on windows fail for paths with : in them, so we
# assume all such paths are invalid/absolute paths.
return None fullpath = os.path.join(base, *href.split('/'))
try:
return unicodedata.normalize('NFC', abspath_to_name(fullpath, root))
except ValueError:
return None def seconds_to_timestamp(duration: float) -> str:
seconds = floor(duration)
float_part = duration - seconds hours = seconds // 3600 minutes = (seconds % 3600) // 60 seconds = seconds % 60 ans = f'{hours:02d}:{minutes:02d}:{seconds:02d}'
if float_part:
ans += f'{float_part:.20f}'.rstrip('0')[1:]
return ans class ContainerBase: # {{{
'''
A base class that implements just the parsing methods. Useful to create virtual containers for testing.
'''

#: The mode used to parse HTML and CSS (polishing uses tweak_mode=False and the editor uses tweak_mode=True)
tweak_mode = False def __init__(self, log=default_log):
self.log = log or default_log self.parsed_cache = {}
self.mime_map = {}
self.encoding_map = {}
self.html_preprocessor = HTMLPreProcessor()
self.css_preprocessor = CSSPreProcessor()

def guess_type(self, name):
' Return the expected mimetype for the specified file name based on its extension. '
return adjust_mime_for_epub(filename=name, opf_version=self.opf_version_parsed)

def decode(self, data, normalize_to_nfc=True):
'''
Automatically decode ``data`` into a ``unicode`` object.

:param normalize_to_nfc: Normalize returned unicode to the NFC normal form as is required by both the EPUB and AZW3 formats.
'''
html, used_encoding = decode_xml(data, normalize_to_nfc)
if used_encoding:
self.used_encoding = used_encoding return html def parse_xml(self, data):
data, self.used_encoding = xml_to_unicode( data, strip_encoding_pats=True, assume_utf8=True, resolve_entities=True)
data = unicodedata.normalize('NFC', data)
return safe_xml_fromstring(data)

def parse_xhtml(self, data, fname='<string>', force_html5_parse=False):
if self.tweak_mode:
return parse_html_tweak(data, log=self.log, decoder=self.decode, force_html5_parse=force_html5_parse)
else:
try:
return parse_html( data, log=self.log, decoder=self.decode, preprocessor=self.html_preprocessor, filename=fname, non_html_file_tags={'ncx'})
except NotHTML:
return self.parse_xml(data)

def parse_css(self, data, fname='<string>', is_declaration=False):
return parse_css(data, fname=fname, is_declaration=is_declaration, decode=self.decode, log_level=logging.WARNING, css_preprocessor=(None if self.tweak_mode else self.css_preprocessor))
# }}}

[документация]
class Container(ContainerBase): # {{{

'''
A container represents an open e-book as a folder full of files and an
OPF file. There are two important concepts:

* The root folder. This is the base of the e-book. All the e-books files are inside this folder or in its sub-folders.

* Names: These are paths to the books' files relative to the root folder. They always contain POSIX separators and are unquoted. They can be thought of as canonical identifiers for files in the book.
Most methods on the container object work with names. Names are always in the NFC Unicode normal form.

* Clones: the container object supports efficient on-disk cloning, which is used to implement checkpoints in the e-book editor. In order to make this work, you should never access files on the filesystem directly. Instead, use :meth:`raw_data` or
:meth:`open` to read/write to component files in the book.

When converting between hrefs and names use the methods provided by this class, they assume all hrefs are quoted.
'''

#: The type of book (epub for EPUB files and azw3 for AZW3 files)
book_type = 'oeb'
#: If this container represents an unzipped book (a directory)
is_dir = False

SUPPORTS_TITLEPAGES = True
SUPPORTS_FILENAMES = True
MAX_HTML_FILE_SIZE = 0

@property def book_type_for_display(self):
return self.book_type.upper()

def __init__(self, rootpath=None, opfpath=None, log=default_log, clone_data=None):
super().__init__(log=log)
self.root = clone_data['root'] if clone_data is not None else os.path.abspath(rootpath)

self.name_path_map = {}
self.dirtied = set()
self.pretty_print = set()
self.cloned = False self.cache_names = ('parsed_cache', 'mime_map', 'name_path_map', 'encoding_map', 'dirtied', 'pretty_print')
self.href_to_name_cache = {}

if clone_data is not None:
for x in ('cloned', 'name_path_map', 'opf_name', 'mime_map', 'pretty_print', 'encoding_map', 'tweak_mode'):
setattr(self, x, clone_data[x])
self.opf_dir = os.path.dirname(self.name_path_map[self.opf_name])
return

# Map of relative paths with '/' separators from root of unzipped ePub
# to absolute paths on filesystem with os-specific separators opfpath = os.path.abspath(os.path.realpath(opfpath))
all_opf_files = []
for dirpath, _dirnames, filenames in os.walk(self.root):
for f in filenames:
path = join(dirpath, f)
name = self.abspath_to_name(path)
self.name_path_map[name] = path self.mime_map[name] = guess_type(path)
# Special case if we have stumbled onto the opf if path == opfpath:
self.opf_name = name self.opf_dir = os.path.dirname(path)
self.mime_map[name] = guess_type('a.opf')
if path.lower().endswith('.opf'):
all_opf_files.append((name, os.path.dirname(path)))

if not hasattr(self, 'opf_name') and all_opf_files:
self.opf_name, self.opf_dir = all_opf_files[0]
self.mime_map[self.opf_name] = guess_type('a.opf')

if not hasattr(self, 'opf_name'):
raise InvalidBook(f'Could not locate opf file: {opfpath!r}')

# Update mime map with data from the OPF self.refresh_mime_map()

def refresh_mime_map(self):
for item in self.opf_xpath('//opf:manifest/opf:item[@href and @media-type]'):
href = item.get('href')
try:
name = self.href_to_name(href, self.opf_name)
except ValueError:
continue # special filenames such as CON on windows cause relpath to fail mt = item.get('media-type')
if name in self.mime_map and name != self.opf_name and mt:
# some epubs include the opf in the manifest with an incorrect mime type self.mime_map[name] = mt def data_for_clone(self, dest_dir=None):
dest_dir = dest_dir or self.root return {
'root': dest_dir,
'opf_name': self.opf_name,
'mime_map': self.mime_map.copy(),
'pretty_print': set(self.pretty_print),
'encoding_map': self.encoding_map.copy(),
'tweak_mode': self.tweak_mode,
'cloned': self.cloned,
'name_path_map': { name: os.path.join(dest_dir, os.path.relpath(path, self.root)) for name, path in self.name_path_map.items()
}
}

def clone_data(self, dest_dir):
Container.commit(self, keep_parsed=False)
clone_dir(self.root, dest_dir)
self.cloned = True return self.data_for_clone(dest_dir)

def __getstate__(self):
Container.commit(self, keep_parsed=True)
return self.data_for_clone()

def __setstate__(self, state):
self.__init__(log=default_log, clone_data=state)

[документация]
def add_name_to_manifest(self, name, process_manifest_item=None, suggested_id=''):
' Add an entry to the manifest for a file with the specified name. Returns the manifest id. '
all_ids = {x.get('id') for x in self.opf_xpath('//*[@id]')}
c = 0 item_id = suggested_id = suggested_id or 'id'
while item_id in all_ids:
c += 1 item_id = f'{suggested_id}-{c}'
manifest = self.opf_xpath('//opf:manifest')[0]
href = self.name_to_href(name, self.opf_name)
item = manifest.makeelement(OPF('item'), id=item_id, href=href)
item.set('media-type', self.mime_map[name])
self.insert_into_xml(manifest, item)
if process_manifest_item is not None:
process_manifest_item(item)
self.dirty(self.opf_name)
return item_id

[документация]
def manifest_has_name(self, name):
''' Return True if the manifest has an entry corresponding to name '''
all_names = {self.href_to_name(x.get('href'), self.opf_name) for x in self.opf_xpath('//opf:manifest/opf:item[@href]')}
return name in all_names

[документация]
def make_name_unique(self, name):
''' Ensure that `name` does not already exist in this book. If it does, return a modified version that does not exist. '''
counter = count()
while self.has_name_case_insensitive(name) or self.manifest_has_name(name):
c = next(counter) + 1 base, ext = name.rpartition('.')[::2]
if c > 1:
base = base.rpartition('-')[0]
name = f'{base}-{c}.{ext}'
return name

[документация]
def add_file( self, name, data=b'', media_type=None, spine_index=None, modify_name_if_needed=False, process_manifest_item=None, suggested_id='',
):
''' Add a file to this container. Entries for the file are automatically created in the OPF manifest and spine
(if the file is a text document) '''
if '..' in name:
raise ValueError('Names are not allowed to have .. in them')
href = self.name_to_href(name, self.opf_name)
if self.has_name_case_insensitive(name) or self.manifest_has_name(name):
if not modify_name_if_needed:
raise ValueError((f'A file with the name {name} already exists') if self.has_name_case_insensitive(name) else
(f'An item with the href {href} already exists in the manifest'))
name = self.make_name_unique(name)
href = self.name_to_href(name, self.opf_name)
path = self.name_to_abspath(name)
base = os.path.dirname(path)
if not os.path.exists(base):
os.makedirs(base)
with open(path, 'wb') as f:
if hasattr(data, 'read'):
shutil.copyfileobj(data, f)
else:
f.write(data)
mt = media_type or self.guess_type(name)
self.name_path_map[name] = path self.mime_map[name] = mt if self.ok_to_be_unmanifested(name):
return name item_id = self.add_name_to_manifest(name, process_manifest_item=process_manifest_item, suggested_id=suggested_id)
if mt in OEB_DOCS:
manifest = self.opf_xpath('//opf:manifest')[0]
spine = self.opf_xpath('//opf:spine')[0]
si = manifest.makeelement(OPF('itemref'), idref=item_id)
self.insert_into_xml(spine, si, index=spine_index)
return name[документация]
def rename(self, current_name, new_name):
''' Renames a file from current_name to new_name. It automatically rebases all links inside the file if the folder the file is in changes. Note however, that links are not updated in the other files that could reference this file. This is for performance, such updates should be done once, in bulk. '''
if current_name in self.names_that_must_not_be_changed:
raise ValueError(f'Renaming of {current_name} is not allowed')
if self.exists(new_name) and (new_name == current_name or new_name.lower() != current_name.lower()):
# The destination exists and does not differ from the current name only by case raise ValueError(f'Cannot rename {current_name} to {new_name} as {new_name} already exists')
new_path = self.name_to_abspath(new_name)
base = os.path.dirname(new_path)
if os.path.isfile(base):
raise ValueError(f'Cannot rename {current_name} to {new_name} as {base} is a file')
if not os.path.exists(base):
os.makedirs(base)
old_path = parent_dir = self.name_to_abspath(current_name)
self.commit_item(current_name)
os.rename(old_path, new_path)
# Remove empty directories while parent_dir:
parent_dir = os.path.dirname(parent_dir)
try:
os.rmdir(parent_dir)
except OSError:
break for x in ('mime_map', 'encoding_map'):
x = getattr(self, x)
if current_name in x:
x[new_name] = x[current_name]
self.name_path_map[new_name] = new_path for x in self.cache_names:
x = getattr(self, x)
try:
x.pop(current_name, None)
except TypeError:
x.discard(current_name)
if current_name == self.opf_name:
self.opf_name = new_name if os.path.dirname(old_path) != os.path.dirname(new_path):
from calibre.ebooks.oeb.polish.replace import LinkRebaser repl = LinkRebaser(self, current_name, new_name)
self.replace_links(new_name, repl)
self.dirty(new_name)

[документация]
def replace_links(self, name, replace_func):
''' Replace all links in name using replace_func, which must be a callable that accepts a URL and returns the replaced URL. It must also have a 'replaced' attribute that is set to True if any actual replacement is done. Convenient ways of creating such callables are using the :class:`LinkReplacer` and :class:`LinkRebaser` classes. '''
media_type = self.mime_map.get(name, guess_type(name))
if name == self.opf_name:
replace_func.file_type = 'opf'
for elem in self.opf_xpath('//*[@href]'):
elem.set('href', replace_func(elem.get('href')))
elif media_type.lower() in OEB_DOCS:
replace_func.file_type = 'text'
rewrite_links(self.parsed(name), replace_func)
elif media_type.lower() in OEB_STYLES:
replace_func.file_type = 'style'
replaceUrls(self.parsed(name), replace_func)
elif media_type.lower() == guess_type('toc.ncx'):
replace_func.file_type = 'ncx'
for elem in self.parsed(name).xpath('//*[@src]'):
elem.set('src', replace_func(elem.get('src')))

if replace_func.replaced:
self.dirty(name)
return replace_func.replaced

[документация]
def iterlinks(self, name, get_line_numbers=True):
''' Iterate over all links in name. If get_line_numbers is True the yields results of the form (link, line_number, offset). Where line_number is the line_number at which the link occurs and offset is the number of characters from the start of the line. Note that offset could actually encompass several lines if not zero. '''
media_type = self.mime_map.get(name, guess_type(name))
if name == self.opf_name:
for elem in self.opf_xpath('//*[@href]'):
yield (elem.get('href'), elem.sourceline, 0) if get_line_numbers else elem.get('href')
elif media_type.lower() in OEB_DOCS:
for el, attr, link, pos in iterlinks(self.parsed(name)):
yield (link, el.sourceline, pos) if get_line_numbers else link elif media_type.lower() in OEB_STYLES:
if get_line_numbers:
with self.open(name, 'rb') as f:
raw = self.decode(f.read()).replace('\r\n', '\n').replace('\r', '\n')
position = PositionFinder(raw)
is_in_comment = CommentFinder(raw)
for link, offset in itercsslinks(raw):
if not is_in_comment(offset):
lnum, col = position(offset)
yield link, lnum, col else:
for link in getUrls(self.parsed(name)):
yield link elif media_type.lower() == guess_type('toc.ncx'):
for elem in self.parsed(name).xpath('//*[@src]'):
yield (elem.get('src'), elem.sourceline, 0) if get_line_numbers else elem.get('src')

[документация]
def abspath_to_name(self, fullpath, root=None):
'''
Convert an absolute path to a canonical name relative to :attr:`root`

:param root: The base folder. By default the root for this container object is used.
'''
# OS X silently changes all file names to NFD form. The EPUB
# spec requires all text including filenames to be in NFC form.
# The proper fix is to implement a VFS that maps between
# canonical names and their file system representation, however,
# I don't have the time for that now. Note that the container
# ensures that all text files are normalized to NFC when
# decoding them anyway, so there should be no mismatch between
# names in the text and NFC canonical file names.
return unicodedata.normalize('NFC', abspath_to_name(fullpath, root or self.root))

[документация]
def name_to_abspath(self, name):
' Convert a canonical name to an absolute OS dependent path '
return name_to_abspath(name, self.root)

[документация]
def exists(self, name):
''' True iff a file/folder corresponding to the canonical name exists. Note that this function suffers from the limitations of the underlying OS filesystem, in particular case (in)sensitivity. So on a case insensitive filesystem this will return True even if the case of name is different from the case of the underlying filesystem file. See also :meth:`has_name`'''
return os.path.exists(self.name_to_abspath(name))

[документация]
def href_to_name(self, href, base=None):
'''
Convert an href (relative to base) to a name. base must be a name or
None, in which case self.root is used.
'''
key = href, base ans = self.href_to_name_cache.get(key, null)
if ans is null:
ans = self.href_to_name_cache[key] = href_to_name(href, self.root, base=base)
return ans

[документация]
def name_to_href(self, name, base=None):
'''Convert a name to a href relative to base, which must be a name or
None in which case self.root is used as the base'''
return name_to_href(name, self.root, base=base)

[документация]
def opf_xpath(self, expr):
' Convenience method to evaluate an XPath expression on the OPF file, has the opf: and dc: namespace prefixes pre-defined. '
return self.opf.xpath(expr, namespaces=OPF_NAMESPACES)

[документация]
def has_name(self, name):
''' Return True iff a file with the same canonical name as that specified exists. Unlike :meth:`exists` this method is always case-sensitive. '''
return name and name in self.name_path_map
def has_name_and_is_not_empty(self, name):
path = self.name_path_map.get(name)
if not path:
return False try:
if (sz := os.path.getsize(path)) == 0:
# this can happen when the directory entry is not flushed (which happens during fast EPUB extraction), so
# open the file and check to be sure.
with open(path) as f:
sz = f.seek(0, os.SEEK_END)
return sz > 0 except OSError:
return False def has_name_case_insensitive(self, name):
if not name:
return False name = name.lower()
for q in self.name_path_map:
if q.lower() == name:
return True return False

[документация]
def relpath(self, path, base=None):
'''Convert an absolute path (with os separators) to a path relative to base (defaults to self.root). The relative path is *not* a name. Use
:meth:`abspath_to_name` for that.'''
return relpath(path, base or self.root)

def ok_to_be_unmanifested(self, name):
return name in self.names_that_need_not_be_manifested

@property def names_that_need_not_be_manifested(self):
' Set of names that are allowed to be missing from the manifest. Depends on the e-book file format. '
return {self.opf_name}

@property def names_that_must_not_be_removed(self):
' Set of names that must never be deleted from the container. Depends on the e-book file format. '
return {self.opf_name}

@property def names_that_must_not_be_changed(self):
' Set of names that must never be renamed. Depends on the e-book file format. '
return set()

def parse(self, path, mime):
with open(path, 'rb') as src:
data = src.read()
if mime in OEB_DOCS:
data = self.parse_xhtml(data, self.relpath(path))
elif mime[-4:] in {'+xml', '/xml'}:
data = self.parse_xml(data)
elif mime in OEB_STYLES:
data = self.parse_css(data, self.relpath(path))
return data

[документация]
def raw_data(self, name, decode=True, normalize_to_nfc=True):
'''
Return the raw data corresponding to the file specified by name

:param decode: If True and the file has a text based MIME type, decode it and return a unicode object instead of raw bytes.
:param normalize_to_nfc: If True the returned unicode object is normalized to the NFC normal form as is required for the EPUB and AZW3 file formats.
'''
with self.open(name) as nf:
ans = nf.read()
mime = self.mime_map.get(name, guess_type(name))
if decode and (mime in OEB_STYLES or mime in OEB_DOCS or mime == 'text/plain' or mime[-4:] in {'+xml', '/xml'}):
ans = self.decode(ans, normalize_to_nfc=normalize_to_nfc)
return ans

[документация]
def parsed(self, name):
''' Return a parsed representation of the file specified by name. For
HTML and XML files an lxml tree is returned. For CSS files a css_parser stylesheet is returned. Note that parsed objects are cached for performance. If you make any changes to the parsed object, you must call :meth:`dirty` so that the container knows to update the cache. See also :meth:`replace`.'''
ans = self.parsed_cache.get(name, None)
if ans is None:
self.used_encoding = None mime = self.mime_map.get(name, guess_type(name))
ans = self.parse(self.name_path_map[name], mime)
self.parsed_cache[name] = ans self.encoding_map[name] = self.used_encoding return ans

[документация]
def replace(self, name, obj):
'''
Replace the parsed object corresponding to name with obj, which must be a similar object, i.e. an lxml tree for HTML/XML or a css_parser stylesheet for a CSS file.
'''
self.parsed_cache[name] = obj self.dirty(name)

@property def opf(self):
' The parsed OPF file '
return self.parsed(self.opf_name)

@property def mi(self):
''' The metadata of this book as a Metadata object. Note that this object is constructed on the fly every time this property is requested, so use it sparingly. '''
from calibre.ebooks.metadata.opf2 import OPF as O mi = self.serialize_item(self.opf_name)
return O(BytesIO(mi), basedir=self.opf_dir, unquote_urls=False, populate_spine=False).to_book_metadata()

@property def opf_version(self):
' The version set on the OPF\'s <package> element '
try:
return self.opf_xpath('//opf:package/@version')[0]
except IndexError:
return ''

@property def opf_version_parsed(self):
' The version set on the OPF\'s <package> element as a tuple of integers '
return parse_opf_version(self.opf_version)

@property def manifest_items(self):
return self.opf_xpath('//opf:manifest/opf:item[@href and @id]')

@property def manifest_id_map(self):
' Mapping of manifest id to canonical names '
return {item.get('id'):self.href_to_name(item.get('href'), self.opf_name) for item in self.manifest_items}

@property def manifest_type_map(self):
' Mapping of manifest media-type to list of canonical names of that media-type '
ans = defaultdict(list)
for item in self.opf_xpath('//opf:manifest/opf:item[@href and @media-type]'):
ans[item.get('media-type').lower()].append(self.href_to_name( item.get('href'), self.opf_name))
return {mt:tuple(v) for mt, v in iteritems(ans)}

[документация]
def manifest_items_with_property(self, property_name):
' All manifest items that have the specified property '
prefixes = read_prefixes(self.opf)
for item in items_with_property(self.opf, property_name, prefixes):
href = item.get('href')
if href:
yield self.href_to_name(item.get('href'), self.opf_name)

[документация]
def manifest_items_of_type(self, predicate):
''' The names of all manifest items whose media-type matches predicate.
`predicate` can be a set, a list, a string or a function taking a single argument, which will be called with the media-type. '''
if isinstance(predicate, str):
predicate = predicate.__eq__ elif hasattr(predicate, '__contains__'):
predicate = predicate.__contains__ for mt, names in iteritems(self.manifest_type_map):
if predicate(mt):
yield from names

[документация]
def apply_unique_properties(self, name, *properties):
''' Ensure that the specified properties are set on only the manifest item identified by name. You can pass None as the name to remove the property from all items. '''
properties = frozenset(properties)
removed_names, added_names = [], []
for p in properties:
if p.startswith('calibre:'):
ensure_prefix(self.opf, None, 'calibre', CALIBRE_PREFIX)
break for item in self.opf_xpath('//opf:manifest/opf:item'):
iname = self.href_to_name(item.get('href'), self.opf_name)
props = (item.get('properties') or '').split()
lprops = {p.lower() for p in props}
for prop in properties:
if prop.lower() in lprops:
if name != iname:
removed_names.append(iname)
props = [p for p in props if p.lower() != prop]
if props:
item.set('properties', ' '.join(props))
else:
del item.attrib['properties']
else:
if name == iname:
added_names.append(iname)
props.append(prop)
item.set('properties', ' '.join(props))
self.dirty(self.opf_name)
return removed_names, added_names

[документация]
def add_properties(self, name, *properties):
''' Add the specified properties to the manifest item identified by name. '''
properties = frozenset(properties)
if not properties:
return True for p in properties:
if p.startswith('calibre:'):
ensure_prefix(self.opf, None, 'calibre', CALIBRE_PREFIX)
break for item in self.opf_xpath('//opf:manifest/opf:item'):
iname = self.href_to_name(item.get('href'), self.opf_name)
if name == iname:
props = frozenset((item.get('properties') or '').split()) | properties item.set('properties', ' '.join(props))
return True return False

@property def guide_type_map(self):
' Mapping of guide type to canonical name '
return {item.get('type', ''):self.href_to_name(item.get('href'), self.opf_name)
for item in self.opf_xpath('//opf:guide/opf:reference[@href and @type]')}

@property def spine_iter(self):
''' An iterator that yields item, name is_linear for every item in the books' spine. item is the lxml element, name is the canonical file name and is_linear is True if the item is linear. See also: :attr:`spine_names` and :attr:`spine_items`. '''
manifest_id_map = self.manifest_id_map non_linear = []
for item in self.opf_xpath('//opf:spine/opf:itemref[@idref]'):
idref = item.get('idref')
name = manifest_id_map.get(idref, None)
path = self.name_path_map.get(name, None)
if path:
if item.get('linear', 'yes') == 'yes':
yield item, name, True else:
non_linear.append((item, name))
for item, name in non_linear:
yield item, name, False def index_in_spine(self, name):
manifest_id_map = self.manifest_id_map for i, item in enumerate(self.opf_xpath('//opf:spine/opf:itemref[@idref]')):
idref = item.get('idref')
q = manifest_id_map.get(idref, None)
if q == name:
return i

@property def spine_names(self):
''' An iterator yielding name and is_linear for every item in the books' spine. See also: :attr:`spine_iter` and :attr:`spine_items`. '''
for item, name, linear in self.spine_iter:
yield name, linear

@property def spine_items(self):
''' An iterator yielding the path for every item in the books' spine. See also: :attr:`spine_iter` and :attr:`spine_items`. '''
for name, linear in self.spine_names:
yield self.name_path_map[name]

[документация]
def remove_from_spine(self, spine_items, remove_if_no_longer_in_spine=True):
'''
Remove the specified items (by canonical name) from the spine. If ``remove_if_no_longer_in_spine`` is True, the items are also deleted from the book, not just from the spine.
'''
nixed = set()
for (name, remove), (item, xname, linear) in zip(spine_items, self.spine_iter):
if remove and name == xname:
self.remove_from_xml(item)
nixed.add(name)
if remove_if_no_longer_in_spine:
# Remove from the book if no longer in spine nixed -= {name for name, linear in self.spine_names}
for name in nixed:
self.remove_item(name)

[документация]
def set_spine(self, spine_items):
''' Set the spine to be spine_items where spine_items is an iterable of the form (name, linear). Will raise an error if one of the names is not present in the manifest. '''
imap = self.manifest_id_map imap = {name:item_id for item_id, name in iteritems(imap)}
items = [item for item, name, linear in self.spine_iter]
tail, last_tail = (items[0].tail, items[-1].tail) if items else ('\n ', '\n ')
for i in items:
self.remove_from_xml(i)
spine = self.opf_xpath('//opf:spine')[0]
spine.text = tail for name, linear in spine_items:
i = spine.makeelement('{{{}}}itemref'.format(OPF_NAMESPACES['opf']), nsmap={'opf':OPF_NAMESPACES['opf']})
i.tail = tail i.set('idref', imap[name])
spine.append(i)
if not linear:
i.set('linear', 'no')
if len(spine) > 0:
spine[-1].tail = last_tail self.dirty(self.opf_name)[документация]
def remove_item(self, name, remove_from_guide=True):
'''
Remove the item identified by name from this container. This removes all references to the item in the OPF manifest, guide and spine as well as from any internal caches.
'''
removed = set()
for elem in self.opf_xpath('//opf:manifest/opf:item[@href]'):
if self.href_to_name(elem.get('href'), self.opf_name) == name:
id_ = elem.get('id', None)
if id_ is not None:
removed.add(id_)
self.remove_from_xml(elem)
self.dirty(self.opf_name)
if removed:
for spine in self.opf_xpath('//opf:spine'):
tocref = spine.attrib.get('toc', None)
if tocref and tocref in removed:
spine.attrib.pop('toc', None)
self.dirty(self.opf_name)

for item in self.opf_xpath('//opf:spine/opf:itemref[@idref]'):
idref = item.get('idref')
if idref in removed:
self.remove_from_xml(item)
self.dirty(self.opf_name)

for meta in self.opf_xpath('//opf:meta[@name="cover" and @content]'):
if meta.get('content') in removed:
self.remove_from_xml(meta)
self.dirty(self.opf_name)

for meta in self.opf_xpath('//opf:meta[@refines]'):
q = meta.get('refines')
if q.startswith('#') and q[1:] in removed:
self.remove_from_xml(meta)
self.dirty(self.opf_name)

if remove_from_guide:
for item in self.opf_xpath('//opf:guide/opf:reference[@href]'):
if self.href_to_name(item.get('href'), self.opf_name) == name:
self.remove_from_xml(item)
self.dirty(self.opf_name)

path = self.name_path_map.pop(name, None)
if path and os.path.exists(path):
os.remove(path)
self.mime_map.pop(name, None)
self.parsed_cache.pop(name, None)
self.dirtied.discard(name)def set_media_overlay_durations(self, duration_map=None):
self.dirty(self.opf_name)
for meta in self.opf_xpath('//opf:meta[@property="media:duration"]'):
self.remove_from_xml(meta)
metadata = self.opf_xpath('//opf:metadata')[0]
total_duration = 0 for item_id, duration in (duration_map or {}).items():
meta = metadata.makeelement(OPF('meta'), property='media:duration', refines='#' + item_id)
meta.text = seconds_to_timestamp(duration)
self.insert_into_xml(metadata, meta)
total_duration += duration if duration_map:
meta = metadata.makeelement(OPF('meta'), property='media:duration')
meta.text = seconds_to_timestamp(total_duration)
self.insert_into_xml(metadata, meta)

[документация]
def dirty(self, name):
''' Mark the parsed object corresponding to name as dirty. See also: :meth:`parsed`. '''
self.dirtied.add(name)

[документация]
def remove_from_xml(self, item):
'Removes item from parent, fixing indentation (works only with self closing items)'
parent = item.getparent()
idx = parent.index(item)
if idx == 0:
# We are removing the first item - only care about adjusting
# the tail if this was the only child if len(parent) == 1:
parent.text = item.tail else:
# Make sure the preceding item has this tail parent[idx-1].tail = item.tail parent.remove(item)
return item

[документация]
def insert_into_xml(self, parent, item, index=None):
'''Insert item into parent (or append if index is None), fixing indentation. Only works with self closing items.'''
insert_self_closing(parent, item, index)

[документация]
def opf_get_or_create(self, name):
''' Convenience method to either return the first XML element with the specified name or create it under the opf:package element and then return it, if it does not already exist. '''
ans = self.opf_xpath('//opf:'+name)
if ans:
return ans[0]
self.dirty(self.opf_name)
package = self.opf_xpath('//opf:package')[0]
item = package.makeelement(OPF(name))
item.tail = '\n'
package.append(item)
return item

[документация]
def generate_item(self, name, id_prefix=None, media_type=None, unique_href=True):
'''Add an item to the manifest with href derived from the given name. Ensures uniqueness of href and id automatically. Returns generated item.'''
id_prefix = id_prefix or 'id'
media_type = media_type or self.guess_type(name)
if unique_href:
name = self.make_name_unique(name)
href = self.name_to_href(name, self.opf_name)
base, ext = href.rpartition('.')[0::2]
all_ids = {x.get('id') for x in self.opf_xpath('//*[@id]')}
if id_prefix.endswith('-'):
all_ids.add(id_prefix)
c = 0 item_id = id_prefix while item_id in all_ids:
c += 1 item_id = f'{id_prefix}{c}'

manifest = self.opf_xpath('//opf:manifest')[0]
item = manifest.makeelement(OPF('item'), id=item_id, href=href)
item.set('media-type', media_type)
self.insert_into_xml(manifest, item)
self.dirty(self.opf_name)
name = self.href_to_name(href, self.opf_name)
self.name_path_map[name] = path = self.name_to_abspath(name)
self.mime_map[name] = media_type
# Ensure that the file corresponding to the newly created item exists
# otherwise cloned containers will fail when they try to get the number
# of links to the file base = os.path.dirname(path)
if not os.path.exists(base):
os.makedirs(base)
open(path, 'wb').close()
return itemdef format_opf(self):
try:
mdata = self.opf_xpath('//opf:metadata')[0]
except IndexError:
pass else:
mdata.text = '\n '
remove = set()
for child in mdata:
child.tail = '\n '
try:
if (child.get('name', '').startswith('calibre:'
) and child.get('content', '').strip() in {'{}', ''}):
remove.add(child)
except AttributeError:
continue # Happens for XML comments for child in remove:
mdata.remove(child)
if len(mdata) > 0:
mdata[-1].tail = '\n '
# Ensure name comes before content, needed for Nooks for meta in self.opf_xpath('//opf:meta[@name="cover"]'):
if 'content' in meta.attrib:
meta.set('content', meta.attrib.pop('content'))

[документация]
def serialize_item(self, name):
''' Convert a parsed object (identified by canonical name) into a bytestring. See :meth:`parsed`. '''
data = root = self.parsed(name)
if name == self.opf_name:
self.format_opf()
data = serialize(data, self.mime_map[name], pretty_print=name in self.pretty_print)
if name == self.opf_name and root.nsmap.get(None) == OPF2_NS:
# Needed as I can't get lxml to output opf:role and
# not output <opf:metadata> as well data = re.sub(br'(<[/]{0,1})opf:', br'\1', data)
return data

[документация]
def commit_item(self, name, keep_parsed=False):
''' Commit a parsed object to disk (it is serialized and written to the underlying file). If ``keep_parsed`` is True the parsed representation is retained in the cache. See also: :meth:`parsed` '''
if name not in self.parsed_cache:
return data = self.serialize_item(name)
self.dirtied.discard(name)
if not keep_parsed:
self.parsed_cache.pop(name)
dest = self.name_path_map[name]
if self.cloned and nlinks_file(dest) > 1:
# Decouple this file from its links os.unlink(dest)
with open(dest, 'wb') as f:
f.write(data)

[документация]
def filesize(self, name):
''' Return the size in bytes of the file represented by the specified canonical name. Automatically handles dirtied parsed objects. See also:
:meth:`parsed` '''
if name in self.dirtied:
self.commit_item(name, keep_parsed=True)
path = self.name_to_abspath(name)
return os.path.getsize(path)

[документация]
def get_file_path_for_processing(self, name, allow_modification=True):
''' Similar to open() except that it returns a file path, instead of an open file object. '''
if name in self.dirtied:
self.commit_item(name)
self.parsed_cache.pop(name, False)
path = self.name_to_abspath(name)
base = os.path.dirname(path)
if not os.path.exists(base):
os.makedirs(base)
else:
if self.cloned and allow_modification and os.path.exists(path) and nlinks_file(path) > 1:
# Decouple this file from its links temp = path + 'xxx'
shutil.copyfile(path, temp)
if iswindows:
retry_on_fail(os.unlink, path)
else:
os.unlink(path)
os.rename(temp, path)
return path

[документация]
def open(self, name, mode='rb'):
''' Open the file pointed to by name for direct read/write. Note that this will commit the file if it is dirtied and remove it from the parse cache. You must finish with this file before accessing the parsed version of it again, or bad things will happen. '''
return open(make_long_path_useable(self.get_file_path_for_processing(name, mode not in {'r', 'rb'})), mode)

[документация]
def commit(self, outpath=None, keep_parsed=False):
'''
Commit all dirtied parsed objects to the filesystem and write out the e-book file at outpath.

:param output: The path to write the saved e-book file to. If None, the path of the original book file is used.
:param keep_parsed: If True the parsed representations of committed items are kept in the cache.
'''
for name in tuple(self.dirtied):
self.commit_item(name, keep_parsed=keep_parsed)

def compare_to(self, other):
if set(self.name_path_map) != set(other.name_path_map):
return 'Set of files is not the same'
mismatches = []
for name, path in iteritems(self.name_path_map):
opath = other.name_path_map[name]
with open(path, 'rb') as f1, open(opath, 'rb') as f2:
if f1.read() != f2.read():
mismatches.append(f'The file {name} is not the same')
return '\n'.join(mismatches)# }}}

# EPUB {{{ class InvalidEpub(InvalidBook):
pass class ObfuscationKeyMissing(InvalidEpub):
pass

OCF_NS = 'urn:oasis:names:tc:opendocument:xmlns:container'
VCS_IGNORE_FILES = frozenset('.gitignore .hgignore .agignore .bzrignore'.split())
VCS_DIRS = frozenset(('.git', '.hg', '.svn', '.bzr'))

def walk_dir(basedir):
for dirpath, dirnames, filenames in os.walk(basedir):
for vcsdir in VCS_DIRS:
try:
dirnames.remove(vcsdir)
except Exception:
pass is_root = os.path.abspath(os.path.normcase(dirpath)) == os.path.abspath(os.path.normcase(basedir))
yield is_root, dirpath, None for fname in filenames:
if fname not in VCS_IGNORE_FILES:
yield is_root, dirpath, fname class EpubContainer(Container):

book_type = 'epub'
MAX_HTML_FILE_SIZE = 260 * 1024

@property def book_type_for_display(self):
ans = self.book_type.upper()
try:
v = self.opf_version_parsed except Exception:
pass else:
try:
if v.major == 2:
ans += ' 2'
else:
if not v.minor:
ans += f' {v.major}'
else:
ans += f' {v.major}.{v.minor}'
except Exception:
pass return ans

META_INF = {
'container.xml': True,
'manifest.xml': False,
'encryption.xml': False,
'metadata.xml': False,
'signatures.xml': False,
'rights.xml': False,
}

def __init__(self, pathtoepub=None, log=default_log, clone_data=None, tdir=None):
if clone_data is not None:
super().__init__(log=log, clone_data=clone_data)
for x in ('pathtoepub', 'obfuscated_fonts', 'is_dir'):
setattr(self, x, clone_data[x])
return self.pathtoepub = pathtoepub if tdir is None:
tdir = PersistentTemporaryDirectory('_epub_container')
tdir = os.path.abspath(os.path.realpath(tdir))
self.root = tdir self.is_dir = os.path.isdir(pathtoepub)
if self.is_dir:
for is_root, dirpath, fname in walk_dir(self.pathtoepub):
if is_root:
base = tdir else:
base = os.path.join(tdir, os.path.relpath(dirpath, self.pathtoepub))
if fname is None:
os.mkdir(base)
if fname is not None:
shutil.copy(os.path.join(dirpath, fname), os.path.join(base, fname))
else:
with open(self.pathtoepub, 'rb') as stream:
try:
zf = ZipFile(stream)
zf.extractall(tdir)
except Exception:
log.exception('EPUB appears to be invalid ZIP file, trying a'
' more forgiving ZIP parser')
from calibre.utils.localunzip import extractall stream.seek(0)
extractall(stream, path=tdir)
try:
os.remove(join(tdir, 'mimetype'))
except OSError:
pass
# Ensure all filenames are in NFC normalized form
# has no effect on HFS+ filesystems as they always store filenames
# in NFD form for filename in walk(self.root):
n = unicodedata.normalize('NFC', filename)
if n != filename:
s = filename + 'suff1x'
os.rename(filename, s)
os.rename(s, n)

container_path = join(self.root, 'META-INF', 'container.xml')
if not exists(container_path):
raise InvalidEpub('No META-INF/container.xml in epub')
with open(container_path, 'rb') as cf:
container = safe_xml_fromstring(cf.read())
opf_files = container.xpath(( r'child::ocf:rootfiles/ocf:rootfile'
'[@media-type="{}" and @full-path]'.format(guess_type('a.opf'))
), namespaces={'ocf':OCF_NS}
)
if not opf_files:
raise InvalidEpub('META-INF/container.xml contains no link to OPF file')
opf_path = os.path.join(self.root, *(urlunquote(opf_files[0].get('full-path')).split('/')))
if not exists(opf_path):
raise InvalidEpub('OPF file does not exist at location pointed to'
' by META-INF/container.xml')

super().__init__(rootpath=tdir, opfpath=opf_path, log=log)

self.obfuscated_fonts = {}
if 'META-INF/encryption.xml' in self.name_path_map:
self.process_encryption()
self.parsed_cache['META-INF/container.xml'] = container def data_for_clone(self, dest_dir=None):
ans = super().data_for_clone(dest_dir)
ans['pathtoepub'] = self.pathtoepub ans['obfuscated_fonts'] = self.obfuscated_fonts.copy()
ans['is_dir'] = self.is_dir return ans def rename(self, old_name, new_name):
is_opf = old_name == self.opf_name super().rename(old_name, new_name)
if is_opf:
for elem in self.parsed('META-INF/container.xml').xpath(( r'child::ocf:rootfiles/ocf:rootfile'
'[@media-type="{}" and @full-path]'.format(guess_type('a.opf'))
), namespaces={'ocf':OCF_NS}
):
# The asinine epubcheck cannot handle quoted filenames in
# container.xml elem.set('full-path', self.opf_name)
self.dirty('META-INF/container.xml')
if old_name in self.obfuscated_fonts:
self.obfuscated_fonts[new_name] = self.obfuscated_fonts.pop(old_name)
enc = self.parsed('META-INF/encryption.xml')
for cr in enc.xpath('//*[local-name()="CipherReference" and @URI]'):
if self.href_to_name(cr.get('URI')) == old_name:
cr.set('URI', self.name_to_href(new_name))
self.dirty('META-INF/encryption.xml')

@property def names_that_need_not_be_manifested(self):
return super().names_that_need_not_be_manifested | {'META-INF/' + x for x in self.META_INF}

def ok_to_be_unmanifested(self, name):
return name in self.names_that_need_not_be_manifested or name.startswith('META-INF/')

@property def names_that_must_not_be_removed(self):
return super().names_that_must_not_be_removed | {'META-INF/container.xml'}

@property def names_that_must_not_be_changed(self):
return super().names_that_must_not_be_changed | {'META-INF/' + x for x in self.META_INF}

def remove_item(self, name, remove_from_guide=True):
# Handle removal of obfuscated fonts if name == 'META-INF/encryption.xml':
self.obfuscated_fonts.clear()
if name in self.obfuscated_fonts:
self.obfuscated_fonts.pop(name, None)
enc = self.parsed('META-INF/encryption.xml')
for em in enc.xpath('//*[local-name()="EncryptionMethod" and @Algorithm]'):
alg = em.get('Algorithm')
if alg not in {ADOBE_OBFUSCATION, IDPF_OBFUSCATION}:
continue try:
cr = em.getparent().xpath('descendant::*[local-name()="CipherReference" and @URI]')[0]
except (IndexError, ValueError, KeyError):
continue if name == self.href_to_name(cr.get('URI')):
self.remove_from_xml(em.getparent())
self.dirty('META-INF/encryption.xml')
super().remove_item(name, remove_from_guide=remove_from_guide)

def read_raw_unique_identifier(self):
package_id = raw_unique_identifier = idpf_key = None for attrib, val in iteritems(self.opf.attrib):
if attrib.endswith('unique-identifier'):
package_id = val break if package_id is not None:
for elem in self.opf_xpath(f'//*[@id={escape_xpath_attr(package_id)}]'):
if elem.text:
raw_unique_identifier = elem.text break if raw_unique_identifier is not None:
idpf_key = raw_unique_identifier idpf_key = re.sub(r'[ \t\r\n]', '', idpf_key)
idpf_key = hashlib.sha1(idpf_key.encode('utf-8')).digest()
return package_id, raw_unique_identifier, idpf_key def iter_encryption_entries(self):
if 'META-INF/encryption.xml' in self.name_path_map:
enc = self.parsed('META-INF/encryption.xml')
for em in enc.xpath('//*[local-name()="EncryptionMethod" and @Algorithm]'):
try:
cr = em.getparent().xpath('descendant::*[local-name()="CipherReference" and @URI]')[0]
except Exception:
cr = None yield em, cr def process_encryption(self):
fonts = {}
for em, cr in self.iter_encryption_entries():
alg = em.get('Algorithm')
if alg not in {ADOBE_OBFUSCATION, IDPF_OBFUSCATION}:
raise DRMError()
if cr is None:
continue name = self.href_to_name(cr.get('URI'))
path = self.name_path_map.get(name, None)
if path is not None:
fonts[name] = alg package_id, raw_unique_identifier, idpf_key = self.read_raw_unique_identifier()
key = None for item in self.opf_xpath('//*[local-name()="metadata"]/*'
'[local-name()="identifier"]'):
scheme = None for xkey in item.attrib.keys():
if xkey.endswith('scheme'):
scheme = item.get(xkey)
if (scheme and scheme.lower() == 'uuid') or \ (item.text and item.text.startswith('urn:uuid:')):
try:
key = item.text.rpartition(':')[-1]
key = uuid.UUID(key).bytes except Exception:
self.log.exception('Failed to parse obfuscation key')
key = None for font, alg in iteritems(fonts):
tkey = key if alg == ADOBE_OBFUSCATION else idpf_key if not tkey:
raise ObfuscationKeyMissing('Failed to find obfuscation key')
raw = self.raw_data(font, decode=False)
raw = decrypt_font_data(tkey, raw, alg)
with self.open(font, 'wb') as f:
f.write(raw)
self.obfuscated_fonts[font] = (alg, tkey)

def update_modified_timestamp(self):
from calibre.ebooks.metadata.opf3 import set_last_modified_in_opf set_last_modified_in_opf(self.opf)
self.dirty(self.opf_name)

def commit(self, outpath=None, keep_parsed=False):
if self.opf_version_parsed.major == 3:
self.update_modified_timestamp()
super().commit(keep_parsed=keep_parsed)
container_path = join(self.root, 'META-INF', 'container.xml')
if not exists(container_path):
raise InvalidEpub('No META-INF/container.xml in EPUB, this typically happens if the temporary files calibre'
' is using are deleted by some other program while calibre is running')
restore_fonts = {}
for name in self.obfuscated_fonts:
if name not in self.name_path_map:
continue alg, key = self.obfuscated_fonts[name]
# Decrypting and encrypting are the same operation (XOR with key)
restore_fonts[name] = data = self.raw_data(name, decode=False)
with self.open(name, 'wb') as f:
f.write(decrypt_font_data(key, data, alg))
if outpath is None:
outpath = self.pathtoepub self.commit_epub(outpath)
for name, data in iteritems(restore_fonts):
with self.open(name, 'wb') as f:
f.write(data)

def commit_epub(self, outpath: str) -> None:
if self.is_dir:
# First remove items from the source dir that do not exist any more for is_root, dirpath, fname in walk_dir(self.pathtoepub):
if fname is not None:
if is_root and fname == 'mimetype':
continue base = self.root if is_root else os.path.join(self.root, os.path.relpath(dirpath, self.pathtoepub))
fpath = os.path.join(base, fname)
if not os.path.exists(fpath):
os.remove(os.path.join(dirpath, fname))
try:
os.rmdir(dirpath)
except OSError as err:
if err.errno != errno.ENOTEMPTY:
raise
# Now copy over everything from root to source dir for dirpath, dirnames, filenames in os.walk(self.root):
is_root = os.path.abspath(os.path.normcase(dirpath)) == os.path.abspath(os.path.normcase(self.root))
base = self.pathtoepub if is_root else os.path.join(self.pathtoepub, os.path.relpath(dirpath, self.root))
try:
os.mkdir(base)
except OSError as err:
if err.errno != errno.EEXIST:
raise for fname in filenames:
with open(os.path.join(dirpath, fname), 'rb') as src, open(os.path.join(base, fname), 'wb') as dest:
shutil.copyfileobj(src, dest)

else:
from calibre.ebooks.tweak import zip_rebuilder with open(join(self.root, 'mimetype'), 'wb') as f:
et = guess_type('a.epub')
if not isinstance(et, bytes):
et = et.encode('ascii')
f.write(et)
zip_rebuilder(self.root, outpath)

@property def path_to_ebook(self):
return self.pathtoepub

@path_to_ebook.setter def path_to_ebook(self, val):
self.pathtoepub = val

# }}}

class KEPUBContainer(EpubContainer):
book_type = 'kepub'
MAX_HTML_FILE_SIZE = 512 * 1024 def __init__(self, pathtokepub=None, log=default_log, clone_data=None, tdir=None):
super().__init__(pathtokepub, log=log, clone_data=clone_data, tdir=tdir)
from calibre.ebooks.oeb.polish.kepubify import unkepubify_container
Container.commit(self, keep_parsed=True)
unkepubify_container(self)

def commit_epub(self, outpath: str) -> None:
if self.is_dir:
return super().commit_epub(outpath)
from calibre.ebooks.oeb.polish.kepubify import Options, kepubify_container with TemporaryDirectory() as tdir:
container = clone_container(self, tdir, container_class=EpubContainer)
kepubify_container(container, Options())
container.commit(outpath)

# AZW3 {{{ class InvalidMobi(InvalidBook):
pass def do_explode(path, dest):
from calibre.ebooks.mobi.reader.mobi6 import MobiReader from calibre.ebooks.mobi.reader.mobi8 import Mobi8Reader with open(path, 'rb') as stream:
mr = MobiReader(stream, default_log, None, None)

with CurrentDir(dest):
mr = Mobi8Reader(mr, default_log, for_tweak=True)
opf = os.path.abspath(mr())
obfuscated_fonts = mr.encrypted_fonts return opf, obfuscated_fonts def opf_to_azw3(opf, outpath, container):
from calibre.ebooks.conversion.plumber import Plumber, create_oebbook from calibre.ebooks.mobi.tweak import set_cover class Item(Manifest.Item):

def _parse_css(self, data):
# The default CSS parser used by oeb.base inserts the h namespace
# and resolves all @import rules. We don't want that.
return container.parse_css(data)

def specialize(oeb):
oeb.manifest.Item = Item plumber = Plumber(opf, outpath, container.log)
plumber.setup_options()
inp = plugin_for_input_format('azw3')
outp = plugin_for_output_format('azw3')
plumber.opts.mobi_passthrough = True plumber.opts.keep_ligatures = True oeb = create_oebbook(container.log, opf, plumber.opts, specialize=specialize)
set_cover(oeb)
outp.convert(oeb, outpath, inp, plumber.opts, container.log)

def epub_to_azw3(epub, outpath=None):
container = get_container(epub, tweak_mode=True)
changed = False for item in container.opf_xpath('//opf:manifest/opf:item[@properties and @href]'):
p = item.get('properties').split()
if 'cover-image' in p:
href = item.get('href')
guides = container.opf_xpath('//opf:guide')
if not guides:
guides = (container.opf.makeelement(OPF('guide')),)
container.opf.append(guides[0])
for guide in guides:
for child in guide:
if child.get('type') == 'cover':
break else:
guide.append(guide.makeelement(OPF('reference'), type='cover', href=href))
changed = True break elif 'calibre:title-page' in p:
item.getparent().remove(item)
if changed:
container.dirty(container.opf_name)
container.commit_item(container.opf_name)
outpath = outpath or (epub.rpartition('.')[0] + '.azw3')
opf_to_azw3(container.name_to_abspath(container.opf_name), outpath, container)

class AZW3Container(Container):

book_type = 'azw3'
SUPPORTS_TITLEPAGES = False
SUPPORTS_FILENAMES = False def __init__(self, pathtoazw3=None, log=default_log, clone_data=None, tdir=None):
if clone_data is not None:
super().__init__(log=log, clone_data=clone_data)
for x in ('pathtoazw3', 'obfuscated_fonts'):
setattr(self, x, clone_data[x])
return self.pathtoazw3 = pathtoazw3 if tdir is None:
tdir = PersistentTemporaryDirectory('_azw3_container')
tdir = os.path.abspath(os.path.realpath(tdir))
self.root = tdir with open(pathtoazw3, 'rb') as stream:
raw = stream.read(3)
if raw == b'TPZ':
raise InvalidMobi(_('This is not a MOBI file. It is a Topaz file.'))

try:
header = MetadataHeader(stream, default_log)
except MobiError:
raise InvalidMobi(_('This is not a MOBI file.'))

if header.encryption_type != 0:
raise DRMError()

kf8_type = header.kf8_type if kf8_type is None:
raise InvalidMobi(_('This MOBI file does not contain a KF8 format '
'book. KF8 is the new format from Amazon. calibre can '
'only edit MOBI files that contain KF8 books. Older '
'MOBI files without KF8 are not editable.'))

if kf8_type == 'joint':
raise InvalidMobi(_('This MOBI file contains both KF8 and '
'older Mobi6 data. calibre can only edit MOBI files '
'that contain only KF8 data.'))

try:
opf_path, obfuscated_fonts = fork_job(
'calibre.ebooks.oeb.polish.container', 'do_explode', args=(pathtoazw3, tdir), no_output=True)['result']
except WorkerError as e:
log(e.orig_tb)
raise InvalidMobi('Failed to explode MOBI')
super().__init__(rootpath=tdir, opfpath=opf_path, log=log)
self.obfuscated_fonts = {x.replace(os.sep, '/') for x in obfuscated_fonts}

def data_for_clone(self, dest_dir=None):
ans = super().data_for_clone(dest_dir)
ans['pathtoazw3'] = self.pathtoazw3 ans['obfuscated_fonts'] = self.obfuscated_fonts.copy()
return ans def commit(self, outpath=None, keep_parsed=False):
super().commit(keep_parsed=keep_parsed)
if outpath is None:
outpath = self.pathtoazw3 opf_to_azw3(self.name_path_map[self.opf_name], outpath, self)

@property def path_to_ebook(self):
return self.pathtoazw3

@path_to_ebook.setter def path_to_ebook(self, val):
self.pathtoazw3 = val

@property def names_that_must_not_be_changed(self):
return set(self.name_path_map)
# }}}

def get_container(path, log=None, tdir=None, tweak_mode=False, ebook_cls=None) -> Container:
try:
isdir = os.path.isdir(path)
except Exception:
isdir = False own_tdir = not tdir if ebook_cls is None:
ext = path.rpartition('.')[-1].lower()
ebook_cls = EpubContainer if not isdir:
if ext in {'azw3', 'mobi', 'original_azw3', 'original_mobi'}:
ebook_cls = AZW3Container elif ext in {'kepub', 'original_kepub'}:
ebook_cls = KEPUBContainer if own_tdir:
tdir = PersistentTemporaryDirectory(f'_{ebook_cls.book_type}_container')
try:
ebook = ebook_cls(path, log=log, tdir=tdir)
ebook.tweak_mode = tweak_mode except BaseException:
if own_tdir:
shutil.rmtree(tdir, ignore_errors=True)
raise return ebook def test_roundtrip():
ebook = get_container(sys.argv[-1])
p = PersistentTemporaryFile(suffix='.'+sys.argv[-1].rpartition('.')[-1])
p.close()
ebook.commit(outpath=p.name)
ebook2 = get_container(p.name)
ebook3 = get_container(p.name)
diff = ebook3.compare_to(ebook2)
if diff is not None:
print(diff)

if __name__ == '__main__':
test_roundtrip()

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.ebooks.oeb.polish.cover

Исходный код calibre.ebooks.oeb.polish.cover

__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import os import re import shutil from calibre.ebooks.oeb.base import OEB_DOCS, OPF, XLINK, XPath, xml2text from calibre.ebooks.oeb.polish.replace import get_recommended_folders, replace_links from calibre.utils.imghdr import identify from polyglot.builtins import iteritems def set_azw3_cover(container, cover_path, report, options=None):
existing_image = options is not None and options.get('existing_image', False)
name = None found = True for gi in container.opf_xpath('//opf:guide/opf:reference[@href and contains(@type, "cover")]'):
href = gi.get('href')
name = container.href_to_name(href, container.opf_name)
container.remove_from_xml(gi)
if existing_image:
name = cover_path found = False else:
if name is None or not container.has_name(name):
item = container.generate_item(name='cover.jpeg', id_prefix='cover')
name = container.href_to_name(item.get('href'), container.opf_name)
found = False href = container.name_to_href(name, container.opf_name)
guide = container.opf_xpath('//opf:guide')[0]
container.insert_into_xml(guide, guide.makeelement(
OPF('reference'), href=href, type='cover'))
if not existing_image:
with open(cover_path, 'rb') as src, container.open(name, 'wb') as dest:
shutil.copyfileobj(src, dest)
container.dirty(container.opf_name)
report(_('Cover updated') if found else _('Cover inserted'))

def get_azw3_raster_cover_name(container):
items = container.opf_xpath('//opf:guide/opf:reference[@href and contains(@type, "cover")]')
if items:
return container.href_to_name(items[0].get('href'))

def mark_as_cover_azw3(container, name):
href = container.name_to_href(name, container.opf_name)
found = False for item in container.opf_xpath('//opf:guide/opf:reference[@href and contains(@type, "cover")]'):
item.set('href', href)
found = True if not found:
for guide in container.opf_xpath('//opf:guide'):
container.insert_into_xml(guide, guide.makeelement(
OPF('reference'), href=href, type='cover'))
container.dirty(container.opf_name)

def get_raster_cover_name(container):
if container.book_type == 'azw3':
return get_azw3_raster_cover_name(container)
return find_cover_image(container, strict=True)

def get_cover_page_name(container):
if container.book_type == 'azw3':
return return find_cover_page(container)

[документация]
def set_cover(container, cover_path, report=None, options=None):
'''
Set the cover of the book to the image pointed to by cover_path.

:param cover_path: Either the absolute path to an image file or the canonical name of an image in the book. When using an image in the book, you must also set options, see below.
:param report: An optional callable that takes a single argument. It will be called with information about the tasks being processed.
:param options: None or a dictionary that controls how the cover is set. The dictionary can have entries:
**keep_aspect**: True or False (Preserve aspect ratio of covers in EPUB)
**no_svg**: True or False (Use an SVG cover wrapper in the EPUB titlepage)
**existing**: True or False (``cover_path`` refers to an existing image in the book)
'''
report = report or (lambda x:x)
if container.book_type == 'azw3':
set_azw3_cover(container, cover_path, report, options=options)
else:
set_epub_cover(container, cover_path, report, options=options)

[документация]
def mark_as_cover(container, name):
'''
Mark the specified image as the cover image.
'''
if name not in container.mime_map:
raise ValueError(f'Cannot mark {name} as cover as it does not exist')
mt = container.mime_map[name]
if not is_raster_image(mt):
raise ValueError(f'Cannot mark {name} as the cover image as it is not a raster image')
if container.book_type == 'azw3':
mark_as_cover_azw3(container, name)
else:
mark_as_cover_epub(container, name)

###############################################################################
# The delightful EPUB cover processing def is_raster_image(media_type):
return media_type and media_type.lower() in {
'image/png', 'image/jpeg', 'image/jpg', 'image/gif'}

COVER_TYPES = {
'coverimagestandard', 'other.ms-coverimage-standard',
'other.ms-titleimage-standard', 'other.ms-titleimage',
'other.ms-coverimage', 'other.ms-thumbimage-standard',
'other.ms-thumbimage', 'thumbimagestandard', 'cover'}

def find_cover_image2(container, strict=False):
manifest_id_map = container.manifest_id_map mm = container.mime_map for meta in container.opf_xpath('//opf:meta[@name="cover" and @content]'):
item_id = meta.get('content')
name = manifest_id_map.get(item_id, None)
media_type = mm.get(name, None)
if is_raster_image(media_type):
return name

# First look for a guide item with type == 'cover'
guide_type_map = container.guide_type_map for ref_type, name in iteritems(guide_type_map):
if ref_type.lower() == 'cover' and is_raster_image(mm.get(name, None)):
return name if strict:
return

# Find the largest image from all possible guide cover items largest_cover = (None, 0)
for ref_type, name in iteritems(guide_type_map):
if ref_type.lower() in COVER_TYPES and is_raster_image(mm.get(name, None)):
path = container.name_path_map.get(name, None)
if path:
sz = os.path.getsize(path)
if sz > largest_cover[1]:
largest_cover = (name, sz)

if largest_cover[0]:
return largest_cover[0]

def find_cover_image3(container):
for name in container.manifest_items_with_property('cover-image'):
return name manifest_id_map = container.manifest_id_map mm = container.mime_map for meta in container.opf_xpath('//opf:meta[@name="cover" and @content]'):
item_id = meta.get('content')
name = manifest_id_map.get(item_id, None)
media_type = mm.get(name, None)
if is_raster_image(media_type):
return name def find_cover_image(container, strict=False):
'Find a raster image marked as a cover in the OPF'
ver = container.opf_version_parsed if ver.major < 3:
return find_cover_image2(container, strict=strict)
else:
return find_cover_image3(container)

def get_guides(container):
guides = container.opf_xpath('//opf:guide')
if not guides:
container.insert_into_xml(container.opf, container.opf.makeelement(
OPF('guide')))
guides = container.opf_xpath('//opf:guide')
return guides def mark_as_cover_epub(container, name):
mmap = {v:k for k, v in iteritems(container.manifest_id_map)}
if name not in mmap:
raise ValueError(f'Cannot mark {name} as cover as it is not in manifest')
mid = mmap[name]
ver = container.opf_version_parsed

# Remove all entries from the opf that identify a raster image as cover for meta in container.opf_xpath('//opf:meta[@name="cover" and @content]'):
container.remove_from_xml(meta)
for ref in container.opf_xpath('//opf:guide/opf:reference[@href and @type]'):
if ref.get('type').lower() not in COVER_TYPES:
continue rname = container.href_to_name(ref.get('href'), container.opf_name)
mt = container.mime_map.get(rname, None)
if is_raster_image(mt):
container.remove_from_xml(ref)

if ver.major < 3:
# Add reference to image in <metadata> for metadata in container.opf_xpath('//opf:metadata'):
m = metadata.makeelement(OPF('meta'), name='cover', content=mid)
container.insert_into_xml(metadata, m)

# If no entry for cover exists in guide, insert one that points to this
# image if not container.opf_xpath('//opf:guide/opf:reference[@type="cover"]'):
for guide in get_guides(container):
container.insert_into_xml(guide, guide.makeelement(
OPF('reference'), type='cover', href=container.name_to_href(name, container.opf_name)))
else:
container.apply_unique_properties(name, 'cover-image')

container.dirty(container.opf_name)

[документация]
def mark_as_titlepage(container, name, move_to_start=True):
'''
Mark the specified HTML file as the titlepage of the EPUB.

:param move_to_start: If True the HTML file is moved to the start of the spine
'''
ver = container.opf_version_parsed if move_to_start:
for item, q, linear in container.spine_iter:
if name == q:
break if not linear:
item.set('linear', 'yes')
if item.getparent().index(item) > 0:
container.insert_into_xml(item.getparent(), item, 0)
if ver.major < 3:
for ref in container.opf_xpath('//opf:guide/opf:reference[@type="cover"]'):
ref.getparent().remove(ref)

for guide in get_guides(container):
container.insert_into_xml(guide, guide.makeelement(
OPF('reference'), type='cover', href=container.name_to_href(name, container.opf_name)))
else:
container.apply_unique_properties(name, 'calibre:title-page')

container.dirty(container.opf_name)

def find_cover_page(container):
'Find a document marked as a cover in the OPF'
ver = container.opf_version_parsed mm = container.mime_map if ver.major < 3:
guide_type_map = container.guide_type_map for ref_type, name in iteritems(guide_type_map):
if ref_type.lower() == 'cover' and mm.get(name, '').lower() in OEB_DOCS:
return name else:
for name in container.manifest_items_with_property('calibre:title-page'):
return name from calibre.ebooks.oeb.polish.toc import get_landmarks for landmark in get_landmarks(container):
if landmark['type'] == 'cover' and mm.get(landmark['dest'], '').lower() in OEB_DOCS:
return landmark['dest']

def fix_conversion_titlepage_links_in_nav(container):
from calibre.ebooks.oeb.polish.toc import find_existing_nav_toc cover_page_name = find_cover_page(container)
if not cover_page_name:
return nav_page_name = find_existing_nav_toc(container)
if not nav_page_name:
return for elem in container.parsed(nav_page_name).xpath('//*[@data-calibre-removed-titlepage]'):
elem.attrib.pop('data-calibre-removed-titlepage')
elem.set('href', container.name_to_href(cover_page_name, nav_page_name))
container.dirty(nav_page_name)

def find_cover_image_in_page(container, cover_page):
root = container.parsed(cover_page)
body = XPath('//h:body')(root)
if len(body) != 1:
return body = body[0]
images = []
for img in XPath('descendant::h:img[@src]|descendant::svg:svg/descendant::svg:image')(body):
href = img.get('src') or img.get(XLINK('href'))
if href:
name = container.href_to_name(href, base=cover_page)
images.append(name)
text = re.sub(r'\s+', '', xml2text(body))
if text or len(images) > 1:
# Document has more content than a single image return if images:
return images[0]

def clean_opf(container):
'Remove all references to covers from the OPF'
manifest_id_map = container.manifest_id_map for meta in container.opf_xpath('//opf:meta[@name="cover" and @content]'):
name = manifest_id_map.get(meta.get('content', None), None)
container.remove_from_xml(meta)
if name and name in container.name_path_map:
yield name gtm = container.guide_type_map for ref in container.opf_xpath('//opf:guide/opf:reference[@type]'):
typ = ref.get('type', '')
if typ.lower() in COVER_TYPES:
container.remove_from_xml(ref)
name = gtm.get(typ, None)
if name and name in container.name_path_map:
yield name ver = container.opf_version_parsed if ver.major > 2:
removed_names = container.apply_unique_properties(None, 'cover-image', 'calibre:title-page')[0]
for name in removed_names:
yield name container.dirty(container.opf_name)

def create_epub_cover(container, cover_path, existing_image, options=None):
from calibre.ebooks.conversion.config import load_defaults from calibre.ebooks.oeb.transforms.cover import CoverManager try:
ext = cover_path.rpartition('.')[-1].lower()
except Exception:
ext = 'jpeg'
cname, tname = 'cover.' + ext, 'titlepage.xhtml'
recommended_folders = get_recommended_folders(container, (cname, tname))

if existing_image:
raster_cover = existing_image manifest_id = {v:k for k, v in iteritems(container.manifest_id_map)}[existing_image]
raster_cover_item = container.opf_xpath(f'//opf:manifest/*[@id="{manifest_id}"]')[0]
else:
folder = recommended_folders[cname]
if folder:
cname = folder + '/' + cname raster_cover_item = container.generate_item(cname, id_prefix='cover')
raster_cover = container.href_to_name(raster_cover_item.get('href'), container.opf_name)

with container.open(raster_cover, 'wb') as dest:
if callable(cover_path):
cover_path('write_image', dest)
else:
with open(cover_path, 'rb') as src:
shutil.copyfileobj(src, dest)
if options is None:
opts = load_defaults('epub_output')
keep_aspect = opts.get('preserve_cover_aspect_ratio', False)
no_svg = opts.get('no_svg_cover', False)
else:
keep_aspect = options.get('keep_aspect', False)
no_svg = options.get('no_svg', False)
if no_svg:
style = 'style="height: 100%%"'
templ = CoverManager.NONSVG_TEMPLATE.replace('__style__', style)
has_svg = False else:
if callable(cover_path):
templ = (options or {}).get('template', CoverManager.SVG_TEMPLATE)
has_svg = 'xlink:href' in templ else:
width, height = 600, 800 has_svg = True try:
if existing_image:
width, height = identify(container.raw_data(existing_image, decode=False))[1:]
else:
with open(cover_path, 'rb') as csrc:
width, height = identify(csrc)[1:]
except Exception:
container.log.exception('Failed to get width and height of cover')
ar = 'xMidYMid meet' if keep_aspect else 'none'
templ = CoverManager.SVG_TEMPLATE.replace('__ar__', ar)
templ = templ.replace('__viewbox__', f'0 0 {width} {height}')
templ = templ.replace('__width__', str(width))
templ = templ.replace('__height__', str(height))
folder = recommended_folders[tname]
if folder:
tname = folder + '/' + tname titlepage_item = container.generate_item(tname, id_prefix='titlepage')
titlepage = container.href_to_name(titlepage_item.get('href'), container.opf_name)
raw = templ % container.name_to_href(raster_cover, titlepage)
with container.open(titlepage, 'wb') as f:
if not isinstance(raw, bytes):
raw = raw.encode('utf-8')
f.write(raw)

# We have to make sure the raster cover item has id="cover" for the moron
# that wrote the Nook firmware if raster_cover_item.get('id') != 'cover':
from calibre.ebooks.oeb.base import uuid_id newid = uuid_id()
for item in container.opf_xpath('//*[@id="cover"]'):
item.set('id', newid)
for item in container.opf_xpath('//*[@idref="cover"]'):
item.set('idref', newid)
raster_cover_item.set('id', 'cover')

spine = container.opf_xpath('//opf:spine')[0]
ref = spine.makeelement(OPF('itemref'), idref=titlepage_item.get('id'))
container.insert_into_xml(spine, ref, index=0)
ver = container.opf_version_parsed if ver.major < 3:
guide = container.opf_get_or_create('guide')
container.insert_into_xml(guide, guide.makeelement(
OPF('reference'), type='cover', title=_('Cover'), href=container.name_to_href(titlepage, base=container.opf_name)))
metadata = container.opf_get_or_create('metadata')
meta = metadata.makeelement(OPF('meta'), name='cover')
meta.set('content', raster_cover_item.get('id'))
container.insert_into_xml(metadata, meta)
else:
container.apply_unique_properties(raster_cover, 'cover-image')
container.apply_unique_properties(titlepage, 'calibre:title-page')
if has_svg:
container.add_properties(titlepage, 'svg')

return raster_cover, titlepage def remove_cover_image_in_page(container, page, cover_images):
for img in container.parsed(page).xpath('//*[local-name()="img" and @src]'):
href = img.get('src')
name = container.href_to_name(href, page)
if name in cover_images:
img.getparent().remove(img)
break def has_epub_cover(container):
if find_cover_image(container):
return True if find_cover_page(container):
return True spine_items = tuple(container.spine_items)
if spine_items:
candidate = container.abspath_to_name(spine_items[0])
if find_cover_image_in_page(container, candidate) is not None:
return True return False def set_epub_cover(container, cover_path, report, options=None, image_callback=None):
existing_image = options is not None and options.get('existing_image', False)
if existing_image:
existing_image = cover_path cover_image = find_cover_image(container)
cover_page = find_cover_page(container)
wrapped_image = extra_cover_page = None updated = False log = container.log possible_removals = set(clean_opf(container))
possible_removals
# TODO: Handle possible_removals and also iterate over links in the removed
# pages and handle possibly removing stylesheets referred to by them.

image_callback_called = False spine_items = tuple(container.spine_items)
if cover_page is None and spine_items:
# Check if the first item in the spine is a simple cover wrapper candidate = container.abspath_to_name(spine_items[0])
if find_cover_image_in_page(container, candidate) is not None:
cover_page = candidate if cover_page is not None:
log('Found existing cover page')
wrapped_image = find_cover_image_in_page(container, cover_page)

if len(spine_items) > 1:
# Look for an extra cover page c = container.abspath_to_name(spine_items[1])
if c != cover_page:
candidate = find_cover_image_in_page(container, c)
if candidate and candidate in {wrapped_image, cover_image}:
log('Found an extra cover page that is a simple wrapper, removing it')
# This page has only a single image and that image is the
# cover image, remove it.
container.remove_item(c)
extra_cover_page = c spine_items = spine_items[:1] + spine_items[2:]
elif candidate is None:
# Remove the cover image if it is the first image in this
# page remove_cover_image_in_page(container, c, {wrapped_image, cover_image})

if wrapped_image is not None:
# The cover page is a simple wrapper around a single cover image,
# we can remove it safely.
log(f'Existing cover page {cover_page} is a simple wrapper, removing it')
container.remove_item(cover_page)
if wrapped_image != existing_image:
if image_callback is not None and not image_callback_called:
image_callback(cover_image, wrapped_image)
image_callback_called = True container.remove_item(wrapped_image)
updated = True if image_callback is not None and not image_callback_called:
image_callback_called = True image_callback(cover_image, wrapped_image)
if cover_image and cover_image != wrapped_image:
# Remove the old cover image if cover_image != existing_image:
container.remove_item(cover_image)

# Insert the new cover raster_cover, titlepage = create_epub_cover(container, cover_path, existing_image, options=options)

report(_('Cover updated') if updated else _('Cover inserted'))

# Replace links to the old cover image/cover page link_sub = {s:d for s, d in iteritems({ cover_page:titlepage, wrapped_image:raster_cover, cover_image:raster_cover, extra_cover_page:titlepage})
if s is not None and s != d}
if link_sub:
replace_links(container, link_sub, frag_map=lambda x, y:None)
return raster_cover, titlepage

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.ebooks.oeb.polish.css

Исходный код calibre.ebooks.oeb.polish.css

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'

import re from collections import defaultdict from functools import partial from operator import itemgetter from css_parser.css import CSSRule, CSSStyleDeclaration from css_selectors import Select, SelectorError, SelectorSyntaxError, parse from calibre import force_unicode from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES, XHTML, css_text from calibre.ebooks.oeb.normalize_css import normalize_filter_css, normalizers from calibre.ebooks.oeb.polish.pretty import pretty_script_or_style, pretty_xml_tree, serialize from calibre.utils.icu import lower as icu_lower from calibre.utils.icu import numeric_sort_key from calibre.utils.localization import ngettext from polyglot.builtins import iteritems, itervalues from polyglot.functools import lru_cache class SelectorStatus:
any_used: bool = False any_unused: bool = False def mark_used_selectors(rules, log, select):
ans = SelectorStatus()
for rule in rules:
for selector in rule.selectorList:
if getattr(selector, 'calibre_used', False):
ans.any_used = True continue try:
if select.has_matches(selector.selectorText):
selector.calibre_used = True ans.any_used = True else:
ans.any_unused = True selector.calibre_used = False except SelectorError:
# Cannot parse/execute this selector, be safe and assume it
# matches something selector.calibre_used = True ans.any_used = True return ans def get_imported_sheets(name, container, sheets, recursion_level=10, sheet=None):
ans = set()
sheet = sheet or sheets[name]
for rule in sheet.cssRules.rulesOfType(CSSRule.IMPORT_RULE):
if rule.href:
iname = container.href_to_name(rule.href, name)
if iname in sheets:
ans.add(iname)
if recursion_level > 0:
for imported_sheet in tuple(ans):
ans |= get_imported_sheets(imported_sheet, container, sheets, recursion_level=recursion_level-1)
ans.discard(name)
return ans def merge_declarations(first, second):
for prop in second.getProperties():
first.setProperty(prop)

def merge_identical_selectors(sheet):
' Merge rules that have identical selectors '
selector_map = defaultdict(list)
for rule in sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE):
selector_map[rule.selectorText].append(rule)
remove = []
for rule_group in itervalues(selector_map):
if len(rule_group) > 1:
for i in range(1, len(rule_group)):
merge_declarations(rule_group[0].style, rule_group[i].style)
remove.append(rule_group[i])
for rule in remove:
sheet.cssRules.remove(rule)
return len(remove)

def merge_identical_properties(sheet):
' Merge rules having identical properties '
properties_map = defaultdict(list)

def declaration_key(declaration):
return tuple(sorted(
((prop.name, prop.propertyValue.value) for prop in declaration.getProperties()), key=itemgetter(0)
))

for idx, rule in enumerate(sheet.cssRules):
if rule.type == CSSRule.STYLE_RULE:
properties_map[declaration_key(rule.style)].append((idx, rule))

removals = []
num_merged = 0 for rule_group in properties_map.values():
if len(rule_group) < 2:
continue num_merged += len(rule_group)
selectors = rule_group[0][1].selectorList seen = {s.selectorText for s in selectors}
rules = iter(rule_group)
next(rules)
for idx, rule in rules:
removals.append(idx)
for s in rule.selectorList:
q = s.selectorText if q not in seen:
seen.add(q)
selectors.append(s)
for idx in sorted(removals, reverse=True):
sheet.cssRules.pop(idx)
return num_merged def remove_unused_selectors_and_rules(rules_container, rules, removal_stats):
ans = SelectorStatus()
for r in rules:
removals = []
for i, sel in enumerate(r.selectorList):
if getattr(sel, 'calibre_used', True):
ans.any_used = True else:
removals.append(i)
if removals:
ans.any_unused = True if len(removals) == len(r.selectorList):
rules_container.remove(r)
removal_stats['rules'] += 1 else:
removal_stats['selectors'] += len(removals)
for i in reversed(removals):
del r.selectorList[i]
return ans

[документация]
def remove_unused_css( container, report=None, remove_unused_classes=False, merge_rules=False, merge_rules_with_identical_properties=False, remove_unreferenced_sheets=False,
):
'''
Remove all unused CSS rules from the book. An unused CSS rule is one that does not match any actual content.

:param report: An optional callable that takes a single argument. It is called with information about the operations being performed.
:param remove_unused_classes: If True, class attributes in the HTML that do not match any CSS rules are also removed.
:param merge_rules: If True, rules with identical selectors are merged.
:param merge_rules_with_identical_properties: If True, rules with identical properties are merged.
:param remove_unreferenced_sheets: If True, stylesheets that are not referenced by any content are removed
'''
report = report or (lambda x:x)

def safe_parse(name):
try:
return container.parsed(name)
except TypeError:
pass sheets = {name:safe_parse(name) for name, mt in iteritems(container.mime_map) if mt in OEB_STYLES}
sheets = {k:v for k, v in iteritems(sheets) if v is not None}
num_merged = num_rules_merged = 0 if merge_rules:
for name, sheet in iteritems(sheets):
num = merge_identical_selectors(sheet)
if num:
container.dirty(name)
num_merged += num if merge_rules_with_identical_properties:
for name, sheet in iteritems(sheets):
num = merge_identical_properties(sheet)
if num:
container.dirty(name)
num_rules_merged += num import_map = {name:get_imported_sheets(name, container, sheets) for name in sheets}
unreferenced_sheets = set(sheets)
if remove_unused_classes:
class_map = {name:{icu_lower(x) for x in classes_in_rule_list(sheet.cssRules)} for name, sheet in iteritems(sheets)}
style_rules = {name:tuple(sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE)) for name, sheet in iteritems(sheets)}

removal_stats = {'rules': 0, 'selectors': 0}
num_of_removed_classes = 0 for name, mt in iteritems(container.mime_map):
if mt not in OEB_DOCS:
continue root = container.parsed(name)
select = Select(root, ignore_inappropriate_pseudo_classes=True)
used_classes = set()
for style in root.xpath('//*[local-name()="style"]'):
if style.get('type', 'text/css') == 'text/css' and style.text:
sheet = container.parse_css(style.text)
if merge_rules:
num = merge_identical_selectors(sheet)
if num:
num_merged += num container.dirty(name)
if merge_rules_with_identical_properties:
num = merge_identical_properties(sheet)
if num:
num_rules_merged += num container.dirty(name)
if remove_unused_classes:
used_classes |= {icu_lower(x) for x in classes_in_rule_list(sheet.cssRules)}
imports = get_imported_sheets(name, container, sheets, sheet=sheet)
for imported_sheet in imports:
unreferenced_sheets.discard(imported_sheet)
mark_used_selectors(style_rules[imported_sheet], container.log, select)
if remove_unused_classes:
used_classes |= class_map[imported_sheet]
rules = tuple(sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE))
if mark_used_selectors(rules, container.log, select).any_unused:
remove_unused_selectors_and_rules(sheet.cssRules, rules, removal_stats)
style.text = force_unicode(sheet.cssText, 'utf-8')
pretty_script_or_style(container, style)
container.dirty(name)

for link in root.xpath('//*[local-name()="link" and @href]'):
sname = container.href_to_name(link.get('href'), name)
if sname not in sheets:
continue mark_used_selectors(style_rules[sname], container.log, select)
if remove_unused_classes:
used_classes |= class_map[sname]
unreferenced_sheets.discard(sname)

for iname in import_map[sname]:
unreferenced_sheets.discard(iname)
mark_used_selectors(style_rules[iname], container.log, select)
if remove_unused_classes:
used_classes |= class_map[iname]

if remove_unused_classes:
for elem in root.xpath('//*[@class]'):
original_classes, classes = elem.get('class', '').split(), []
for x in original_classes:
if icu_lower(x) in used_classes:
classes.append(x)
if len(classes) != len(original_classes):
if classes:
elem.set('class', ' '.join(classes))
else:
del elem.attrib['class']
num_of_removed_classes += len(original_classes) - len(classes)
container.dirty(name)

for name, sheet in iteritems(sheets):
if name in unreferenced_sheets:
continue q = remove_unused_selectors_and_rules(sheet.cssRules, style_rules[name], removal_stats)
if q.any_unused:
container.dirty(name)
num_sheets_removed = 0 if remove_unreferenced_sheets and len(unreferenced_sheets):
num_sheets_removed += len(unreferenced_sheets)
for uname in unreferenced_sheets:
container.remove_item(uname)

num_changes = num_merged + num_of_removed_classes + num_rules_merged + removal_stats['rules'] + removal_stats['selectors'] + num_sheets_removed if num_changes > 0:
if removal_stats['rules']:
report(ngettext('Removed one unused CSS style rule', 'Removed {} unused CSS style rules', removal_stats['rules']).format(removal_stats['rules']))
if removal_stats['selectors']:
report(ngettext('Removed one unused CSS selector', 'Removed {} unused CSS selectors', removal_stats['selectors']).format(removal_stats['selectors']))
if num_of_removed_classes > 0:
report(ngettext('Removed one unused class from the HTML', 'Removed {} unused classes from the HTML', num_of_removed_classes).format(num_of_removed_classes))
if num_merged > 0:
report(ngettext('Merged one CSS style rule with identical selectors', 'Merged {} CSS style rules with identical selectors', num_merged).format(num_merged))
if num_rules_merged > 0:
report(ngettext('Merged one CSS style rule with identical properties', 'Merged {} CSS style rules with identical properties', num_rules_merged).format(num_rules_merged))
if num_sheets_removed:
report(ngettext('Removed one unreferenced stylesheet', 'Removed {} unreferenced stylesheets', num_sheets_removed).format(num_sheets_removed))
if not removal_stats['rules']:
report(_('No unused CSS style rules found'))
if not removal_stats['selectors']:
report(_('No unused CSS selectors found'))
if remove_unused_classes and num_of_removed_classes == 0:
report(_('No unused class attributes found'))
if merge_rules and num_merged == 0:
report(_('No style rules that could be merged found'))
if remove_unreferenced_sheets and num_sheets_removed == 0:
report(_('No unused stylesheets found'))
return num_changes > 0def filter_declaration(style, properties=()):
changed = False for prop in properties:
if style.removeProperty(prop) != '':
changed = True all_props = set(style.keys())
for prop in style.getProperties():
n = normalizers.get(prop.name, None)
if n is not None:
normalized = n(prop.name, prop.propertyValue)
removed = properties.intersection(set(normalized))
if removed:
changed = True style.removeProperty(prop.name)
for prop in set(normalized) - removed - all_props:
style.setProperty(prop, normalized[prop])
return changed def filter_sheet(sheet, properties=()):
from css_parser.css import CSSRule changed = False remove = []
for rule in sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE):
if filter_declaration(rule.style, properties):
changed = True if rule.style.length == 0:
remove.append(rule)
for rule in remove:
sheet.cssRules.remove(rule)
return changed def transform_inline_styles(container, name, transform_sheet, transform_style):
root = container.parsed(name)
changed = False for style in root.xpath('//*[local-name()="style"]'):
if style.text and (style.get('type') or 'text/css').lower() == 'text/css':
sheet = container.parse_css(style.text)
if transform_sheet(sheet):
changed = True style.text = force_unicode(sheet.cssText, 'utf-8')
pretty_script_or_style(container, style)
for elem in root.xpath('//*[@style]'):
text = elem.get('style', None)
if text:
style = container.parse_css(text, is_declaration=True)
if transform_style(style):
changed = True if style.length == 0:
del elem.attrib['style']
else:
elem.set('style', force_unicode(style.getCssText(separator=' '), 'utf-8'))
return changed def transform_css(container, transform_sheet=None, transform_style=None, names=()):
if not names:
types = OEB_STYLES | OEB_DOCS names = []
for name, mt in iteritems(container.mime_map):
if mt in types:
names.append(name)

doc_changed = False for name in names:
mt = container.mime_map[name]
if mt in OEB_STYLES:
sheet = container.parsed(name)
if transform_sheet(sheet):
container.dirty(name)
doc_changed = True elif mt in OEB_DOCS:
if transform_inline_styles(container, name, transform_sheet, transform_style):
container.dirty(name)
doc_changed = True return doc_changed

[документация]
def filter_css(container, properties, names=()):
'''
Remove the specified CSS properties from all CSS rules in the book.

:param properties: Set of properties to remove. For example: :code:`{'font-family', 'color'}`.
:param names: The files from which to remove the properties. Defaults to all HTML and CSS files in the book.
'''
properties = normalize_filter_css(properties)
return transform_css(container, transform_sheet=partial(filter_sheet, properties=properties), transform_style=partial(filter_declaration, properties=properties), names=names)

def _classes_in_selector(selector, classes):
for attr in ('selector', 'subselector', 'parsed_tree'):
s = getattr(selector, attr, None)
if s is not None:
_classes_in_selector(s, classes)
cn = getattr(selector, 'class_name', None)
if cn is not None:
classes.add(cn)

@lru_cache(maxsize=4096)
def classes_in_selector(text):
classes = set()
try:
for selector in parse(text):
_classes_in_selector(selector, classes)
except SelectorSyntaxError:
pass return classes def classes_in_rule_list(css_rules):
classes = set()
for rule in css_rules:
if rule.type == rule.STYLE_RULE:
classes |= classes_in_selector(rule.selectorText)
elif hasattr(rule, 'cssRules'):
classes |= classes_in_rule_list(rule.cssRules)
return classes def iter_declarations(sheet_or_rule):
if hasattr(sheet_or_rule, 'cssRules'):
for rule in sheet_or_rule.cssRules:
yield from iter_declarations(rule)
elif hasattr(sheet_or_rule, 'style'):
yield sheet_or_rule.style elif isinstance(sheet_or_rule, CSSStyleDeclaration):
yield sheet_or_rule def remove_property_value(prop, predicate):
''' Remove the Values that match the predicate from this property. If all values of the property would be removed, the property is removed from its parent instead. Note that this means the property must have a parent (a
CSSStyleDeclaration). '''
removed_vals = list(filter(predicate, prop.propertyValue))
if len(removed_vals) == len(prop.propertyValue):
prop.parent.removeProperty(prop.name)
else:
x = css_text(prop.propertyValue)
for v in removed_vals:
x = x.replace(css_text(v), '').strip()
prop.propertyValue.cssText = x return bool(removed_vals)

RULE_PRIORITIES = {t:i for i, t in enumerate((CSSRule.COMMENT, CSSRule.CHARSET_RULE, CSSRule.IMPORT_RULE, CSSRule.NAMESPACE_RULE))}

def sort_sheet(container, sheet_or_text):
''' Sort the rules in a stylesheet. Note that in the general case this can change the effective styles, but for most common sheets, it should be safe.
'''
sheet = container.parse_css(sheet_or_text) if isinstance(sheet_or_text, str) else sheet_or_text def text_sort_key(x):
return numeric_sort_key(str(x or ''))

def selector_sort_key(x):
return (x.specificity, text_sort_key(x.selectorText))

def rule_sort_key(rule):
primary = RULE_PRIORITIES.get(rule.type, len(RULE_PRIORITIES))
secondary = text_sort_key(getattr(rule, 'atkeyword', '') or '')
tertiary = None if rule.type == CSSRule.STYLE_RULE:
primary += 1 selectors = sorted(rule.selectorList, key=selector_sort_key)
tertiary = selector_sort_key(selectors[0])
rule.selectorText = ', '.join(s.selectorText for s in selectors)
elif rule.type == CSSRule.FONT_FACE_RULE:
try:
tertiary = text_sort_key(rule.style.getPropertyValue('font-family'))
except Exception:
pass return primary, secondary, tertiary sheet.cssRules.sort(key=rule_sort_key)
return sheet def add_stylesheet_links(container, name, text):
root = container.parse_xhtml(text, name)
head = root.xpath('//*[local-name() = "head"]')
if not head:
return head = head[0]
sheets = tuple(container.manifest_items_of_type(lambda mt: mt in OEB_STYLES))
if not sheets:
return for sname in sheets:
link = head.makeelement(XHTML('link'), type='text/css', rel='stylesheet', href=container.name_to_href(sname, name))
head.append(link)
pretty_xml_tree(head)
return serialize(root, 'text/html')

def rename_class_in_rule_list(css_rules, old_name, new_name):
# this regex will not match class names inside attribute value selectors
# and it will match id selectors that contain .old_name but its the best
# that can be done without implementing a full parser for CSS selectors pat = re.compile(rf'(?<=\.){re.escape(old_name)}(?:\W|$)')

def repl(m):
return m.group().replace(old_name, new_name)

changed = False for rule in css_rules:
if rule.type == rule.STYLE_RULE:
old = rule.selectorText q = pat.sub(repl, old)
if q != old:
changed = True rule.selectorText = q elif hasattr(rule, 'cssRules'):
if rename_class_in_rule_list(rule.cssRules, old_name, new_name):
changed = True return changed def rename_class_in_doc(container, root, old_name, new_name):
changed = False pat = re.compile(rf'(?:^|\W){re.escape(old_name)}(?:\W|$)')

def repl(m):
return m.group().replace(old_name, new_name)

for elem in root.xpath('//*[@class]'):
old = elem.get('class')
if old:
new = pat.sub(repl, old)
if new != old:
changed = True elem.set('class', new)
for style in root.xpath('//*[local-name()="style"]'):
if style.get('type', 'text/css') == 'text/css' and style.text:
sheet = container.parse_css(style.text)
if rename_class_in_rule_list(sheet.cssRules, old_name, new_name):
changed = True style.text = force_unicode(sheet.cssText, 'utf-8')
return changed def rename_class(container, old_name, new_name):
changed = False if not old_name or old_name == new_name:
return changed for sheet_name in container.manifest_items_of_type(lambda mt: mt in OEB_STYLES):
sheet = container.parsed(sheet_name)
if rename_class_in_rule_list(sheet.cssRules, old_name, new_name):
container.dirty(sheet_name)
changed = True for doc_name in container.manifest_items_of_type(lambda mt: mt in OEB_DOCS):
doc = container.parsed(doc_name)
if rename_class_in_doc(container, doc, old_name, new_name):
container.dirty(doc_name)
changed = True return changed

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.ebooks.oeb.polish.fonts

Исходный код calibre.ebooks.oeb.polish.fonts

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'

from tinycss.fonts3 import parse_font, parse_font_family, serialize_font, serialize_font_family from calibre.ebooks.oeb.base import css_text from calibre.ebooks.oeb.normalize_css import normalize_font from calibre.ebooks.oeb.polish.container import OEB_DOCS, OEB_STYLES from polyglot.builtins import iteritems def unquote(x):
if x and len(x) > 1 and x[0] == x[-1] and x[0] in ('"', "'"):
x = x[1:-1]
return x def font_family_data_from_declaration(style, families):
font_families = []
f = style.getProperty('font')
if f is not None:
f = normalize_font(f.propertyValue, font_family_as_list=True).get('font-family', None)
if f is not None:
font_families = [unquote(x) for x in f]
f = style.getProperty('font-family')
if f is not None:
font_families = parse_font_family(css_text(f.propertyValue))

for f in font_families:
families[f] = families.get(f, False)

def font_family_data_from_sheet(sheet, families):
for rule in sheet.cssRules:
if rule.type == rule.STYLE_RULE:
font_family_data_from_declaration(rule.style, families)
elif rule.type == rule.FONT_FACE_RULE:
ff = rule.style.getProperty('font-family')
if ff is not None:
for f in parse_font_family(css_text(ff.propertyValue)):
families[f] = True def font_family_data(container):
families = {}
for name, mt in iteritems(container.mime_map):
if mt in OEB_STYLES:
sheet = container.parsed(name)
font_family_data_from_sheet(sheet, families)
elif mt in OEB_DOCS:
root = container.parsed(name)
for style in root.xpath('//*[local-name() = "style"]'):
if style.text and style.get('type', 'text/css').lower() == 'text/css':
sheet = container.parse_css(style.text)
font_family_data_from_sheet(sheet, families)
for style in root.xpath('//*/@style'):
if style:
style = container.parse_css(style, is_declaration=True)
font_family_data_from_declaration(style, families)
return families def change_font_in_declaration(style, old_name, new_name=None):
changed = False ff = style.getProperty('font-family')
if ff is not None:
fams = parse_font_family(css_text(ff.propertyValue))
nfams = list(filter(None, [new_name if x == old_name else x for x in fams]))
if fams != nfams:
if nfams:
ff.propertyValue.cssText = serialize_font_family(nfams)
else:
style.removeProperty(ff.name)
changed = True ff = style.getProperty('font')
if ff is not None:
props = parse_font(css_text(ff.propertyValue))
fams = props.get('font-family') or []
nfams = list(filter(None, [new_name if x == old_name else x for x in fams]))
if fams != nfams:
props['font-family'] = nfams if nfams:
ff.propertyValue.cssText = serialize_font(props)
else:
style.removeProperty(ff.name)
changed = True return changed def remove_embedded_font(container, sheet, rule, sheet_name):
src = getattr(rule.style.getProperty('src'), 'value', None)
if src is not None:
if src.startswith('url('):
src = src[4:-1]
sheet.cssRules.remove(rule)
if src:
src = unquote(src)
name = container.href_to_name(src, sheet_name)
if container.has_name(name):
container.remove_item(name)

def change_font_in_sheet(container, sheet, old_name, new_name, sheet_name):
changed = False removals = []
for rule in sheet.cssRules:
if rule.type == rule.STYLE_RULE:
changed |= change_font_in_declaration(rule.style, old_name, new_name)
elif rule.type == rule.FONT_FACE_RULE:
ff = rule.style.getProperty('font-family')
if ff is not None:
families = set(parse_font_family(css_text(ff.propertyValue)))
if old_name in families:
changed = True removals.append(rule)
for rule in reversed(removals):
remove_embedded_font(container, sheet, rule, sheet_name)
return changed

[документация]
def change_font(container, old_name, new_name=None):
'''
Change a font family from old_name to new_name. Changes all occurrences of the font family in stylesheets, style tags and style attributes.
If the old_name refers to an embedded font, it is removed. You can set new_name to None to remove the font family instead of changing it.
'''
changed = False for name, mt in tuple(iteritems(container.mime_map)):
if mt in OEB_STYLES:
sheet = container.parsed(name)
if change_font_in_sheet(container, sheet, old_name, new_name, name):
container.dirty(name)
changed = True elif mt in OEB_DOCS:
root = container.parsed(name)
for style in root.xpath('//*[local-name() = "style"]'):
if style.text and style.get('type', 'text/css').lower() == 'text/css':
sheet = container.parse_css(style.text)
if change_font_in_sheet(container, sheet, old_name, new_name, name):
container.dirty(name)
changed = True for elem in root.xpath('//*[@style]'):
style = elem.get('style', '')
if style:
style = container.parse_css(style, is_declaration=True)
if change_font_in_declaration(style, old_name, new_name):
style = css_text(style).strip().rstrip(';').strip()
if style:
elem.set('style', style)
else:
del elem.attrib['style']
container.dirty(name)
changed = True return changed

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.ebooks.oeb.polish.jacket

Исходный код calibre.ebooks.oeb.polish.jacket

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

from calibre.customize.ui import output_profiles from calibre.ebooks.conversion.config import load_defaults from calibre.ebooks.oeb.base import OPF, XPath from calibre.ebooks.oeb.polish.cover import find_cover_page from calibre.ebooks.oeb.transforms.jacket import referenced_images from calibre.ebooks.oeb.transforms.jacket import render_jacket as render def render_jacket(container, jacket):
mi = container.mi ps = load_defaults('page_setup')
op = ps.get('output_profile', 'default')
opmap = {x.short_name:x for x in output_profiles()}
output_profile = opmap.get(op, opmap['default'])
root = render(mi, output_profile)
for img, path in referenced_images(root):
container.log(f'Embedding referenced image: {path} into jacket')
ext = path.rpartition('.')[-1]
jacket_item = container.generate_item('jacket_image.'+ext, id_prefix='jacket_img')
name = container.href_to_name(jacket_item.get('href'), container.opf_name)
with open(path, 'rb') as f:
container.parsed_cache[name] = f.read()
container.commit_item(name)
href = container.name_to_href(name, jacket)
img.set('src', href)
return root def is_legacy_jacket(root):
return len(root.xpath(
'//*[starts-with(@class,"calibrerescale") and (local-name()="h1" or local-name()="h2")]')) > 0 def is_current_jacket(root):
return len(XPath(
'//h:meta[@name="calibre-content" and @content="jacket"]')(root)) > 0 def find_existing_jacket(container):
for item in container.spine_items:
name = container.abspath_to_name(item)
if container.book_type == 'azw3':
root = container.parsed(name)
if is_current_jacket(root):
return name else:
if name.rpartition('/')[-1].startswith('jacket') and name.endswith('.xhtml'):
root = container.parsed(name)
if is_current_jacket(root) or is_legacy_jacket(root):
return name def replace_jacket(container, name):
root = render_jacket(container, name)
container.parsed_cache[name] = root container.dirty(name)

[документация]
def remove_jacket(container):
' Remove an existing jacket, if any. Returns False if no existing jacket was found. '
name = find_existing_jacket(container)
if name is not None:
remove_jacket_images(container, name)
container.remove_item(name)
return True return False
def remove_jacket_images(container, name):
root = container.parsed_cache[name]
for img in root.xpath('//*[local-name() = "img" and @src]'):
iname = container.href_to_name(img.get('src'), name)
if container.has_name(iname):
container.remove_item(iname)

[документация]
def add_or_replace_jacket(container):
''' Either create a new jacket from the book's metadata or replace an existing jacket. Returns True if an existing jacket was replaced. '''
name = find_existing_jacket(container)
found = True if name is None:
jacket_item = container.generate_item('jacket.xhtml', id_prefix='jacket')
name = container.href_to_name(jacket_item.get('href'), container.opf_name)
found = False if found:
remove_jacket_images(container, name)

replace_jacket(container, name)
if not found:
# Insert new jacket into spine index = 0 sp = container.abspath_to_name(next(container.spine_items))
if sp == find_cover_page(container):
index = 1 itemref = container.opf.makeelement(OPF('itemref'), idref=jacket_item.get('id'))
container.insert_into_xml(container.opf_xpath('//opf:spine')[0], itemref, index=index)
return found

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.ebooks.oeb.polish.pretty

Исходный код calibre.ebooks.oeb.polish.pretty

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'

import textwrap

# from lxml.etree import Element from calibre import force_unicode from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES, SVG, XHTML, XPNSMAP, barename, serialize from calibre.ebooks.oeb.polish.container import OPF_NAMESPACES from calibre.ebooks.oeb.polish.utils import guess_type from calibre.utils.icu import sort_key from polyglot.builtins import iteritems def isspace(x):
return not x.strip('\u0009\u000a\u000c\u000d\u0020')

def pretty_xml_tree(elem, level=0, indent=' '):
''' XML beautifier, assumes that elements that have children do not have textual content. Also assumes that there is no text immediately after closing tags. These are true for opf/ncx and container.xml files. If either of the assumptions are violated, there should be no data loss, but pretty printing won't produce optimal results.'''
if (not elem.text and len(elem) > 0) or (elem.text and isspace(elem.text)):
elem.text = '\n' + (indent * (level+1))
for i, child in enumerate(elem):
pretty_xml_tree(child, level=level+1, indent=indent)
if not child.tail or isspace(child.tail):
l = level + 1 if i == len(elem) - 1:
l -= 1 child.tail = '\n' + (indent * l)

def pretty_opf(root):
# Put all dc: tags first starting with title and author. Preserve order for
# the rest.
def dckey(x):
return {'title':0, 'creator':1}.get(barename(x.tag), 2)
for metadata in root.xpath('//opf:metadata', namespaces=OPF_NAMESPACES):
dc_tags = metadata.xpath('./*[namespace-uri()="{}"]'.format(OPF_NAMESPACES['dc']))
dc_tags.sort(key=dckey)
for x in reversed(dc_tags):
metadata.insert(0, x)

# Group items in the manifest spine_ids = root.xpath('//opf:spine/opf:itemref/@idref', namespaces=OPF_NAMESPACES)
spine_ids = {x:i for i, x in enumerate(spine_ids)}

def manifest_key(x):
mt = x.get('media-type', '')
href = x.get('href', '')
ext = href.rpartition('.')[-1].lower()
cat = 1000 if mt in OEB_DOCS:
cat = 0 elif mt == guess_type('a.ncx'):
cat = 1 elif mt in OEB_STYLES:
cat = 2 elif mt.startswith('image/'):
cat = 3 elif ext in {'otf', 'ttf', 'woff', 'woff2'}:
cat = 4 elif mt.startswith('audio/'):
cat = 5 elif mt.startswith('video/'):
cat = 6 if cat == 0:
i = spine_ids.get(x.get('id', None), 1000000000)
else:
i = sort_key(href)
return cat, i for manifest in root.xpath('//opf:manifest', namespaces=OPF_NAMESPACES):
try:
children = sorted(manifest, key=manifest_key)
except AttributeError:
continue # There are comments so don't sort since that would mess up the comments for x in reversed(children):
manifest.insert(0, x)

SVG_TAG = SVG('svg')
BLOCK_TAGS = frozenset(map(XHTML, (
'address', 'article', 'aside', 'audio', 'blockquote', 'body', 'canvas', 'col', 'colgroup', 'dd',
'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'li',
'noscript', 'ol', 'output', 'p', 'pre', 'script', 'section', 'style', 'table', 'tbody', 'td',
'tfoot', 'th', 'thead', 'tr', 'ul', 'video', 'img'))) | {SVG_TAG}

def isblock(x):
if callable(x.tag) or not x.tag:
return True if x.tag in BLOCK_TAGS:
return True return False def has_only_blocks(x):
if hasattr(x.tag, 'split') and len(x) == 0:
# Tag with no children, return False if x.text and not isspace(x.text):
return False for child in x:
if not isblock(child) or (child.tail and not isspace(child.tail)):
return False return True def indent_for_tag(x):
prev = x.getprevious()
x = x.getparent().text if prev is None else prev.tail if not x:
return ''
s = x.rpartition('\n')[-1]
return s if isspace(s) else ''

def set_indent(elem, attr, indent):
x = getattr(elem, attr)
if not x:
x = indent else:
lines = x.splitlines()
if isspace(lines[-1]):
lines[-1] = indent else:
lines.append(indent)
x = '\n'.join(lines)
setattr(elem, attr, x)

def pretty_block(parent, level=1, indent=' '):
''' Surround block tags with blank lines and recurse into child block tags that contain only other block tags '''
if not parent.text or isspace(parent.text):
parent.text = ''
nn = '\n' if hasattr(parent.tag, 'strip') and barename(parent.tag) in {'tr', 'td', 'th'} else '\n\n'
parent.text = parent.text + nn + (indent * level)
for i, child in enumerate(parent):
if isblock(child) and has_only_blocks(child):
pretty_block(child, level=level+1, indent=indent)
elif child.tag == SVG_TAG:
pretty_xml_tree(child, level=level, indent=indent)
l = level if i == len(parent) - 1:
l -= 1 if not child.tail or isspace(child.tail):
child.tail = ''
child.tail = child.tail + nn + (indent * l)

def pretty_script_or_style(container, child):
if child.text:
indent = indent_for_tag(child)
if child.tag.endswith('style'):
child.text = force_unicode(pretty_css(container, '', child.text), 'utf-8')
child.text = textwrap.dedent(child.text)
child.text = '\n' + '\n'.join([(indent + x) if x else '' for x in child.text.splitlines()])
set_indent(child, 'text', indent)

def pretty_html_tree(container, root):
root.text = '\n\n'
for child in root:
child.tail = '\n\n'
if hasattr(child.tag, 'endswith') and child.tag.endswith('}head'):
pretty_xml_tree(child)
for body in root.findall('h:body', namespaces=XPNSMAP):
pretty_block(body)
# Special case the handling of a body that contains a single block tag
# with all content. In this case we prettify the containing block tag
# even if it has non block children.
if (len(body) == 1 and not callable(body[0].tag) and isblock(body[0]) and not has_only_blocks( body[0]) and barename(body[0].tag) not in (
'pre', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6') and len(body[0]) > 0):
pretty_block(body[0], level=2)

if container is not None:
# Handle <script> and <style> tags for child in root.xpath('//*[local-name()="script" or local-name()="style"]'):
pretty_script_or_style(container, child)

[документация]
def fix_html(container, raw):
' Fix any parsing errors in the HTML represented as a string in raw. Fixing is done using the HTML5 parsing algorithm. '
root = container.parse_xhtml(raw)
return serialize(root, 'text/html')

[документация]
def pretty_html(container, name, raw):
' Pretty print the HTML represented as a string in raw '
root = container.parse_xhtml(raw)
pretty_html_tree(container, root)
return serialize(root, 'text/html')

[документация]
def pretty_css(container, name, raw):
' Pretty print the CSS represented as a string in raw '
sheet = container.parse_css(raw)
return serialize(sheet, 'text/css')

[документация]
def pretty_xml(container, name, raw):
' Pretty print the XML represented as a string in raw. If ``name`` is the name of the OPF, extra OPF-specific prettying is performed. '
root = container.parse_xml(raw)
if name == container.opf_name:
pretty_opf(root)
pretty_xml_tree(root)
return serialize(root, 'text/xml')

[документация]
def fix_all_html(container):
' Fix any parsing errors in all HTML files in the container. Fixing is done using the HTML5 parsing algorithm. '
for name, mt in iteritems(container.mime_map):
if mt in OEB_DOCS:
container.parsed(name)
container.dirty(name)

[документация]
def pretty_all(container):
' Pretty print all HTML/CSS/XML files in the container '
xml_types = {guess_type('a.ncx'), guess_type('a.xml'), guess_type('a.svg')}
for name, mt in iteritems(container.mime_map):
prettied = False if mt in OEB_DOCS:
pretty_html_tree(container, container.parsed(name))
prettied = True elif mt in OEB_STYLES:
container.parsed(name)
prettied = True elif name == container.opf_name:
root = container.parsed(name)
pretty_opf(root)
pretty_xml_tree(root)
prettied = True elif mt in xml_types:
pretty_xml_tree(container.parsed(name))
prettied = True if prettied:
container.dirty(name)

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.ebooks.oeb.polish.replace

Исходный код calibre.ebooks.oeb.polish.replace

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import codecs import os import posixpath import shutil from collections import Counter, defaultdict from functools import partial from calibre import sanitize_file_name from calibre.ebooks.chardet import strip_encoding_declarations from calibre.ebooks.oeb.base import css_text from calibre.ebooks.oeb.polish.css import iter_declarations, remove_property_value from calibre.ebooks.oeb.polish.utils import extract from polyglot.builtins import iteritems, itervalues from polyglot.urllib import urlparse, urlunparse class LinkReplacer:

def __init__(self, base, container, link_map, frag_map):
self.base = base self.frag_map = frag_map self.link_map = link_map self.container = container self.replaced = False def __call__(self, url):
if url and url.startswith('#'):
repl = self.frag_map(self.base, url[1:])
if not repl or repl == url[1:]:
return url self.replaced = True return '#' + repl name = self.container.href_to_name(url, self.base)
if not name:
return url nname = self.link_map.get(name, None)
if not nname:
return url purl = urlparse(url)
href = self.container.name_to_href(nname, self.base)
if purl.fragment:
nfrag = self.frag_map(name, purl.fragment)
if nfrag:
href += f'#{nfrag}'
if href != url:
self.replaced = True return href class IdReplacer:

def __init__(self, base, container, id_map):
self.base, self.container, self.replaced = base, container, False self.id_map = id_map def __call__(self, url):
if url and url.startswith('#'):
repl = self.id_map.get(self.base, {}).get(url[1:])
if repl is None or repl == url[1:]:
return url self.replaced = True return '#' + repl name = self.container.href_to_name(url, self.base)
if not name:
return url id_map = self.id_map.get(name)
if id_map is None:
return url purl = urlparse(url)
nfrag = id_map.get(purl.fragment)
if nfrag is None:
return url purl = purl._replace(fragment=nfrag)
href = urlunparse(purl)
if href != url:
self.replaced = True return href class LinkRebaser:

def __init__(self, container, old_name, new_name):
self.old_name, self.new_name = old_name, new_name self.container = container self.replaced = False def __call__(self, url):
if url and url.startswith('#'):
return url purl = urlparse(url)
frag = purl.fragment name = self.container.href_to_name(url, self.old_name)
if not name:
return url if name == self.old_name:
name = self.new_name href = self.container.name_to_href(name, self.new_name)
if frag:
href += '#' + frag if href != url:
self.replaced = True return href

[документация]
def replace_links(container, link_map, frag_map=lambda name, frag:frag, replace_in_opf=False):
'''
Replace links to files in the container. Will iterate over all files in the container and change the specified links in them.

:param link_map: A mapping of old canonical name to new canonical name. For example: :code:`{'images/old.png': 'images/new.png'}`
:param frag_map: A callable that takes two arguments ``(name, anchor)`` and returns a new anchor. This is useful if you need to change the anchors in
HTML files. By default, it does nothing.
:param replace_in_opf: If False, links are not replaced in the OPF file.

'''
for name, media_type in iteritems(container.mime_map):
if name == container.opf_name and not replace_in_opf:
continue repl = LinkReplacer(name, container, link_map, frag_map)
container.replace_links(name, repl)

def replace_ids(container, id_map):
'''
Replace all links in the container that pointed to the changed ids.

:param id_map: A mapping of {name:id_map} where each id_map is a mapping of {old_id:new_id}
:return: True iff at least one link was changed

'''
changed = False for name, media_type in iteritems(container.mime_map):
repl = IdReplacer(name, container, id_map)
container.replace_links(name, repl)
if name == container.opf_name:
imap = id_map.get(name, {})
for item in container.opf_xpath('//*[@idref]'):
old_id = item.get('idref')
if old_id is not None:
new_id = imap.get(old_id)
if new_id is not None:
item.set('idref', new_id)
if repl.replaced:
changed = True return changed def smarten_punctuation(container, report):
from calibre.ebooks.conversion.preprocess import smarten_punctuation smartened = False for path in container.spine_items:
name = container.abspath_to_name(path)
changed = False with container.open(name, 'r+b') as f:
html = container.decode(f.read())
newhtml = smarten_punctuation(html, container.log)
if newhtml != html:
changed = True report(_('Smartened punctuation in: %s')%name)
newhtml = strip_encoding_declarations(newhtml)
f.seek(0)
f.truncate()
f.write(codecs.BOM_UTF8 + newhtml.encode('utf-8'))
if changed:
# Add an encoding declaration (it will be added automatically when
# serialized)
root = container.parsed(name)
for m in root.xpath('descendant::*[local-name()="meta" and @http-equiv]'):
m.getparent().remove(m)
container.dirty(name)
smartened = True if not smartened:
report(_('No punctuation that could be smartened found'))
return smartened

[документация]
def rename_files(container, file_map):
'''
Rename files in the container, automatically updating all links to them.

:param file_map: A mapping of old canonical name to new canonical name, for example: :code:`{'text/chapter1.html': 'chapter1.html'}`.
'''
overlap = set(file_map).intersection(set(itervalues(file_map)))
if overlap:
raise ValueError('Circular rename detected. The files {} are both rename targets and destinations'.format(', '.join(overlap)))
for name, dest in iteritems(file_map):
if container.exists(dest):
if name != dest and name.lower() == dest.lower():
# A case change on an OS with a case insensitive file-system.
continue raise ValueError(f'Cannot rename {name} to {dest} as {dest} already exists')
if len(tuple(itervalues(file_map))) != len(set(itervalues(file_map))):
raise ValueError('Cannot rename, the set of destination files contains duplicates')
link_map = {}
for current_name, new_name in iteritems(file_map):
container.rename(current_name, new_name)
if new_name != container.opf_name: # OPF is handled by the container link_map[current_name] = new_name replace_links(container, link_map, replace_in_opf=True)

def replace_file(container, name, path, basename, force_mt=None):
dirname, base = name.rpartition('/')[0::2]
nname = sanitize_file_name(basename)
if dirname:
nname = dirname + '/' + nname with open(path, 'rb') as src:
if name != nname:
count = 0 b, e = nname.rpartition('.')[0::2]
while container.exists(nname):
count += 1 nname = b + f'_{count}.{e}'
rename_files(container, {name:nname})
mt = force_mt or container.guess_type(nname)
container.mime_map[nname] = mt for itemid, q in iteritems(container.manifest_id_map):
if q == nname:
for item in container.opf_xpath(f'//opf:manifest/opf:item[@href and @id="{itemid}"]'):
item.set('media-type', mt)
container.dirty(container.opf_name)
with container.open(nname, 'wb') as dest:
shutil.copyfileobj(src, dest)

def mt_to_category(container, mt):
from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES from calibre.ebooks.oeb.polish.utils import OEB_FONTS, guess_type if mt in OEB_DOCS:
category = 'text'
elif mt in OEB_STYLES:
category = 'style'
elif mt in OEB_FONTS:
category = 'font'
elif mt == guess_type('a.opf'):
category = 'opf'
elif mt == guess_type('a.ncx'):
category = 'toc'
else:
category = mt.partition('/')[0]
return category

[документация]
def get_recommended_folders(container, names):
''' Return the folders that are recommended for the given filenames. The recommendation is based on where the majority of files of the same type are located in the container. If no files of a particular type are present, the recommended folder is assumed to be the folder containing the OPF file. '''
from calibre.ebooks.oeb.polish.utils import guess_type counts = defaultdict(Counter)
for name, mt in iteritems(container.mime_map):
folder = name.rpartition('/')[0] if '/' in name else ''
counts[mt_to_category(container, mt)][folder] += 1 try:
opf_folder = counts['opf'].most_common(1)[0][0]
except KeyError:
opf_folder = ''

recommendations = {category:counter.most_common(1)[0][0] for category, counter in iteritems(counts)}
return {n:recommendations.get(mt_to_category(container, guess_type(os.path.basename(n))), opf_folder) for n in names}

def normalize_case(container, val):

def safe_listdir(x):
try:
return os.listdir(x)
except OSError:
return ()

parts = val.split('/')
ans = []
for i in range(len(parts)):
q = '/'.join(parts[:i+1])
x = container.name_to_abspath(q)
xl = parts[i].lower()
candidates = [c for c in safe_listdir(os.path.dirname(x)) if c != parts[i] and c.lower() == xl]
ans.append(candidates[0] if candidates else parts[i])
return '/'.join(ans)

def rationalize_folders(container, folder_type_map):
all_names = set(container.mime_map)
new_names = set()
name_map = {}
for key in tuple(folder_type_map):
val = folder_type_map[key]
folder_type_map[key] = normalize_case(container, val)
for name in all_names:
if name.startswith('META-INF/'):
continue category = mt_to_category(container, container.mime_map[name])
folder = folder_type_map.get(category, None)
if folder is not None:
bn = posixpath.basename(name)
new_name = posixpath.join(folder, bn)
if new_name != name:
c = 0 while new_name in all_names or new_name in new_names:
c += 1 n, ext = bn.rpartition('.')[0::2]
new_name = posixpath.join(folder, f'{n}_{c}.{ext}')
name_map[name] = new_name new_names.add(new_name)
return name_map def remove_links_in_sheet(href_to_name, sheet, predicate):
import_rules_to_remove = []
changed = False for i, r in enumerate(sheet):
if r.type == r.IMPORT_RULE:
name = href_to_name(r.href)
if predicate(name, r.href, None):
import_rules_to_remove.append(i)
for i in sorted(import_rules_to_remove, reverse=True):
sheet.deleteRule(i)
changed = True for dec in iter_declarations(sheet):
changed = remove_links_in_declaration(href_to_name, dec, predicate) or changed return changed def remove_links_in_declaration(href_to_name, style, predicate):
def check_pval(v):
if v.type == v.URI:
name = href_to_name(v.uri)
return predicate(name, v.uri, None)
return False changed = False for p in tuple(style.getProperties(all=True)):
changed = remove_property_value(p, check_pval) or changed return changed def remove_links_to(container, predicate):
''' predicate must be a function that takes the arguments (name, href, fragment=None) and returns True iff the link should be removed '''
from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES, XHTML, XPath, iterlinks stylepath = XPath('//h:style')
styleattrpath = XPath('//*[@style]')
changed = set()
for name, mt in iteritems(container.mime_map):
removed = False if mt in OEB_DOCS:
root = container.parsed(name)
for el, attr, href, pos in iterlinks(root, find_links_in_css=False):
hname = container.href_to_name(href, name)
frag = href.partition('#')[-1]
if predicate(hname, href, frag):
if attr is None:
el.text = None else:
if el.tag == XHTML('link') or el.tag == XHTML('img'):
extract(el)
else:
del el.attrib[attr]
removed = True for tag in stylepath(root):
if tag.text and (tag.get('type') or 'text/css').lower() == 'text/css':
sheet = container.parse_css(tag.text)
if remove_links_in_sheet(partial(container.href_to_name, base=name), sheet, predicate):
tag.text = css_text(sheet)
removed = True for tag in styleattrpath(root):
style = tag.get('style')
if style:
style = container.parse_css(style, is_declaration=True)
if remove_links_in_declaration(partial(container.href_to_name, base=name), style, predicate):
removed = True tag.set('style', css_text(style))
elif mt in OEB_STYLES:
removed = remove_links_in_sheet(partial(container.href_to_name, base=name), container.parsed(name), predicate)
if removed:
changed.add(name)
for i in changed:
container.dirty(i)
return changed def get_spine_order_for_all_files(container):
linear_names, non_linear_names = [], []
for name, is_linear in container.spine_names:
(linear_names if is_linear else non_linear_names).append(name)
all_names = linear_names + non_linear_names spine_names = frozenset(all_names)
ans = {}
for spine_pos, name in enumerate(all_names):
ans.setdefault(name, (spine_pos, -1))
for i, href in enumerate(container.iterlinks(name, get_line_numbers=False)):
lname = container.href_to_name(href, name)
if lname not in spine_names:
ans.setdefault(lname, (spine_pos, i))
return ans

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.ebooks.oeb.polish.split

Исходный код calibre.ebooks.oeb.polish.split

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'

import copy import os import re from calibre.ebooks.oeb.base import OEB_DOCS, OPF, XHTML, XPNSMAP, XPath, barename from calibre.ebooks.oeb.polish.errors import MalformedMarkup from calibre.ebooks.oeb.polish.replace import LinkRebaser from calibre.ebooks.oeb.polish.toc import node_from_loc from polyglot.builtins import iteritems, string_or_bytes from polyglot.urllib import urlparse class AbortError(ValueError):
pass def in_table(node):
while node is not None:
if node.tag.endswith('}table'):
return True node = node.getparent()
return False def adjust_split_point(split_point, log):
'''
Move the split point up its ancestor chain if it has no content before it. This handles the common case:
<div id="chapter1"><h2>Chapter 1</h2>...</div> with a page break on the h2.
'''
sp = split_point while True:
parent = sp.getparent()
if ( parent is None or barename(parent.tag) in {'body', 'html'} or
(parent.text and parent.text.strip()) or parent.index(sp) > 0
):
break sp = parent if sp is not split_point:
log.debug('Adjusted split point to ancestor')

return sp def get_body(root):
return root.find('h:body', namespaces=XPNSMAP)

def do_split(split_point, log, before=True):
'''
Split tree into a *before* and an *after* tree at ``split_point``.

:param split_point: The Element at which to split
:param before: If True tree is split before split_point, otherwise after split_point
:return: before_tree, after_tree
'''
if before:
# We cannot adjust for after since moving an after split point to a
# parent will cause breakage if the parent contains any content
# after the original split point split_point = adjust_split_point(split_point, log)
tree = split_point.getroottree()
path = tree.getpath(split_point)

tree, tree2 = copy.deepcopy(tree), copy.deepcopy(tree)
root, root2 = tree.getroot(), tree2.getroot()
body, body2 = map(get_body, (root, root2))
split_point = root.xpath(path)[0]
split_point2 = root2.xpath(path)[0]

def nix_element(elem, top=True):
# Remove elem unless top is False in which case replace elem by its
# children parent = elem.getparent()
if top:
parent.remove(elem)
else:
index = parent.index(elem)
parent[index:index+1] = list(elem.iterchildren())

# Tree 1 hit_split_point = False keep_descendants = False split_point_descendants = frozenset(split_point.iterdescendants())
for elem in tuple(body.iterdescendants()):
if elem is split_point:
hit_split_point = True if before:
nix_element(elem)
else:
# We want to keep the descendants of the split point in
# Tree 1 keep_descendants = True
# We want the split point element, but not its tail elem.tail = '\n'

continue if hit_split_point:
if keep_descendants:
if elem in split_point_descendants:
# elem is a descendant keep it continue else:
# We are out of split_point, so prevent further set
# lookups of split_point_descendants keep_descendants = False nix_element(elem)

# Tree 2 ancestors = frozenset(XPath('ancestor::*')(split_point2))
for elem in tuple(body2.iterdescendants()):
if elem is split_point2:
if not before:
# Keep the split point element's tail, if it contains non-whitespace
# text tail = elem.tail if tail and not tail.isspace():
parent = elem.getparent()
idx = parent.index(elem)
if idx == 0:
parent.text = (parent.text or '') + tail else:
sib = parent[idx-1]
sib.tail = (sib.tail or '') + tail
# Remove the element itself nix_element(elem)
break if elem in ancestors:
# We have to preserve the ancestors as they could have CSS
# styles that are inherited/applicable, like font or
# width. So we only remove the text, if any.
elem.text = '\n'
else:
nix_element(elem, top=False)

body2.text = '\n'

return tree, tree2 class SplitLinkReplacer:

def __init__(self, base, bottom_anchors, top_name, bottom_name, container):
self.bottom_anchors, self.bottom_name = bottom_anchors, bottom_name self.container, self.top_name = container, top_name self.base = base self.replaced = False def __call__(self, url):
if url and url.startswith('#'):
return url name = self.container.href_to_name(url, self.base)
if name != self.top_name:
return url purl = urlparse(url)
if purl.fragment and purl.fragment in self.bottom_anchors:
url = self.container.name_to_href(self.bottom_name, self.base) + '#' + purl.fragment self.replaced = True return url

[документация]
def split(container, name, loc_or_xpath, before=True, totals=None):
'''
Split the file specified by name at the position specified by loc_or_xpath.
Splitting automatically migrates all links and references to the affected files.

:param loc_or_xpath: Should be an XPath expression such as
//h:div[@id="split_here"]. Can also be a *loc* which is used internally to implement splitting in the preview panel.
:param before: If True the split occurs before the identified element otherwise after it.
:param totals: Used internally
'''

root = container.parsed(name)
if isinstance(loc_or_xpath, str):
split_point = root.xpath(loc_or_xpath)[0]
else:
try:
split_point = node_from_loc(root, loc_or_xpath, totals=totals)
except MalformedMarkup:
# The webkit HTML parser and the container parser have yielded
# different node counts, this can happen if the file is valid XML
# but contains constructs like nested <p> tags. So force parse it
# with the HTML 5 parser and try again.
raw = container.raw_data(name)
root = container.parse_xhtml(raw, fname=name, force_html5_parse=True)
try:
split_point = node_from_loc(root, loc_or_xpath, totals=totals)
except MalformedMarkup:
raise MalformedMarkup(_('The file %s has malformed markup. Try running the Fix HTML tool'
' before splitting') % name)
container.replace(name, root)
if in_table(split_point):
raise AbortError('Cannot split inside tables')
if split_point.tag.endswith('}body'):
raise AbortError('Cannot split on the <body> tag')
tree1, tree2 = do_split(split_point, container.log, before=before)
root1, root2 = tree1.getroot(), tree2.getroot()
anchors_in_top = frozenset(root1.xpath('//*/@id')) | frozenset(root1.xpath('//*/@name')) | {''}
anchors_in_bottom = frozenset(root2.xpath('//*/@id')) | frozenset(root2.xpath('//*/@name'))
base, ext = name.rpartition('.')[0::2]
base = re.sub(r'_split\d+$', '', base)
nname, s = None, 0 while not nname or container.exists(nname):
s += 1 nname = f'{base}_split{s}.{ext}'
manifest_item = container.generate_item(nname, media_type=container.mime_map[name])
bottom_name = container.href_to_name(manifest_item.get('href'), container.opf_name)

# Fix links in the split trees for r in (root1, root2):
for a in r.xpath('//*[@href]'):
url = a.get('href')
if url.startswith('#'):
fname = name else:
fname = container.href_to_name(url, name)
if fname == name:
purl = urlparse(url)
if purl.fragment in anchors_in_top:
if r is root2:
a.set('href', f'{container.name_to_href(name, bottom_name)}#{purl.fragment}')
else:
a.set('href', '#' + purl.fragment)
elif purl.fragment in anchors_in_bottom:
if r is root1:
a.set('href', f'{container.name_to_href(bottom_name, name)}#{purl.fragment}')
else:
a.set('href', '#' + purl.fragment)

# Fix all links in the container that point to anchors in the bottom tree for fname, media_type in iteritems(container.mime_map):
if fname not in {name, bottom_name}:
repl = SplitLinkReplacer(fname, anchors_in_bottom, name, bottom_name, container)
container.replace_links(fname, repl)

container.replace(name, root1)
container.replace(bottom_name, root2)

spine = container.opf_xpath('//opf:spine')[0]
for spine_item, spine_name, linear in container.spine_iter:
if spine_name == name:
break index = spine.index(spine_item) + 1 si = spine.makeelement(OPF('itemref'), idref=manifest_item.get('id'))
if not linear:
si.set('linear', 'no')
container.insert_into_xml(spine, si, index=index)
container.dirty(container.opf_name)
return bottom_name

[документация]
def multisplit(container, name, xpath, before=True):
'''
Split the specified file at multiple locations (all tags that match the specified XPath expression). See also: :func:`split`.
Splitting automatically migrates all links and references to the affected files.

:param before: If True the splits occur before the identified element otherwise after it.
'''
root = container.parsed(name)
nodes = root.xpath(xpath, namespaces=XPNSMAP)
if not nodes:
raise AbortError(_('The expression %s did not match any nodes') % xpath)
for split_point in nodes:
if in_table(split_point):
raise AbortError('Cannot split inside tables')
if split_point.tag.endswith('}body'):
raise AbortError('Cannot split on the <body> tag')

for i, tag in enumerate(nodes):
tag.set('calibre-split-point', str(i))

current = name all_names = [name]
for i in range(len(nodes)):
current = split(container, current, f'//*[@calibre-split-point="{i}"]', before=before)
all_names.append(current)

for x in all_names:
for tag in container.parsed(x).xpath('//*[@calibre-split-point]'):
tag.attrib.pop('calibre-split-point')
container.dirty(x)

return all_names[1:]

class MergeLinkReplacer:

def __init__(self, base, anchor_map, master, container):
self.container, self.anchor_map = container, anchor_map self.master = master self.base = base self.replaced = False def __call__(self, url):
if url and url.startswith('#'):
return url name = self.container.href_to_name(url, self.base)
amap = self.anchor_map.get(name, None)
if amap is None:
return url purl = urlparse(url)
frag = purl.fragment or ''
frag = amap.get(frag, frag)
url = self.container.name_to_href(self.master, self.base) + '#' + frag self.replaced = True return url def add_text(body, text):
if len(body) > 0:
body[-1].tail = (body[-1].tail or '') + text else:
body.text = (body.text or '') + text def all_anchors(root):
return set(root.xpath('//*/@id')) | set(root.xpath('//*/@name'))

def all_stylesheets(container, name):
for link in XPath('//h:head/h:link[@href]')(container.parsed(name)):
name = container.href_to_name(link.get('href'), name)
typ = link.get('type', 'text/css')
if typ == 'text/css':
yield name def unique_anchor(seen_anchors, current):
c = 0 ans = current while ans in seen_anchors:
c += 1 ans = f'{current}_{c}'
return ans def remove_name_attributes(root):
# Remove all name attributes, replacing them with id attributes for elem in root.xpath('//*[@id and @name]'):
del elem.attrib['name']
for elem in root.xpath('//*[@name]'):
elem.set('id', elem.attrib.pop('name'))

def merge_html(container, names, master, insert_page_breaks=False):
p = container.parsed root = p(master)

# Ensure master has a <head> head = root.find('h:head', namespaces=XPNSMAP)
if head is None:
head = root.makeelement(XHTML('head'))
container.insert_into_xml(root, head, 0)

seen_anchors = all_anchors(root)
seen_stylesheets = set(all_stylesheets(container, master))
master_body = p(master).findall('h:body', namespaces=XPNSMAP)[-1]
master_base = os.path.dirname(master)
anchor_map = {n:{} for n in names if n != master}
first_anchor_map = {}

for name in names:
if name == master:
continue
# Insert new stylesheets into master for sheet in all_stylesheets(container, name):
if sheet not in seen_stylesheets:
seen_stylesheets.add(sheet)
link = head.makeelement(XHTML('link'), rel='stylesheet', type='text/css', href=container.name_to_href(sheet, master))
container.insert_into_xml(head, link)

# Rebase links if master is in a different directory if os.path.dirname(name) != master_base:
container.replace_links(name, LinkRebaser(container, name, master))

root = p(name)
children = []
for body in p(name).findall('h:body', namespaces=XPNSMAP):
children.append(body.text if body.text and body.text.strip() else '\n\n')
children.extend(body)

first_child = ''
for first_child in children:
if not isinstance(first_child, string_or_bytes):
break if isinstance(first_child, string_or_bytes):
# body contained only text, no tags first_child = body.makeelement(XHTML('p'))
first_child.text, children[0] = children[0], first_child amap = anchor_map[name]
remove_name_attributes(root)

for elem in root.xpath('//*[@id]'):
val = elem.get('id')
if not val:
continue if val in seen_anchors:
nval = unique_anchor(seen_anchors, val)
elem.set('id', nval)
amap[val] = nval else:
seen_anchors.add(val)

if 'id' not in first_child.attrib:
first_child.set('id', unique_anchor(seen_anchors, 'top'))
seen_anchors.add(first_child.get('id'))
first_anchor_map[name] = first_child.get('id')

if insert_page_breaks:
first_child.set('style', first_child.get('style', '') + '; page-break-before: always')

amap[''] = first_child.get('id')

# Fix links that point to local changed anchors for a in XPath('//h:a[starts-with(@href, "#")]')(root):
q = a.get('href')[1:]
if q in amap:
a.set('href', '#' + amap[q])

for child in children:
if isinstance(child, string_or_bytes):
add_text(master_body, child)
else:
master_body.append(copy.deepcopy(child))

container.remove_item(name, remove_from_guide=False)

# Fix all links in the container that point to merged files for fname, media_type in iteritems(container.mime_map):
repl = MergeLinkReplacer(fname, anchor_map, master, container)
container.replace_links(fname, repl)

return first_anchor_map def merge_css(container, names, master):
p = container.parsed msheet = p(master)
master_base = os.path.dirname(master)
merged = set()

for name in names:
if name == master:
continue
# Rebase links if master is in a different directory if os.path.dirname(name) != master_base:
container.replace_links(name, LinkRebaser(container, name, master))

sheet = p(name)

# Remove charset rules cr = [r for r in sheet.cssRules if r.type == r.CHARSET_RULE]
[sheet.deleteRule(sheet.cssRules.index(r)) for r in cr]
for rule in sheet.cssRules:
msheet.add(rule)

container.remove_item(name)
merged.add(name)

# Remove links to merged stylesheets in the html files, replacing with a
# link to the master sheet for name, mt in iteritems(container.mime_map):
if mt in OEB_DOCS:
removed = False root = p(name)
for link in XPath('//h:link[@href]')(root):
q = container.href_to_name(link.get('href'), name)
if q in merged:
container.remove_from_xml(link)
removed = True if removed:
container.dirty(name)
if removed and master not in set(all_stylesheets(container, name)):
head = root.find('h:head', namespaces=XPNSMAP)
if head is not None:
link = head.makeelement(XHTML('link'), type='text/css', rel='stylesheet', href=container.name_to_href(master, name))
container.insert_into_xml(head, link)

[документация]
def merge(container, category, names, master):
'''
Merge the specified files into a single file, automatically migrating all links and references to the affected files. The file must all either be HTML or CSS files.

:param category: Must be either ``'text'`` for HTML files or ``'styles'`` for CSS files
:param names: The list of files to be merged
:param master: Which of the merged files is the *master* file, that is, the file that will remain after merging.
'''
if category not in {'text', 'styles'}:
raise AbortError(f'Cannot merge files of type: {category}')
if len(names) < 2:
raise AbortError('Must specify at least two files to be merged')
if master not in names:
raise AbortError(f'The master file ({master}) must be one of the files being merged')

if category == 'text':
merge_html(container, names, master)
elif category == 'styles':
merge_css(container, names, master)

container.dirty(master)

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.ebooks.oeb.polish.toc

Исходный код calibre.ebooks.oeb.polish.toc

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import re from collections import Counter, OrderedDict from functools import partial from operator import itemgetter from lxml import etree from lxml.builder import ElementMaker from calibre import __version__ from calibre.ebooks.oeb.base import EPUB_NS, NCX, NCX_NS, OEB_DOCS, XHTML, XHTML_NS, XML, XML_NS, XPath, serialize, uuid_id, xml2text from calibre.ebooks.oeb.polish.errors import MalformedMarkup from calibre.ebooks.oeb.polish.opf import get_book_language, set_guide_item from calibre.ebooks.oeb.polish.pretty import pretty_html_tree, pretty_xml_tree from calibre.ebooks.oeb.polish.utils import extract, guess_type from calibre.translations.dynamic import translate from calibre.utils.localization import canonicalize_lang, get_lang, lang_as_iso639_1 from calibre.utils.resources import get_path as P from polyglot.builtins import iteritems from polyglot.urllib import urlparse ns = etree.FunctionNamespace('calibre_xpath_extensions')
ns.prefix = 'calibre'
ns['lower-case'] = lambda c, x: x.lower() if hasattr(x, 'lower') else x class TOC:

toc_title = None def __init__(self, title=None, dest=None, frag=None):
self.title, self.dest, self.frag = title, dest, frag self.dest_exists = self.dest_error = None if self.title:
self.title = self.title.strip()
self.parent = None self.children = []
self.page_list = []

def add(self, title, dest, frag=None):
c = TOC(title, dest, frag)
self.children.append(c)
c.parent = self return c def remove(self, child):
self.children.remove(child)
child.parent = None def remove_from_parent(self):
if self.parent is None:
return idx = self.parent.children.index(self)
for child in reversed(self.children):
child.parent = self.parent self.parent.children.insert(idx, child)
self.parent.children.remove(self)
self.parent = None def __iter__(self):
yield from self.children def __len__(self):
return len(self.children)

def iterdescendants(self, level=None):
gc_level = None if level is None else level + 1 for child in self:
if level is None:
yield child else:
yield level, child yield from child.iterdescendants(level=gc_level)

def remove_duplicates(self, only_text=True):
seen = set()
remove = []
for child in self:
key = child.title if only_text else (child.title, child.dest, (child.frag or None))
if key in seen:
remove.append(child)
else:
seen.add(key)
child.remove_duplicates()
for child in remove:
self.remove(child)

@property def depth(self):
'''The maximum depth of the navigation tree rooted at this node.'''
try:
return max(node.depth for node in self) + 1 except ValueError:
return 1

@property def last_child(self):
return self.children[-1] if self.children else None def get_lines(self, lvl=0):
frag = ('#'+self.frag) if self.frag else ''
ans = [('\t'*lvl) + f'TOC: {self.title} --> {self.dest}{frag}']
for child in self:
ans.extend(child.get_lines(lvl+1))
return ans def __str__(self):
return '\n'.join(self.get_lines())

def to_dict(self, node_counter=None):
ans = {
'title':self.title, 'dest':self.dest, 'frag':self.frag,
'children':[c.to_dict(node_counter) for c in self.children]
}
if self.dest_exists is not None:
ans['dest_exists'] = self.dest_exists if self.dest_error is not None:
ans['dest_error'] = self.dest_error if node_counter is not None:
ans['id'] = next(node_counter)
return ans

@property def as_dict(self):
return self.to_dict()

def child_xpath(tag, name):
return tag.xpath(f'./*[calibre:lower-case(local-name()) = "{name}"]')

def add_from_navpoint(container, navpoint, parent, ncx_name):
dest = frag = text = None nl = child_xpath(navpoint, 'navlabel')
if nl:
nl = nl[0]
text = ''
for txt in child_xpath(nl, 'text'):
text += etree.tostring(txt, method='text', encoding='unicode', with_tail=False)
content = child_xpath(navpoint, 'content')
if content:
content = content[0]
href = content.get('src', None)
if href:
dest = container.href_to_name(href, base=ncx_name)
frag = urlparse(href).fragment or None return parent.add(text or None, dest or None, frag or None)

def process_ncx_node(container, node, toc_parent, ncx_name):
for navpoint in node.xpath('./*[calibre:lower-case(local-name()) = "navpoint"]'):
child = add_from_navpoint(container, navpoint, toc_parent, ncx_name)
if child is not None:
process_ncx_node(container, navpoint, child, ncx_name)

def parse_ncx(container, ncx_name):
root = container.parsed(ncx_name)
toc_root = TOC()
navmaps = root.xpath('//*[calibre:lower-case(local-name()) = "navmap"]')
if navmaps:
process_ncx_node(container, navmaps[0], toc_root, ncx_name)
toc_root.lang = toc_root.uid = None for attr, val in iteritems(root.attrib):
if attr.endswith('lang'):
toc_root.lang = str(val)
break for uid in root.xpath('//*[calibre:lower-case(local-name()) = "meta" and @name="dtb:uid"]/@content'):
if uid:
toc_root.uid = str(uid)
break for pl in root.xpath('//*[calibre:lower-case(local-name()) = "pagelist"]'):
for pt in pl.xpath('descendant::*[calibre:lower-case(local-name()) = "pagetarget"]'):
pagenum = pt.get('value')
if pagenum:
href = pt.xpath('descendant::*[calibre:lower-case(local-name()) = "content"]/@src')
if href:
dest = container.href_to_name(href[0], base=ncx_name)
frag = urlparse(href[0]).fragment or None toc_root.page_list.append({'dest': dest, 'pagenum': pagenum, 'frag': frag})
return toc_root def add_from_li(container, li, parent, nav_name):
dest = frag = text = None for x in li.iterchildren(XHTML('a'), XHTML('span')):
text = etree.tostring(x, method='text', encoding='unicode', with_tail=False).strip() or ' '.join(x.xpath('descendant-or-self::*/@title')).strip()
href = x.get('href')
if href:
dest = nav_name if href.startswith('#') else container.href_to_name(href, base=nav_name)
frag = urlparse(href).fragment or None break return parent.add(text or None, dest or None, frag or None)

def first_child(parent, tagname):
try:
return next(parent.iterchildren(tagname))
except StopIteration:
return None def process_nav_node(container, node, toc_parent, nav_name):
for li in node.iterchildren(XHTML('li')):
child = add_from_li(container, li, toc_parent, nav_name)
ol = first_child(li, XHTML('ol'))
if child is not None and ol is not None:
process_nav_node(container, ol, child, nav_name)

def parse_nav(container, nav_name):
root = container.parsed(nav_name)
toc_root = TOC()
toc_root.lang = toc_root.uid = None seen_toc = seen_pagelist = False et = f'{{{EPUB_NS}}}type'
for nav in XPath('descendant::h:nav[@epub:type]')(root):
nt = nav.get(et)
if nt == 'toc' and not seen_toc:
ol = first_child(nav, XHTML('ol'))
if ol is not None:
seen_toc = True process_nav_node(container, ol, toc_root, nav_name)
for h in nav.iterchildren(*map(XHTML, 'h1 h2 h3 h4 h5 h6'.split())):
text = etree.tostring(h, method='text', encoding='unicode', with_tail=False) or h.get('title')
if text:
toc_root.toc_title = text break elif nt == 'page-list' and not seen_pagelist:
ol = first_child(nav, XHTML('ol'))
if ol is not None and not seen_pagelist:
seen_pagelist = True for li in ol.iterchildren(XHTML('li')):
for a in li.iterchildren(XHTML('a')):
href = a.get('href')
if href:
text = (etree.tostring(a, method='text', encoding='unicode', with_tail=False) or a.get('title')).strip()
if text:
dest = nav_name if href.startswith('#') else container.href_to_name(href, base=nav_name)
frag = urlparse(href).fragment or None toc_root.page_list.append({'dest': dest, 'pagenum': text, 'frag': frag})
return toc_root def verify_toc_destinations(container, toc):
anchor_map = {}
anchor_xpath = XPath('//*/@id|//h:a/@name')
for item in toc.iterdescendants():
name = item.dest if not name:
item.dest_exists = False item.dest_error = _('No file named %s exists')%name continue try:
root = container.parsed(name)
except KeyError:
item.dest_exists = False item.dest_error = _('No file named %s exists')%name continue if not hasattr(root, 'xpath'):
item.dest_exists = False item.dest_error = _('No HTML file named %s exists')%name continue if not item.frag:
item.dest_exists = True continue if name not in anchor_map:
anchor_map[name] = frozenset(anchor_xpath(root))
item.dest_exists = item.frag in anchor_map[name]
if not item.dest_exists:
item.dest_error = _(
'The anchor %(a)s does not exist in file %(f)s')%dict( a=item.frag, f=name)

def find_existing_ncx_toc(container):
toc = container.opf_xpath('//opf:spine/@toc')
if toc:
toc = container.manifest_id_map.get(toc[0], None)
if not toc:
ncx = guess_type('a.ncx')
toc = container.manifest_type_map.get(ncx, [None])[0]
return toc or None def find_existing_nav_toc(container):
for name in container.manifest_items_with_property('nav'):
return name def mark_as_nav(container, name):
if container.opf_version_parsed.major > 2:
container.apply_unique_properties(name, 'nav')

def get_x_toc(container, find_toc, parse_toc, verify_destinations=True):
def empty_toc():
ans = TOC()
ans.lang = ans.uid = None return ans toc = find_toc(container)
ans = empty_toc() if toc is None or not container.has_name(toc) else parse_toc(container, toc)
ans.toc_file_name = toc if toc and container.has_name(toc) else None if verify_destinations:
verify_toc_destinations(container, ans)
return ans def get_toc(container, verify_destinations=True):
ver = container.opf_version_parsed if ver.major < 3:
return get_x_toc(container, find_existing_ncx_toc, parse_ncx, verify_destinations=verify_destinations)
else:
ans = get_x_toc(container, find_existing_nav_toc, parse_nav, verify_destinations=verify_destinations)
if len(ans) == 0:
ans = get_x_toc(container, find_existing_ncx_toc, parse_ncx, verify_destinations=verify_destinations)
return ans def get_guide_landmarks(container):
for ref in container.opf_xpath('./opf:guide/opf:reference'):
href, title, rtype = ref.get('href'), ref.get('title'), ref.get('type')
href, frag = href.partition('#')[::2]
name = container.href_to_name(href, container.opf_name)
if container.has_name(name):
yield {'dest':name, 'frag':frag, 'title':title or '', 'type':rtype or ''}

def get_nav_landmarks(container):
nav = find_existing_nav_toc(container)
if nav and container.has_name(nav):
root = container.parsed(nav)
et = f'{{{EPUB_NS}}}type'
for elem in root.iterdescendants(XHTML('nav')):
if elem.get(et) == 'landmarks':
for li in elem.iterdescendants(XHTML('li')):
for a in li.iterdescendants(XHTML('a')):
href, rtype = a.get('href'), a.get(et)
if href:
title = etree.tostring(a, method='text', encoding='unicode', with_tail=False).strip()
href, frag = href.partition('#')[::2]
name = container.href_to_name(href, nav)
if container.has_name(name):
yield {'dest':name, 'frag':frag, 'title':title or '', 'type':rtype or ''}
break def get_landmarks(container):
ver = container.opf_version_parsed if ver.major < 3:
return list(get_guide_landmarks(container))
ans = list(get_nav_landmarks(container))
if len(ans) == 0:
ans = list(get_guide_landmarks(container))
return ans def ensure_id(elem, all_ids):
elem_id = elem.get('id')
if elem_id:
return False, elem_id if elem.tag == XHTML('a'):
anchor = elem.get('name', None)
if anchor:
elem.set('id', anchor)
return False, anchor c = 0 while True:
c += 1 q = f'toc_{c}'
if q not in all_ids:
elem.set('id', q)
all_ids.add(q)
break return True, elem.get('id')

def elem_to_toc_text(elem, prefer_title=False):
text = xml2text(elem).strip()
if prefer_title:
text = elem.get('title', '').strip() or text if not text:
text = elem.get('title', '')
if not text:
text = elem.get('alt', '')
text = re.sub(r'\s+', ' ', text.strip())
text = text[:1000].strip()
if not text:
text = _('(Untitled)')
return text def item_at_top(elem):
try:
body = XPath('//h:body')(elem.getroottree().getroot())[0]
except (TypeError, IndexError, KeyError, AttributeError):
return False tree = body.getroottree()
path = tree.getpath(elem)
for el in body.iterdescendants(etree.Element):
epath = tree.getpath(el)
if epath == path:
break try:
if el.tag.endswith('}img') or (el.text and el.text.strip()):
return False except Exception:
return False if not path.startswith(epath):
# Only check tail of non-parent elements if el.tail and el.tail.strip():
return False return True

[документация]
def from_xpaths(container, xpaths, prefer_title=False):
'''
Generate a Table of Contents from a list of XPath expressions. Each expression in the list corresponds to a level of the generate ToC. For example: :code:`['//h:h1', '//h:h2', '//h:h3']` will generate a three level
Table of Contents from the ``<h1>``, ``<h2>`` and ``<h3>`` tags.
'''
tocroot = TOC()
xpaths = [XPath(xp) for xp in xpaths]

# Find those levels that have no elements in all spine items maps = OrderedDict()
empty_levels = {i+1 for i, xp in enumerate(xpaths)}
for spinepath in container.spine_items:
name = container.abspath_to_name(spinepath)
root = container.parsed(name)
level_item_map = maps[name] = {i+1:frozenset(xp(root)) for i, xp in enumerate(xpaths)}
for lvl, elems in iteritems(level_item_map):
if elems:
empty_levels.discard(lvl)
# Remove empty levels from all level_maps if empty_levels:
for name, lmap in tuple(iteritems(maps)):
lmap = {lvl:items for lvl, items in iteritems(lmap) if lvl not in empty_levels}
lmap = sorted(iteritems(lmap), key=itemgetter(0))
lmap = {i+1:items for i, (l, items) in enumerate(lmap)}
maps[name] = lmap node_level_map = {tocroot: 0}

def parent_for_level(child_level):
limit = child_level - 1 def process_node(node):
child = node.last_child if child is None:
return node lvl = node_level_map[child]
return node if lvl > limit else child if lvl == limit else process_node(child)

return process_node(tocroot)

for name, level_item_map in iteritems(maps):
root = container.parsed(name)
item_level_map = {e:i for i, elems in iteritems(level_item_map) for e in elems}
item_dirtied = False all_ids = set(root.xpath('//*/@id'))

for item in root.iterdescendants(etree.Element):
lvl = item_level_map.get(item, None)
if lvl is None:
continue text = elem_to_toc_text(item, prefer_title)
parent = parent_for_level(lvl)
if item_at_top(item):
dirtied, elem_id = False, None else:
dirtied, elem_id = ensure_id(item, all_ids)
item_dirtied = dirtied or item_dirtied toc = parent.add(text, name, elem_id)
node_level_map[toc] = lvl toc.dest_exists = True if item_dirtied:
container.commit_item(name, keep_parsed=True)

return tocroot[документация]
def from_links(container):
'''
Generate a Table of Contents from links in the book.
'''
toc = TOC()
link_path = XPath('//h:a[@href]')
seen_titles, seen_dests = set(), set()
for name, is_linear in container.spine_names:
root = container.parsed(name)
for a in link_path(root):
href = a.get('href')
if not href or not href.strip():
continue frag = None if href.startswith('#'):
dest = name frag = href[1:]
else:
href, _, frag = href.partition('#')
dest = container.href_to_name(href, base=name)
frag = frag or None if (dest, frag) in seen_dests:
continue seen_dests.add((dest, frag))
text = elem_to_toc_text(a)
if text in seen_titles:
continue seen_titles.add(text)
toc.add(text, dest, frag=frag)
verify_toc_destinations(container, toc)
for child in toc:
if not child.dest_exists:
toc.remove(child)
return tocdef find_text(node):
LIMIT = 200 pat = re.compile(r'\s+')
for child in node:
if isinstance(child, etree._Element):
text = xml2text(child).strip()
text = pat.sub(' ', text)
if len(text) < 1:
continue if len(text) > LIMIT:
# Look for less text in a child of this node, recursively ntext = find_text(child)
return ntext or (text[:LIMIT] + '...')
else:
return text

[документация]
def from_files(container):
'''
Generate a Table of Contents from files in the book.
'''
toc = TOC()
for i, spinepath in enumerate(container.spine_items):
name = container.abspath_to_name(spinepath)
root = container.parsed(name)
body = XPath('//h:body')(root)
if not body:
continue text = find_text(body[0])
if not text:
text = name.rpartition('/')[-1]
if i == 0 and text.rpartition('.')[0].lower() in {'titlepage', 'cover'}:
text = _('Cover')
toc.add(text, name)
return toc
def node_from_loc(root, locs, totals=None):
node = root.xpath('//*[local-name()="body"]')[0]
for i, loc in enumerate(locs):
children = tuple(node.iterchildren(etree.Element))
if totals is not None and totals[i] != len(children):
raise MalformedMarkup()
node = children[loc]
return node def add_id(container, name, loc, totals=None):
root = container.parsed(name)
try:
node = node_from_loc(root, loc, totals=totals)
except MalformedMarkup:
# The webkit HTML parser and the container parser have yielded
# different node counts, this can happen if the file is valid XML
# but contains constructs like nested <p> tags. So force parse it
# with the HTML 5 parser and try again.
raw = container.raw_data(name)
root = container.parse_xhtml(raw, fname=name, force_html5_parse=True)
try:
node = node_from_loc(root, loc, totals=totals)
except MalformedMarkup:
raise MalformedMarkup(_('The file %s has malformed markup. Try running the Fix HTML tool'
' before editing.') % name)
container.replace(name, root)

if not node.get('id'):
ensure_id(node, set(root.xpath('//*/@id')))
container.commit_item(name, keep_parsed=True)
return node.get('id')

def create_ncx(toc, to_href, btitle, lang, uid):
lang = lang.replace('_', '-')
ncx = etree.Element(NCX('ncx'), attrib={'version': '2005-1', XML('lang'): lang}, nsmap={None: NCX_NS})
head = etree.SubElement(ncx, NCX('head'))
etree.SubElement(head, NCX('meta'), name='dtb:uid', content=str(uid))
etree.SubElement(head, NCX('meta'), name='dtb:depth', content=str(toc.depth))
generator = ''.join(['calibre (', __version__, ')'])
etree.SubElement(head, NCX('meta'), name='dtb:generator', content=generator)
etree.SubElement(head, NCX('meta'), name='dtb:totalPageCount', content='0')
etree.SubElement(head, NCX('meta'), name='dtb:maxPageNumber', content='0')
title = etree.SubElement(ncx, NCX('docTitle'))
text = etree.SubElement(title, NCX('text'))
text.text = btitle navmap = etree.SubElement(ncx, NCX('navMap'))
spat = re.compile(r'\s+')

play_order = Counter()

def process_node(xml_parent, toc_parent):
for child in toc_parent:
play_order['c'] += 1 point = etree.SubElement(xml_parent, NCX('navPoint'), id=f"num_{play_order['c']}", playOrder=str(play_order['c']))
label = etree.SubElement(point, NCX('navLabel'))
title = child.title if title:
title = spat.sub(' ', title)
etree.SubElement(label, NCX('text')).text = title if child.dest:
href = to_href(child.dest)
if child.frag:
href += '#'+child.frag etree.SubElement(point, NCX('content'), src=href)
process_node(point, child)

process_node(navmap, toc)
return ncx def commit_ncx_toc(container, toc, lang=None, uid=None):
tocname = find_existing_ncx_toc(container)
if tocname is None:
item = container.generate_item('toc.ncx', id_prefix='toc')
tocname = container.href_to_name(item.get('href'), base=container.opf_name)
ncx_id = item.get('id')
[s.set('toc', ncx_id) for s in container.opf_xpath('//opf:spine')]
if not lang:
lang = get_lang()
for l in container.opf_xpath('//dc:language'):
l = canonicalize_lang(xml2text(l).strip())
if l:
lang = l lang = lang_as_iso639_1(l) or l break lang = lang_as_iso639_1(lang) or lang if not uid:
uid = uuid_id()
eid = container.opf.get('unique-identifier', None)
if eid:
m = container.opf_xpath(f'//*[@id="{eid}"]')
if m:
uid = xml2text(m[0])

title = _('Table of Contents')
m = container.opf_xpath('//dc:title')
if m:
x = xml2text(m[0]).strip()
title = x or title to_href = partial(container.name_to_href, base=tocname)
root = create_ncx(toc, to_href, title, lang, uid)
container.replace(tocname, root)
container.pretty_print.add(tocname)

def ensure_single_nav_of_type(root, ntype='toc'):
et = f'{{{EPUB_NS}}}type'
navs = [n for n in root.iterdescendants(XHTML('nav')) if n.get(et) == ntype]
for x in navs[1:]:
extract(x)
if navs:
nav = navs[0]
tail = nav.tail attrib = dict(nav.attrib)
nav.clear()
nav.attrib.update(attrib)
nav.tail = tail else:
nav = root.makeelement(XHTML('nav'))
first_child(root, XHTML('body')).append(nav)
nav.set(f'{{{EPUB_NS}}}type', ntype)
return nav def ensure_container_has_nav(container, lang=None, previous_nav=None):
tocname = find_existing_nav_toc(container)
if previous_nav is not None:
nav_name = container.href_to_name(previous_nav[0])
if nav_name and container.exists(nav_name):
tocname = nav_name container.apply_unique_properties(tocname, 'nav')
if tocname is None:
item = container.generate_item('nav.xhtml', id_prefix='nav')
item.set('properties', 'nav')
tocname = container.href_to_name(item.get('href'), base=container.opf_name)
if previous_nav is not None:
root = previous_nav[1]
else:
root = container.parse_xhtml(P('templates/new_nav.html', data=True).decode('utf-8'))
container.replace(tocname, root)
else:
root = container.parsed(tocname)
if lang:
lang = lang_as_iso639_1(lang) or lang root.set('lang', lang)
root.set(f'{{{XML_NS}}}lang', lang)
return tocname, root def collapse_li(parent):
for li in parent.iterdescendants(XHTML('li')):
if len(li) == 1:
li.text = None li[0].tail = None def create_nav_li(container, ol, entry, tocname):
li = ol.makeelement(XHTML('li'))
ol.append(li)
a = li.makeelement(XHTML('a'))
li.append(a)
href = container.name_to_href(entry['dest'], tocname)
if entry['frag']:
href += '#' + entry['frag']
a.set('href', href)
return a def set_landmarks(container, root, tocname, landmarks):
nav = ensure_single_nav_of_type(root, 'landmarks')
nav.set('hidden', '')
ol = nav.makeelement(XHTML('ol'))
nav.append(ol)
for entry in landmarks:
if entry['type'] and container.has_name(entry['dest']) and container.mime_map[entry['dest']] in OEB_DOCS:
a = create_nav_li(container, ol, entry, tocname)
a.set(f'{{{EPUB_NS}}}type', entry['type'])
a.text = entry['title'] or None pretty_xml_tree(nav)
collapse_li(nav)

def commit_nav_toc(container, toc, lang=None, landmarks=None, previous_nav=None):
tocname, root = ensure_container_has_nav(container, lang=lang, previous_nav=previous_nav)
nav = ensure_single_nav_of_type(root, 'toc')
if toc.toc_title:
nav.append(nav.makeelement(XHTML('h1')))
nav[-1].text = toc.toc_title rnode = nav.makeelement(XHTML('ol'))
nav.append(rnode)
to_href = partial(container.name_to_href, base=tocname)
spat = re.compile(r'\s+')

def process_node(xml_parent, toc_parent):
for child in toc_parent:
li = xml_parent.makeelement(XHTML('li'))
xml_parent.append(li)
title = child.title or ''
title = spat.sub(' ', title).strip()
a = li.makeelement(XHTML('a' if child.dest else 'span'))
a.text = title li.append(a)
if child.dest:
href = to_href(child.dest)
if child.frag:
href += '#'+child.frag a.set('href', href)
if len(child):
ol = li.makeelement(XHTML('ol'))
li.append(ol)
process_node(ol, child)
process_node(rnode, toc)
pretty_xml_tree(nav)

collapse_li(nav)
nav.tail = '\n'

if toc.page_list:
nav = ensure_single_nav_of_type(root, 'page-list')
nav.set('hidden', '')
ol = nav.makeelement(XHTML('ol'))
nav.append(ol)
for entry in toc.page_list:
if container.has_name(entry['dest']) and container.mime_map[entry['dest']] in OEB_DOCS:
a = create_nav_li(container, ol, entry, tocname)
a.text = str(entry['pagenum'])
pretty_xml_tree(nav)
collapse_li(nav)
container.replace(tocname, root)

def commit_toc(container, toc, lang=None, uid=None):
commit_ncx_toc(container, toc, lang=lang, uid=uid)
if container.opf_version_parsed.major > 2:
commit_nav_toc(container, toc, lang=lang)

def remove_names_from_toc(container, names):
changed = []
names = frozenset(names)
for find_toc, parse_toc, commit_toc in (
(find_existing_ncx_toc, parse_ncx, commit_ncx_toc),
(find_existing_nav_toc, parse_nav, commit_nav_toc),
):
toc = get_x_toc(container, find_toc, parse_toc, verify_destinations=False)
if len(toc) > 0:
remove = []
for node in toc.iterdescendants():
if node.dest in names:
remove.append(node)
if remove:
for node in reversed(remove):
node.remove_from_parent()
commit_toc(container, toc)
changed.append(find_toc(container))
return changed def find_inline_toc(container):
for name, linear in container.spine_names:
if container.parsed(name).xpath('//*[local-name()="body" and @id="calibre_generated_inline_toc"]'):
return name def toc_to_html(toc, container, toc_name, title, lang=None):

def process_node(html_parent, toc, level=1, indent=' ', style_level=2):
li = html_parent.makeelement(XHTML('li'))
li.tail = '\n'+ (indent*level)
html_parent.append(li)
name, frag = toc.dest, toc.frag href = '#'
if name:
href = container.name_to_href(name, toc_name)
if frag:
href += '#' + frag a = li.makeelement(XHTML('a'), href=href)
a.text = toc.title li.append(a)
if len(toc) > 0:
parent = li.makeelement(XHTML('ul'))
parent.set('class', f'level{style_level}')
li.append(parent)
a.tail = '\n\n' + (indent*(level+2))
parent.text = '\n'+(indent*(level+3))
parent.tail = '\n\n' + (indent*(level+1))
for child in toc:
process_node(parent, child, level+3, style_level=style_level + 1)
parent[-1].tail = '\n' + (indent*(level+2))

E = ElementMaker(namespace=XHTML_NS, nsmap={None:XHTML_NS})
html = E.html(
E.head(
E.title(title),
E.style(P('templates/inline_toc_styles.css', data=True).decode('utf-8'), type='text/css'),
),
E.body(
E.h2(title),
E.ul(), id='calibre_generated_inline_toc',
)
)

ul = html[1][1]
ul.set('class', 'level1')
for child in toc:
process_node(ul, child)
if lang:
html.set('lang', lang)
pretty_html_tree(container, html)
return html

[документация]
def create_inline_toc(container, title=None):
'''
Create an inline (HTML) Table of Contents from an existing NCX Table of Contents.

:param title: The title for this table of contents.
'''
lang = get_book_language(container)
default_title = 'Table of Contents'
if lang:
lang = lang_as_iso639_1(lang) or lang default_title = translate(lang, default_title)
title = title or default_title toc = get_toc(container)
if len(toc) == 0:
return None toc_name = find_inline_toc(container)

name = toc_name html = toc_to_html(toc, container, name, title, lang)
raw = serialize(html, 'text/html')
if name is None:
name, c = 'toc.xhtml', 0 while container.has_name(name):
c += 1 name = f'toc{c}.xhtml'
container.add_file(name, raw, spine_index=0)
else:
with container.open(name, 'wb') as f:
f.write(raw)
set_guide_item(container, 'toc', title, name, frag='calibre_generated_inline_toc')
return name

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.gui2.actions

Исходный код calibre.gui2.actions

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

from functools import partial from zipfile import ZipFile from qt.core import QAction, QIcon, QKeySequence, QMenu, QObject, QPoint, QTimer, QToolButton from calibre import prints from calibre.constants import ismacos from calibre.gui2 import Dispatcher from calibre.gui2.keyboard import NameConflict from polyglot.builtins import string_or_bytes def toolbar_widgets_for_action(gui, action):
# Search the toolbars for the widget associated with an action, passing
# them to the caller for further processing for x in gui.bars_manager.bars:
try:
w = x.widgetForAction(action)
# It seems that multiple copies of the action can exist, such as
# when the device-connected menu is changed while the device is
# connected. Use the one that has an actual position.
if w is None or w.pos().x() == 0:
continue
# The button might be hidden if not w.isVisible():
continue yield w except Exception:
continue def show_menu_under_widget(gui, menu, action, name):
# First try the tool bar for w in toolbar_widgets_for_action(gui, action):
try:
# The w.height() assures that the menu opens below the button.
menu.exec(w.mapToGlobal(QPoint(0, w.height())))
return except Exception:
continue
# Now try the menu bar for x in gui.bars_manager.menu_bar.added_actions:
# This depends on no two menus with the same name.
# I don't know if this works on a Mac if x.text() == name:
try:
# The menu item might be hidden if not x.isVisible():
continue
# We can't use x.trigger() because it doesn't put the menu
# in the right place. Instead get the position of the menu
# widget on the menu bar p = x.parent().menu_bar r = p.actionGeometry(x)
# Make sure that the menu item is actually displayed in the menu
# and not the overflow if p.geometry().width() < (r.x() + r.width()):
continue
# Show the menu under the name in the menu bar menu.exec(p.mapToGlobal(QPoint(r.x()+2, r.height()-2)))
return except Exception:
continue
# Is it one of the status bar buttons?
for button in gui.status_bar_extra_buttons:
if name == button.action_name and button.isVisible():
r = button.geometry()
p = gui.status_bar menu.exec(p.mapToGlobal(QPoint(r.x()+2, r.height()-2)))
return
# No visible button found. Fall back to displaying in upper left corner
# of the library view.
menu.exec(gui.library_view.mapToGlobal(QPoint(10, 10)))

def menu_action_unique_name(plugin, unique_name):
return f'{plugin.unique_name} : menu action : {unique_name}'

[документация]
class InterfaceAction(QObject):
'''
A plugin representing an "action" that can be taken in the graphical user interface. All the items in the toolbar and context menus are implemented by these plugins.

Note that this class is the base class for these plugins, however, to integrate the plugin with calibre's plugin system, you have to make a wrapper class that references the actual plugin. See the
:mod:`calibre.customize.builtins` module for examples.

If two :class:`InterfaceAction` objects have the same name, the one with higher priority takes precedence.

Sub-classes should implement the :meth:`genesis`, :meth:`library_changed`,
:meth:`location_selected`, :meth:`shutting_down`,
:meth:`initialization_complete` and :meth:`tag_browser_context_action` methods.

Once initialized, this plugin has access to the main calibre GUI via the
:attr:`gui` member. You can access other plugins by name, for example::

self.gui.iactions['Save To Disk']

To access the actual plugin, use the :attr:`interface_action_base_plugin` attribute, this attribute only becomes available after the plugin has been initialized. Useful if you want to use methods from the plugin class like do_user_config().

The QAction specified by :attr:`action_spec` is automatically create and made available as ``self.qaction``.

'''

#: The plugin name. If two plugins with the same name are present, the one
#: with higher priority takes precedence.
name = 'Implement me'

#: The plugin priority. If two plugins with the same name are present, the one
#: with higher priority takes precedence.
priority = 1

#: The menu popup type for when this plugin is added to a toolbar popup_type = QToolButton.ToolButtonPopupMode.MenuButtonPopup

#: Whether this action should be auto repeated when its shortcut
#: key is held down.
auto_repeat = False

#: Of the form: (text, icon_path, tooltip, keyboard shortcut).
#: icon, tooltip and keyboard shortcut can be None.
#: keyboard shortcut must be either a string, None or tuple of shortcuts.
#: If None, a keyboard shortcut corresponding to the action is not
#: registered. If you pass an empty tuple, then the shortcut is registered
#: with no default key binding.
action_spec = ('text', 'icon', None, None)

#: If not None, used for the name displayed to the user when customizing
#: the keyboard shortcuts for the above action spec instead of action_spec[0]
action_shortcut_name = None

#: If True, a menu is automatically created and added to self.qaction action_add_menu = False

#: If True, a clone of self.qaction is added to the menu of self.qaction
#: If you want the text of this action to be different from that of
#: self.qaction, set this variable to the new text action_menu_clone_qaction = False

#: Set of locations to which this action must not be added.
#: See :attr:`all_locations` for a list of possible locations dont_add_to = frozenset()

#: Set of locations from which this action must not be removed.
#: See :attr:`all_locations` for a list of possible locations dont_remove_from = frozenset()

all_locations = frozenset(['toolbar', 'toolbar-device', 'context-menu',
'context-menu-device', 'toolbar-child', 'menubar', 'menubar-device',
'context-menu-cover-browser', 'context-menu-split', 'searchbar'])

#: Type of action
#: 'current' means acts on the current view
#: 'global' means an action that does not act on the current view, but rather
#: on calibre as a whole action_type = 'global'

#: If True, then this InterfaceAction will have the opportunity to interact
#: with drag and drop events. See the methods, :meth:`accept_enter_event`,
#: :meth`:accept_drag_move_event`, :meth:`drop_event` for details.
accepts_drops = False def __init__(self, parent, site_customization):
QObject.__init__(self, parent)
self.setObjectName(self.name)
self.gui = parent self.site_customization = site_customization self.interface_action_base_plugin = None

[документация]
def accept_enter_event(self, event, mime_data):
''' This method should return True iff this interface action is capable of handling the drag event. Do not call accept/ignore on the event, that will be taken care of by the calibre UI.'''
return False

[документация]
def accept_drag_move_event(self, event, mime_data):
''' This method should return True iff this interface action is capable of handling the drag event. Do not call accept/ignore on the event, that will be taken care of by the calibre UI.'''
return False

[документация]
def drop_event(self, event, mime_data):
''' This method should perform some useful action and return True iff this interface action is capable of handling the drop event. Do not call accept/ignore on the event, that will be taken care of by the calibre UI. You should not perform blocking/long operations in this function. Instead emit a signal or use QTimer.singleShot and return quickly. See the builtin actions for examples.'''
return False
def do_genesis(self):
self.Dispatcher = partial(Dispatcher, parent=self)
self.create_action()
self.gui.addAction(self.qaction)
self.gui.addAction(self.menuless_qaction)
self.genesis()
self.location_selected('library')

@property def unique_name(self):
bn = self.__class__.__name__ if getattr(self.interface_action_base_plugin, 'name'):
bn = self.interface_action_base_plugin.name return f'Interface Action: {bn} ({self.name})'

def create_action(self, spec=None, attr='qaction', shortcut_name=None, persist_shortcut=False):
if spec is None:
spec = self.action_spec text, icon, tooltip, shortcut = spec if icon is not None:
action = QAction(QIcon.ic(icon), text, self.gui)
else:
action = QAction(text, self.gui)
if attr == 'qaction':
if hasattr(self.action_menu_clone_qaction, 'rstrip'):
mt = str(self.action_menu_clone_qaction)
else:
mt = action.text()
self.menuless_qaction = ma = QAction(action.icon(), mt, self.gui)
ma.triggered.connect(action.trigger)
for a in ((action, ma) if attr == 'qaction' else (action,)):
a.setAutoRepeat(self.auto_repeat)
text = tooltip if tooltip else text a.setToolTip(text)
a.setStatusTip(text)
a.setWhatsThis(text)
shortcut_action = action desc = tooltip if tooltip else None if attr == 'qaction':
shortcut_action = ma if shortcut is not None:
keys = ((shortcut,) if isinstance(shortcut, string_or_bytes) else tuple(shortcut))
if shortcut_name is None:
if self.action_shortcut_name is not None:
shortcut_name = self.action_shortcut_name elif spec[0]:
shortcut_name = str(spec[0])
if shortcut_name and self.action_spec[0] and not ( attr == 'qaction' and self.popup_type == QToolButton.ToolButtonPopupMode.InstantPopup):
try:
self.gui.keyboard.register_shortcut(self.unique_name + ' - ' + attr, shortcut_name, default_keys=keys, action=shortcut_action, description=desc, group=self.action_spec[0], persist_shortcut=persist_shortcut)
except NameConflict as e:
try:
prints(str(e))
except Exception:
pass shortcut_action.setShortcuts([QKeySequence(key,
QKeySequence.SequenceFormat.PortableText) for key in keys])
else:
self.shortcut_action_for_context_menu = shortcut_action if ismacos:
# In Qt 5 keyboard shortcuts don't work unless the
# action is explicitly added to the main window self.gui.addAction(shortcut_action)

if attr is not None:
setattr(self, attr, action)
if attr == 'qaction' and self.action_add_menu:
menu = QMenu()
action.setMenu(menu)
if self.action_menu_clone_qaction:
menu.addAction(self.menuless_qaction)
return action

[документация]
def create_menu_action(self, menu, unique_name, text, icon=None, shortcut=None, description=None, triggered=None, shortcut_name=None, persist_shortcut=False):
'''
Convenience method to easily add actions to a QMenu.
Returns the created QAction. This action has one extra attribute calibre_shortcut_unique_name which if not None refers to the unique name under which this action is registered with the keyboard manager.

:param menu: The QMenu the newly created action will be added to
:param unique_name: A unique name for this action, this must be globally unique, so make it as descriptive as possible. If in doubt, add an UUID to it.
:param text: The text of the action.
:param icon: Either a QIcon or a file name. The file name is passed to the QIcon.ic() builtin, so you do not need to pass the full path to the images folder.
:param shortcut: A string, a list of strings, None or False. If False, no keyboard shortcut is registered for this action. If None, a keyboard shortcut with no default keybinding is registered. String and list of strings register a shortcut with default keybinding as specified.
:param description: A description for this action. Used to set tooltips.
:param triggered: A callable which is connected to the triggered signal of the created action.
:param shortcut_name: The text displayed to the user when customizing the keyboard shortcuts for this action. By default it is set to the value of ``text``.
:param persist_shortcut: Shortcuts for actions that don't always appear, or are library dependent, may disappear when other keyboard shortcuts are edited unless
```persist_shortcut``` is set True.

'''
if shortcut_name is None:
shortcut_name = str(text)
ac = menu.addAction(text)
if icon is not None:
if not isinstance(icon, QIcon):
icon = QIcon.ic(icon)
ac.setIcon(icon)
keys = ()
if shortcut is not None and shortcut is not False:
keys = ((shortcut,) if isinstance(shortcut, string_or_bytes) else tuple(shortcut))
unique_name = menu_action_unique_name(self, unique_name)
if description is not None:
ac.setToolTip(description)
ac.setStatusTip(description)
ac.setWhatsThis(description)

ac.calibre_shortcut_unique_name = unique_name if shortcut is not False:
self.gui.keyboard.register_shortcut(unique_name, shortcut_name, default_keys=keys, action=ac, description=description, group=self.action_spec[0], persist_shortcut=persist_shortcut)
# In Qt 5 keyboard shortcuts don't work unless the
# action is explicitly added to the main window and on OSX and
# Unity since the menu might be exported, the shortcuts won't work self.gui.addAction(ac)
if triggered is not None:
ac.triggered.connect(triggered)
return ac

[документация]
def load_resources(self, names):
'''
If this plugin comes in a ZIP file (user added plugin), this method will allow you to load resources from the ZIP file.

For example to load an image::

pixmap = QPixmap()
pixmap.loadFromData(tuple(self.load_resources(['images/icon.png']).values())[0])
icon = QIcon(pixmap)

:param names: List of paths to resources in the ZIP file using / as separator

:return: A dictionary of the form ``{name : file_contents}``. Any names that were not found in the ZIP file will not be present in the dictionary.

'''
if self.plugin_path is None:
raise ValueError('This plugin was not loaded from a ZIP file')
ans = {}
with ZipFile(self.plugin_path, 'r') as zf:
for candidate in zf.namelist():
if candidate in names:
ans[candidate] = zf.read(candidate)
return ans

[документация]
def genesis(self):
'''
Setup this plugin. Only called once during initialization. self.gui is available. The action specified by :attr:`action_spec` is available as
``self.qaction``.
'''
pass

[документация]
def location_selected(self, loc):
'''
Called whenever the book list being displayed in calibre changes.
Currently values for loc are: ``library, main, card and cardb``.

This method should enable/disable this action and its sub actions as appropriate for the location.
'''
pass

[документация]
def library_about_to_change(self, olddb, db):
'''
Called whenever the current library is changed.

:param olddb: The LibraryDatabase corresponding to the previous library.
:param db: The LibraryDatabase corresponding to the new library.

'''
pass

[документация]
def library_changed(self, db):
'''
Called whenever the current library is changed.

:param db: The LibraryDatabase corresponding to the current library.

'''
pass

[документация]
def gui_layout_complete(self):
'''
Called once per action when the layout of the main GUI is completed. If your action needs to make changes to the layout, they should be done here, rather than in :meth:`initialization_complete`.
'''
pass

[документация]
def initialization_complete(self):
'''
Called once per action when the initialization of the main GUI is completed.
'''
pass

[документация]
def tag_browser_context_action(self, index):
'''
Called when displaying the context menu in the Tag browser. ``index`` is the QModelIndex that points to the Tag browser item that was right clicked.
Test it for validity with index.valid() and get the underlying TagTreeItem object with index.data(Qt.ItemDataRole.UserRole). Any action objects yielded by this method will be added to the context menu.
'''
if False:
yield QAction()

[документация]
def shutting_down(self):
'''
Called once per plugin when the main GUI is in the process of shutting down. Release any used resources, but try not to block the shutdown for long periods of time.
'''
pass
class InterfaceActionWithLibraryDrop(InterfaceAction):
'''
Subclass of InterfaceAction that implements methods to execute the default action by drop some books from the library.

Inside the do_drop() method, the ids of the dropped books are provided by the attribute self.dropped_ids
'''

accepts_drops = True mimetype_for_drop = 'application/calibre+from_library'

def accept_enter_event(self, event, mime_data):
if mime_data.hasFormat(self.mimetype_for_drop):
return True return False def accept_drag_move_event(self, event, mime_data):
if mime_data.hasFormat(self.mimetype_for_drop):
return True return False def drop_event(self, event, mime_data):
if mime_data.hasFormat(self.mimetype_for_drop):
self.dropped_ids = tuple(map(int, mime_data.data(self.mimetype_for_drop).data().split()))
QTimer.singleShot(1, self.do_drop)
return True return False def do_drop(self):
raise NotImplementedError()

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.gui2.preferences

Исходный код calibre.gui2.preferences

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import textwrap from qt.core import (
QAbstractSpinBox,
QApplication,
QCheckBox,
QComboBox,
QDialog,
QDialogButtonBox,
QEvent,
QIcon,
QLineEdit,
QListView,
QListWidget,
Qt,
QTableWidget,
QVBoxLayout,
QWidget, pyqtSignal,
)

from calibre.customize.ui import preferences_plugins from calibre.gui2.complete2 import EditWithComplete from calibre.gui2.widgets import HistoryLineEdit from calibre.utils.config import ConfigProxy from polyglot.builtins import string_or_bytes class AbortCommit(Exception):
pass class AbortInitialize(Exception):
pass

[документация]
class ConfigWidgetInterface:
'''
This class defines the interface that all widgets displayed in the
Preferences dialog must implement. See :class:`ConfigWidgetBase` for a base class that implements this interface and defines various convenience methods as well.
'''

#: This signal must be emitted whenever the user changes a value in this
#: widget changed_signal = None

#: Set to True iff the :meth:`restore_to_defaults` method is implemented.
supports_restoring_to_defaults = True

#: The tooltip for the "Restore defaults" button restore_defaults_desc = _('Restore settings to default values. '
'You have to click Apply to actually save the default settings.')

#: If True the Preferences dialog will not allow the user to set any more
#: preferences. Only has effect if :meth:`commit` returns True.
restart_critical = False

[документация]
def genesis(self, gui):
'''
Called once before the widget is displayed, should perform any necessary setup.

:param gui: The main calibre graphical user interface
'''
raise NotImplementedError()

[документация]
def initialize(self):
'''
Should set all config values to their initial values (the values stored in the config files). A "return" statement is optional. Return
False if the dialog is not to be shown.
'''
raise NotImplementedError()

[документация]
def restore_defaults(self):
'''
Should set all config values to their defaults.
'''
pass

[документация]
def commit(self):
'''
Save any changed settings. Return True if the changes require a restart, False otherwise. Raise an :class:`AbortCommit` exception to indicate that an error occurred. You are responsible for giving the user feedback about what the error is and how to correct it.
'''
return False

[документация]
def refresh_gui(self, gui):
'''
Called once after this widget is committed. Responsible for causing the gui to reread any changed settings. Note that by default the GUI re-initializes various elements anyway, so most widgets won't need to use this method.
'''
pass

[документация]
def initial_tab_changed(self):
'''
Called if the initially displayed tab is changed before the widget is shown, but after it is initialized.
'''
pass
def do_on_child_tabs(self, method, *args):
r = False for t in self.child_tabs:
lazy_init_called = getattr(t, 'lazy_init_called', True)
if method in ('commit', 'refresh_gui') and not lazy_init_called:
continue if method == 'restore_defaults' and not lazy_init_called:
if hasattr(t, 'lazy_initialize'):
t.lazy_initialize()
t.lazy_init_called = True r = r | bool(getattr(t, method)(*args))
return rdef set_help_tips(gui_obj, tt):
if tt:
if not str(gui_obj.whatsThis()):
gui_obj.setWhatsThis(tt)
if not str(gui_obj.statusTip()):
gui_obj.setStatusTip(tt)
tt = '\n'.join(textwrap.wrap(tt, 70))
gui_obj.setToolTip(tt)

class Setting:

CHOICES_SEARCH_FLAGS = Qt.MatchFlag.MatchExactly | Qt.MatchFlag.MatchCaseSensitive def __init__(self, name, config_obj, widget, gui_name=None, empty_string_is_None=True, choices=None, restart_required=False):
self.name, self.gui_name = name, gui_name self.empty_string_is_None = empty_string_is_None self.restart_required = restart_required self.choices = choices if gui_name is None:
self.gui_name = 'opt_'+name self.config_obj = config_obj self.gui_obj = getattr(widget, self.gui_name)
self.widget = widget if isinstance(self.gui_obj, QCheckBox):
self.datatype = 'bool'
self.gui_obj.stateChanged.connect(self.changed)
elif isinstance(self.gui_obj, QAbstractSpinBox):
self.datatype = 'number'
self.gui_obj.valueChanged.connect(self.changed)
elif isinstance(self.gui_obj, (QLineEdit, HistoryLineEdit)):
self.datatype = 'string'
self.gui_obj.textChanged.connect(self.changed)
if isinstance(self.gui_obj, HistoryLineEdit):
self.gui_obj.initialize('preferences_setting_' + self.name)
elif isinstance(self.gui_obj, QComboBox):
self.datatype = 'choice'
self.gui_obj.editTextChanged.connect(self.changed)
self.gui_obj.currentIndexChanged.connect(self.changed)
else:
raise ValueError(f'Unknown data type {self.gui_obj.__class__}')

if isinstance(self.config_obj, ConfigProxy) and \ not str(self.gui_obj.toolTip()):
h = self.config_obj.help(self.name)
if h:
self.gui_obj.setToolTip(h)
tt = str(self.gui_obj.toolTip())
set_help_tips(self.gui_obj, tt)

def changed(self, *args):
self.widget.changed_signal.emit()

def initialize(self):
self.gui_obj.blockSignals(True)
if self.datatype == 'choice':
choices = self.choices or []
if isinstance(self.gui_obj, EditWithComplete):
self.gui_obj.all_items = choices else:
self.gui_obj.clear()
for x in choices:
if isinstance(x, string_or_bytes):
x = (x, x)
self.gui_obj.addItem(x[0], (x[1]))
self.set_gui_val(self.get_config_val(default=False))
self.gui_obj.blockSignals(False)
self.initial_value = self.get_gui_val()

def commit(self):
val = self.get_gui_val()
oldval = self.get_config_val()
changed = val != oldval if changed:
self.set_config_val(self.get_gui_val())
return changed and self.restart_required def restore_defaults(self):
self.set_gui_val(self.get_config_val(default=True))

def get_config_val(self, default=False):
if default:
val = self.config_obj.defaults[self.name]
else:
val = self.config_obj[self.name]
return val def set_config_val(self, val):
self.config_obj[self.name] = val def set_gui_val(self, val):
if self.datatype == 'bool':
self.gui_obj.setChecked(bool(val))
elif self.datatype == 'number':
self.gui_obj.setValue(val)
elif self.datatype == 'string':
self.gui_obj.setText(val if val else '')
elif self.datatype == 'choice':
if isinstance(self.gui_obj, EditWithComplete):
self.gui_obj.setText(val)
else:
idx = self.gui_obj.findData((val), role=Qt.ItemDataRole.UserRole, flags=self.CHOICES_SEARCH_FLAGS)
if idx == -1:
idx = 0 self.gui_obj.setCurrentIndex(idx)

def get_gui_val(self):
if self.datatype == 'bool':
val = bool(self.gui_obj.isChecked())
elif self.datatype == 'number':
val = self.gui_obj.value()
elif self.datatype == 'string':
val = str(self.gui_obj.text()).strip()
if self.empty_string_is_None and not val:
val = None elif self.datatype == 'choice':
if isinstance(self.gui_obj, EditWithComplete):
val = str(self.gui_obj.text())
else:
idx = self.gui_obj.currentIndex()
if idx < 0:
idx = 0 val = str(self.gui_obj.itemData(idx) or '')
return val class CommaSeparatedList(Setting):

def set_gui_val(self, val):
x = ''
if val:
x = ', '.join(val)
self.gui_obj.setText(x)

def get_gui_val(self):
val = str(self.gui_obj.text()).strip()
ans = []
if val:
ans = [x.strip() for x in val.split(',')]
ans = [x for x in ans if x]
return ans

[документация]
class ConfigWidgetBase(QWidget, ConfigWidgetInterface):
'''
Base class that contains code to easily add standard config widgets like checkboxes, combo boxes, text fields and so on. See the :meth:`register` method.

This class automatically handles change notification, resetting to default, translation between gui objects and config objects, etc. for registered settings.

If your config widget inherits from this class but includes setting that are not registered, you should override the :class:`ConfigWidgetInterface` methods and call the base class methods inside the overrides.
'''

changed_signal = pyqtSignal()
restart_now = pyqtSignal()
supports_restoring_to_defaults = True restart_critical = False def __init__(self, parent=None):
QWidget.__init__(self, parent)
if hasattr(self, 'setupUi'):
self.setupUi(self)
self.settings = {}
self.child_tabs = []
for v in self.__dict__.values():
if isinstance(v, LazyConfigWidgetBase):
self.child_tabs.append(v)

[документация]
def register(self, name, config_obj, gui_name=None, choices=None, restart_required=False, empty_string_is_None=True, setting=Setting):
'''
Register a setting.

:param name: The setting name
:param config_obj: The config object that reads/writes the setting
:param gui_name: The name of the GUI object that presents an interface to change the setting. By default it is assumed to be
``'opt_' + name``.
:param choices: If this setting is a multiple choice (combobox) based setting, the list of choices. The list is a list of two element tuples of the form: ``[(gui name, value), ...]``
:param setting: The class responsible for managing this setting. The default class handles almost all cases, so this param is rarely used.
'''
setting = setting(name, config_obj, self, gui_name=gui_name, choices=choices, restart_required=restart_required, empty_string_is_None=empty_string_is_None)
return self.register_setting(setting)

def register_setting(self, setting):
self.settings[setting.name] = setting return setting

[документация]
def initialize(self):
for setting in self.settings.values():
setting.initialize()

[документация]
def commit(self, *args):
restart_required = False for setting in self.settings.values():
rr = setting.commit()
if rr:
restart_required = True return restart_required

[документация]
def restore_defaults(self, *args):
for setting in self.settings.values():
setting.restore_defaults()

def register_child_tab(self, tab):
self.child_tabs.append(tab)def get_plugin(category, name):
for plugin in preferences_plugins():
if plugin.category == category and plugin.name == name:
return plugin raise ValueError( f'No Preferences Plugin with category: {category} and name: {name} found')

class LazyConfigWidgetBase(ConfigWidgetBase):
'''
Use this for dialogs that are tabs, accessed either from the left or on the top. It directly replaces ConfigWidgetBase, supporting the lazy operations.
'''

def __init__(self, parent=None):
super().__init__(parent)
self.lazy_init_called = False def ensure_lazy_initialized(self):
if not self.lazy_init_called:
if hasattr(self, 'lazy_initialize'):
self.lazy_initialize()
self.lazy_init_called = True def set_changed_signal(self, changed_signal):
self.changed_signal.connect(changed_signal)

def restore_defaults(self):
self.ensure_lazy_initialized()
super().restore_defaults()

def showEvent(self, event):
# called when the widget is actually displays. We can't do something like
# lazy_genesis because Qt does "things" before showEvent() is called. In
# particular, the register function doesn't work with combo boxes if
# genesis isn't called before everything else. Why is a mystery.
self.ensure_lazy_initialized()
super().showEvent(event)

class ConfigDialog(QDialog):

def set_widget(self, w):
self.w = w def accept(self):
try:
self.restart_required = self.w.commit()
except AbortCommit:
return
QDialog.accept(self)

def init_gui():
from calibre.gui2.main import option_parser from calibre.gui2.ui import Main from calibre.library import db parser = option_parser()
opts, args = parser.parse_args([])
actions = tuple(Main.create_application_menubar())
db = db()
gui = Main(opts)
gui.initialize(db.library_path, db, actions, show_gui=False)
return gui def show_config_widget(category, name, gui=None, show_restart_msg=False, parent=None, never_shutdown=False, callback=None):
'''
Show the preferences plugin identified by category and name

:param gui: gui instance, if None a hidden gui is created
:param show_restart_msg: If True and the preferences plugin indicates a restart is required, show a message box telling the user to restart
:param parent: The parent of the displayed dialog

:return: True iff a restart is required for the changes made by the user to take effect
'''
from calibre.gui2 import gprefs pl = get_plugin(category, name)
d = ConfigDialog(parent)
d.resize(750, 550)
conf_name = f'config_widget_dialog_geometry_{category}_{name}'
d.setWindowTitle(_('Configure ') + pl.gui_name)
d.setWindowIcon(QIcon.ic('config.png'))
bb = QDialogButtonBox(d)
bb.setStandardButtons(QDialogButtonBox.StandardButton.Apply|QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.RestoreDefaults)
bb.accepted.connect(d.accept)
bb.rejected.connect(d.reject)
w = pl.create_widget(d)
d.set_widget(w)
bb.button(QDialogButtonBox.StandardButton.RestoreDefaults).clicked.connect(w.restore_defaults)
bb.button(QDialogButtonBox.StandardButton.RestoreDefaults).setEnabled(w.supports_restoring_to_defaults)
bb.button(QDialogButtonBox.StandardButton.Apply).setEnabled(False)
bb.button(QDialogButtonBox.StandardButton.Apply).clicked.connect(d.accept)

def onchange():
b = bb.button(QDialogButtonBox.StandardButton.Apply)
b.setEnabled(True)
b.setDefault(True)
b.setAutoDefault(True)
w.changed_signal.connect(onchange)
bb.button(QDialogButtonBox.StandardButton.Cancel).setFocus(Qt.FocusReason.OtherFocusReason)
l = QVBoxLayout()
d.setLayout(l)
l.addWidget(w)
l.addWidget(bb)
mygui = gui is None if gui is None:
gui = init_gui()
mygui = True w.genesis(gui)
w.do_on_child_tabs('genesis', gui)
w.initialize()
w.do_on_child_tabs('initialize')
d.restore_geometry(gprefs, conf_name)
if callback is not None:
callback(w)
d.exec()
d.save_geometry(gprefs, conf_name)
rr = getattr(d, 'restart_required', False)
if show_restart_msg and rr:
from calibre.gui2 import warning_dialog warning_dialog(gui, 'Restart required', 'Restart required', show=True)
if mygui and not never_shutdown:
gui.shutdown()
return rr class ListViewWithMoveByKeyPress(QListView):

def set_movement_functions(self, up_function, down_function):
self.up_function = up_function self.down_function = down_function def event(self, event):
if (event.type() == QEvent.KeyPress and
QApplication.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier):
if event.key() == Qt.Key.Key_Up:
self.up_function()
elif event.key() == Qt.Key.Key_Down:
self.down_function()
return True return QListView.event(self, event)

class ListWidgetWithMoveByKeyPress(QListWidget):

def set_movement_functions(self, up_function, down_function):
self.up_function = up_function self.down_function = down_function def event(self, event):
if (event.type() == QEvent.KeyPress and
QApplication.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier):
if event.key() == Qt.Key.Key_Up:
self.up_function()
elif event.key() == Qt.Key.Key_Down:
self.down_function()
return True return QListWidget.event(self, event)

class TableWidgetWithMoveByKeyPress(QTableWidget):

def set_movement_functions(self, up_function, down_function):
self.up_function = up_function self.down_function = down_function def event(self, event):
if (event.type() == QEvent.KeyPress and
QApplication.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier):
if event.key() == Qt.Key.Key_Up:
self.up_function()
elif event.key() == Qt.Key.Key_Down:
self.down_function()
return True return QTableWidget.event(self, event)

# Testing {{{ def test_widget(category, name, gui=None, callback=None):
show_config_widget(category, name, gui=gui, show_restart_msg=True, callback=callback)

def test_all():
from qt.core import QApplication app = QApplication([])
app gui = init_gui()
for plugin in preferences_plugins():
test_widget(plugin.category, plugin.name, gui=gui)
gui.shutdown()

if __name__ == '__main__':
test_all()
# }}}

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.gui2.tweak_book.boss

Исходный код calibre.gui2.tweak_book.boss

#!/usr/bin/env python
# License: GPLv3 Copyright: 2013, Kovid Goyal <kovid at kovidgoyal.net> import errno import os import shutil import subprocess import sys import tempfile from functools import partial, wraps from qt.core import (
QApplication,
QCheckBox,
QDialog,
QDialogButtonBox,
QGridLayout,
QIcon,
QInputDialog,
QLabel,
QMimeData,
QObject,
QSize,
Qt,
QTimer,
QUrl,
QVBoxLayout, pyqtSignal,
)

from calibre import isbytestring, prints from calibre.constants import cache_dir, islinux, ismacos, iswindows from calibre.ebooks.oeb.base import urlnormalize from calibre.ebooks.oeb.polish.container import OEB_DOCS, OEB_STYLES, clone_container, guess_type from calibre.ebooks.oeb.polish.container import get_container as _gc from calibre.ebooks.oeb.polish.cover import mark_as_cover, mark_as_titlepage, set_cover from calibre.ebooks.oeb.polish.css import filter_css, rename_class from calibre.ebooks.oeb.polish.main import SUPPORTED, tweak_polish from calibre.ebooks.oeb.polish.pretty import fix_all_html, pretty_all from calibre.ebooks.oeb.polish.replace import get_recommended_folders, rationalize_folders, rename_files, replace_file from calibre.ebooks.oeb.polish.split import AbortError, merge, multisplit, split from calibre.ebooks.oeb.polish.toc import create_inline_toc, mark_as_nav, remove_names_from_toc from calibre.ebooks.oeb.polish.utils import link_stylesheets from calibre.ebooks.oeb.polish.utils import setup_css_parser_serialization as scs from calibre.gui2 import ( add_to_recent_docs, choose_dir, choose_files, choose_save_file, error_dialog, info_dialog, open_url, question_dialog, sanitize_env_vars, warning_dialog,
)
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.tweak_book import actions, current_container, dictionaries, editor_name, editors, set_book_locale, set_current_container, tprefs from calibre.gui2.tweak_book.completion.worker import completion_worker from calibre.gui2.tweak_book.editor import editor_from_syntax, syntax_from_mime from calibre.gui2.tweak_book.editor.insert_resource import NewBook, get_resource_data from calibre.gui2.tweak_book.file_list import FILE_COPY_MIME, NewFileDialog from calibre.gui2.tweak_book.preferences import Preferences from calibre.gui2.tweak_book.preview import parse_worker from calibre.gui2.tweak_book.save import SaveManager, find_first_existing_ancestor, save_container from calibre.gui2.tweak_book.search import run_search, validate_search_request from calibre.gui2.tweak_book.spell import find_next as find_next_word from calibre.gui2.tweak_book.spell import find_next_error from calibre.gui2.tweak_book.toc import TOCEditor from calibre.gui2.tweak_book.undo import GlobalUndoHistory from calibre.gui2.tweak_book.widgets import (
AddCover,
FilterCSS,
ImportForeign,
InsertLink,
InsertSemantics,
InsertTag,
MultiSplit,
QuickOpen,
RationalizeFolders,
)
from calibre.gui2.widgets import BusyCursor from calibre.ptempfile import PersistentTemporaryDirectory, TemporaryDirectory from calibre.startup import connect_lambda from calibre.utils.config import JSONConfig from calibre.utils.icu import numeric_sort_key from calibre.utils.imghdr import identify from calibre.utils.ipc.launch import exe_path, macos_edit_book_bundle_path from calibre.utils.localization import ngettext from calibre.utils.tdir_in_cache import tdir_in_cache from polyglot.builtins import as_bytes, iteritems, itervalues, string_or_bytes from polyglot.urllib import urlparse

_diff_dialogs = []
last_used_transform_rules = []
last_used_html_transform_rules = []

def get_container(*args, **kwargs):
kwargs['tweak_mode'] = True container = _gc(*args, **kwargs)
return container def setup_css_parser_serialization():
scs(tprefs['editor_tab_stop_width'])

def in_thread_job(func):
@wraps(func)
def ans(*args, **kwargs):
with BusyCursor():
return func(*args, **kwargs)
return ans def get_boss():
return get_boss.boss def open_path_in_new_editor_instance(path: str):
import subprocess from calibre.gui2 import sanitize_env_vars with sanitize_env_vars():
if ismacos:
from calibre.utils.ipc.launch import macos_edit_book_bundle_path bundle = os.path.dirname(os.path.dirname(macos_edit_book_bundle_path().rstrip('/')))
subprocess.Popen(['open', '-n', '-a', bundle, path])
else:
subprocess.Popen([sys.executable, path])

[документация]
class Boss(QObject):

handle_completion_result_signal = pyqtSignal(object)

def __init__(self, parent, notify=None):
QObject.__init__(self, parent)
self.global_undo = GlobalUndoHistory()
self.file_was_readonly = False self.container_count = 0 self.tdir = None self.save_manager = SaveManager(parent, notify)
self.save_manager.report_error.connect(self.report_save_error)
self.save_manager.check_for_completion.connect(self.check_terminal_save)
self.doing_terminal_save = False self.ignore_preview_to_editor_sync = False setup_css_parser_serialization()
get_boss.boss = self self.gui = parent completion_worker().result_callback = self.handle_completion_result_signal.emit self.handle_completion_result_signal.connect(self.handle_completion_result, Qt.ConnectionType.QueuedConnection)
self.completion_request_count = 0 self.editor_cache = JSONConfig('editor-cache', base_path=cache_dir())
d = self.editor_cache.defaults d['edit_book_state'] = {}
d['edit_book_state_order'] = []

def __call__(self, gui):
self.gui = gui gui.message_popup.undo_requested.connect(self.do_global_undo)
fl = gui.file_list fl.delete_requested.connect(self.delete_requested)
fl.reorder_spine.connect(self.reorder_spine)
fl.rename_requested.connect(self.rename_requested)
fl.bulk_rename_requested.connect(self.bulk_rename_requested)
fl.edit_file.connect(self.edit_file_requested)
fl.merge_requested.connect(self.merge_requested)
fl.mark_requested.connect(self.mark_requested)
fl.export_requested.connect(self.export_requested)
fl.replace_requested.connect(self.replace_requested)
fl.link_stylesheets_requested.connect(self.link_stylesheets_requested)
fl.initiate_file_copy.connect(self.copy_files_to_clipboard)
fl.initiate_file_paste.connect(self.paste_files_from_clipboard)
fl.open_file_with.connect(self.open_file_with)
self.gui.central.current_editor_changed.connect(self.apply_current_editor_state)
self.gui.central.close_requested.connect(self.editor_close_requested)
self.gui.central.search_panel.search_triggered.connect(self.search)
self.gui.text_search.find_text.connect(self.find_text)
self.gui.preview.sync_requested.connect(self.sync_editor_to_preview)
self.gui.preview.split_start_requested.connect(self.split_start_requested)
self.gui.preview.split_requested.connect(self.split_requested)
self.gui.preview.link_clicked.connect(self.link_clicked)
self.gui.preview.render_process_restarted.connect(self.report_render_process_restart)
self.gui.preview.open_file_with.connect(self.open_file_with)
self.gui.preview.edit_file.connect(self.edit_file_requested)
self.gui.check_book.item_activated.connect(self.check_item_activated)
self.gui.check_book.check_requested.connect(self.check_requested)
self.gui.check_book.fix_requested.connect(self.fix_requested)
self.gui.toc_view.navigate_requested.connect(self.link_clicked)
self.gui.toc_view.refresh_requested.connect(self.commit_all_editors_to_container)
self.gui.image_browser.image_activated.connect(self.image_activated)
self.gui.checkpoints.revert_requested.connect(self.revert_requested)
self.gui.checkpoints.compare_requested.connect(self.compare_requested)
self.gui.saved_searches.run_saved_searches.connect(self.run_saved_searches)
self.gui.saved_searches.copy_search_to_search_panel.connect(self.gui.central.search_panel.paste_saved_search)
self.gui.central.search_panel.save_search.connect(self.save_search)
self.gui.central.search_panel.show_saved_searches.connect(self.show_saved_searches)
self.gui.spell_check.find_word.connect(self.find_word)
self.gui.spell_check.refresh_requested.connect(self.commit_all_editors_to_container)
self.gui.spell_check.word_replaced.connect(self.word_replaced)
self.gui.spell_check.word_ignored.connect(self.word_ignored)
self.gui.spell_check.change_requested.connect(self.word_change_requested)
self.gui.live_css.goto_declaration.connect(self.goto_style_declaration)
self.gui.manage_fonts.container_changed.connect(self.apply_container_update_to_gui)
self.gui.manage_fonts.embed_all_fonts.connect(self.manage_fonts_embed)
self.gui.manage_fonts.subset_all_fonts.connect(self.manage_fonts_subset)
self.gui.reports.edit_requested.connect(self.reports_edit_requested)
self.gui.reports.refresh_starting.connect(self.commit_all_editors_to_container)
self.gui.reports.delete_requested.connect(self.delete_requested)

def report_render_process_restart(self):
self.gui.show_status_message(_('The Qt WebEngine Render process crashed and has been restarted'))

@property def currently_editing(self):
' Return the name of the file being edited currently or None if no file is being edited '
return editor_name(self.gui.central.current_editor)

def preferences(self):
orig_spell = tprefs['inline_spell_check']
orig_size = tprefs['toolbar_icon_size']
p = Preferences(self.gui)
ret = p.exec()
if p.dictionaries_changed:
dictionaries.clear_caches()
dictionaries.initialize(force=True) # Reread user dictionaries if p.toolbars_changed:
self.gui.populate_toolbars()
for ed in itervalues(editors):
if hasattr(ed, 'populate_toolbars'):
ed.populate_toolbars()
if orig_size != tprefs['toolbar_icon_size']:
for ed in itervalues(editors):
if hasattr(ed, 'bars'):
for bar in ed.bars:
bar.setIconSize(QSize(tprefs['toolbar_icon_size'], tprefs['toolbar_icon_size']))

if ret == QDialog.DialogCode.Accepted:
setup_css_parser_serialization()
self.gui.apply_settings()
self.refresh_file_list()
self.gui.preview.start_refresh_timer()
if ret == QDialog.DialogCode.Accepted or p.dictionaries_changed:
for ed in itervalues(editors):
ed.apply_settings(dictionaries_changed=p.dictionaries_changed)
if orig_spell != tprefs['inline_spell_check']:
from calibre.gui2.tweak_book.editor.syntax.html import refresh_spell_check_status refresh_spell_check_status()
for ed in itervalues(editors):
try:
ed.editor.highlighter.rehighlight()
except AttributeError:
pass def mark_requested(self, name, action):
self.commit_dirty_opf()
c = current_container()
if action == 'cover':
mark_as_cover(current_container(), name)
elif action.startswith('titlepage:'):
action, move_to_start = action.partition(':')[0::2]
move_to_start = move_to_start == 'True'
mark_as_titlepage(current_container(), name, move_to_start=move_to_start)
elif action == 'nav':
mark_as_nav(current_container(), name)

if c.opf_name in editors:
editors[c.opf_name].replace_data(c.raw_data(c.opf_name))
self.gui.file_list.build(c)
self.set_modified()

def mkdtemp(self, prefix=''):
self.container_count += 1 return tempfile.mkdtemp(prefix=f'{prefix}{self.container_count:05}-', dir=self.tdir)

def _check_before_open(self):
if self.gui.action_save.isEnabled():
if not question_dialog(self.gui, _('Unsaved changes'), _(
'The current book has unsaved changes. If you open a new book, they will be lost.'
' Are you sure you want to proceed?')):
return if self.save_manager.has_tasks:
return info_dialog(self.gui, _('Cannot open'),
_('The current book is being saved, you cannot open a new book until'
' the saving is completed'), show=True)
return True def new_book(self):
if not self._check_before_open():
return d = NewBook(self.gui)
if d.exec() == QDialog.DialogCode.Accepted:
fmt = d.fmt.lower()
path = choose_save_file(self.gui, 'edit-book-new-book', _('Choose file location'), filters=[(fmt.upper(), (fmt,))], all_files=False)
if path is not None:
if not path.lower().endswith('.' + fmt):
path = path + '.' + fmt from calibre.ebooks.oeb.polish.create import create_book create_book(d.mi, path, fmt=fmt)
self.open_book(path=path)

def import_book(self, path=None):
if not self._check_before_open():
return d = ImportForeign(self.gui)
if hasattr(path, 'rstrip'):
d.set_src(os.path.abspath(path))
if d.exec() == QDialog.DialogCode.Accepted:
for name in tuple(editors):
self.close_editor(name)
from calibre.ebooks.oeb.polish.import_book import import_book_as_epub src, dest = d.data self._clear_notify_data = True def func(src, dest, tdir):
import_book_as_epub(src, dest)
return get_container(dest, tdir=tdir)
self.gui.blocking_job('import_book', _('Importing book, please wait...'), self.book_opened, func, src, dest, tdir=self.mkdtemp())

[документация]
def open_book(self, path=None, edit_file=None, clear_notify_data=True, open_folder=False, search_text=None):
'''
Open the e-book at ``path`` for editing. Will show an error if the e-book is not in a supported format or the current book has unsaved changes.

:param edit_file: The name of a file inside the newly opened book to start editing. Can also be a list of names.
'''
if isinstance(path, (list, tuple)) and path:
# Can happen from an file_event_hook on OS X when drag and dropping
# onto the icon in the dock or using open -a extra_paths = path[1:]
path = path[0]
for x in extra_paths:
open_path_in_new_editor_instance(x)
if not self._check_before_open():
return if not hasattr(path, 'rpartition'):
if open_folder:
path = choose_dir(self.gui, 'open-book-folder-for-tweaking', _('Choose book folder'))
if path:
path = [path]
else:
path = choose_files(self.gui, 'open-book-for-tweaking', _('Choose book'),
[(_('Books'), [x.lower() for x in SUPPORTED])], all_files=False, select_only_single_file=True)

if not path:
return path = path[0]

if not os.path.exists(path):
return error_dialog(self.gui, _('File not found'), _(
'The file %s does not exist.') % path, show=True)
isdir = os.path.isdir(path)
ext = path.rpartition('.')[-1].upper()
if ext not in SUPPORTED and not isdir:
from calibre.ebooks.oeb.polish.import_book import IMPORTABLE if ext.lower() in IMPORTABLE:
return self.import_book(path)
return error_dialog(self.gui, _('Unsupported format'),
_('Tweaking is only supported for books in the %s formats.'
' Convert your book to one of these formats first.') % _(' and ').join(sorted(SUPPORTED)), show=True)

self.file_was_readonly = not os.access(path, os.W_OK)
if self.file_was_readonly:
warning_dialog(self.gui, _('Read-only file'), _(
'The file {} is read-only. Saving changes to it will either fail or cause its permissions to be reset.').format(path), show=True)
with self.editor_cache:
self.save_book_edit_state()

for name in tuple(editors):
self.close_editor(name)
self.gui.preview.clear()
self.gui.live_css.clear()
self.container_count = -1 if self.tdir:
shutil.rmtree(self.tdir, ignore_errors=True)
# We use the cache dir rather than the temporary dir to try and prevent
# temp file cleaners from nuking ebooks. See https://bugs.launchpad.net/bugs/1740460 self.tdir = tdir_in_cache('ee')
self._edit_file_on_open = edit_file self._search_text_on_open = search_text self._clear_notify_data = clear_notify_data self.gui.blocking_job('open_book', _('Opening book, please wait...'), self.book_opened, get_container, path, tdir=self.mkdtemp())def book_opened(self, job):
ef = getattr(self, '_edit_file_on_open', None)
cn = getattr(self, '_clear_notify_data', True)
st = getattr(self, '_search_text_on_open', None)
self._edit_file_on_open = self._search_text_on_open = None if job.traceback is not None:
self.gui.update_status_bar_default_message()
if 'DRMError:' in job.traceback:
from calibre.gui2.dialogs.drm_error import DRMErrorMessage return DRMErrorMessage(self.gui).exec()
if 'ObfuscationKeyMissing:' in job.traceback:
return error_dialog(self.gui, _('Failed to open book'), _(
'Failed to open book, it has obfuscated fonts, but the obfuscation key is missing from the OPF.'
' Do an EPUB to EPUB conversion before trying to edit this book.'), show=True)

return error_dialog(self.gui, _('Failed to open book'),
_('Failed to open book, click "Show details" for more information.'), det_msg=job.traceback, show=True)
if cn:
self.save_manager.clear_notify_data()
self.gui.check_book.clear_at_startup()
self.gui.spell_check.clear_caches()
dictionaries.clear_ignored(), dictionaries.clear_caches()
parse_worker.clear()
container = job.result set_current_container(container)
completion_worker().clear_caches()
with BusyCursor():
self.current_metadata = self.gui.current_metadata = container.mi lang = container.opf_xpath('//dc:language/text()') or [self.current_metadata.language]
set_book_locale(lang[0])
self.global_undo.open_book(container)
self.gui.update_window_title()
self.gui.file_list.current_edited_name = None self.gui.file_list.build(container, preserve_state=False)
self.gui.action_save.setEnabled(False)
self.update_global_history_actions()
recent_books = list(tprefs.get('recent-books', []))
path = os.path.abspath(container.path_to_ebook)
if path in recent_books:
recent_books.remove(path)
self.gui.update_status_bar_default_message(path)
recent_books.insert(0, path)
tprefs['recent-books'] = recent_books[:10]
self.gui.update_recent_books()
if iswindows:
try:
add_to_recent_docs(path)
except Exception:
import traceback traceback.print_exc()
if ef:
if isinstance(ef, str):
ef = [ef]
for i in ef:
self.gui.file_list.request_edit(i)
else:
if tprefs['restore_book_state']:
self.restore_book_edit_state()
self.gui.toc_view.update_if_visible()
self.add_savepoint(_('Start of editing session'))
if st:
self.find_initial_text(st)

def update_editors_from_container(self, container=None, names=None):
c = container or current_container()
for name, ed in tuple(iteritems(editors)):
if c.has_name(name):
if names is None or name in names:
ed.replace_data(c.raw_data(name))
ed.is_synced_to_container = True else:
self.close_editor(name)

def refresh_file_list(self):
container = current_container()
self.gui.file_list.build(container)
completion_worker().clear_caches('names')

[документация]
def apply_container_update_to_gui(self, mark_as_modified=True):
'''
Update all the components of the user interface to reflect the latest data in the current book container.

:param mark_as_modified: If True, the book will be marked as modified, so the user will be prompted to save it when quitting.
'''
self.refresh_file_list()
self.update_global_history_actions()
self.update_editors_from_container()
if mark_as_modified:
self.set_modified()
self.gui.toc_view.update_if_visible()
completion_worker().clear_caches()
self.gui.preview.start_refresh_timer()

@in_thread_job def delete_requested(self, spine_items, other_items):
self.add_savepoint(_('Before: Delete files'))
self.commit_dirty_opf()
c = current_container()
c.remove_from_spine(spine_items)
for name in other_items:
c.remove_item(name)
self.set_modified()
self.gui.file_list.delete_done(spine_items, other_items)
spine_names = [x for x, remove in spine_items if remove]
completion_worker().clear_caches('names')
items = spine_names + list(other_items)
for name in items:
if name in editors:
self.close_editor(name)
if not editors:
self.gui.preview.clear()
self.gui.live_css.clear()
changed = remove_names_from_toc(current_container(), spine_names + list(other_items))
if changed:
self.gui.toc_view.update_if_visible()
for toc in changed:
if toc and toc in editors:
editors[toc].replace_data(c.raw_data(toc))
if c.opf_name in editors:
editors[c.opf_name].replace_data(c.raw_data(c.opf_name))
self.gui.message_popup(ngettext(
'One file deleted', '{} files deleted', len(items)).format(len(items)))

def commit_dirty_opf(self):
c = current_container()
if c.opf_name in editors and not editors[c.opf_name].is_synced_to_container:
self.commit_editor_to_container(c.opf_name)
self.gui.update_window_title()

def reorder_spine(self, items):
if not self.ensure_book():
return self.add_savepoint(_('Before: Re-order text'))
c = current_container()
c.set_spine(items)
self.set_modified()
self.gui.file_list.build(current_container()) # needed as the linear flag may have changed on some items if c.opf_name in editors:
editors[c.opf_name].replace_data(c.raw_data(c.opf_name))
completion_worker().clear_caches('names')

def add_file(self):
if not self.ensure_book(_('You must first open a book to edit, before trying to create new files in it.')):
return self.commit_dirty_opf()
d = NewFileDialog(self.gui)
if d.exec() != QDialog.DialogCode.Accepted:
return added_name = self.do_add_file(d.file_name, d.file_data, using_template=d.using_template, edit_file=True)
if d.file_name.rpartition('.')[2].lower() in ('ttf', 'otf', 'woff'):
from calibre.gui2.tweak_book.manage_fonts import show_font_face_rule_for_font_file show_font_face_rule_for_font_file(d.file_data, added_name, self.gui)

def do_add_file(self, file_name, data, using_template=False, edit_file=False):
self.add_savepoint(_('Before: Add file %s') % self.gui.elided_text(file_name))
c = current_container()
adata = data.replace(b'%CURSOR%', b'') if using_template else data spine_index = c.index_in_spine(self.currently_editing or '')
if spine_index is not None:
spine_index += 1 try:
added_name = c.add_file(file_name, adata, spine_index=spine_index)
except Exception:
self.rewind_savepoint()
raise self.gui.file_list.build(c)
self.gui.file_list.select_name(file_name)
if c.opf_name in editors:
editors[c.opf_name].replace_data(c.raw_data(c.opf_name))
mt = c.mime_map[file_name]
syntax = syntax_from_mime(file_name, mt)
if syntax and edit_file:
if using_template:
self.edit_file(file_name, syntax, use_template=data.decode('utf-8'))
else:
self.edit_file(file_name, syntax)
self.set_modified()
completion_worker().clear_caches('names')
return added_name def add_files(self):
if not self.ensure_book(_('You must first open a book to edit, before trying to create new files in it.')):
return files = choose_files(self.gui, 'tweak-book-bulk-import-files', _('Choose files'))
if files:
folder_map = get_recommended_folders(current_container(), files)
files = {x:('/'.join((folder, os.path.basename(x))) if folder else os.path.basename(x))
for x, folder in iteritems(folder_map)}
self.add_savepoint(_('Before: Add files'))
c = current_container()
added_fonts = set()
for path in sorted(files, key=numeric_sort_key):
name = files[path]
i = 0 while c.exists(name) or c.manifest_has_name(name) or c.has_name_case_insensitive(name):
i += 1 name, ext = name.rpartition('.')[0::2]
name = f'{name}_{i}.{ext}'
try:
with open(path, 'rb') as f:
c.add_file(name, f.read())
except Exception:
self.rewind_savepoint()
raise if name.rpartition('.')[2].lower() in ('ttf', 'otf', 'woff'):
added_fonts.add(name)
self.gui.file_list.build(c)
if c.opf_name in editors:
editors[c.opf_name].replace_data(c.raw_data(c.opf_name))
self.set_modified()
completion_worker().clear_caches('names')
if added_fonts:
from calibre.gui2.tweak_book.manage_fonts import show_font_face_rule_for_font_files show_font_face_rule_for_font_files(c, added_fonts, self.gui)

def add_cover(self):
if not self.ensure_book():
return d = AddCover(current_container(), self.gui)
d.import_requested.connect(self.do_add_file)
try:
if d.exec() == QDialog.DialogCode.Accepted and d.file_name is not None:
report = []
with BusyCursor():
self.add_savepoint(_('Before: Add cover'))
set_cover(current_container(), d.file_name, report.append, options={
'existing_image':True, 'keep_aspect':tprefs['add_cover_preserve_aspect_ratio']})
self.apply_container_update_to_gui()
finally:
d.import_requested.disconnect()

def ensure_book(self, msg=None):
msg = msg or _('No book is currently open. You must first open a book.')
if current_container() is None:
error_dialog(self.gui, _('No book open'), msg, show=True)
return False return True def edit_toc(self):
if not self.ensure_book(_('You must open a book before trying to edit the Table of Contents.')):
return self.add_savepoint(_('Before: Edit Table of Contents'))
self.__current_toc_editor = d = TOCEditor(title=self.current_metadata.title, parent=self.gui)
d.finished.connect(self.toc_edit_finished)
# Using d.exec() causes showing the webview to hide the dialog d.open()

def toc_edit_finished(self, retcode: int):
self.__current_toc_editor = None if retcode != QDialog.DialogCode.Accepted:
self.rewind_savepoint()
return with BusyCursor():
self.set_modified()
self.update_editors_from_container()
self.gui.toc_view.update_if_visible()
self.gui.file_list.build(current_container())

def insert_inline_toc(self):
if not self.ensure_book():
return self.commit_all_editors_to_container()
self.add_savepoint(_('Before: Insert inline Table of Contents'))
name = create_inline_toc(current_container())
if name is None:
self.rewind_savepoint()
return error_dialog(self.gui, _('No Table of Contents'), _(
'Cannot create an inline Table of Contents as this book has no existing'
' Table of Contents. You must first create a Table of Contents using the'
' Edit Table of Contents tool.'), show=True)
self.apply_container_update_to_gui()
self.edit_file(name, 'html')

def polish(self, action, name, parent=None):
if not self.ensure_book():
return from calibre.gui2.tweak_book.polish import get_customization, show_report customization = get_customization(action, name, parent or self.gui)
if customization is None:
return with BusyCursor():
self.add_savepoint(_('Before: %s') % name)
try:
report, changed = tweak_polish(current_container(), {action:True}, customization=customization)
except Exception:
self.rewind_savepoint()
raise if changed:
self.apply_container_update_to_gui()
self.gui.update_window_title()
if not changed:
self.rewind_savepoint()
show_report(changed, self.current_metadata.title, report, parent or self.gui, self.show_current_diff)

def transform_html(self):
global last_used_html_transform_rules if not self.ensure_book(_('You must first open a book in order to transform styles.')):
return from calibre.ebooks.html_transform_rules import transform_container from calibre.gui2.html_transform_rules import RulesDialog d = RulesDialog(self.gui)
d.rules = last_used_html_transform_rules d.transform_scope = tprefs['html_transform_scope']
ret = d.exec()
last_used_html_transform_rules = d.rules scope = d.transform_scope tprefs.set('html_transform_scope', scope)
if ret != QDialog.DialogCode.Accepted:
return mime_map = current_container().mime_map names = ()
if scope == 'current':
if not self.currently_editing or mime_map.get(self.currently_editing) not in OEB_DOCS:
return error_dialog(self.gui, _('No HTML file'), _('Not currently editing an HTML file'), show=True)
names = (self.currently_editing,)
elif scope == 'open':
names = tuple(name for name in editors if mime_map.get(name) in OEB_DOCS)
if not names:
return error_dialog(self.gui, _('No HTML files'), _('Not currently editing any HTML files'), show=True)
elif scope == 'selected':
names = tuple(name for name in self.gui.file_list.file_list.selected_names if mime_map.get(name) in OEB_DOCS)
if not names:
return error_dialog(self.gui, _('No HTML files'), _('No HTML files are currently selected in the File browser'), show=True)
with BusyCursor():
self.add_savepoint(_('Before HTML transformation'))
try:
changed = transform_container(current_container(), last_used_html_transform_rules, names)
except Exception:
self.rewind_savepoint()
raise if changed:
self.apply_container_update_to_gui()
if not changed:
self.rewind_savepoint()
return info_dialog(self.gui, _('No changes'), _('No HTML was changed.'), show=True)
self.show_current_diff()

def transform_styles(self):
global last_used_transform_rules if not self.ensure_book(_('You must first open a book in order to transform styles.')):
return from calibre.ebooks.css_transform_rules import transform_container from calibre.gui2.css_transform_rules import RulesDialog d = RulesDialog(self.gui)
d.rules = last_used_transform_rules ret = d.exec()
last_used_transform_rules = d.rules if ret != QDialog.DialogCode.Accepted:
return with BusyCursor():
self.add_savepoint(_('Before style transformation'))
try:
changed = transform_container(current_container(), last_used_transform_rules)
except Exception:
self.rewind_savepoint()
raise if changed:
self.apply_container_update_to_gui()
if not changed:
self.rewind_savepoint()
info_dialog(self.gui, _('No changes'), _(
'No styles were changed.'), show=True)
return self.show_current_diff()

def get_external_resources(self):
if not self.ensure_book(_('You must first open a book in order to transform styles.')):
return from calibre.gui2.tweak_book.download import DownloadResources with BusyCursor():
self.add_savepoint(_('Before: Get external resources'))
try:
d = DownloadResources(self.gui)
d.exec()
except Exception:
self.rewind_savepoint()
raise if d.resources_replaced:
self.apply_container_update_to_gui()
if d.show_diff:
self.show_current_diff()
else:
self.rewind_savepoint()

def manage_fonts(self):
if not self.ensure_book(_('No book is currently open. You must first open a book to manage fonts.')):
return self.commit_all_editors_to_container()
self.gui.manage_fonts.display()

def manage_fonts_embed(self):
self.polish('embed', _('Embed all fonts'), parent=self.gui.manage_fonts)
self.gui.manage_fonts.refresh()

def manage_fonts_subset(self):
self.polish('subset', _('Subset all fonts'), parent=self.gui.manage_fonts)

# Renaming {{{ def rationalize_folders(self):
if not self.ensure_book():
return c = current_container()
if not c.SUPPORTS_FILENAMES:
return error_dialog(self.gui, _('Not supported'),
_('The %s format does not support file and folder names internally, therefore'
' arranging files into folders is not allowed.') % c.book_type.upper(), show=True)
d = RationalizeFolders(self.gui)
if d.exec() != QDialog.DialogCode.Accepted:
return self.commit_all_editors_to_container()
name_map = rationalize_folders(c, d.folder_map)
if not name_map:
confirm(_(
'The files in this book are already arranged into folders'), 'already-arranged-into-folders', self.gui, pixmap='dialog_information.png', title=_('Nothing to do'), show_cancel_button=False, config_set=tprefs, confirm_msg=_('Show this message &again'))
return self.add_savepoint(_('Before: Arrange into folders'))
self.gui.blocking_job(
'rationalize_folders', _('Renaming and updating links...'), partial(self.rename_done, name_map), rename_files, current_container(), name_map)

def rename_requested(self, oldname, newname):
self.commit_all_editors_to_container()
if guess_type(oldname) != guess_type(newname):
args = os.path.splitext(oldname) + os.path.splitext(newname)
if not confirm(
_('You are changing the file type of {0}<b>{1}</b> to {2}<b>{3}</b>.'
' Doing so can cause problems, are you sure?').format(*args),
'confirm-filetype-change', parent=self.gui, title=_('Are you sure?'), config_set=tprefs):
return if urlnormalize(newname) != newname:
if not confirm(
_('The name you have chosen {0} contains special characters, internally'
' it will look like: {1}Try to use only the English alphabet [a-z], numbers [0-9],'
' hyphens and underscores for file names. Other characters can cause problems for '
' different e-book viewers. Are you sure you want to proceed?').format( f'<pre>{newname}</pre>', f'<pre>{urlnormalize(newname)}</pre>'),
'confirm-urlunsafe-change', parent=self.gui, title=_('Are you sure?'), config_set=tprefs):
return self.add_savepoint(_('Before: Rename %s') % oldname)
name_map = {oldname:newname}
self.gui.blocking_job(
'rename_file', _('Renaming and updating links...'), partial(self.rename_done, name_map, from_filelist=self.gui.file_list.current_name), rename_files, current_container(), name_map)

def bulk_rename_requested(self, name_map):
self.add_savepoint(_('Before: Bulk rename'))
self.gui.blocking_job(
'bulk_rename_files', _('Renaming and updating links...'), partial(self.rename_done, name_map, from_filelist=self.gui.file_list.current_name), rename_files, current_container(), name_map)

def rename_done(self, name_map, job, from_filelist=None):
if job.traceback is not None:
self.gui.file_list.restore_temp_names()
return error_dialog(self.gui, _('Failed to rename files'),
_('Failed to rename files, click "Show details" for more information.'), det_msg=job.traceback, show=True)
self.gui.file_list.build(current_container())
self.set_modified()
for oldname, newname in iteritems(name_map):
if oldname in editors:
editors[newname] = ed = editors.pop(oldname)
ed.change_document_name(newname)
self.gui.central.rename_editor(editors[newname], newname)
if self.gui.preview.current_name == oldname:
self.gui.preview.current_name = newname self.apply_container_update_to_gui()
if from_filelist:
self.gui.file_list.select_names(frozenset(itervalues(name_map)), current_name=name_map.get(from_filelist))
self.gui.file_list.file_list.setFocus(Qt.FocusReason.PopupFocusReason)

# }}}

# Global history {{{ def do_global_undo(self, *a):
container = self.global_undo.undo()
if container is not None:
set_current_container(container)
self.apply_container_update_to_gui()

def do_global_redo(self):
container = self.global_undo.redo()
if container is not None:
set_current_container(container)
self.apply_container_update_to_gui()

def update_global_history_actions(self):
gu = self.global_undo for x, text in (('undo', _('&Revert to')), ('redo', _('&Revert to'))):
ac = getattr(self.gui, f'action_global_{x}')
ac.setEnabled(getattr(gu, 'can_' + x))
ac.setText(text + ' "%s"'%(getattr(gu, x + '_msg') or '...'))

[документация]
def add_savepoint(self, msg):
' Create a restore checkpoint with the name specified as ``msg`` '
self.commit_all_editors_to_container()
nc = clone_container(current_container(), self.mkdtemp())
self.global_undo.add_savepoint(nc, msg)
set_current_container(nc)
self.update_global_history_actions()

[документация]
def rewind_savepoint(self):
' Undo the previous creation of a restore checkpoint, useful if you create a checkpoint, then abort the operation with no changes '
container = self.global_undo.rewind_savepoint()
if container is not None:
set_current_container(container)
self.update_global_history_actions()

def create_diff_dialog(self, revert_msg=_('&Revert changes'), show_open_in_editor=True):
global _diff_dialogs from calibre.gui2.tweak_book.diff.main import Diff def line_activated(name, lnum, right):
if right:
self.edit_file_requested(name, None, guess_type(name))
if name in editors:
editor = editors[name]
editor.go_to_line(lnum)
editor.setFocus(Qt.FocusReason.OtherFocusReason)
self.gui.raise_and_focus()
d = Diff(revert_button_msg=revert_msg, show_open_in_editor=show_open_in_editor)
[x.break_cycles() for x in _diff_dialogs if not x.isVisible()]
_diff_dialogs = [x for x in _diff_dialogs if x.isVisible()] + [d]
d.show(), d.raise_and_focus(), d.setFocus(Qt.FocusReason.OtherFocusReason), d.setWindowModality(Qt.WindowModality.NonModal)
if show_open_in_editor:
d.line_activated.connect(line_activated)
return d

[документация]
def show_current_diff(self, allow_revert=True, to_container=None):
'''
Show the changes to the book from its last checkpointed state

:param allow_revert: If True the diff dialog will have a button to allow the user to revert all changes
:param to_container: A container object to compare the current container to. If None, the previously checkpointed container is used
'''
self.commit_all_editors_to_container()
k = {} if allow_revert else {'revert_msg': None}
d = self.create_diff_dialog(**k)
previous_container = self.global_undo.previous_container connect_lambda(d.revert_requested, self, lambda self: self.revert_requested(previous_container))
other = to_container or self.global_undo.previous_container d.container_diff(other, self.global_undo.current_container, names=(self.global_undo.label_for_container(other), self.global_undo.label_for_container(self.global_undo.current_container)))

def ask_to_show_current_diff(self, name, title, msg, allow_revert=True, to_container=None):
if tprefs.get('skip_ask_to_show_current_diff_for_' + name):
return d = QDialog(self.gui)
k = QVBoxLayout(d)
d.setWindowTitle(title)
k.addWidget(QLabel(msg))
k.confirm = cb = QCheckBox(_('Show this popup again'))
k.addWidget(cb)
cb.setChecked(True)
connect_lambda(cb.toggled, d, lambda d, checked: tprefs.set('skip_ask_to_show_current_diff_for_' + name, not checked))
d.bb = bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Close, d)
k.addWidget(bb)
bb.accepted.connect(d.accept)
bb.rejected.connect(d.reject)
d.b = b = bb.addButton(_('See what &changed'), QDialogButtonBox.ButtonRole.AcceptRole)
b.setIcon(QIcon.ic('diff.png')), b.setAutoDefault(False)
bb.button(QDialogButtonBox.StandardButton.Close).setDefault(True)
if d.exec() == QDialog.DialogCode.Accepted:
self.show_current_diff(allow_revert=allow_revert, to_container=to_container)

def compare_book(self):
if not self.ensure_book():
return self.commit_all_editors_to_container()
c = current_container()
path = choose_files(self.gui, 'select-book-for-comparison', _('Choose book'), filters=[
(_('%s books') % c.book_type.upper(), (c.book_type,))], select_only_single_file=True, all_files=False)
if path and path[0]:
with TemporaryDirectory('_compare') as tdir:
other = _gc(path[0], tdir=tdir, tweak_mode=True)
d = self.create_diff_dialog(revert_msg=None)
d.container_diff(other, c, names=(_('Other book'), _('Current book')))

def revert_requested(self, container):
self.commit_all_editors_to_container()
nc = self.global_undo.revert_to(container)
set_current_container(nc)
self.apply_container_update_to_gui()

def compare_requested(self, container):
self.show_current_diff(to_container=container)

# }}}

[документация]
def set_modified(self):
' Mark the book as having been modified '
self.gui.action_save.setEnabled(True)

def request_completion(self, name, completion_type, completion_data, query=None):
if completion_type is None:
completion_worker().clear_caches(completion_data)
return request_id = (self.completion_request_count, name)
self.completion_request_count += 1 completion_worker().queue_completion(request_id, completion_type, completion_data, query)
return request_id[0]

def handle_completion_result(self, result):
name = result.request_id[1]
editor = editors.get(name)
if editor is not None:
editor.handle_completion_result(result)

def fix_html(self, current):
if current:
ed = self.gui.central.current_editor if hasattr(ed, 'fix_html'):
ed.fix_html()
else:
with BusyCursor():
self.add_savepoint(_('Before: Fix HTML'))
fix_all_html(current_container())
self.update_editors_from_container()
self.set_modified()
self.ask_to_show_current_diff('html-fix', _('Fixing done'), _('All HTML files fixed'))

def pretty_print(self, current):
if current:
ed = self.gui.central.current_editor ed.pretty_print(editor_name(ed))
else:
with BusyCursor():
self.add_savepoint(_('Before: Beautify files'))
pretty_all(current_container())
self.update_editors_from_container()
self.set_modified()
QApplication.alert(self.gui)
self.ask_to_show_current_diff('beautify', _('Beautified'), _('All files beautified'))

def mark_selected_text(self):
ed = self.gui.central.current_editor if ed is not None:
ed.mark_selected_text()
if ed.has_marked_text:
self.gui.central.search_panel.set_where('selected-text')
else:
self.gui.central.search_panel.unset_marked()

def editor_action(self, action):
ed = self.gui.central.current_editor edname = editor_name(ed)
if hasattr(ed, 'action_triggered'):
if action and action[0] == 'insert_resource':
rtype = action[1]
if rtype == 'image' and ed.syntax not in {'css', 'html'}:
return error_dialog(self.gui, _('Not supported'), _(
'Inserting images is only supported for HTML and CSS files.'), show=True)
rdata = get_resource_data(rtype, self.gui)
if rdata is None:
return if rtype == 'image':
chosen_name, chosen_image_is_external, fullpage, preserve_ar = rdata if chosen_image_is_external:
with open(chosen_image_is_external[1], 'rb') as f:
current_container().add_file(chosen_image_is_external[0], f.read())
self.refresh_file_list()
chosen_name = chosen_image_is_external[0]
href = current_container().name_to_href(chosen_name, edname)
fmt, width, height = identify(current_container().raw_data(chosen_name, decode=False))
ed.insert_image(href, fullpage=fullpage, preserve_aspect_ratio=preserve_ar, width=width, height=height)
elif action[0] == 'insert_hyperlink':
self.commit_all_editors_to_container()
d = InsertLink(current_container(), edname, initial_text=ed.get_smart_selection(), parent=self.gui)
if d.exec() == QDialog.DialogCode.Accepted:
ed.insert_hyperlink(d.href, d.text, template=d.rendered_template)
elif action[0] == 'insert_tag':
d = InsertTag(parent=self.gui)
if d.exec() == QDialog.DialogCode.Accepted:
ed.insert_tag(d.tag)
else:
ed.action_triggered(action)

def rename_class(self, class_name):
self.commit_all_editors_to_container()
text, ok = QInputDialog.getText(self.gui, _('New class name'), _(
'Rename the class {} to?').format(class_name))
if ok:
self.add_savepoint(_('Before: Rename {}').format(class_name))
with BusyCursor():
changed = rename_class(current_container(), class_name, text.strip())
if changed:
self.apply_container_update_to_gui()
self.show_current_diff()
else:
self.rewind_savepoint()
return info_dialog(self.gui, _('No matches'), _(
'No class {} found to change').format(class_name), show=True)

def set_semantics(self):
if not self.ensure_book():
return self.commit_all_editors_to_container()
c = current_container()
if c.book_type == 'azw3':
return error_dialog(self.gui, _('Not supported'), _(
'Semantics are not supported for the AZW3 format.'), show=True)
d = InsertSemantics(c, parent=self.gui)
if d.exec() == QDialog.DialogCode.Accepted and d.changes:
self.add_savepoint(_('Before: Set Semantics'))
d.apply_changes(current_container())
self.apply_container_update_to_gui()

def filter_css(self):
self.commit_all_editors_to_container()
c = current_container()
ed = self.gui.central.current_editor current_name = editor_name(ed)
if current_name and c.mime_map[current_name] not in OEB_DOCS | OEB_STYLES:
current_name = None d = FilterCSS(current_name=current_name, parent=self.gui)
if d.exec() == QDialog.DialogCode.Accepted and d.filtered_properties:
self.add_savepoint(_('Before: Filter style information'))
with BusyCursor():
changed = filter_css(current_container(), d.filtered_properties, names=d.filter_names)
if changed:
self.apply_container_update_to_gui()
self.show_current_diff()
else:
self.rewind_savepoint()
return info_dialog(self.gui, _('No matches'), _(
'No matching style rules were found'), show=True)

def show_find(self):
self.gui.central.show_find()
ed = self.gui.central.current_editor if ed is not None and hasattr(ed, 'selected_text'):
text = ed.selected_text if text and text.strip():
self.gui.central.pre_fill_search(text)

def show_text_search(self):
self.gui.text_search_dock.show()
self.gui.text_search.find.setFocus(Qt.FocusReason.OtherFocusReason)

def search_action_triggered(self, action, overrides=None):
ss = self.gui.saved_searches.isVisible()
trigger_saved_search = ss and (not self.gui.central.search_panel.isVisible() or self.gui.saved_searches.has_focus())
if trigger_saved_search:
return self.gui.saved_searches.trigger_action(action, overrides=overrides)
self.search(action, overrides)

def run_saved_searches(self, searches, action):
ed = self.gui.central.current_editor name = editor_name(ed)
searchable_names = self.gui.file_list.searchable_names if not searches or not validate_search_request(name, searchable_names, getattr(ed, 'has_marked_text', False), searches[0], self.gui):
return ret = run_search(searches, action, ed, name, searchable_names, self.gui, self.show_editor, self.edit_file, self.show_current_diff, self.add_savepoint, self.rewind_savepoint, self.set_modified)
ed = ret is True and self.gui.central.current_editor if getattr(ed, 'has_line_numbers', False):
ed.editor.setFocus(Qt.FocusReason.OtherFocusReason)
else:
self.gui.saved_searches.setFocus(Qt.FocusReason.OtherFocusReason)

def search(self, action, overrides=None):
# Run a search/replace sp = self.gui.central.search_panel
# Ensure the search panel is visible sp.setVisible(True)
ed = self.gui.central.current_editor name = editor_name(ed)
state = sp.state if overrides:
state.update(overrides)
searchable_names = self.gui.file_list.searchable_names if not validate_search_request(name, searchable_names, getattr(ed, 'has_marked_text', False), state, self.gui):
return ret = run_search(state, action, ed, name, searchable_names, self.gui, self.show_editor, self.edit_file, self.show_current_diff, self.add_savepoint, self.rewind_savepoint, self.set_modified)
ed = ret is True and self.gui.central.current_editor if getattr(ed, 'has_line_numbers', False):
ed.editor.setFocus(Qt.FocusReason.OtherFocusReason)
else:
self.gui.saved_searches.setFocus(Qt.FocusReason.OtherFocusReason)

def find_text(self, state):
from calibre.gui2.tweak_book.text_search import run_text_search searchable_names = self.gui.file_list.searchable_names ed = self.gui.central.current_editor name = editor_name(ed)
if not validate_search_request(name, searchable_names, getattr(ed, 'has_marked_text', False), state, self.gui):
return ret = run_text_search(state, ed, name, searchable_names, self.gui, self.show_editor, self.edit_file)
ed = ret is True and self.gui.central.current_editor if getattr(ed, 'has_line_numbers', False):
ed.editor.setFocus(Qt.FocusReason.OtherFocusReason)

def find_initial_text(self, text):
from calibre.gui2.tweak_book.search import get_search_regex from calibre.gui2.tweak_book.text_search import file_matches_pattern search = {'find': text, 'mode': 'normal', 'case_sensitive': True, 'direction': 'down'}
pat = get_search_regex(search)
searchable_names = set(self.gui.file_list.searchable_names['text'])
for name, ed in iteritems(editors):
searchable_names.discard(name)
if ed.find_text(pat, complete=True):
self.show_editor(name)
return for name in searchable_names:
if file_matches_pattern(name, pat):
self.edit_file(name)
if editors[name].find_text(pat, complete=True):
return def find_word(self, word, locations):
# Go to a word from the spell check dialog ed = self.gui.central.current_editor name = editor_name(ed)
find_next_word(word, locations, ed, name, self.gui, self.show_editor, self.edit_file)

def next_spell_error(self):
# Go to the next spelling error ed = self.gui.central.current_editor name = editor_name(ed)
find_next_error(ed, name, self.gui, self.show_editor, self.edit_file, self.close_editor)

def word_change_requested(self, w, new_word):
if self.commit_all_editors_to_container():
self.gui.spell_check.change_word_after_update(w, new_word)
else:
self.gui.spell_check.do_change_word(w, new_word)

def word_replaced(self, changed_names):
self.set_modified()
self.update_editors_from_container(names=set(changed_names))

def word_ignored(self, word, locale):
if tprefs['inline_spell_check']:
for ed in itervalues(editors):
try:
ed.editor.recheck_word(word, locale)
except AttributeError:
pass def editor_link_clicked(self, url):
ed = self.gui.central.current_editor name = editor_name(ed)
if url.startswith('#'):
target = name else:
target = current_container().href_to_name(url, name)
frag = url.partition('#')[-1]
if current_container().has_name(target):
self.link_clicked(target, frag, show_anchor_not_found=True)
else:
try:
purl = urlparse(url)
except ValueError:
return if purl.scheme not in {'', 'file'}:
open_url(QUrl(url))
else:
error_dialog(self.gui, _('Not found'), _(
'No file with the name %s was found in the book') % target, show=True)

def editor_class_clicked(self, class_data):
from calibre.gui2.tweak_book.jump_to_class import NoMatchingRuleFound, NoMatchingTagFound, find_first_matching_rule ed = self.gui.central.current_editor name = editor_name(ed)
try:
res = find_first_matching_rule(current_container(), name, ed.get_raw_data(), class_data)
except (NoMatchingTagFound, NoMatchingRuleFound):
res = None if res is not None and res.file_name and res.rule_address:
editor = self.open_editor_for_name(res.file_name)
if editor:
editor.goto_css_rule(res.rule_address, sourceline_address=res.style_tag_address)
else:
error_dialog(self.gui, _('No matches found'), _('No style rules that match the class {} were found').format( class_data['class']), show=True)

def save_search(self):
state = self.gui.central.search_panel.state self.show_saved_searches()
self.gui.saved_searches.add_predefined_search(state)

def show_saved_searches(self):
self.gui.saved_searches_dock.show()
saved_searches = show_saved_searches def create_checkpoint(self):
text, ok = QInputDialog.getText(self.gui, _('Choose name'), _(
'Choose a name for the checkpoint.\nYou can later restore the book'
' to this checkpoint via the\n"Revert to..." entries in the Edit menu.'))
if ok:
self.add_savepoint(text)

def commit_editor_to_container(self, name, container=None):
container = container or current_container()
ed = editors[name]
with container.open(name, 'wb') as f:
f.write(ed.data)
if name == container.opf_name:
container.refresh_mime_map()
lang = container.opf_xpath('//dc:language/text()') or [self.current_metadata.language]
set_book_locale(lang[0])
if container is current_container():
ed.is_synced_to_container = True if name == container.opf_name:
self.gui.file_list.build(container)

[документация]
def commit_all_editors_to_container(self):
''' Commit any changes that the user has made to files open in editors to the container. You should call this method before performing any actions on the current container '''
changed = False with BusyCursor():
for name, ed in iteritems(editors):
if not ed.is_synced_to_container:
self.commit_editor_to_container(name)
ed.is_synced_to_container = True changed = True return changed

[документация]
def save_book(self):
' Save the book. Saving is performed in the background '
self.gui.update_window_title()
c = current_container()
for name, ed in iteritems(editors):
if ed.is_modified or not ed.is_synced_to_container:
self.commit_editor_to_container(name, c)
ed.is_modified = False path_to_ebook = os.path.abspath(c.path_to_ebook)
destdir = os.path.dirname(path_to_ebook)
if not c.is_dir and not os.path.exists(destdir):
info_dialog(self.gui, _('Path does not exist'), _(
'The file you are editing (%s) no longer exists. You have to choose a new save location.') % path_to_ebook, show_copy_button=False, show=True)
fmt = path_to_ebook.rpartition('.')[-1].lower()
start_dir = find_first_existing_ancestor(path_to_ebook)
path = choose_save_file( self.gui, 'choose-new-save-location', _('Choose file location'), initial_path=os.path.join(start_dir, os.path.basename(path_to_ebook)), filters=[(fmt.upper(), (fmt,))], all_files=False)
if path is not None:
if not path.lower().endswith('.' + fmt):
path = path + '.' + fmt path = os.path.abspath(path)
c.path_to_ebook = path self.global_undo.update_path_to_ebook(path)
else:
return if os.path.exists(c.path_to_ebook) and not os.access(c.path_to_ebook, os.W_OK):
if not question_dialog(self.gui, _('File is read-only'), _(
'The file at {} is read-only. The editor will try to reset its permissions before saving. Proceed with saving?'
).format(c.path_to_ebook), override_icon='dialog_warning.png', yes_text=_('&Save'), no_text=_('&Cancel'), yes_icon='save.png'):
return self.gui.action_save.setEnabled(False)
tdir = self.mkdtemp(prefix='save-')
container = clone_container(c, tdir)
self.save_manager.schedule(tdir, container)def _save_copy(self, post_action=None):
self.gui.update_window_title()
c = current_container()
if c.is_dir:
return error_dialog(self.gui, _('Cannot save a copy'), _(
'Saving a copy of a folder based book is not supported'), show=True)
ext = c.path_to_ebook.rpartition('.')[-1]
path = choose_save_file( self.gui, 'tweak_book_save_copy', _('Choose path'), initial_filename=self.current_metadata.title + '.' + ext, filters=[(_('Book (%s)') % ext.upper(), [ext.lower()])], all_files=False)
if not path:
return if '.' not in os.path.basename(path):
path += '.' + ext.lower()
tdir = self.mkdtemp(prefix='save-copy-')
container = clone_container(c, tdir)
for name, ed in iteritems(editors):
if ed.is_modified or not ed.is_synced_to_container:
self.commit_editor_to_container(name, container)

def do_save(c, path, tdir):
save_container(c, path)
shutil.rmtree(tdir, ignore_errors=True)
return path self.gui.blocking_job('save_copy', _('Saving copy, please wait...'), partial(self.copy_saved, post_action=post_action), do_save, container, path, tdir)

def save_copy(self):
self._save_copy()

def copy_saved(self, job, post_action=None):
if job.traceback is not None:
return error_dialog(self.gui, _('Failed to save copy'),
_('Failed to save copy, click "Show details" for more information.'), det_msg=job.traceback, show=True)
msg = _('Copy saved to %s') % job.result if post_action is None:
info_dialog(self.gui, _('Copy saved'), msg, show=True)
elif post_action == 'replace':
msg = _('Editing saved copy at: %s') % job.result self.open_book(job.result, edit_file=self.currently_editing)
elif post_action == 'edit':
if ismacos:
cmd = ['open', '-F', '-n', '-a', os.path.dirname(os.path.dirname(os.path.dirname(macos_edit_book_bundle_path()))), '--args']
else:
cmd = [exe_path('ebook-edit')]
if islinux:
cmd.append('--detach')
cmd.append(job.result)
ce = self.currently_editing if ce:
cmd.append(ce)
with sanitize_env_vars():
subprocess.Popen(cmd)
self.gui.show_status_message(msg, 5)

def report_save_error(self, tb):
if self.doing_terminal_save:
prints(tb, file=sys.stderr)
self.abort_terminal_save()
self.set_modified()
error_dialog(self.gui, _('Could not save'),
_('Saving of the book failed. Click "Show details"'
' for more information. You can try to save a copy'
' to a different location, via File->Save a copy'), det_msg=tb, show=True)

def go_to_line_number(self):
ed = self.gui.central.current_editor if ed is None or not ed.has_line_numbers:
return num, ok = QInputDialog.getInt(self.gui, _('Enter line number'), ('Line number:'), ed.current_line, 1, max(100000, ed.number_of_lines))
if ok:
ed.current_line = num def split_start_requested(self):
self.commit_all_editors_to_container()
self.gui.preview.do_start_split()

@in_thread_job def split_requested(self, name, loc, totals):
self.add_savepoint(_('Before: Split %s') % self.gui.elided_text(name))
try:
bottom_name = split(current_container(), name, loc, totals=totals)
except AbortError:
self.rewind_savepoint()
raise self.apply_container_update_to_gui()
self.edit_file(bottom_name, 'html')

def multisplit(self):
ed = self.gui.central.current_editor if ed.syntax != 'html':
return name = editor_name(ed)
if name is None:
return d = MultiSplit(self.gui)
if d.exec() == QDialog.DialogCode.Accepted:
with BusyCursor():
self.add_savepoint(_('Before: Split %s') % self.gui.elided_text(name))
try:
multisplit(current_container(), name, d.xpath)
except AbortError:
self.rewind_savepoint()
raise self.apply_container_update_to_gui()

def open_editor_for_name(self, name):
if name in editors:
editor = editors[name]
self.gui.central.show_editor(editor)
else:
try:
mt = current_container().mime_map[name]
except KeyError:
error_dialog(self.gui, _('Does not exist'), _(
'The file %s does not exist. If you were trying to click an item in'
' the Table of Contents, you may'
' need to refresh it by right-clicking and choosing "Refresh".') % name, show=True)
return None syntax = syntax_from_mime(name, mt)
if not syntax:
error_dialog( self.gui, _('Unsupported file format'),
_('Editing files of type %s is not supported') % mt, show=True)
return None editor = self.edit_file(name, syntax)
return editor def link_clicked(self, name, anchor, show_anchor_not_found=False):
if not name:
return editor = self.open_editor_for_name(name)
if anchor and hasattr(editor, 'go_to_anchor'):
if editor.go_to_anchor(anchor):
self.gui.preview.pending_go_to_anchor = anchor elif show_anchor_not_found:
error_dialog(self.gui, _('Not found'), _(
'The anchor %s was not found in this file') % anchor, show=True)

@in_thread_job def check_item_activated(self, item):
is_mult = item.has_multiple_locations and getattr(item, 'current_location_index', None) is not None name = item.all_locations[item.current_location_index][0] if is_mult else item.name editor = None if name in editors:
editor = editors[name]
self.gui.central.show_editor(editor)
else:
try:
editor = self.edit_file_requested(name, None, current_container().mime_map[name])
except KeyError:
error_dialog(self.gui, _('File deleted'), _(
'The file {} has already been deleted, re-run Check Book to update the results.').format(name), show=True)
if getattr(editor, 'has_line_numbers', False):
if is_mult:
editor.go_to_line(*(item.all_locations[item.current_location_index][1:3]))
else:
editor.go_to_line(item.line or 0, item.col or 0)
editor.set_focus()

@in_thread_job def check_requested(self, *args):
if current_container() is None:
return self.commit_all_editors_to_container()
c = self.gui.check_book c.parent().show()
c.parent().raise_and_focus()
c.run_checks(current_container())

def spell_check_requested(self):
if current_container() is None:
return self.commit_all_editors_to_container()
self.add_savepoint(_('Before: Spell Check'))
self.gui.spell_check.show()

@in_thread_job def fix_requested(self, errors):
self.add_savepoint(_('Before: Auto-fix errors'))
c = self.gui.check_book c.parent().show()
c.parent().raise_and_focus()
changed = c.fix_errors(current_container(), errors)
if changed:
self.apply_container_update_to_gui()
self.set_modified()
else:
self.rewind_savepoint()

@in_thread_job def merge_requested(self, category, names, master):
self.add_savepoint(_('Before: Merge files into %s') % self.gui.elided_text(master))
try:
merge(current_container(), category, names, master)
except AbortError:
self.rewind_savepoint()
raise self.apply_container_update_to_gui()
if master in editors:
self.show_editor(master)
self.gui.file_list.merge_completed(master)
self.gui.message_popup(_('{} files merged').format(len(names)))

@in_thread_job def link_stylesheets_requested(self, names, sheets, remove):
self.add_savepoint(_('Before: Link stylesheets'))
changed_names = link_stylesheets(current_container(), names, sheets, remove)
if changed_names:
self.update_editors_from_container(names=changed_names)
self.set_modified()

@in_thread_job def export_requested(self, name_or_names, path):
if isinstance(name_or_names, string_or_bytes):
return self.export_file(name_or_names, path)
for name in name_or_names:
dest = os.path.abspath(os.path.join(path, name))
if '/' in name or os.sep in name:
try:
os.makedirs(os.path.dirname(dest))
except OSError as err:
if err.errno != errno.EEXIST:
raise self.export_file(name, dest)

def open_file_with(self, file_name, fmt, entry):
if file_name in editors and not editors[file_name].is_synced_to_container:
self.commit_editor_to_container(file_name)
with current_container().open(file_name) as src:
tdir = PersistentTemporaryDirectory(suffix='-ee-ow')
with open(os.path.join(tdir, os.path.basename(file_name)), 'wb') as dest:
shutil.copyfileobj(src, dest)
from calibre.gui2.open_with import run_program run_program(entry, dest.name, self)
if question_dialog(self.gui, _('File opened'), _(
'When you are done editing {0} click "Import" to update'
' the file in the book or "Discard" to lose any changes.').format(file_name), yes_text=_('Import'), no_text=_('Discard')
):
self.add_savepoint(_('Before: Replace %s') % file_name)
with open(dest.name, 'rb') as src, current_container().open(file_name, 'wb') as cdest:
shutil.copyfileobj(src, cdest)
self.apply_container_update_to_gui()
try:
shutil.rmtree(tdir)
except Exception:
pass

@in_thread_job def copy_files_to_clipboard(self, names):
names = tuple(names)
for name in names:
if name in editors and not editors[name].is_synced_to_container:
self.commit_editor_to_container(name)
container = current_container()
md = QMimeData()
url_map = { name:container.get_file_path_for_processing(name, allow_modification=False)
for name in names
}
from calibre.gui2.dnd import set_urls_from_local_file_paths set_urls_from_local_file_paths(md, *url_map.values())
import json md.setData(FILE_COPY_MIME, as_bytes(json.dumps({ name: (url_map[name], container.mime_map.get(name)) for name in names
})))
QApplication.instance().clipboard().setMimeData(md)

@in_thread_job def paste_files_from_clipboard(self):
md = QApplication.instance().clipboard().mimeData()
if md.hasUrls() and md.hasFormat(FILE_COPY_MIME):
import json self.commit_all_editors_to_container()
name_map = json.loads(bytes(md.data(FILE_COPY_MIME)))
container = current_container()
for name, (path, mt) in iteritems(name_map):
with open(path, 'rb') as f:
container.add_file(name, f.read(), media_type=mt, modify_name_if_needed=True)
self.apply_container_update_to_gui()

def export_file(self, name, path):
if name in editors and not editors[name].is_synced_to_container:
self.commit_editor_to_container(name)
with current_container().open(name, 'rb') as src, open(path, 'wb') as dest:
shutil.copyfileobj(src, dest)

@in_thread_job def replace_requested(self, name, path, basename, force_mt):
self.add_savepoint(_('Before: Replace %s') % name)
replace_file(current_container(), name, path, basename, force_mt)
self.apply_container_update_to_gui()

def browse_images(self):
self.gui.image_browser.refresh()
self.gui.image_browser.show()
self.gui.image_browser.raise_and_focus()

def show_reports(self):
if not self.ensure_book(_('You must first open a book in order to see the report.')):
return self.gui.reports.refresh()
self.gui.reports.show()
self.gui.reports.raise_and_focus()

def reports_edit_requested(self, name):
mt = current_container().mime_map.get(name, guess_type(name))
self.edit_file_requested(name, None, mt)

def image_activated(self, name):
mt = current_container().mime_map.get(name, guess_type(name))
self.edit_file_requested(name, None, mt)

def check_external_links(self):
if self.ensure_book(_('You must first open a book in order to check links.')):
self.commit_all_editors_to_container()
self.gui.check_external_links.show()

def embed_tts(self):
if not self.ensure_book(_('You must first open a book in order to add Text-to-speech narration.')):
return self.commit_all_editors_to_container()
from calibre.gui2.tweak_book.tts import TTSEmbed self.add_savepoint(_('Before: adding narration'))
d = TTSEmbed(current_container(), self.gui)
if d.exec() == QDialog.DialogCode.Accepted:
self.apply_container_update_to_gui()
self.show_current_diff()
else:
self.rewind_savepoint()

def compress_images(self):
if not self.ensure_book(_('You must first open a book in order to compress images.')):
return from calibre.gui2.tweak_book.polish import CompressImages, CompressImagesProgress, show_report d = CompressImages(self.gui)
if d.exec() == QDialog.DialogCode.Accepted:
with BusyCursor():
self.add_savepoint(_('Before: compress images'))
d = CompressImagesProgress(names=d.names, jpeg_quality=d.jpeg_quality, webp_quality=d.webp_quality, parent=self.gui)
if d.exec() != QDialog.DialogCode.Accepted:
self.rewind_savepoint()
return changed, report = d.result if changed is None and report:
self.rewind_savepoint()
return error_dialog(self.gui, _('Unexpected error'), _(
'Failed to compress images, click "Show details" for more information'), det_msg=report, show=True)
if changed:
self.apply_container_update_to_gui()
else:
self.rewind_savepoint()
show_report(changed, self.current_metadata.title, report, self.gui, self.show_current_diff)

def sync_editor_to_preview(self, name, sourceline_address):
editor = self.edit_file(name, 'html')
self.ignore_preview_to_editor_sync = True try:
editor.goto_sourceline(*sourceline_address)
finally:
self.ignore_preview_to_editor_sync = False def do_sync_preview_to_editor(self, wait_for_highlight_to_finish=False):
if self.ignore_preview_to_editor_sync:
return ed = self.gui.central.current_editor if ed is not None:
name = editor_name(ed)
if name is not None and getattr(ed, 'syntax', None) == 'html':
hl = getattr(ed, 'highlighter', None)
if wait_for_highlight_to_finish:
if getattr(hl, 'is_working', False):
QTimer.singleShot(75, self.sync_preview_to_editor_on_highlight_finish)
return ct = ed.current_tag()
self.gui.preview.sync_to_editor(name, ct)
if hl is not None and hl.is_working:
QTimer.singleShot(75, self.sync_preview_to_editor_on_highlight_finish)

[документация]
def sync_preview_to_editor(self):
' Sync the position of the preview panel to the current cursor position in the current editor '
self.do_sync_preview_to_editor()

def sync_preview_to_editor_on_highlight_finish(self):
self.do_sync_preview_to_editor(wait_for_highlight_to_finish=True)

def show_partial_cfi_in_editor(self, name, cfi):
editor = self.edit_file(name, 'html')
if not editor or not editor.has_line_numbers:
return False from calibre.ebooks.epub.cfi.parse import decode_cfi from calibre.ebooks.oeb.polish.parsing import parse root = parse( editor.get_raw_data(), decoder=lambda x: x.decode('utf-8'), line_numbers=True, linenumber_attribute='data-lnum')
node = decode_cfi(root, cfi)

def barename(x):
return x.tag.partition('}')[-1]

if node is not None:
lnum = node.get('data-lnum')
if lnum:
tags_before = []
for tag in root.xpath(f'//*[@data-lnum="{lnum}"]'):
tags_before.append(barename(tag))
if tag is node:
break else:
tags_before.append(barename(node))
lnum = int(lnum)
return editor.goto_sourceline(lnum, tags_before, attribute='id' if node.get('id') else None)
return False def goto_style_declaration(self, data):
name = data['name']
editor = self.edit_file(name, syntax=data['syntax'])
self.gui.live_css.navigate_to_declaration(data, editor)

def init_editor(self, name, editor, data=None, use_template=False):
editor.undo_redo_state_changed.connect(self.editor_undo_redo_state_changed)
editor.data_changed.connect(self.editor_data_changed)
editor.copy_available_state_changed.connect(self.editor_copy_available_state_changed)
editor.cursor_position_changed.connect(self.sync_preview_to_editor)
editor.cursor_position_changed.connect(self.update_cursor_position)
if hasattr(editor, 'word_ignored'):
editor.word_ignored.connect(self.word_ignored)
if hasattr(editor, 'link_clicked'):
editor.link_clicked.connect(self.editor_link_clicked)
if hasattr(editor, 'class_clicked'):
editor.class_clicked.connect(self.editor_class_clicked)
if hasattr(editor, 'rename_class'):
editor.rename_class.connect(self.rename_class)
if getattr(editor, 'syntax', None) == 'html':
editor.smart_highlighting_updated.connect(self.gui.live_css.sync_to_editor)
if hasattr(editor, 'set_request_completion'):
editor.set_request_completion(partial(self.request_completion, name), name)
if data is not None:
if use_template:
editor.init_from_template(data)
else:
editor.data = data editor.is_synced_to_container = True editor.modification_state_changed.connect(self.editor_modification_state_changed)
self.gui.central.add_editor(name, editor)

[документация]
def edit_file(self, name, syntax=None, use_template=None):
''' Open the file specified by name in an editor

:param syntax: The media type of the file, for example, ``'text/html'``. If not specified it is guessed from the file extension.
:param use_template: A template to initialize the opened editor with
'''
editor = editors.get(name, None)
if editor is None:
syntax = syntax or syntax_from_mime(name, guess_type(name))
if use_template is None:
data = current_container().raw_data(name)
if isbytestring(data) and syntax in {'html', 'css', 'text', 'xml', 'javascript'}:
try:
data = data.decode('utf-8')
except UnicodeDecodeError:
return error_dialog(self.gui, _('Cannot decode'), _(
'Cannot edit %s as it appears to be in an unknown character encoding') % name, show=True)
else:
data = use_template editor = editors[name] = editor_from_syntax(syntax, self.gui.editor_tabs)
self.init_editor(name, editor, data, use_template=bool(use_template))
if tprefs['pretty_print_on_open']:
editor.pretty_print(name)
self.show_editor(name)
return editor

[документация]
def show_editor(self, name):
' Show the editor that is editing the file specified by ``name`` '
self.gui.central.show_editor(editors[name])
editors[name].set_focus()

def edit_file_requested(self, name, syntax=None, mime=None):
if name in editors:
self.gui.central.show_editor(editors[name])
return editors[name]
mime = mime or current_container().mime_map.get(name, guess_type(name))
syntax = syntax or syntax_from_mime(name, mime)
if not syntax:
return error_dialog( self.gui, _('Unsupported file format'),
_('Editing files of type %s is not supported') % mime, show=True)
return self.edit_file(name, syntax)

def edit_next_file(self, backwards=False):
self.gui.file_list.edit_next_file(self.currently_editing, backwards)

def quick_open(self):
if not self.ensure_book(_('No book is currently open. You must first open a book to edit.')):
return c = current_container()
files = [name for name, mime in iteritems(c.mime_map) if c.exists(name) and syntax_from_mime(name, mime) is not None]
d = QuickOpen(files, parent=self.gui)
if d.exec() == QDialog.DialogCode.Accepted and d.selected_result is not None:
self.edit_file_requested(d.selected_result, None, c.mime_map[d.selected_result])

# Editor basic controls {{{ def do_editor_undo(self):
ed = self.gui.central.current_editor if ed is not None:
ed.undo()

def do_editor_redo(self):
ed = self.gui.central.current_editor if ed is not None:
ed.redo()

def do_editor_copy(self):
ed = self.gui.central.current_editor if ed is not None:
ed.copy()

def do_editor_cut(self):
ed = self.gui.central.current_editor if ed is not None:
ed.cut()

def do_editor_paste(self):
ed = self.gui.central.current_editor if ed is not None:
ed.paste()

def toggle_line_wrapping_in_all_editors(self):
tprefs['editor_line_wrap'] ^= True yes = tprefs['editor_line_wrap']
for ed in editors.values():
if getattr(ed, 'editor', None) and hasattr(ed.editor, 'apply_line_wrap_mode'):
ed.editor.apply_line_wrap_mode(yes)

def editor_data_changed(self, editor):
self.gui.preview.start_refresh_timer()
for name, ed in iteritems(editors):
if ed is editor:
self.gui.toc_view.start_refresh_timer(name)
break def editor_undo_redo_state_changed(self, *args):
self.apply_current_editor_state()

def editor_copy_available_state_changed(self, *args):
self.apply_current_editor_state()

def editor_modification_state_changed(self, is_modified):
self.apply_current_editor_state()
if is_modified:
self.set_modified()
# }}}

def apply_current_editor_state(self):
ed = self.gui.central.current_editor self.gui.cursor_position_widget.update_position()
if ed is not None:
actions['editor-undo'].setEnabled(ed.undo_available)
actions['editor-redo'].setEnabled(ed.redo_available)
actions['editor-copy'].setEnabled(ed.copy_available)
actions['editor-cut'].setEnabled(ed.cut_available)
actions['go-to-line-number'].setEnabled(ed.has_line_numbers)
actions['fix-html-current'].setEnabled(ed.syntax == 'html')
name = editor_name(ed)
mime = current_container().mime_map.get(name)
if name is not None and (getattr(ed, 'syntax', None) == 'html' or mime == 'image/svg+xml'):
if self.gui.preview.show(name):
# The file being displayed by the preview has changed.
# Set the preview's position to the current cursor
# position in the editor, in case the editors' cursor
# position has not changed, since the last time it was
# focused. This is not inefficient since multiple requests
# to sync are de-bounced with a 100 msec wait.
self.sync_preview_to_editor()
if name is not None:
self.gui.file_list.mark_name_as_current(name)
if ed.has_line_numbers:
self.gui.cursor_position_widget.update_position(*ed.cursor_position)
else:
actions['go-to-line-number'].setEnabled(False)
self.gui.file_list.clear_currently_edited_name()

def update_cursor_position(self):
ed = self.gui.central.current_editor if getattr(ed, 'has_line_numbers', False):
self.gui.cursor_position_widget.update_position(*ed.cursor_position)
else:
self.gui.cursor_position_widget.update_position()

def editor_close_requested(self, editor):
name = editor_name(editor)
if not name:
return if not editor.is_synced_to_container:
self.commit_editor_to_container(name)
self.close_editor(name)

[документация]
def close_editor(self, name):
' Close the editor that is editing the file specified by ``name`` '
editor = editors.pop(name)
self.gui.central.close_editor(editor)
editor.break_cycles()
if not editors or getattr(self.gui.central.current_editor, 'syntax', None) != 'html':
self.gui.preview.clear()
self.gui.live_css.clear()

def insert_character(self):
self.gui.insert_char.show()

def manage_snippets(self):
from calibre.gui2.tweak_book.editor.snippets import UserSnippets
UserSnippets(self.gui).exec()

def merge_files(self):
self.gui.file_list.merge_files()

# Shutdown {{{ def quit(self):
if self.doing_terminal_save:
return False if self.save_manager.has_tasks:
if question_dialog( self.gui, _('Are you sure?'), _(
'The current book is being saved in the background. Quitting now will'
' <b>abort the save process</b>! Finish saving first?'), yes_text=_('Finish &saving first'), no_text=_('&Quit immediately')):
if self.save_manager.has_tasks:
self.start_terminal_save_indicator()
return False if not self.confirm_quit():
return False self.shutdown()
QApplication.instance().exit()
return True def confirm_quit(self):
if self.gui.action_save.isEnabled():
d = QDialog(self.gui)
d.l = QGridLayout(d)
d.setLayout(d.l)
d.setWindowTitle(_('Unsaved changes'))
d.i = QLabel('')
d.i.setMaximumSize(QSize(64, 64))
d.i.setPixmap(QIcon.ic('dialog_warning.png').pixmap(d.i.maximumSize()))
d.l.addWidget(d.i, 0, 0)
d.m = QLabel(_('There are unsaved changes, if you quit without saving, you will lose them.'))
d.m.setWordWrap(True)
d.l.addWidget(d.m, 0, 1)
d.bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Cancel)
d.bb.rejected.connect(d.reject)
d.bb.accepted.connect(d.accept)
d.l.addWidget(d.bb, 1, 0, 1, 2)
d.do_save = None def endit(d, x):
d.do_save = x d.accept()
b = d.bb.addButton(_('&Save and Quit'), QDialogButtonBox.ButtonRole.ActionRole)
b.setIcon(QIcon.ic('save.png'))
connect_lambda(b.clicked, d, lambda d: endit(d, True))
b = d.bb.addButton(_('&Quit without saving'), QDialogButtonBox.ButtonRole.ActionRole)
connect_lambda(b.clicked, d, lambda d: endit(d, False))
d.resize(d.sizeHint())
if d.exec() != QDialog.DialogCode.Accepted or d.do_save is None:
return False if d.do_save:
self.gui.action_save.trigger()
self.start_terminal_save_indicator()
return False return True def start_terminal_save_indicator(self):
self.save_state()
self.gui.blocking_job.set_msg(_('Saving, please wait...'))
self.gui.blocking_job.start()
self.doing_terminal_save = True def abort_terminal_save(self):
self.doing_terminal_save = False self.gui.blocking_job.stop()

def check_terminal_save(self):
if self.doing_terminal_save and not self.save_manager.has_tasks: # terminal save could have been aborted self.shutdown()
QApplication.instance().exit()

def shutdown(self):
self.save_state()
completion_worker().shutdown()
self.save_manager.check_for_completion.disconnect()
self.gui.preview.stop_refresh_timer()
self.gui.live_css.stop_update_timer()
[x.reject() for x in _diff_dialogs]
del _diff_dialogs[:]
self.save_manager.shutdown()
parse_worker.shutdown()
self.save_manager.wait(0.1)

def save_state(self):
with self.editor_cache:
self.save_book_edit_state()
with tprefs:
self.gui.save_state()

def save_book_edit_state(self):
c = current_container()
if c and c.path_to_ebook:
tprefs = self.editor_cache mem = tprefs['edit_book_state']
order = tprefs['edit_book_state_order']
extra = len(order) - 99 if extra > 0:
order = [k for k in order[extra:] if k in mem]
mem = {k:mem[k] for k in order}
mem[c.path_to_ebook] = {
'editors':{name:ed.current_editing_state for name, ed in iteritems(editors)},
'currently_editing':self.currently_editing,
'tab_order':self.gui.central.tab_order,
}
try:
order.remove(c.path_to_ebook)
except ValueError:
pass order.append(c.path_to_ebook)
tprefs['edit_book_state'] = mem tprefs['edit_book_state_order'] = order def restore_book_edit_state(self):
c = current_container()
if c and c.path_to_ebook:
tprefs = self.editor_cache state = tprefs['edit_book_state'].get(c.path_to_ebook)
if state is not None:
opened = set()
eds = state.get('editors', {})
for name in state.get('tab_order', ()):
if c.has_name(name):
try:
editor = self.edit_file_requested(name)
if editor is not None:
opened.add(name)
es = eds.get(name)
if es is not None:
editor.current_editing_state = es except Exception:
import traceback traceback.print_exc()
ce = state.get('currently_editing')
if ce in opened:
self.show_editor(ce)# }}}

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.gui2.tweak_book.plugin

Исходный код calibre.gui2.tweak_book.plugin

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'

import importlib import sys from qt.core import QToolButton from calibre import prints from calibre.customize import PluginInstallationType from calibre.customize.ui import all_edit_book_tool_plugins from calibre.gui2.tweak_book import current_container, tprefs from calibre.gui2.tweak_book.boss import get_boss from polyglot.builtins import itervalues

[документация]
class Tool:
'''
The base class for individual tools in an Edit Book plugin. Useful members include:

* ``self.plugin``: A reference to the :class:`calibre.customize.Plugin` object to which this tool belongs.
* self. :attr:`boss`
* self. :attr:`gui`

Methods that must be overridden in sub classes:

* :meth:`create_action`
* :meth:`register_shortcut`

'''

#: Set this to a unique name it will be used as a key name = None

#: If True the user can choose to place this tool in the plugins toolbar allowed_in_toolbar = True

#: If True the user can choose to place this tool in the plugins menu allowed_in_menu = True

#: The popup mode for the menu (if any) of the toolbar button. Possible values are 'delayed', 'instant', 'button'
toolbar_button_popup_mode = 'delayed'

@property def boss(self):
' The :class:`calibre.gui2.tweak_book.boss.Boss` object. Used to control the user interface. '
return get_boss()

@property def gui(self):
' The main window of the user interface '
return self.boss.gui

@property def current_container(self):
' Return the current :class:`calibre.ebooks.oeb.polish.container.Container` object that represents the book being edited. '
return current_container()

[документация]
def register_shortcut(self, qaction, unique_name, default_keys=(), short_text=None, description=None, **extra_data):
'''
Register a keyboard shortcut that will trigger the specified ``qaction``. This keyboard shortcut will become automatically customizable by the user in the Keyboard shortcuts section of the editor preferences.

:param qaction: A QAction object, it will be triggered when the configured key combination is pressed by the user.
:param unique_name: A unique name for this shortcut/action. It will be used internally, it must not be shared by any other actions in this plugin.
:param default_keys: A list of the default keyboard shortcuts. If not specified no default shortcuts will be set. If the shortcuts specified here conflict with either builtin shortcuts or shortcuts from user configuration/other plugins, they will be ignored. In that case, users will have to configure the shortcuts manually via Preferences. For example:
``default_keys=('Ctrl+J', 'F9')``.
:param short_text: An optional short description of this action. If not specified the text from the QAction will be used.
:param description: An optional longer description of this action, it will be used in the preferences entry for this shortcut.
'''
short_text = short_text or str(qaction.text()).replace('&&', '\0').replace('&', '').replace('\0', '&')
self.gui.keyboard.register_shortcut( self.name + '_' + unique_name, short_text, default_keys=default_keys, action=qaction, description=description or '', group=_('Plugins'))

[документация]
def create_action(self, for_toolbar=True):
'''
Create a QAction that will be added to either the plugins toolbar or the plugins menu depending on ``for_toolbar``. For example::

def create_action(self, for_toolbar=True):
ac = QAction(get_icons('myicon.png'), 'Do something')
if for_toolbar:
# We want the toolbar button to have a popup menu menu = QMenu()
ac.setMenu(menu)
menu.addAction('Do something else')
subaction = menu.addAction('And another')

# Register a keyboard shortcut for this toolbar action be
# careful to do this for only one of the toolbar action or
# the menu action, not both.
self.register_shortcut(ac, 'some-unique-name', default_keys=('Ctrl+K',))
return ac

.. seealso:: Method :meth:`register_shortcut`.
'''
raise NotImplementedError()

def load_plugin_tools(plugin):
try:
main = importlib.import_module(plugin.__class__.__module__+'.main')
except ImportError:
import traceback traceback.print_exc()
else:
for x in itervalues(vars(main)):
if isinstance(x, type) and x is not Tool and issubclass(x, Tool):
ans = x()
ans.plugin = plugin yield ans def plugin_action_sid(plugin, tool, for_toolbar=True):
return plugin.name + tool.name + ('toolbar' if for_toolbar else 'menu')

plugin_toolbar_actions = []

def create_plugin_action(plugin, tool, for_toolbar, actions=None, toolbar_actions=None, plugin_menu_actions=None):
try:
ac = tool.create_action(for_toolbar=for_toolbar)
if ac is None:
raise RuntimeError('create_action() failed to return an action')
except Exception:
prints('Failed to create action for tool:', tool.name)
import traceback traceback.print_exc()
return sid = plugin_action_sid(plugin, tool, for_toolbar)
if actions is not None and sid in actions:
prints(f'The {tool.name} tool from the {plugin.name} plugin has a non unique name, ignoring')
else:
if actions is not None:
actions[sid] = ac ac.sid = sid if for_toolbar:
if toolbar_actions is not None:
toolbar_actions[sid] = ac plugin_toolbar_actions.append(ac)
ac.popup_mode = {'instant':QToolButton.ToolButtonPopupMode.InstantPopup, 'button':QToolButton.ToolButtonPopupMode.MenuButtonPopup}.get( tool.toolbar_button_popup_mode, QToolButton.ToolButtonPopupMode.DelayedPopup)
else:
if plugin_menu_actions is not None:
plugin_menu_actions.append(ac)
return ac

_tool_memory = [] # Needed to prevent the tool object from being garbage collected def create_plugin_actions(actions, toolbar_actions, plugin_menu_actions):
del _tool_memory[:]
del plugin_toolbar_actions[:]

for plugin in all_edit_book_tool_plugins():
try:
tools = tuple(load_plugin_tools(plugin))
except Exception:
if plugin.installation_type is PluginInstallationType.BUILTIN:
raise print('Failed to load third-party plugin:', plugin.name, file=sys.stderr)
import traceback traceback.print_exc()
continue for tool in tools:
_tool_memory.append(tool)
if tool.allowed_in_toolbar:
create_plugin_action(plugin, tool, True, actions, toolbar_actions, plugin_menu_actions)
if tool.allowed_in_menu:
create_plugin_action(plugin, tool, False, actions, toolbar_actions, plugin_menu_actions)

def install_plugin(plugin):
for tool in load_plugin_tools(plugin):
if tool.allowed_in_toolbar:
sid = plugin_action_sid(plugin, tool, True)
if sid not in tprefs['global_plugins_toolbar']:
tprefs['global_plugins_toolbar'] = tprefs['global_plugins_toolbar'] + [sid]

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.utils.formatter_functions

Исходный код calibre.utils.formatter_functions

#!/usr/bin/env python

'''
Created on 13 Jan 2011

@author: charles
'''

__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import inspect import numbers import os.path import posixpath import re import traceback from contextlib import suppress from datetime import datetime, timedelta from enum import Enum, auto from functools import partial from math import ceil, floor, modf, trunc from calibre import human_readable, prepare_string_for_xml, prints from calibre.constants import DEBUG from calibre.db.constants import DATA_DIR_NAME, DATA_FILE_PATTERN from calibre.ebooks.metadata import title_sort from calibre.ebooks.metadata.book.base import field_metadata from calibre.ebooks.metadata.search_internet import qquote from calibre.utils.config import tweaks from calibre.utils.date import UNDEFINED_DATE, format_date, now, parse_date from calibre.utils.icu import capitalize, sort_key, strcmp from calibre.utils.icu import lower as icu_lower from calibre.utils.localization import _ as xlated from calibre.utils.localization import calibre_langcode_to_name, canonicalize_lang from calibre.utils.titlecase import titlecase from polyglot.builtins import iteritems, itervalues

UNKNOWN = _('Unknown')
RELATIONAL = _('Relational')
STRING_MANIPULATION = _('String manipulation')
IF_THEN_ELSE = _('If-then-else')
ARITHMETIC = _('Arithmetic')
RECURSION = _('Recursion')
OTHER = _('Other')
LIST_MANIPULATION = _('List manipulation')
LIST_LOOKUP = _('List lookup')
GET_FROM_METADATA = _('Get values from metadata')
ITERATING_VALUES = _('Iterate over values')
BOOLEAN = _('Boolean')
FORMATTING_VALUES = _('Formatting values')
CASE_CHANGES = _('Case changes')
DATE_FUNCTIONS = _('Date functions')
DB_FUNCS = _('Database functions')
URL_FUNCTIONS = _('URL functions')

# Class and method to save an untranslated copy of translated strings class TranslatedStringWithRaw(str):

def __new__(cls, raw_english, raw_other, formatted_english, formatted_other):
instance = super().__new__(cls, formatted_other)
instance.raw_english = raw_english instance.raw_other = raw_other instance.formatted_english = formatted_english instance.formatted_other = formatted_other instance.did_format = False return instance def format(self, *args, **kw):
formatted_english = self.raw_english.format(*args, **kw)
formatted_other = self.raw_other.format(*args, **kw)
v = TranslatedStringWithRaw(self.raw_english, self.raw_other, formatted_english, formatted_other)
v.saved_args = args v.saved_kwargs = kw v.did_format = True return v def format_again(self, txt):
if self.did_format:
return txt.format(*self.saved_args, **self.saved_kwargs)
return txt def _(txt):
translated = xlated(txt)
return TranslatedStringWithRaw(txt, translated, txt, translated)

class StoredObjectType(Enum):
PythonFunction = auto()
StoredGPMTemplate = auto()
StoredPythonTemplate = auto()

class FormatterFunctions:

error_function_body = ('def evaluate(self, formatter, kwargs, mi, locals):\n'
'\treturn "' +
_('Duplicate user function name {0}. '
'Change the name or ensure that the functions are identical') + '"')

def __init__(self):
self._builtins = {}
self._functions = {}
self._functions_from_library = {}

def register_builtin(self, func_class):
if not isinstance(func_class, FormatterFunction):
raise ValueError(f'Class {func_class.__class__.__name__} is not an instance of FormatterFunction')
name = func_class.name if name in self._functions:
raise ValueError(f'Name {name} already used')
self._builtins[name] = func_class self._functions[name] = func_class for a in func_class.aliases:
self._functions[a] = func_class def _register_function(self, func_class, replace=False):
if not isinstance(func_class, FormatterFunction):
raise ValueError(f'Class {func_class.__class__.__name__} is not an instance of FormatterFunction')
name = func_class.name if not replace and name in self._functions:
raise ValueError(f'Name {name} already used')
self._functions[name] = func_class def register_functions(self, library_uuid, funcs):
self._functions_from_library[library_uuid] = funcs self._register_functions()

def _register_functions(self):
for compiled_funcs in itervalues(self._functions_from_library):
for cls in compiled_funcs:
f = self._functions.get(cls.name, None)
replace = False if f is not None:
existing_body = f.program_text new_body = cls.program_text if new_body != existing_body:
# Change the body of the template function to one that will
# return an error message. Also change the arg count to
# -1 (variable) to avoid template compilation errors if DEBUG:
print(f'attempt to replace formatter function {f.name} with a different body')
replace = True func = [cls.name, '', -1, self.error_function_body.format(cls.name)]
cls = compile_user_function(*func)
else:
continue formatter_functions()._register_function(cls, replace=replace)

def unregister_functions(self, library_uuid):
if library_uuid in self._functions_from_library:
for cls in self._functions_from_library[library_uuid]:
self._functions.pop(cls.name, None)
self._functions_from_library.pop(library_uuid)
self._register_functions()

def get_builtins(self):
return self._builtins def get_builtins_and_aliases(self):
res = {}
for f in itervalues(self._builtins):
res[f.name] = f for a in f.aliases:
res[a] = f return res def get_functions(self):
return self._functions def reset_to_builtins(self):
self._functions = {}
for n,c in self._builtins.items():
self._functions[n] = c for a in c.aliases:
self._functions[a] = c

_ff = FormatterFunctions()

def formatter_functions():
global _ff return _ff def only_in_gui_error(name):
raise ValueError(_('The function {} can be used only in the GUI').format(name))

def get_database(mi, name):
try:
proxy = mi.get('_proxy_metadata', None)
except Exception:
proxy = None if proxy is None:
if name is not None:
only_in_gui_error(name)
return None wr = proxy.get('_db', None)
if wr is None:
if name is not None:
raise ValueError(_('In function {}: The database has been closed').format(name))
return None cache = wr()
if cache is None:
if name is not None:
raise ValueError(_('In function {}: The database has been closed').format(name))
return None wr = getattr(cache, 'database_instance', None)
if wr is None:
if name is not None:
only_in_gui_error(name)
return None db = wr()
if db is None:
if name is not None:
raise ValueError(_('In function {}: The database has been closed').format(name))
return None return db class FormatterFunction:

doc = _('No documentation provided')
name = 'no name provided'
category = UNKNOWN arg_count = 0 aliases = []
object_type = StoredObjectType.PythonFunction def evaluate(self, formatter, kwargs, mi, locals, *args):
raise NotImplementedError()

def eval_(self, formatter, kwargs, mi, locals, *args):
ret = self.evaluate(formatter, kwargs, mi, locals, *args)
if isinstance(ret, (bytes, str)):
return ret if isinstance(ret, list):
return ','.join(ret)
if isinstance(ret, (numbers.Number, bool)):
return str(ret)

def only_in_gui_error(self):
only_in_gui_error(self.name)

def get_database(self, mi, formatter=None):
# Prefer the db that comes from proxy_metadata because it is probably an
# instance of LibraryDatabase where the one in the formatter might be an
# instance of Cache formatter_db = getattr(formatter, 'database', None)
if formatter_db is None:
# The formatter doesn't have a database. Try to get one from
# proxy_metadata. This will raise an exception because the name
# parameter is not None return get_database(mi, self.name)
else:
# We have a formatter db. Try to get the db from proxy_metadata but
# don't raise an exception if one isn't available.
legacy_db = get_database(mi, None)
return legacy_db if legacy_db is not None else formatter_db class BuiltinFormatterFunction(FormatterFunction):

def __init__(self):
formatter_functions().register_builtin(self)
eval_func = inspect.getmembers(self.__class__, lambda x: inspect.isfunction(x) and x.__name__ == 'evaluate')
try:
lines = [l[4:] for l in inspect.getsourcelines(eval_func[0][1])[0]]
except Exception:
lines = []
self.program_text = ''.join(lines)

[документация]
class BuiltinStrcmp(BuiltinFormatterFunction):
name = 'strcmp'
arg_count = 5 category = RELATIONAL
__doc__ = doc = _( r'''
``strcmp(x, y, lt, eq, gt)`` -- does a case-insensitive lexical comparison of
``x`` and ``y``.[/] Returns ``lt`` if ``x < y``, ``eq`` if ``x == y``, otherwise
``gt``. This function can often be replaced by one of the lexical comparison operators (``==``, ``>``, ``<``, etc.)
''')

def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):
v = strcmp(x, y)
if v < 0:
return lt if v == 0:
return eq return gt

[документация]
class BuiltinStrcmpcase(BuiltinFormatterFunction):
name = 'strcmpcase'
arg_count = 5 category = RELATIONAL
__doc__ = doc = _( r'''
``strcmpcase(x, y, lt, eq, gt)`` -- does a case-sensitive lexical comparison of
``x`` and ``y``.[/] Returns ``lt`` if ``x < y``, ``eq`` if ``x == y``, otherwise
``gt``.

Note: This is NOT the default behavior used by calibre, for example, in the lexical comparison operators (``==``, ``>``, ``<``, etc.). This function could cause unexpected results, preferably use ``strcmp()`` whenever possible.
''')

def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):
from calibre.utils.icu import case_sensitive_strcmp as case_strcmp v = case_strcmp(x, y)
if v < 0:
return lt if v == 0:
return eq return gt

[документация]
class BuiltinCmp(BuiltinFormatterFunction):
name = 'cmp'
category = RELATIONAL arg_count = 5
__doc__ = doc = _( r'''
``cmp(value, y, lt, eq, gt)`` -- compares ``value`` and ``y`` after converting both to numbers.[/] Returns ``lt`` if ``value <# y``, ``eq`` if ``value ==# y``, otherwise ``gt``.
This function can usually be replaced with one of the numeric compare operators
(``==#``, ``<#``, ``>#``, etc).
''')

def evaluate(self, formatter, kwargs, mi, locals, value, y, lt, eq, gt):
value = float(value if value and value != 'None' else 0)
y = float(y if y and y != 'None' else 0)
if value < y:
return lt if value == y:
return eq return gt

[документация]
class BuiltinFirstMatchingCmp(BuiltinFormatterFunction):
name = 'first_matching_cmp'
category = RELATIONAL arg_count = -1
__doc__ = doc = _( r'''
``first_matching_cmp(val, [ cmp, result, ]* else_result)`` -- compares ``val < cmp`` in sequence, returning the associated ``result`` for the first comparison that succeeds.[/] Returns ``else_result`` if no comparison succeeds.

Example:
[CODE]
i = 10; first_matching_cmp(i,5,"small",10,"middle",15,"large","giant")
[/CODE]
returns ``"large"``. The same example with a first value of 16 returns ``"giant"``.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
if (len(args) % 2) != 0:
raise ValueError(_('first_matching_cmp requires an even number of arguments'))
val = float(args[0] if args[0] and args[0] != 'None' else 0)
for i in range(1, len(args) - 1, 2):
c = float(args[i] if args[i] and args[i] != 'None' else 0)
if val < c:
return args[i+1]
return args[len(args)-1]

[документация]
class BuiltinStrcat(BuiltinFormatterFunction):
name = 'strcat'
arg_count = -1 category = STRING_MANIPULATION
__doc__ = doc = _( r'''
``strcat(a [, b]*)`` -- returns a string formed by concatenating all the arguments.[/] Can take any number of arguments. In most cases you can use the
``&`` operator instead of this function.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
i = 0 res = ''
for i in range(len(args)):
res += args[i]
return res

[документация]
class BuiltinStrlen(BuiltinFormatterFunction):
name = 'strlen'
arg_count = 1 category = STRING_MANIPULATION
__doc__ = doc = _( r'''
``strlen(value)`` -- Returns the length of the string ``value``.
''')

def evaluate(self, formatter, kwargs, mi, locals, a):
try:
return len(a)
except Exception:
return -1

[документация]
class BuiltinAdd(BuiltinFormatterFunction):
name = 'add'
arg_count = -1 category = ARITHMETIC
__doc__ = doc = _(
'''
``add(x [, y]*)`` -- returns the sum of its arguments.[/] Throws an exception if an argument is not a number. In most cases you can use the ``+`` operator instead of this function.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
res = 0 for v in args:
v = float(v if v and v != 'None' else 0)
res += v return str(res)

[документация]
class BuiltinSubtract(BuiltinFormatterFunction):
name = 'subtract'
arg_count = 2 category = ARITHMETIC
__doc__ = doc = _( r'''
``subtract(x, y)`` -- returns ``x - y``.[/] Throws an exception if either ``x`` or
``y`` are not numbers. This function can usually be replaced by the ``-`` operator.
''')

def evaluate(self, formatter, kwargs, mi, locals, x, y):
x = float(x if x and x != 'None' else 0)
y = float(y if y and y != 'None' else 0)
return str(x - y)

[документация]
class BuiltinMultiply(BuiltinFormatterFunction):
name = 'multiply'
arg_count = -1 category = ARITHMETIC
__doc__ = doc = _( r'''
``multiply(x [, y]*)`` -- returns the product of its arguments.[/] Throws an exception if any argument is not a number. This function can usually be replaced by the ``*`` operator.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
res = 1 for v in args:
v = float(v if v and v != 'None' else 0)
res *= v return str(res)

[документация]
class BuiltinDivide(BuiltinFormatterFunction):
name = 'divide'
arg_count = 2 category = ARITHMETIC
__doc__ = doc = _( r'''
``divide(x, y)`` -- returns ``x / y``.[/] Throws an exception if either ``x`` or
``y`` are not numbers. This function can usually be replaced by the ``/`` operator.
''')

def evaluate(self, formatter, kwargs, mi, locals, x, y):
x = float(x if x and x != 'None' else 0)
y = float(y if y and y != 'None' else 0)
return str(x / y)

[документация]
class BuiltinCeiling(BuiltinFormatterFunction):
name = 'ceiling'
arg_count = 1 category = ARITHMETIC
__doc__ = doc = _( r'''
``ceiling(value)`` -- returns the smallest integer greater than or equal to ``value``.[/]
Throws an exception if ``value`` is not a number.
''')

def evaluate(self, formatter, kwargs, mi, locals, value):
value = float(value if value and value != 'None' else 0)
return str(ceil(value))

[документация]
class BuiltinFloor(BuiltinFormatterFunction):
name = 'floor'
arg_count = 1 category = ARITHMETIC
__doc__ = doc = _( r'''
``floor(value)`` -- returns the largest integer less than or equal to ``value``.[/] Throws an exception if ``value`` is not a number.
''')

def evaluate(self, formatter, kwargs, mi, locals, value):
value = float(value if value and value != 'None' else 0)
return str(floor(value))

[документация]
class BuiltinRound(BuiltinFormatterFunction):
name = 'round'
arg_count = 1 category = ARITHMETIC
__doc__ = doc = _( r'''
``round(value)`` -- returns the nearest integer to ``value``.[/] Throws an exception if
``value`` is not a number.
''')

def evaluate(self, formatter, kwargs, mi, locals, value):
value = float(value if value and value != 'None' else 0)
return str(round(value))

[документация]
class BuiltinMod(BuiltinFormatterFunction):
name = 'mod'
arg_count = 2 category = ARITHMETIC
__doc__ = doc = _( r'''
``mod(value, y)`` -- returns the ``floor`` of the remainder of ``value / y``.[/] Throws an exception if either ``value`` or ``y`` is not a number.
''')

def evaluate(self, formatter, kwargs, mi, locals, value, y):
value = float(value if value and value != 'None' else 0)
y = float(y if y and y != 'None' else 0)
return str(int(value % y))

[документация]
class BuiltinFractionalPart(BuiltinFormatterFunction):
name = 'fractional_part'
arg_count = 1 category = ARITHMETIC
__doc__ = doc = _( r'''
``fractional_part(value)`` -- returns the part of the value after the decimal point.[/] For example, ``fractional_part(3.14)`` returns ``0.14``. Throws an exception if ``value`` is not a number.
''')

def evaluate(self, formatter, kwargs, mi, locals, value):
value = float(value if value and value != 'None' else 0)
return str(modf(value)[0])

[документация]
class BuiltinTemplate(BuiltinFormatterFunction):
name = 'template'
arg_count = 1 category = RECURSION

__doc__ = doc = _( r'''
``template(x)`` -- evaluates ``x`` as a template.[/] The evaluation is done in its own context, meaning that variables are not shared between the caller and the template evaluation. If not using General Program Mode, because the ``{`` and
``}`` characters are special, you must use ``[[`` for the ``{`` character and
``]]`` for the } character; they are converted automatically. For example,
``template(\'[[title_sort]]\')`` will evaluate the template ``{title_sort}`` and return its value. Note also that prefixes and suffixes (the ``|prefix|suffix`` syntax)
cannot be used in the argument to this function when using template program mode.
''')

def evaluate(self, formatter, kwargs, mi, locals, template):
template = template.replace('[[', '{').replace(']]', '}')
return formatter.__class__().safe_format(template, kwargs, 'TEMPLATE', mi)

[документация]
class BuiltinEval(BuiltinFormatterFunction):
name = 'eval'
arg_count = 1 category = RECURSION
__doc__ = doc = _( r'''
``eval(string)`` -- evaluates the string as a program, passing the local variables.[/] This permits using the template processor to construct complex results from local variables. In
[URL href="https://manual.calibre-ebook.com/template_lang.html#more-complex-programs-in-template-expressions-template-program-mode"]
Template Program Mode[/URL], because the ``{`` and ``}`` characters are interpreted before the template is evaluated you must use ``[[`` for the ``{`` character and ``]]`` for the ``}`` character. They are converted automatically. Note also that prefixes and suffixes (the ``|prefix|suffix`` syntax) cannot be used in the argument to this function when using Template Program Mode.
''')

def evaluate(self, formatter, kwargs, mi, locals, template):
from calibre.utils.formatter import EvalFormatter template = template.replace('[[', '{').replace(']]', '}')
return EvalFormatter().safe_format(template, locals, 'EVAL', None)

[документация]
class BuiltinAssign(BuiltinFormatterFunction):
name = 'assign'
arg_count = 2 category = OTHER
__doc__ = doc = _( r'''
``assign(id, value)`` -- assigns ``value`` to ``id``[/], then returns ``value``. ``id`` must be an identifier, not an expression. In most cases you can use the ``=`` operator instead of this function.
''')

def evaluate(self, formatter, kwargs, mi, locals, target, value):
locals[target] = value return value

[документация]
class BuiltinListSplit(BuiltinFormatterFunction):
name = 'list_split'
arg_count = 3 category = LIST_MANIPULATION
__doc__ = doc = _( r'''
``list_split(list_val, sep, id_prefix)`` -- splits ``list_val`` into separate values using ``sep``[/], then assigns the values to local variables named
``id_prefix_N`` where N is the position of the value in the list. The first item has position 0 (zero). The function returns the last element in the list.

Example:
[CODE]
list_split('one:two:foo', ':', 'var')
[/CODE]
is equivalent to:
[CODE]
var_0 = 'one'
var_1 = 'two'
var_2 = 'foo'
[/CODE]
''')

def evaluate(self, formatter, kwargs, mi, locals, list_val, sep, id_prefix):
l = [v.strip() for v in list_val.split(sep)]
res = ''
for i,v in enumerate(l):
res = locals[id_prefix+'_'+str(i)] = v return res

[документация]
class BuiltinPrint(BuiltinFormatterFunction):
name = 'print'
arg_count = -1 category = OTHER
__doc__ = doc = _( r'''
``print(a [, b]*)`` -- prints the arguments to standard output.[/] Unless you start calibre from the command line (``calibre-debug -g``), the output will go into a black hole. The ``print`` function always returns its first argument.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
print(args)
return ''

[документация]
class BuiltinField(BuiltinFormatterFunction):
name = 'field'
arg_count = 1 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``field(lookup_name)`` -- returns the value of the metadata field with lookup name ``lookup_name``.[/]
The ``$`` prefix can be used instead of the function, as in ``$tags``.
''')

def evaluate(self, formatter, kwargs, mi, locals, name):
return formatter.get_value(name, [], kwargs)

[документация]
class BuiltinRawField(BuiltinFormatterFunction):
name = 'raw_field'
arg_count = -1 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``raw_field(lookup_name [, optional_default])`` -- returns the metadata field named by ``lookup_name`` without applying any formatting.[/] It evaluates and returns the optional second argument ``optional_default`` if the field's value is undefined (``None``). The ``$$`` prefix can be used instead of the function, as in ``$$pubdate``.
''')

def evaluate(self, formatter, kwargs, mi, locals, name, default=None):
res = getattr(mi, name, None)
if res is None and default is not None:
return default if isinstance(res, list):
fm = mi.metadata_for_field(name)
if fm is None:
return ', '.join(res)
return fm['is_multiple']['list_to_ui'].join(res)
return str(res)

[документация]
class BuiltinRawList(BuiltinFormatterFunction):
name = 'raw_list'
arg_count = 2 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``raw_list(lookup_name, separator)`` -- returns the metadata list named by
``lookup_name`` without applying any formatting or sorting[/], with the items separated by ``separator``.
''')

def evaluate(self, formatter, kwargs, mi, locals, name, separator):
res = getattr(mi, name, None)
if not isinstance(res, list):
return f'{name} is not a list'
return separator.join(res)

[документация]
class BuiltinSubstr(BuiltinFormatterFunction):
name = 'substr'
arg_count = 3 category = STRING_MANIPULATION
__doc__ = doc = _( r'''
``substr(value, start, end)`` -- returns the ``start``'th through the ``end``'th characters of ``value``[/]. The first character in ``value`` is the zero'th character.
If ``end`` is negative then it indicates that many characters counting from the right. If ``end`` is zero, then it indicates the last character. For example,
``substr('12345', 1, 0)`` returns ``'2345'``, and ``substr('12345', 1, -1)`` returns ``'234'``.
''')

def evaluate(self, formatter, kwargs, mi, locals, value, start_, end_):
return value[int(start_): len(value) if int(end_) == 0 else int(end_)]

[документация]
class BuiltinLookup(BuiltinFormatterFunction):
name = 'lookup'
arg_count = -1 category = ITERATING_VALUES
__doc__ = doc = _( r'''
``lookup(value, [ pattern, key, ]* else_key)`` -- The patterns will be checked against the ``value`` in order[/]. If a ``pattern`` matches then the value of the field named by
``key`` is returned. If no pattern matches then the value of the field named by
``else_key`` is returned. See also the :ref:`switch` function.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, *args):
if len(args) == 2: # here for backwards compatibility if val:
return formatter.vformat('{'+args[0].strip()+'}', [], kwargs)
else:
return formatter.vformat('{'+args[1].strip()+'}', [], kwargs)
if (len(args) % 2) != 1:
raise ValueError(_('lookup requires either 2 or an odd number of arguments'))
i = 0 while i < len(args):
if i + 1 >= len(args):
return formatter.vformat('{' + args[i].strip() + '}', [], kwargs)
if re.search(args[i], val, flags=re.I):
return formatter.vformat('{'+args[i+1].strip() + '}', [], kwargs)
i += 2

[документация]
class BuiltinTest(BuiltinFormatterFunction):
name = 'test'
arg_count = 3 category = STRING_MANIPULATION
__doc__ = doc = _( r'''
``test(value, text_if_not_empty, text_if_empty)`` -- return ``text_if_not_empty`` if the value is not empty, otherwise return ``text_if_empty``.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, value_if_set, value_not_set):
if val:
return value_if_set else:
return value_not_set

[документация]
class BuiltinContains(BuiltinFormatterFunction):
name = 'contains'
arg_count = 4 category = STRING_MANIPULATION
__doc__ = doc = _( r'''
``contains(value, pattern, text_if_match, text_if_not_match)`` -- checks if the value is matched by the regular expression ``pattern``[/]. Returns ``text_if_match`` if the pattern matches the value, otherwise returns ``text_if_not_match``.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, test, value_if_present, value_if_not):
if re.search(test, val, flags=re.I):
return value_if_present else:
return value_if_not

[документация]
class BuiltinSwitch(BuiltinFormatterFunction):
name = 'switch'
arg_count = -1 category = ITERATING_VALUES
__doc__ = doc = _( r'''
``switch(value, [patternN, valueN,]+ else_value)`` -- for each ``patternN, valueN`` pair, checks if the ``value`` matches the regular expression ``patternN``[/] and if so returns the associated ``valueN``. If no ``patternN`` matches, then ``else_value`` is returned. You can have as many ``patternN, valueN`` pairs as you wish. The first match is returned.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, *args):
if (len(args) % 2) != 1:
raise ValueError(_('switch requires an even number of arguments'))
i = 0 while i < len(args):
if i + 1 >= len(args):
return args[i]
if re.search(args[i], val, flags=re.I):
return args[i+1]
i += 2

[документация]
class BuiltinSwitchIf(BuiltinFormatterFunction):
name = 'switch_if'
arg_count = -1 category = ITERATING_VALUES
__doc__ = doc = _( r'''
``switch_if([test_expression, value_expression,]+ else_expression)`` -- for each
``test_expression, value_expression`` pair, checks if ``test_expression`` is
True (non-empty) and if so returns the result of ``value_expression``.[/] If no
``test_expression`` is True then the result of ``else_expression`` is returned.
You can have as many ``test_expression, value_expression`` pairs as you want.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
if (len(args) % 2) != 1:
raise ValueError(_('switch_if requires an odd number of arguments'))
# We shouldn't get here because the function is inlined. However, someone
# might call it directly.
i = 0 while i < len(args):
if i + 1 >= len(args):
return args[i]
if args[i]:
return args[i+1]
i += 2

[документация]
class BuiltinStrcatMax(BuiltinFormatterFunction):
name = 'strcat_max'
arg_count = -1 category = STRING_MANIPULATION
__doc__ = doc = _( r'''
``strcat_max(max, string1 [, prefix2, string2]*)`` -- Returns a string formed by concatenating the arguments.[/] The returned value is initialized to ``string1``.
Strings made from ``prefix, string`` pairs are added to the end of the value as long as the resulting string length is less than ``max``. Prefixes can be empty.
Returns ``string1`` even if ``string1`` is longer than ``max``. You can pass as many ``prefix, string`` pairs as you wish.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
if len(args) < 2:
raise ValueError(_('strcat_max requires 2 or more arguments'))
if (len(args) % 2) != 0:
raise ValueError(_('strcat_max requires an even number of arguments'))
try:
max = int(args[0])
except Exception:
raise ValueError(_('first argument to strcat_max must be an integer'))

i = 2 result = args[1]
try:
while i < len(args):
if (len(result) + len(args[i]) + len(args[i+1])) > max:
break result = result + args[i] + args[i+1]
i += 2 except Exception:
pass return result.strip()

[документация]
class BuiltinInList(BuiltinFormatterFunction):
name = 'list_contains'
arg_count = -1 category = LIST_LOOKUP
__doc__ = doc = _( r'''
``list_contains(value, separator, [ pattern, found_val, ]* not_found_val)`` -- interpret the
``value`` as a list of items separated by ``separator``, checking the ``pattern`` against each item in the list.[/] If the ``pattern`` matches an item then return
``found_val``, otherwise return ``not_found_val``. The pair ``pattern`` and
``found_value`` can be repeated as many times as desired, permitting returning different values depending on the item's value. The patterns are checked in order, and the first match is returned.

Aliases: in_list(), list_contains()
''')

aliases = ['in_list']

def evaluate(self, formatter, kwargs, mi, locals, val, sep, *args):
if (len(args) % 2) != 1:
raise ValueError(_('in_list requires an odd number of arguments'))
l = [v.strip() for v in val.split(sep) if v.strip()]
i = 0 while i < len(args):
if i + 1 >= len(args):
return args[i]
sf = args[i]
fv = args[i+1]
if l:
for v in l:
if re.search(sf, v, flags=re.I):
return fv i += 2

[документация]
class BuiltinStrInList(BuiltinFormatterFunction):
name = 'str_in_list'
arg_count = -1 category = LIST_LOOKUP
__doc__ = doc = _( r'''
``str_in_list(value, separator, [ string, found_val, ]+ not_found_val)`` -- interpret the ``value`` as a list of items separated by ``separator`` then compare ``string`` against each value in the list.[/] The ``string`` is not a regular expression. If
``string`` is equal to any item (ignoring case) then return the corresponding
``found_val``. If ``string`` contains ``separators`` then it is also treated as a list and each subvalue is checked. The ``string`` and ``found_value`` pairs can be repeated as many times as desired, permitting returning different values depending on string's value. If none of the strings match then
``not_found_value`` is returned. The strings are checked in order. The first match is returned.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, sep, *args):
if (len(args) % 2) != 1:
raise ValueError(_('str_in_list requires an odd number of arguments'))
l = [v.strip() for v in val.split(sep) if v.strip()]
i = 0 while i < len(args):
if i + 1 >= len(args):
return args[i]
sf = args[i]
fv = args[i+1]
c = [v.strip() for v in sf.split(sep) if v.strip()]
if l:
for v in l:
for t in c:
if strcmp(t, v) == 0:
return fv i += 2[документация]
class BuiltinIdentifierInList(BuiltinFormatterFunction):
name = 'identifier_in_list'
arg_count = -1 category = LIST_LOOKUP
__doc__ = doc = _( r'''
``identifier_in_list(val, id_name [, found_val, not_found_val])`` -- treat
``val`` as a list of identifiers separated by commas. An identifier has the format ``id_name:value``.[/] The ``id_name`` parameter is the id_name text to search for, either ``id_name`` or ``id_name:regexp``. The first case matches if there is any identifier matching that id_name. The second case matches if id_name matches an identifier and the regexp matches the identifier's value. If
``found_val`` and ``not_found_val`` are provided then if there is a match then return ``found_val``, otherwise return ``not_found_val``. If ``found_val`` and
``not_found_val`` are not provided then if there is a match then return the
``identifier:value`` pair, otherwise the empty string (``''``).
''')

def evaluate(self, formatter, kwargs, mi, locals, val, ident, *args):
if len(args) == 0:
fv_is_id = True nfv = ''
elif len(args) == 2:
fv_is_id = False fv = args[0]
nfv = args[1]
else:
raise ValueError(_('{} requires 2 or 4 arguments').format(self.name))

l = [v.strip() for v in val.split(',') if v.strip()]
id_, __, regexp = ident.partition(':')
if not id_:
return nfv for candidate in l:
i, __, v = candidate.partition(':')
if v and i == id_:
if not regexp or re.search(regexp, v, flags=re.I):
return candidate if fv_is_id else fv return nfv

[документация]
class BuiltinRe(BuiltinFormatterFunction):
name = 're'
arg_count = 3 category = STRING_MANIPULATION
__doc__ = doc = _( r'''
``re(value, pattern, replacement)`` -- return the ``value`` after applying the regular expression.[/] All instances of ``pattern`` in the value are replaced with
``replacement``. The template language uses case insensitive
[URL href="https://docs.python.org/3/library/re.html"]Python regular expressions[/URL].
''')

def evaluate(self, formatter, kwargs, mi, locals, val, pattern, replacement):
return re.sub(pattern, replacement, val, flags=re.I)

[документация]
class BuiltinReGroup(BuiltinFormatterFunction):
name = 're_group'
arg_count = -1 category = STRING_MANIPULATION
__doc__ = doc = _( r'''
``re_group(value, pattern [, template_for_group]*)`` -- return a string made by applying the regular expression ``pattern`` to ``value`` and replacing each matched instance[/] with the value returned by the corresponding template. In
[URL href="https://manual.calibre-ebook.com/template_lang.html#more-complex-programs-in-template-expressions-template-program-mode"]
Template Program Mode[/URL], like for the ``template`` and the
``eval`` functions, you use ``[[`` for ``{`` and ``]]`` for ``}``.

The following example looks for a series with more than one word and uppercases the first word:
[CODE]
program: re_group(field('series'), "(\S* )(.*)", "{$:uppercase()}", "{$}")'}
[/CODE]
''')

def evaluate(self, formatter, kwargs, mi, locals, val, pattern, *args):
from calibre.utils.formatter import EvalFormatter def repl(mo):
res = ''
if mo and mo.lastindex:
for dex in range(mo.lastindex):
gv = mo.group(dex+1)
if gv is None:
continue if len(args) > dex:
template = args[dex].replace('[[', '{').replace(']]', '}')
res += EvalFormatter().safe_format(template, {'$': gv},
'EVAL', None, strip_results=False)
else:
res += gv return res return re.sub(pattern, repl, val, flags=re.I)

[документация]
class BuiltinSwapAroundComma(BuiltinFormatterFunction):
name = 'swap_around_comma'
arg_count = 1 category = STRING_MANIPULATION
__doc__ = doc = _( r'''
``swap_around_comma(value)`` -- given a ``value`` of the form ``B, A``, return ``A B``.[/]
This is most useful for converting names in LN, FN format to FN LN. If there is no comma in the ``value`` then the function returns the value unchanged.
''')

def evaluate(self, formatter, kwargs, mi, locals, val):
return re.sub(r'^(.*?),\s*(.*$)', r'\2 \1', val, flags=re.I).strip()

[документация]
class BuiltinIfempty(BuiltinFormatterFunction):
name = 'ifempty'
arg_count = 2 category = STRING_MANIPULATION
__doc__ = doc = _( r'''
``ifempty(value, text_if_empty)`` -- if the ``value`` is not empty then return that ``value``, otherwise return ``text_if_empty``.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, value_if_empty):
if val:
return val else:
return value_if_empty

[документация]
class BuiltinShorten(BuiltinFormatterFunction):
name = 'shorten'
arg_count = 4 category = STRING_MANIPULATION
__doc__ = doc = _( r'''
``shorten(value, left_chars, middle_text, right_chars)`` -- Return a shortened version of the ``value``[/], consisting of ``left_chars`` characters from the beginning of the
``value``, followed by ``middle_text``, followed by ``right_chars`` characters from the end of the ``value``. ``left_chars`` and ``right_chars`` must be non-negative integers.

Example: assume you want to display the title with a length of at most
15 characters in length. One template that does this is
``{title:shorten(9,-,5)}``. For a book with the title `Ancient English Laws in the Times of Ivanhoe` the result will be `Ancient E-anhoe`: the first 9 characters of the title, a ``-``, then the last 5 characters. If the value's length is less than ``left chars`` + ``right chars`` + the length of ``middle text`` then the value will be returned unchanged. For example, the title `The
Dome` would not be changed.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, leading, center_string, trailing):
l = max(0, int(leading))
t = max(0, int(trailing))
if len(val) > l + len(center_string) + t:
return val[0:l] + center_string + ('' if t == 0 else val[-t:])
else:
return val

[документация]
class BuiltinCount(BuiltinFormatterFunction):
name = 'list_count'
arg_count = 2 category = LIST_MANIPULATION aliases = ['count']

__doc__ = doc = _( r'''
``list_count(value, separator)`` -- interprets the value as a list of items separated by
``separator`` and returns the number of items in the list.[/] Most lists use a comma as the separator, but ``authors`` uses an ampersand (&).

Examples: ``{tags:list_count(,)}``, ``{authors:list_count(&)}``.

Aliases: ``count()``, ``list_count()``
''')

def evaluate(self, formatter, kwargs, mi, locals, val, sep):
return str(len([v for v in val.split(sep) if v]))

[документация]
class BuiltinListCountMatching(BuiltinFormatterFunction):
name = 'list_count_matching'
arg_count = 3 category = LIST_MANIPULATION aliases = ['count_matching']

__doc__ = doc = _( r'''
``list_count_matching(value, pattern, separator)`` -- interprets ``value`` as a list of items separated by ``separator``, returning the number of items in the list that match the regular expression ``pattern``.[/]

Aliases: ``list_count_matching()``, ``count_matching()``
''')

def evaluate(self, formatter, kwargs, mi, locals, value, pattern, sep):
res = 0 for v in [x.strip() for x in value.split(sep) if x.strip()]:
if re.search(pattern, v, flags=re.I):
res += 1 return str(res)

[документация]
class BuiltinListitem(BuiltinFormatterFunction):
name = 'list_item'
arg_count = 3 category = LIST_LOOKUP
__doc__ = doc = _( r'''
``list_item(value, index, separator)`` -- interpret the ``value`` as a list of items separated by ``separator``, returning the 'index'th item.[/] The first item is number zero. The last item has the index ``-1`` as in
``list_item(-1,separator)``. If the item is not in the list, then the empty string is returned. The separator has the same meaning as in the count function, usually comma but is ampersand for author-like lists.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, index, sep):
if not val:
return ''
index = int(index)
val = val.split(sep)
try:
return val[index].strip()
except Exception:
return ''

[документация]
class BuiltinSelect(BuiltinFormatterFunction):
name = 'select'
arg_count = 2 category = LIST_LOOKUP
__doc__ = doc = _( r'''
``select(value, key)`` -- interpret the ``value`` as a comma-separated list of items with each item having the form ``id:id_value`` (the calibre ``identifier`` format).[/] The function finds the first pair with the id equal to ``key`` and returns the corresponding ``id_value``. If no id matches then the function returns the empty string.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, key):
if not val:
return ''
vals = [v.strip() for v in val.split(',')]
tkey = key+':'
for v in vals:
if v.startswith(tkey):
return v[len(tkey):]
return ''

[документация]
class BuiltinApproximateFormats(BuiltinFormatterFunction):
name = 'approximate_formats'
arg_count = 0 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``approximate_formats()`` -- return a comma-separated list of formats associated with the book.[/] Because the list comes from calibre's database instead of the file system, there is no guarantee that the list is correct, although it probably is. Note that resulting format names are always uppercase, as in EPUB.
The ``approximate_formats()`` function is much faster than the ``formats_...`` functions.

This function works only in the GUI. If you want to use these values in save-to-disk or send-to-device templates then you must make a custom "Column built from other columns", use the function in that column's template, and use that column's value in your save/send templates.
''')

def evaluate(self, formatter, kwargs, mi, locals):
if hasattr(mi, '_proxy_metadata'):
fmt_data = mi._proxy_metadata.db_approx_formats if not fmt_data:
return ''
data = sorted(fmt_data)
return ','.join(v.upper() for v in data)
self.only_in_gui_error()

[документация]
class BuiltinFormatsModtimes(BuiltinFormatterFunction):
name = 'formats_modtimes'
arg_count = 1 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``formats_modtimes(date_format_string)`` -- return a comma-separated list of colon-separated items ``FMT:DATE`` representing modification times for the formats of a book.[/] The ``date_format_string`` parameter specifies how the date is to be formatted. See the :ref:`format_date` function for details. You can use the :ref:`select` function to get the modification time for a specific format. Note that format names are always uppercase, as in EPUB.
''')

def evaluate(self, formatter, kwargs, mi, locals, fmt):
fmt_data = mi.get('format_metadata', {})
try:
data = sorted(fmt_data.items(), key=lambda x:x[1]['mtime'], reverse=True)
return ','.join(k.upper()+':'+format_date(v['mtime'], fmt)
for k,v in data)
except Exception:
return ''

[документация]
class BuiltinFormatsSizes(BuiltinFormatterFunction):
name = 'formats_sizes'
arg_count = 0 category = GET_FROM_METADATA
__doc__ = doc = _( r'''

``formats_sizes()`` -- return a comma-separated list of colon-separated
``FMT:SIZE`` items giving the sizes of the formats of a book in bytes.[/] You can use the ``select()`` function to get the size for a specific format. Note that format names are always uppercase, as in EPUB.
''')

def evaluate(self, formatter, kwargs, mi, locals):
fmt_data = mi.get('format_metadata', {})
try:
return ','.join(k.upper()+':'+str(v['size']) for k,v in iteritems(fmt_data))
except Exception:
return ''

[документация]
class BuiltinFormatsPaths(BuiltinFormatterFunction):
name = 'formats_paths'
arg_count = -1 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``formats_paths([separator])`` -- return a ``separator``-separated list of colon-separated items ``FMT:PATH`` giving the full path to the formats of a book.[/] The ``separator`` argument is optional. If not supplied then the separator is ``', '`` (comma space). If the separator is a comma then you can use the ``select()`` function to get the path for a specific format. Note that format names are always uppercase, as in EPUB.
''')

def evaluate(self, formatter, kwargs, mi, locals, sep=','):
fmt_data = mi.get('format_metadata', {})
try:
return sep.join(k.upper()+':'+str(v['path']) for k,v in iteritems(fmt_data))
except Exception:
return ''

[документация]
class BuiltinFormatsPathSegments(BuiltinFormatterFunction):
name = 'formats_path_segments'
arg_count = 5 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``formats_path_segments(with_author, with_title, with_format, with_ext, sep)``
-- return parts of the path to a book format in the calibre library separated by ``sep``.[/] The parameter ``sep`` should usually be a slash (``'/'``). One use is to be sure that paths generated in Save to disk and Send to device templates are shortened consistently. Another is to be sure the paths on the device match the paths in the calibre library.

A book path consists of 3 segments: the author, the title including the calibre database id in parentheses, and the format (author - title). Calibre can shorten any of the three because of file name length limitations. You choose which segments to include by passing ``1`` for that segment. If you don't want a segment then pass ``0`` or the empty string for that segment. For example, the following returns just the format name without the extension:
[CODE]
formats_path_segments(0, 0, 1, 0, '/')
[/CODE]
Because there is only one segment the separator is ignored.

If there are multiple formats (multiple extensions) then one of the extensions will be picked at random. If you care about which extension is used then get the path without the extension then add the desired extension to it.

Examples: Assume there is a book in the calibre library with an epub format by
Joe Blogs with title 'Help'. It would have the path
[CODE]
Joe Blogs/Help - (calibre_id)/Help - Joe Blogs.epub
[/CODE]
The following shows what is returned for various parameters:
[LIST]
[*]``formats_path_segments(0, 0, 1, 0, '/')`` returns `Help - Joe Blogs`
[*]``formats_path_segments(0, 0, 1, 1, '/')`` returns `Help - Joe Blogs.epub`
[*]``formats_path_segments(1, 0, 1, 1, '/')`` returns `Joe Blogs/Help - Joe Blogs.epub`
[*]``formats_path_segments(1, 0, 1, 0, '/')`` returns `Joe Blogs/Help - Joe Blogs`
[*]``formats_path_segments(0, 1, 0, 0, '/')`` returns `Help - (calibre_id)`
[/LIST]
''')

def evaluate(self, formatter, kwargs, mi, locals, with_author, with_title, with_format, with_ext, sep):
fmt_metadata = mi.get('format_metadata', {})
if fmt_metadata:
for v in fmt_metadata.values():
p = v['path']
r,fmt = os.path.split(p)
if with_ext == '0' or not with_ext:
fmt = os.path.splitext(fmt)[0]
r,title = os.path.split(r)
r,author = os.path.split(r)
parts = []
if with_author == '1':
parts.append(author)
if with_title == '1':
parts.append(title)
if with_format == '1':
parts.append(fmt)
return sep.join(parts)
else:
return _("No book formats found so the path can't be generated")

[документация]
class BuiltinHumanReadable(BuiltinFormatterFunction):
name = 'human_readable'
arg_count = 1 category = FORMATTING_VALUES
__doc__ = doc = _( r'''
``human_readable(value)`` -- expects the ``value`` to be a number and returns a string representing that number in KB, MB, GB, etc.
''')

def evaluate(self, formatter, kwargs, mi, locals, val):
try:
return human_readable(round(float(val)))
except Exception:
return ''

[документация]
class BuiltinFormatNumber(BuiltinFormatterFunction):
name = 'format_number'
arg_count = 2 category = FORMATTING_VALUES
__doc__ = doc = _( r'''
``format_number(value, template)`` -- interprets the ``value`` as a number and formats that number using a Python formatting template such as ``{0:5.2f}`` or ``{0:,d}`` or
``${0:5,.2f}``.[/] The formatting template must begin with ``{0:`` and end with
``}`` as in the above examples. Exception: you can leave off the leading "{0:"
and trailing "}" if the format template contains only a format. See the
[URL href="https://manual.calibre-ebook.com/template_lang.html"]
Template Language[/URL] and the
[URL href="https://docs.python.org/3/library/string.html#formatstrings"]
Python[/URL] documentation for more examples. Returns the empty string if formatting fails.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, template):
if val == '' or val == 'None':
return ''
if '{' not in template:
template = '{0:' + template + '}'
try:
v1 = float(val)
except Exception:
return ''
try: # Try formatting the value as a float return template.format(v1)
except Exception:
pass try: # Try formatting the value as an int v2 = trunc(v1)
if v2 == v1:
return template.format(v2)
except Exception:
pass return ''

[документация]
class BuiltinSublist(BuiltinFormatterFunction):
name = 'sublist'
arg_count = 4 category = LIST_MANIPULATION
__doc__ = doc = _( r'''
``sublist(value, start_index, end_index, separator)`` -- interpret the ``value`` as a list of items separated by ``separator``, returning a new list made from the items from ``start_index`` to ``end_index``.[/] The first item is number zero. If an index is negative, then it counts from the end of the list. As a special case, an end_index of zero is assumed to be the length of the list.

Examples assuming that the tags column (which is comma-separated) contains "A, B, C":
[LIST]
[*]``{tags:sublist(0,1,\,)}`` returns "A"
[*]``{tags:sublist(-1,0,\,)}`` returns "C"
[*]``{tags:sublist(0,-1,\,)}`` returns "A, B"
[/LIST]
''')

def evaluate(self, formatter, kwargs, mi, locals, val, start_index, end_index, sep):
if not val:
return ''
si = int(start_index)
ei = int(end_index)
# allow empty list items so counts are what the user expects val = [v.strip() for v in val.split(sep)]

if sep == ',':
sep = ', '
try:
if ei == 0:
return sep.join(val[si:])
else:
return sep.join(val[si:ei])
except Exception:
return ''

[документация]
class BuiltinSubitems(BuiltinFormatterFunction):
name = 'subitems'
arg_count = 3 category = LIST_MANIPULATION
__doc__ = doc = _( r'''
``subitems(value, start_index, end_index)`` -- This function breaks apart lists of tag-like hierarchical items such as genres.[/] It interprets the ``value`` as a comma- separated list of tag-like items, where each item is a period-separated list. It returns a new list made by extracting from each item the components from
``start_index`` to ``end_index``, then merging the results back together.
Duplicates are removed. The first subitem in a period-separated list has an index of zero. If an index is negative then it counts from the end of the list.
As a special case, an ``end_index`` of zero is assumed to be the length of the list.

Examples:
[LIST]
[*]Assuming a #genre column containing "A.B.C":
[LIST]
[*]``{#genre:subitems(0,1)}`` returns "A"
[*]``{#genre:subitems(0,2)}`` returns "A.B"
[*]``{#genre:subitems(1,0)}`` returns "B.C"
[/LIST]
[*]Assuming a #genre column containing "A.B.C, D.E":
[LIST]
[*]``{#genre:subitems(0,1)}`` returns "A, D"
[*]``{#genre:subitems(0,2)}`` returns "A.B, D.E"
[/LIST]
[/LIST]
''')

period_pattern = re.compile(r'(?<=[^\.\s])\.(?=[^\.\s])', re.U)

def evaluate(self, formatter, kwargs, mi, locals, val, start_index, end_index):
if not val:
return ''
si = int(start_index)
ei = int(end_index)
has_periods = '.' in val items = [v.strip() for v in val.split(',') if v.strip()]
rv = set()
for item in items:
if has_periods and '.' in item:
components = self.period_pattern.split(item)
else:
components = [item]
try:
if ei == 0:
t = '.'.join(components[si:]).strip()
else:
t = '.'.join(components[si:ei]).strip()
if t:
rv.add(t)
except Exception:
pass return ', '.join(sorted(rv, key=sort_key))[документация]
class BuiltinFormatDate(BuiltinFormatterFunction):
name = 'format_date'
arg_count = 2 category = FORMATTING_VALUES
__doc__ = doc = _( r'''
``format_date(value, format_string)`` -- format the ``value``, which must be a date string, using the ``format_string``, returning a string.[/] It is best if the date is in ISO format as using other date formats often causes errors because the actual date value cannot be unambiguously determined. Note that the
``format_date_field()`` function is both faster and more reliable.

The formatting codes are:
[LIST]
[*]``d :`` the day as number without a leading zero (1 to 31)
[*]``dd :`` the day as number with a leading zero (01 to 31)
[*]``ddd :`` the abbreviated localized day name (e.g. "Mon" to "Sun")
[*]``dddd :`` the long localized day name (e.g. "Monday" to "Sunday")
[*]``M :`` the month as number without a leading zero (1 to 12)
[*]``MM :`` the month as number with a leading zero (01 to 12)
[*]``MMM :`` the abbreviated localized month name (e.g. "Jan" to "Dec")
[*]``MMMM :`` the long localized month name (e.g. "January" to "December")
[*]``yy :`` the year as two digit number (00 to 99)
[*]``yyyy :`` the year as four digit number.
[*]``h :`` the hours without a leading 0 (0 to 11 or 0 to 23, depending on am/pm)
[*]``hh :`` the hours with a leading 0 (00 to 11 or 00 to 23, depending on am/pm)
[*]``m :`` the minutes without a leading 0 (0 to 59)
[*]``mm :`` the minutes with a leading 0 (00 to 59)
[*]``s :`` the seconds without a leading 0 (0 to 59)
[*]``ss :`` the seconds with a leading 0 (00 to 59)
[*]``ap :`` use a 12-hour clock instead of a 24-hour clock, with 'ap' replaced by the lowercase localized string for am or pm
[*]``AP :`` use a 12-hour clock instead of a 24-hour clock, with 'AP' replaced by the uppercase localized string for AM or PM
[*]``aP :`` use a 12-hour clock instead of a 24-hour clock, with 'aP' replaced by the localized string for AM or PM
[*]``Ap :`` use a 12-hour clock instead of a 24-hour clock, with 'Ap' replaced by the localized string for AM or PM
[*]``iso :`` the date with time and timezone. Must be the only format present
[*]``to_number :`` convert the date & time into a floating point number (a `timestamp`)
[*]``from_number :`` convert a floating point number (a `timestamp`) into an
ISO-formatted date. If you want a different date format then add the desired formatting string after ``from_number`` and a colon (``:``). Example:
[CODE]
format_date(val, 'from_number:MMM dd yyyy')
[/CODE]
[/LIST]
You might get unexpected results if the date you are formatting contains localized month names, which can happen if you changed the date format to contain ``MMMM``. Using ``format_date_field()`` avoids this problem.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, format_string):
if not val or val == 'None':
return ''
try:
if format_string == 'to_number':
s = parse_date(val).timestamp()
elif format_string.startswith('from_number'):
val = datetime.fromtimestamp(float(val))
f = format_string[12:]
s = format_date(val, f if f else 'iso')
else:
s = format_date(parse_date(val), format_string)
return s except Exception:
s = 'BAD DATE'
return s[документация]
class BuiltinFormatDateField(BuiltinFormatterFunction):
name = 'format_date_field'
arg_count = 2 category = FORMATTING_VALUES
__doc__ = doc = _( r'''
``format_date_field(field_name, format_string)`` -- format the value in the field ``field_name``, which must be the lookup name of a date field, either standard or custom.[/] See :ref:`format_date` for the formatting codes. This function is much faster than format_date() and should be used when you are formatting the value in a field (column). It is also more reliable because it works directly on the underlying date. It can't be used for computed dates or dates in string variables. Examples:
[CODE]
format_date_field('pubdate', 'yyyy.MM.dd')
format_date_field('#date_read', 'MMM dd, yyyy')
[/CODE]
''')

def evaluate(self, formatter, kwargs, mi, locals, field, format_string):
try:
field = field_metadata.search_term_to_field_key(field)
if field not in mi.all_field_keys():
raise ValueError(_("Function {0}: Unknown field '{1}'").format('format_date_field', field))
val = mi.get(field, None)
if mi.metadata_for_field(field)['datatype'] != 'datetime':
raise ValueError(_("Function {0}: field '{1}' is not a date").format('format_date_field', field))
if val is None:
s = ''
elif format_string == 'to_number':
s = val.timestamp()
elif format_string.startswith('from_number'):
val = datetime.fromtimestamp(float(val))
f = format_string[12:]
s = format_date(val, f if f else 'iso')
else:
s = format_date(val, format_string)
return s except ValueError:
raise except Exception:
traceback.print_exc()
raise return s

[документация]
class BuiltinUppercase(BuiltinFormatterFunction):
name = 'uppercase'
arg_count = 1 category = CASE_CHANGES
__doc__ = doc = _( r'''
``uppercase(value)`` -- returns the ``value`` in upper case.
''')

def evaluate(self, formatter, kwargs, mi, locals, val):
return val.upper()

[документация]
class BuiltinLowercase(BuiltinFormatterFunction):
name = 'lowercase'
arg_count = 1 category = CASE_CHANGES
__doc__ = doc = _( r'''
``lowercase(value)`` -- returns the ``value`` in lower case.
''')

def evaluate(self, formatter, kwargs, mi, locals, val):
return val.lower()

[документация]
class BuiltinTitlecase(BuiltinFormatterFunction):
name = 'titlecase'
arg_count = 1 category = CASE_CHANGES
__doc__ = doc = _( r'''
``titlecase(value)`` -- returns the ``value`` in title case.
''')

def evaluate(self, formatter, kwargs, mi, locals, val):
return titlecase(val)

[документация]
class BuiltinCapitalize(BuiltinFormatterFunction):
name = 'capitalize'
arg_count = 1 category = CASE_CHANGES
__doc__ = doc = _( r'''
``capitalize(value)`` -- returns the ``value`` with the first letter in upper case and the rest lower case.
''')

def evaluate(self, formatter, kwargs, mi, locals, val):
return capitalize(val)

[документация]
class BuiltinBooksize(BuiltinFormatterFunction):
name = 'booksize'
arg_count = 0 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``booksize()`` -- returns the value of the calibre ``size`` field. Returns '' if the book has no formats.[/]

This function works only in the GUI. If you want to use this value in save-to-disk or send-to-device templates then you must make a custom "Column built from other columns", use the function in that column's template, and use that column's value in your save/send templates
''')

def evaluate(self, formatter, kwargs, mi, locals):
if hasattr(mi, '_proxy_metadata'):
try:
v = mi._proxy_metadata.book_size if v is not None:
return str(mi._proxy_metadata.book_size)
return ''
except Exception:
pass return ''
self.only_in_gui_error()

[документация]
class BuiltinOndevice(BuiltinFormatterFunction):
name = 'ondevice'
arg_count = 0 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``ondevice()`` -- return the string ``'Yes'`` if ``ondevice`` is set, otherwise return the empty string.[/] This function works only in the GUI. If you want to use this value in save-to-disk or send-to-device templates then you must make a custom "Column built from other columns", use the function in that column\'s template, and use that column\'s value in your save/send templates.
''')

def evaluate(self, formatter, kwargs, mi, locals):
if hasattr(mi, '_proxy_metadata'):
if mi._proxy_metadata.ondevice_col:
return _('Yes')
return ''
self.only_in_gui_error()

[документация]
class BuiltinAnnotationCount(BuiltinFormatterFunction):
name = 'annotation_count'
arg_count = 0 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``annotation_count()`` -- return the total number of annotations of all types attached to the current book.[/] This function works only in the GUI and the content server.
''')

def evaluate(self, formatter, kwargs, mi, locals):
c = self.get_database(mi, formatter=formatter).new_api.annotation_count_for_book(mi.id)
return '' if c == 0 else str(c)

[документация]
class BuiltinIsMarked(BuiltinFormatterFunction):
name = 'is_marked'
arg_count = 0 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``is_marked()`` -- check whether the book is `marked` in calibre.[/] If it is then return the value of the mark, either ``'true'`` (lower case) or a comma-separated list of named marks. Returns ``''`` (the empty string) if the book is not marked. This function works only in the GUI.
''')

def evaluate(self, formatter, kwargs, mi, locals):
c = self.get_database(mi, formatter=formatter).data.get_marked(mi.id)
return c if c else ''

[документация]
class BuiltinSeriesSort(BuiltinFormatterFunction):
name = 'series_sort'
arg_count = 0 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``series_sort()`` -- returns the series sort value.
''')

def evaluate(self, formatter, kwargs, mi, locals):
if mi.series:
langs = mi.languages lang = langs[0] if langs else None return title_sort(mi.series, lang=lang)
return ''

[документация]
class BuiltinHasCover(BuiltinFormatterFunction):
name = 'has_cover'
arg_count = 0 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``has_cover()`` -- return ``'Yes'`` if the book has a cover, otherwise the empty string.
''')

def evaluate(self, formatter, kwargs, mi, locals):
if mi.has_cover:
return _('Yes')
return ''

[документация]
class BuiltinFirstNonEmpty(BuiltinFormatterFunction):
name = 'first_non_empty'
arg_count = -1 category = ITERATING_VALUES
__doc__ = doc = _( r'''
``first_non_empty(value [, value]*)`` -- returns the first ``value`` that is not empty.[/] If all values are empty, then the empty string is returned. You can have as many values as you want.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
i = 0 while i < len(args):
if args[i]:
return args[i]
i += 1 return ''

[документация]
class BuiltinAnd(BuiltinFormatterFunction):
name = 'and'
arg_count = -1 category = BOOLEAN
__doc__ = doc = _( r'''
``and(value [, value]*)`` -- returns the string ``'1'`` if all values are not empty, otherwise returns the empty string.[/] You can have as many values as you want. In most cases you can use the ``&&`` operator instead of this function. One reason not to replace ``and()`` with ``&&`` is when short-circuiting can change the results because of side effects. For example, ``and(a='',b=5)`` will always do both assignments, where the ``&&`` operator won't do the second.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
i = 0 while i < len(args):
if not args[i]:
return ''
i += 1 return '1'

[документация]
class BuiltinOr(BuiltinFormatterFunction):
name = 'or'
arg_count = -1 category = BOOLEAN
__doc__ = doc = _( r'''
``or(value [, value]*)`` -- returns the string ``'1'`` if any value is not empty, otherwise returns the empty string.[/] You can have as many values as you want. This function can usually be replaced by the ``||`` operator. A reason it cannot be replaced is if short-circuiting will change the results because of side effects.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
i = 0 while i < len(args):
if args[i]:
return '1'
i += 1 return ''

[документация]
class BuiltinNot(BuiltinFormatterFunction):
name = 'not'
arg_count = 1 category = BOOLEAN
__doc__ = doc = _( r'''
``not(value)`` -- returns the string ``'1'`` if the value is empty, otherwise returns the empty string.[/] This function can usually be replaced with the unary not (``!``) operator.
''')

def evaluate(self, formatter, kwargs, mi, locals, val):
return '' if val else '1'

[документация]
class BuiltinListJoin(BuiltinFormatterFunction):
name = 'list_join'
arg_count = -1 category = LIST_MANIPULATION
__doc__ = doc = _( r'''
``list_join(with_separator, list1, separator1 [, list2, separator2]*)`` -- return a list made by joining the items in the source lists[/] (``list1`` etc)
using ``with_separator`` between the items in the result list. Items in each source ``list[123...]`` are separated by the associated ``separator[123...]``. A list can contain zero values. It can be a field like ``publisher`` that is single-valued, effectively a one-item list. Duplicates are removed using a case-insensitive comparison. Items are returned in the order they appear in the source lists. If items on lists differ only in letter case then the last is used. All separators can be more than one character.

Example:
[CODE]
program:
list_join('#@#', $authors, '&', $tags, ',')
[/CODE]
You can use ``list_join`` on the results of previous calls to ``list_join`` as follows:
[CODE]
program:
a = list_join('#@#', $authors, '&', $tags, ','); b = list_join('#@#', a, '#@#', $#genre, ',', $#people, '&', 'some value', ',')
[/CODE]
You can use expressions to generate a list. For example, assume you want items for ``authors`` and ``#genre``, but with the genre changed to the word "Genre: "
followed by the first letter of the genre, i.e. the genre "Fiction" becomes
"Genre: F". The following will do that:
{}''').format('''\
[CODE]
program:
list_join('#@#', $authors, '&', list_re($#genre, ',', '^(.).*$', 'Genre: \\1'), ',')
[/CODE]
''') # not translated as \1 gets mistranslated as a control char in transifex
# for some reason. And yes, the double backslash is required, for some reason.

def evaluate(self, formatter, kwargs, mi, locals, with_separator, *args):
if len(args) % 2 != 0:
raise ValueError(
_("Invalid 'List, separator' pairs. Every list must have one "
"associated separator"))

# Starting in python 3.7 dicts preserve order so we don't need OrderedDict result = {}
i = 0 while i < len(args):
lst = [v.strip() for v in args[i].split(args[i+1]) if v.strip()]
result.update({item.lower():item for item in lst})
i += 2 return with_separator.join(result.values())

[документация]
class BuiltinListUnion(BuiltinFormatterFunction):
name = 'list_union'
arg_count = 3 category = LIST_MANIPULATION
__doc__ = doc = _( r'''
``list_union(list1, list2, separator)`` -- return a list made by merging the items in ``list1`` and ``list2``[/], removing duplicate items using a case-insensitive comparison. If items differ in case, the one in ``list1`` is used.
The items in ``list1`` and ``list2`` are separated by ``separator``, as are the items in the returned list. Aliases: ``merge_lists()``, ``list_union()``
''')
aliases = ['merge_lists']

def evaluate(self, formatter, kwargs, mi, locals, list1, list2, separator):
res = {icu_lower(l.strip()): l.strip() for l in list2.split(separator) if l.strip()}
res.update({icu_lower(l.strip()): l.strip() for l in list1.split(separator) if l.strip()})
if separator == ',':
separator = ', '
return separator.join(res.values())

[документация]
class BuiltinRange(BuiltinFormatterFunction):
name = 'range'
arg_count = -1 category = LIST_MANIPULATION
__doc__ = doc = _( r'''
``range(start, stop, step, limit)`` -- returns a list of numbers generated by looping over the range specified by the parameters ``start``, ``stop``, and ``step``, with a maximum length of ``limit``.[/] The first value produced is 'start'. Subsequent values
``next_v = current_v + step``. The loop continues while ``next_v < stop`` assuming ``step`` is positive, otherwise while ``next_v > stop``. An empty list is produced if ``start`` fails the test: ``start >= stop`` if ``step`` is positive. The ``limit`` sets the maximum length of the list and has a default of
1000. The parameters ``start``, ``step``, and ``limit`` are optional. Calling
``range()`` with one argument specifies ``stop``. Two arguments specify
``start`` and ``stop``. Three arguments specify ``start``, ``stop``, and
``step``. Four arguments specify ``start``, ``stop``, ``step`` and ``limit``.

Examples:
[CODE]
range(5) -> '0, 1, 2, 3, 4'
range(0, 5) -> '0, 1, 2, 3, 4'
range(-1, 5) -> '-1, 0, 1, 2, 3, 4'
range(1, 5) -> '1, 2, 3, 4'
range(1, 5, 2) -> '1, 3'
range(1, 5, 2, 5) -> '1, 3'
range(1, 5, 2, 1) -> error(limit exceeded)
[/CODE]
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
limit_val = 1000 start_val = 0 step_val = 1 if len(args) == 1:
stop_val = int(args[0] if args[0] and args[0] != 'None' else 0)
elif len(args) == 2:
start_val = int(args[0] if args[0] and args[0] != 'None' else 0)
stop_val = int(args[1] if args[1] and args[1] != 'None' else 0)
elif len(args) >= 3:
start_val = int(args[0] if args[0] and args[0] != 'None' else 0)
stop_val = int(args[1] if args[1] and args[1] != 'None' else 0)
step_val = int(args[2] if args[2] and args[2] != 'None' else 0)
if len(args) > 3:
limit_val = int(args[3] if args[3] and args[3] != 'None' else 0)
r = range(start_val, stop_val, step_val)
if len(r) > limit_val:
raise ValueError(
_('{0}: length ({1}) longer than limit ({2})').format(
'range', len(r), str(limit_val)))
return ', '.join([str(v) for v in r])

[документация]
class BuiltinListRemoveDuplicates(BuiltinFormatterFunction):
name = 'list_remove_duplicates'
arg_count = 2 category = LIST_MANIPULATION
__doc__ = doc = _( r'''
``list_remove_duplicates(list, separator)`` -- return a list made by removing duplicate items in ``list``.[/] If items differ only in case then the last is returned. The items in ``list`` are separated by ``separator``, as are the items in the returned list.
''')

def evaluate(self, formatter, kwargs, mi, locals, list_, separator):
res = {icu_lower(l.strip()): l.strip() for l in list_.split(separator) if l.strip()}
if separator == ',':
separator = ', '
return separator.join(res.values())

[документация]
class BuiltinListDifference(BuiltinFormatterFunction):
name = 'list_difference'
arg_count = 3 category = LIST_MANIPULATION
__doc__ = doc = _( r'''
``list_difference(list1, list2, separator)`` -- return a list made by removing from ``list1`` any item found in ``list2``[/] using a case-insensitive comparison.
The items in ``list1`` and ``list2`` are separated by ``separator``, as are the items in the returned list.
''')

def evaluate(self, formatter, kwargs, mi, locals, list1, list2, separator):
l1 = [l.strip() for l in list1.split(separator) if l.strip()]
l2 = {icu_lower(l.strip()) for l in list2.split(separator) if l.strip()}

res = []
for i in l1:
if icu_lower(i) not in l2 and i not in res:
res.append(i)
if separator == ',':
return ', '.join(res)
return separator.join(res)

[документация]
class BuiltinListIntersection(BuiltinFormatterFunction):
name = 'list_intersection'
arg_count = 3 category = LIST_MANIPULATION
__doc__ = doc = _( r'''
``list_intersection(list1, list2, separator)`` -- return a list made by removing from ``list1`` any item not found in ``list2``[/], using a case-insensitive comparison. The items in ``list1`` and ``list2`` are separated by ``separator``, as are the items in the returned list.
''')

def evaluate(self, formatter, kwargs, mi, locals, list1, list2, separator):
l1 = [l.strip() for l in list1.split(separator) if l.strip()]
l2 = {icu_lower(l.strip()) for l in list2.split(separator) if l.strip()}

res = []
for i in l1:
if icu_lower(i) in l2 and i not in res:
res.append(i)
if separator == ',':
return ', '.join(res)
return separator.join(res)

[документация]
class BuiltinListSort(BuiltinFormatterFunction):
name = 'list_sort'
arg_count = 3 category = LIST_MANIPULATION
__doc__ = doc = _( r'''
``list_sort(value, direction, separator)`` -- return ``value`` sorted using a case-insensitive lexical sort.[/] If ``direction`` is zero (number or character),
``value`` is sorted ascending, otherwise descending. The list items are separated by ``separator``, as are the items in the returned list.
''')

def evaluate(self, formatter, kwargs, mi, locals, value, direction, separator):
res = [l.strip() for l in value.split(separator) if l.strip()]
if separator == ',':
return ', '.join(sorted(res, key=sort_key, reverse=direction != '0'))
return separator.join(sorted(res, key=sort_key, reverse=direction != '0'))

[документация]
class BuiltinListEquals(BuiltinFormatterFunction):
name = 'list_equals'
arg_count = 6 category = LIST_MANIPULATION
__doc__ = doc = _( r'''
``list_equals(list1, sep1, list2, sep2, yes_val, no_val)`` -- return ``yes_val`` if ``list1`` and ``list2`` contain the same items, otherwise return ``no_val``.[/]
The items are determined by splitting each list using the appropriate separator character (``sep1`` or ``sep2``). The order of items in the lists is not relevant. The comparison is case-insensitive.
''')

def evaluate(self, formatter, kwargs, mi, locals, list1, sep1, list2, sep2, yes_val, no_val):
s1 = {icu_lower(l.strip()) for l in list1.split(sep1) if l.strip()}
s2 = {icu_lower(l.strip()) for l in list2.split(sep2) if l.strip()}
if s1 == s2:
return yes_val return no_val

[документация]
class BuiltinListRe(BuiltinFormatterFunction):
name = 'list_re'
arg_count = 4 category = LIST_MANIPULATION
__doc__ = doc = _( r'''
``list_re(src_list, separator, include_re, opt_replace)`` -- Construct a list by first separating ``src_list`` into items using the ``separator`` character.[/] For each item in the list, check if it matches ``include_re``. If it does then add it to the list to be returned. If ``opt_replace`` is not the empty string then apply the replacement before adding the item to the returned list.
''')

def evaluate(self, formatter, kwargs, mi, locals, src_list, separator, include_re, opt_replace):
l = [l.strip() for l in src_list.split(separator) if l.strip()]
res = []
for item in l:
if re.search(include_re, item, flags=re.I) is not None:
if opt_replace:
item = re.sub(include_re, opt_replace, item)
for i in [t.strip() for t in item.split(separator) if t.strip()]:
if i not in res:
res.append(i)
if separator == ',':
return ', '.join(res)
return separator.join(res)

[документация]
class BuiltinListReGroup(BuiltinFormatterFunction):
name = 'list_re_group'
arg_count = -1 category = LIST_MANIPULATION
__doc__ = doc = _( r'''
``list_re_group(src_list, separator, include_re, search_re [,template_for_group]*)``
-- Like list_re except replacements are not optional.[/] It uses ``re_group(item, search_re, template ...)`` when doing the replacements.
''')

def evaluate(self, formatter, kwargs, mi, locals, src_list, separator, include_re, search_re, *args):
from calibre.utils.formatter import EvalFormatter l = [l.strip() for l in src_list.split(separator) if l.strip()]
res = []
for item in l:
def repl(mo):
newval = ''
if mo and mo.lastindex:
for dex in range(mo.lastindex):
gv = mo.group(dex+1)
if gv is None:
continue if len(args) > dex:
template = args[dex].replace('[[', '{').replace(']]', '}')
newval += EvalFormatter().safe_format(template, {'$': gv},
'EVAL', None, strip_results=False)
else:
newval += gv return newval if re.search(include_re, item, flags=re.I) is not None:
item = re.sub(search_re, repl, item, flags=re.I)
for i in [t.strip() for t in item.split(separator) if t.strip()]:
if i not in res:
res.append(i)
if separator == ',':
return ', '.join(res)
return separator.join(res)

[документация]
class BuiltinToday(BuiltinFormatterFunction):
name = 'today'
arg_count = 0 category = DATE_FUNCTIONS
__doc__ = doc = _( r'''
``today()`` -- return a date+time string for today (now).[/] This value is designed for use in ``format_date`` or ``days_between``, but can be manipulated like any other string. The date is in [URL href="https://en.wikipedia.org/wiki/ISO_8601"]ISO[/URL]
date/time format.
''')

def evaluate(self, formatter, kwargs, mi, locals):
return format_date(now(), 'iso')

[документация]
class BuiltinDaysBetween(BuiltinFormatterFunction):
name = 'days_between'
arg_count = 2 category = DATE_FUNCTIONS
__doc__ = doc = _( r'''
``days_between(date1, date2)`` -- return the number of days between ``date1`` and ``date2``.[/] The number is positive if ``date1`` is greater than ``date2``, otherwise negative. If either ``date1`` or ``date2`` are not dates, the function returns the empty string.
''')

def evaluate(self, formatter, kwargs, mi, locals, date1, date2):
try:
d1 = parse_date(date1)
if d1 == UNDEFINED_DATE:
return ''
d2 = parse_date(date2)
if d2 == UNDEFINED_DATE:
return ''
except Exception:
return ''
i = d1 - d2 return f'{i.days+(i.seconds/(24.0*60.0*60.0)):.1f}'

[документация]
class BuiltinDateArithmetic(BuiltinFormatterFunction):
name = 'date_arithmetic'
arg_count = -1 category = DATE_FUNCTIONS
__doc__ = doc = _( r'''
``date_arithmetic(value, calc_spec, fmt)`` -- Calculate a new date from ``value`` using ``calc_spec``.[/] Return the new date formatted according to optional
``fmt``: if not supplied then the result will be in ISO format. The ``calc_spec`` is a string formed by concatenating pairs of ``vW`` (``valueWhat``) where ``v`` is a possibly-negative number and W is one of the following letters:
[LIST]
[*]``s``: add ``v`` seconds to ``date``
[*]``m``: add ``v`` minutes to ``date``
[*]``h``: add ``v`` hours to ``date``
[*]``d``: add ``v`` days to ``date``
[*]``w``: add ``v`` weeks to ``date``
[*]``y``: add ``v`` years to ``date``, where a year is 365 days.
[/LIST]
Example: ``'1s3d-1m'`` will add 1 second, add 3 days, and subtract 1 minute from ``date``.
''')

calc_ops = {
's': lambda v: timedelta(seconds=v),
'm': lambda v: timedelta(minutes=v),
'h': lambda v: timedelta(hours=v),
'd': lambda v: timedelta(days=v),
'w': lambda v: timedelta(weeks=v),
'y': lambda v: timedelta(days=v * 365),
}

def evaluate(self, formatter, kwargs, mi, locals, value, calc_spec, fmt=None):
try:
d = parse_date(value)
if d == UNDEFINED_DATE:
return ''
while calc_spec:
mo = re.match(r'([-+\d]+)([smhdwy])', calc_spec)
if mo is None:
raise ValueError(
_("{0}: invalid calculation specifier '{1}'").format(
'date_arithmetic', calc_spec))
d += self.calc_ops[mo[2]](int(mo[1]))
calc_spec = calc_spec[len(mo[0]):]
return format_date(d, fmt if fmt else 'iso')
except ValueError as e:
raise e except Exception as e:
traceback.print_exc()
raise ValueError(_('{0}: error: {1}').format('date_arithmetic', str(e)))

[документация]
class BuiltinLanguageStrings(BuiltinFormatterFunction):
name = 'language_strings'
arg_count = 2 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``language_strings(value, localize)`` -- return the language names for the language codes
([URL href="https://www.loc.gov/standards/iso639-2/php/code_list.php"]
see here for names and codes[/URL])
passed in ``value``.[/] Example: ``{languages:language_strings()}``.
If ``localize`` is zero, return the strings in English. If ``localize`` is not zero, return the strings in the language of the current locale. ``lang_codes`` is a comma-separated list.
''')

def evaluate(self, formatter, kwargs, mi, locals, lang_codes, localize):
retval = []
for c in [c.strip() for c in lang_codes.split(',') if c.strip()]:
try:
n = calibre_langcode_to_name(c, localize != '0')
if n:
retval.append(n)
except Exception:
pass return ', '.join(retval)

[документация]
class BuiltinLanguageCodes(BuiltinFormatterFunction):
name = 'language_codes'
arg_count = 1 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``language_codes(lang_strings)`` -- return the
[URL href="https://www.loc.gov/standards/iso639-2/php/code_list.php"]language codes[/URL] for the language names passed in ``lang_strings``.[/] The strings must be in the language of the current locale. ``lang_strings`` is a comma-separated list.
''')

def evaluate(self, formatter, kwargs, mi, locals, lang_strings):
retval = []
for c in [c.strip() for c in lang_strings.split(',') if c.strip()]:
try:
cv = canonicalize_lang(c)
if cv:
retval.append(canonicalize_lang(cv))
except Exception:
pass return ', '.join(retval)

[документация]
class BuiltinCurrentLibraryName(BuiltinFormatterFunction):
name = 'current_library_name'
arg_count = 0 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``current_library_name()`` -- return the last name on the path to the current calibre library.
''')

def evaluate(self, formatter, kwargs, mi, locals):
from calibre.library import current_library_name return current_library_name()

[документация]
class BuiltinCurrentLibraryPath(BuiltinFormatterFunction):
name = 'current_library_path'
arg_count = 0 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``current_library_path()`` -- return the full path to the current calibre library.
''')

def evaluate(self, formatter, kwargs, mi, locals):
from calibre.library import current_library_path return current_library_path()

[документация]
class BuiltinFinishFormatting(BuiltinFormatterFunction):
name = 'finish_formatting'
arg_count = 4 category = FORMATTING_VALUES
__doc__ = doc = _( r'''
``finish_formatting(value, format, prefix, suffix)`` -- apply the ``format``, ``prefix``, and
``suffix`` to the ``value`` in the same way as done in a template like
``{series_index:05.2f| - |- }``.[/] This function is provided to ease conversion of complex single-function- or template-program-mode templates to `GPM` Templates.
For example, the following program produces the same output as the above template:
[CODE]
program: finish_formatting(field("series_index"), "05.2f", " - ", " - ")
[/CODE]
Another example: for the template:
[CODE]
{series:re(([^\s])[^\s]+(\s|$),\1)}{series_index:0>2s| - | - }{title}
[/CODE]
use:
[CODE]
program:
strcat( re(field('series'), '([^\s])[^\s]+(\s|$)', '\1'), finish_formatting(field('series_index'), '0>2s', ' - ', ' - '), field('title')
)
[/CODE]
''')

def evaluate(self, formatter, kwargs, mi, locals_, val, fmt, prefix, suffix):
if not val:
return val return prefix + formatter._do_format(val, fmt) + suffix

[документация]
class BuiltinVirtualLibraries(BuiltinFormatterFunction):
name = 'virtual_libraries'
arg_count = 0 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``virtual_libraries()`` -- return a comma-separated list of Virtual libraries that contain this book.[/] This function works only in the GUI. If you want to use these values in save-to-disk or send-to-device templates then you must make a custom
"Column built from other columns", use the function in that column's template, and use that column's value in your save/send templates.
''')

def evaluate(self, formatter, kwargs, mi, locals_):
db = self.get_database(mi, formatter=formatter)
try:
a = db.data.get_virtual_libraries_for_books((mi.id,))
return ', '.join(a[mi.id])
except ValueError as v:
return str(v)

[документация]
class BuiltinCurrentVirtualLibraryName(BuiltinFormatterFunction):
name = 'current_virtual_library_name'
arg_count = 0 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``current_virtual_library_name()`` -- return the name of the current virtual library if there is one, otherwise the empty string.[/] Library name case is preserved. Example:
[CODE]
program: current_virtual_library_name()
[/CODE]
This function works only in the GUI.
''')

def evaluate(self, formatter, kwargs, mi, locals):
return self.get_database(mi, formatter=formatter).data.get_base_restriction_name()

[документация]
class BuiltinUserCategories(BuiltinFormatterFunction):
name = 'user_categories'
arg_count = 0 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``user_categories()`` -- return a comma-separated list of the user categories that contain this book.[/] This function works only in the GUI. If you want to use these values in save-to-disk or send-to-device templates then you must make a custom
`Column built from other columns`, use the function in that column's template, and use that column's value in your save/send templates
''')

def evaluate(self, formatter, kwargs, mi, locals_):
if hasattr(mi, '_proxy_metadata'):
cats = {k for k, v in iteritems(mi._proxy_metadata.user_categories) if v}
cats = sorted(cats, key=sort_key)
return ', '.join(cats)
self.only_in_gui_error()

[документация]
class BuiltinTransliterate(BuiltinFormatterFunction):
name = 'transliterate'
arg_count = 1 category = STRING_MANIPULATION
__doc__ = doc = _( r'''
``transliterate(value)`` -- Return a string in a latin alphabet formed by approximating the sound of the words in ``value``.[/] For example, if ``value`` is ``{0}`` this function returns ``{1}``.
''').format('Фёдор Миха́йлович Достоевский', 'Fiodor Mikhailovich Dostoievskii')

def evaluate(self, formatter, kwargs, mi, locals, source):
from calibre.utils.filenames import ascii_text return ascii_text(source)

[документация]
class BuiltinGetLink(BuiltinFormatterFunction):
name = 'get_link'
arg_count = 2 category = DB_FUNCS
__doc__ = doc = _( r'''
``get_link(field_name, field_value)`` -- fetch the link for field ``field_name`` with value ``field_value``.[/] If there is no attached link, return the empty string. Examples:
[LIST]
[*]The following returns the link attached to the tag ``Fiction``:
[CODE]
get_link('tags', 'Fiction')
[/CODE]
[*]This template makes a list of the links for all the tags associated with a book in the form ``value:link, ...``:
[CODE]
program:
ans = ''; for t in $tags:
l = get_link('tags', t); if l then ans = list_join(', ', ans, ',', t & ':' & get_link('tags', t), ',')
fi rof; ans
[/CODE]
[/LIST]
This function works only in the GUI and the content server.
''')

def evaluate(self, formatter, kwargs, mi, locals, field_name, field_value):
db = self.get_database(mi, formatter=formatter).new_api try:
link = None item_id = db.get_item_id(field_name, field_value, case_sensitive=True)
if item_id is not None:
link = db.link_for(field_name, item_id)
return link if link is not None else ''
except Exception as e:
traceback.print_exc()
raise ValueError(e)

[документация]
class BuiltinAuthorLinks(BuiltinFormatterFunction):
name = 'author_links'
arg_count = 2 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``author_links(val_separator, pair_separator)`` -- returns a string containing a list of authors and those authors' link values[/] in the form:
``author1 val_separator author1_link pair_separator author2 val_separator author2_link`` etc.

An author is separated from its link value by the ``val_separator`` string with no added spaces. Assuming the ``val_separator`` is a colon,
``author:link value`` pairs are separated by the
``pair_separator`` string argument with no added spaces. It is up to you to choose separators that do not occur in author names or links. An author is included even if the author link is empty.
''')

def evaluate(self, formatter, kwargs, mi, locals, val_sep, pair_sep):
if hasattr(mi, '_proxy_metadata'):
link_data = mi._proxy_metadata.link_maps if not link_data:
return ''
link_data = link_data.get('authors')
if not link_data:
return ''
names = sorted(link_data.keys(), key=sort_key)
return pair_sep.join(n + val_sep + link_data[n] for n in names)
self.only_in_gui_error()

[документация]
class BuiltinAuthorSorts(BuiltinFormatterFunction):
name = 'author_sorts'
arg_count = 1 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``author_sorts(val_separator)`` -- returns a string containing a list of author's sort values for the authors of the book.[/] The sort is the one in the author metadata information, which can be different from the author_sort in books. The returned list has the form ``author sort 1`` ``val_separator`` ``author sort 2`` etc. with no added spaces. The author sort values in this list are in the same order as the authors of the book. If you want spaces around ``val_separator`` then include them in the ``val_separator`` string.
''')

def evaluate(self, formatter, kwargs, mi, locals, val_sep):
sort_data = mi.author_sort_map if not sort_data:
return ''
names = [sort_data.get(n) for n in mi.authors if n.strip()]
return val_sep.join(n for n in names)

[документация]
class BuiltinConnectedDeviceName(BuiltinFormatterFunction):
name = 'connected_device_name'
arg_count = 1 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``connected_device_name(storage_location_key)`` -- if a device is connected then return the device name, otherwise return the empty string.[/] Each storage location on a device has its own device name. The ``storage_location_key`` names are
``'main'``, ``'carda'`` and ``'cardb'``. This function works only in the GUI.
''')

def evaluate(self, formatter, kwargs, mi, locals, storage_location):
# We can't use get_database() here because we need the device manager.
# In other words, the function really does need the GUI with suppress(Exception):
# Do the import here so that we don't entangle the GUI when using
# command line functions from calibre.gui2.ui import get_gui info = get_gui().device_manager.get_current_device_information()
if info is None:
return ''
try:
if storage_location not in {'main', 'carda', 'cardb'}:
raise ValueError(
_('connected_device_name: invalid storage location "{}"').format(storage_location))
info = info['info'][4]
if storage_location not in info:
return ''
return info[storage_location]['device_name']
except Exception:
traceback.print_exc()
raise self.only_in_gui_error()

[документация]
class BuiltinConnectedDeviceUUID(BuiltinFormatterFunction):
name = 'connected_device_uuid'
arg_count = 1 category = GET_FROM_METADATA
__doc__ = doc = _( r'''
``connected_device_uuid(storage_location_key)`` -- if a device is connected then return the device uuid (unique id), otherwise return the empty string.[/] Each storage location on a device has a different uuid. The ``storage_location_key`` location names are ``'main'``, ``'carda'`` and ``'cardb'``. This function works only in the GUI.
''')

def evaluate(self, formatter, kwargs, mi, locals, storage_location):
# We can't use get_database() here because we need the device manager.
# In other words, the function really does need the GUI with suppress(Exception):
# Do the import here so that we don't entangle the GUI when using
# command line functions from calibre.gui2.ui import get_gui info = get_gui().device_manager.get_current_device_information()
if info is None:
return ''
try:
if storage_location not in {'main', 'carda', 'cardb'}:
raise ValueError(
_('connected_device_name: invalid storage location "{}"').format(storage_location))
info = info['info'][4]
if storage_location not in info:
return ''
return info[storage_location]['device_store_uuid']
except Exception:
traceback.print_exc()
raise self.only_in_gui_error()

[документация]
class BuiltinCheckYesNo(BuiltinFormatterFunction):
name = 'check_yes_no'
arg_count = 4 category = STRING_MANIPULATION
__doc__ = doc = _( r'''
``check_yes_no(field_name, is_undefined, is_false, is_true)`` -- checks if the value of the yes/no field named by the lookup name ``field_name`` is one of the values specified by the parameters[/], returning ``'Yes'`` if a match is found otherwise returning the empty string. Set the parameter ``is_undefined``,
``is_false``, or ``is_true`` to 1 (the number) to check that condition, otherwise set it to 0.

Example: ``check_yes_no("#bool", 1, 0, 1)`` returns ``'Yes'`` if the yes/no field
``#bool`` is either True or undefined (neither True nor False).

More than one of ``is_undefined``, ``is_false``, or ``is_true`` can be set to 1.
''')

def evaluate(self, formatter, kwargs, mi, locals, field, is_undefined, is_false, is_true):
res = getattr(mi, field, None)
# Missing fields will return None. Oh well, this lets it be used everywhere,
# not just in the GUI.
if res is None:
if is_undefined == '1':
return 'Yes'
return ''
if not isinstance(res, bool):
raise ValueError(_('check_yes_no requires the field be a Yes/No custom column'))
if is_false == '1' and not res:
return 'Yes'
if is_true == '1' and res:
return 'Yes'
return ''

[документация]
class BuiltinRatingToStars(BuiltinFormatterFunction):
name = 'rating_to_stars'
arg_count = 2 category = FORMATTING_VALUES
__doc__ = doc = _( r'''
``rating_to_stars(value, use_half_stars)`` -- Returns the ``value`` as string of star
(``{}``) characters.[/] The value must be a number between ``0`` and ``5``. Set
``use_half_stars`` to ``1`` if you want half star characters for fractional numbers available with custom ratings columns.
''').format('★')

def evaluate(self, formatter, kwargs, mi, locals, value, use_half_stars):
if not value:
return ''
err_msg = _('The rating must be a number between 0 and 5')
try:
v = float(value) * 2 except Exception:
raise ValueError(err_msg)
if v < 0 or v > 10:
raise ValueError(err_msg)
from calibre.ebooks.metadata import rating_to_stars return rating_to_stars(v, use_half_stars == '1')

[документация]
class BuiltinSwapAroundArticles(BuiltinFormatterFunction):
name = 'swap_around_articles'
arg_count = 2 category = STRING_MANIPULATION
__doc__ = doc = _( r''' ``swap_around_articles(value, separator)`` -- returns the ``value`` with articles moved to the end, separated by a semicolon.[/] The ``value`` can be a list, in which case each item in the list is processed. If the ``value`` is a list then you must provide the ``separator``. If no ``separator`` is provided or the separator is the empty string then the ``value`` is treated as being a single value, not a list. The `articles` are those used by calibre to generate the ``title_sort``.
''')

def evaluate(self, formatter, kwargs, mi, locals, val, separator):
if not val:
return ''
if not separator:
return title_sort(val).replace(',', ';')
result = []
try:
for v in [x.strip() for x in val.split(separator)]:
result.append(title_sort(v).replace(',', ';'))
except Exception:
traceback.print_exc()
return separator.join(sorted(result, key=sort_key))

[документация]
class BuiltinArguments(BuiltinFormatterFunction):
name = 'arguments'
arg_count = -1 category = OTHER
__doc__ = doc = _( r'''
``arguments(id[=expression] [, id[=expression]]*)`` -- Used in a stored template to retrieve the arguments passed in the call.[/] It both declares and initializes local variables with the supplied names, the ``id``s, making them effectively parameters. The variables are positional; they get the value of the argument given in the call in the same position. If the corresponding argument is not provided in the call then ``arguments()`` assigns that variable the provided default value. If there is no default value then the variable is set to the empty string.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
# The arguments function is implemented in-line in the formatter raise NotImplementedError()

[документация]
class BuiltinGlobals(BuiltinFormatterFunction):
name = 'globals'
arg_count = -1 category = OTHER
__doc__ = doc = _( r'''
``globals(id[=expression] [, id[=expression]]*)`` -- Retrieves "global variables"
that can be passed into the formatter.[/] The name ``id`` is the name of the global variable. It both declares and initializes local variables with the names of the global variables passed in (the ``id`` parameters. If the corresponding variable is not provided in the globals then it assigns that variable the provided default value. If there is no default value then the variable is set to the empty string.)
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
# The globals function is implemented in-line in the formatter raise NotImplementedError()

[документация]
class BuiltinSetGlobals(BuiltinFormatterFunction):
name = 'set_globals'
arg_count = -1 category = OTHER
__doc__ = doc = _( r'''
``set_globals(id[=expression] [, id[=expression]]*)`` -- Sets `global variables` that can be passed into the formatter.[/] The globals are given the name of the ``id`` passed in. The value of the ``id`` is used unless an expression is provided.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
# The globals function is implemented in-line in the formatter raise NotImplementedError()

[документация]
class BuiltinFieldExists(BuiltinFormatterFunction):
name = 'field_exists'
arg_count = 1 category = STRING_MANIPULATION
__doc__ = doc = _( r'''
``field_exists(lookup_name)`` -- checks if a field (column) with the lookup name
``lookup_name`` exists, returning ``'1'`` if so and the empty string if not.
''')

def evaluate(self, formatter, kwargs, mi, locals, field_name):
if field_name.lower() in mi.all_field_keys():
return '1'
return ''

[документация]
class BuiltinCharacter(BuiltinFormatterFunction):
name = 'character'
arg_count = 1 category = STRING_MANIPULATION
__doc__ = doc = _( r'''
``character(character_name)`` -- returns the character named by character_name.[/]
For example, ``character('newline')`` returns a newline character (``'\n'``).
The supported character names are ``newline``, ``return``, ``tab``, and
``backslash``. This function is used to put these characters into the output of templates.
''')

def evaluate(self, formatter, kwargs, mi, locals, character_name):
# The globals function is implemented in-line in the formatter raise NotImplementedError()

[документация]
class BuiltinToHex(BuiltinFormatterFunction):
name = 'to_hex'
arg_count = 1 category = URL_FUNCTIONS
__doc__ = doc = _( r'''
``to_hex(val)`` -- returns the string ``val`` encoded into hex.[/] This is useful when constructing calibre URLs.
''')

def evaluate(self, formatter, kwargs, mi, locals, val):
return val.encode().hex()

[документация]
class BuiltinUrlsFromIdentifiers(BuiltinFormatterFunction):
name = 'urls_from_identifiers'
arg_count = 2 category = URL_FUNCTIONS
__doc__ = doc = _( r'''
``urls_from_identifiers(identifiers, sort_results)`` -- given a comma-separated list of ``identifiers``, where an ``identifier`` is a colon-separated pair of values (``id_name:id_value``), returns a comma-separated list of HTML URLs generated from the identifiers.[/] The list not sorted if ``sort_results`` is ``0``
(character or number), otherwise it is sorted alphabetically by the identifier name. The URLs are generated in the same way as the built-in identifiers column when shown in Book Details.
''')

def evaluate(self, formatter, kwargs, mi, locals, identifiers, sort_results):
from calibre.ebooks.metadata.sources.identify import urls_from_identifiers try:
v = {}
for id_ in identifiers.split(','):
if id_:
pair = id_.split(':', maxsplit=1)
if len(pair) == 2:
l = pair[0].strip()
r = pair[1].strip()
if l and r:
v[l] = r urls = urls_from_identifiers(v, sort_results=str(sort_results) != '0')
p = prepare_string_for_xml a = partial(prepare_string_for_xml, attribute=True)
links = [f'<a href="{a(url)}" title="{a(id_typ)}:{a(id_val)}">{p(name)}</a>'
for name, id_typ, id_val, url in urls]
return ', '.join(links)
except Exception as e:
return str(e)[документация]
class BuiltinBookCount(BuiltinFormatterFunction):
name = 'book_count'
arg_count = 2 category = DB_FUNCS
__doc__ = doc = _( r'''
``book_count(query, use_vl)`` -- returns the count of books found by searching for ``query``.[/] If ``use_vl`` is ``0`` (zero) then virtual libraries are ignored.
This function and its companion ``book_values()`` are particularly useful in template searches, supporting searches that combine information from many books such as looking for series with only one book. It cannot be used in composite columns unless the tweak ``allow_template_database_functions_in_composites`` is set to True. It can be used only in the GUI.

For example this template search uses this function and its companion to find all series with only one book:
[LIST]
[*]Define a stored template (using :guilabel:`Preferences->Advanced->Template functions`)
named ``series_only_one_book`` (the name is arbitrary). The template is:
[CODE]
program:
vals = globals(vals=''); if !vals then all_series = book_values('series', 'series:true', ',', 0); for series in all_series:
if book_count('series:="' & series & '"', 0) == 1 then vals = list_join(',', vals, ',', series, ',')
fi rof; set_globals(vals)
fi; str_in_list(vals, ',', $series, 1, '')
[/CODE]
The first time the template runs (the first book checked) it stores the results of the database lookups in a ``global`` template variable named ``vals``. These results are used to check subsequent books without redoing the lookups.
[*] Use the stored template in a template search:
[CODE]
template:"program: series_only_one_book()#@#:n:1"
[/CODE]
Using a stored template instead of putting the template into the search eliminates problems caused by the requirement to escape quotes in search expressions.
[/LIST]
This function can be used only in the GUI and the content server.
''')

def evaluate(self, formatter, kwargs, mi, locals, query, use_vl):
from calibre.db.fields import rendering_composite_name if (not tweaks.get('allow_template_database_functions_in_composites', False) and formatter.global_vars.get(rendering_composite_name, None)):
raise ValueError(_('The book_count() function cannot be used in a composite column'))
db = self.get_database(mi, formatter=formatter)
try:
if use_vl == '0':
# use the new_api search that doesn't use virtual libraries to let
# the function work in content server icon rules.
ids = db.new_api.search(query, None)
else:
ids = db.search_getting_ids(query, None, use_virtual_library=True)
return str(len(ids))
except Exception:
traceback.print_exc()[документация]
class BuiltinBookValues(BuiltinFormatterFunction):
name = 'book_values'
arg_count = 4 category = DB_FUNCS
__doc__ = doc = _( r'''
``book_values(column, query, sep, use_vl)`` -- returns a list of the unique values contained in the column ``column`` (a lookup name), separated by ``sep``, in the books found by searching for ``query``.[/] If ``use_vl`` is ``0`` (zero)
then virtual libraries are ignored. This function and its companion
``book_count()`` are particularly useful in template searches, supporting searches that combine information from many books such as looking for series with only one book. It cannot be used in composite columns unless the tweak
``allow_template_database_functions_in_composites`` is set to True. This function can be used only in the GUI and the content server.
''')

def evaluate(self, formatter, kwargs, mi, locals, column, query, sep, use_vl):
from calibre.db.fields import rendering_composite_name if (not tweaks.get('allow_template_database_functions_in_composites', False) and formatter.global_vars.get(rendering_composite_name, None)):
raise ValueError(_('The book_values() function cannot be used in a composite column'))
db = self.get_database(mi, formatter=formatter)
if column not in db.field_metadata:
raise ValueError(_("The column {} doesn't exist").format(column))
try:
if use_vl == '0':
ids = db.new_api.search(query, None)
else:
ids = db.search_getting_ids(query, None, use_virtual_library=True)
s = set()
for id_ in ids:
f = db.new_api.get_proxy_metadata(id_).get(column, None)
if isinstance(f, (tuple, list)):
s.update(f)
elif f is not None:
s.add(str(f))
return sep.join(s)
except Exception as e:
raise ValueError(e)[документация]
class BuiltinHasExtraFiles(BuiltinFormatterFunction):
name = 'has_extra_files'
arg_count = -1 category = DB_FUNCS
__doc__ = doc = _( r'''
``has_extra_files([pattern])`` -- returns the count of extra files, otherwise ''
(the empty string).[/] If the optional parameter ``pattern`` (a regular expression)
is supplied then the list is filtered to files that match ``pattern`` before the files are counted. The pattern match is case insensitive. See also the functions
:ref:`extra_file_names`, :ref:`extra_file_size` and :ref:`extra_file_modtime`.
This function can be used only in the GUI and the content server.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
if len(args) > 1:
raise ValueError(_('Incorrect number of arguments for function {0}').format('has_extra_files'))
pattern = args[0] if len(args) == 1 else None db = self.get_database(mi, formatter=formatter).new_api try:
files = tuple(f.relpath.partition('/')[-1] for f in db.list_extra_files(mi.id, use_cache=True, pattern=DATA_FILE_PATTERN))
if pattern:
r = re.compile(pattern, re.IGNORECASE)
files = tuple(filter(r.search, files))
return len(files) if len(files) > 0 else ''
except Exception as e:
traceback.print_exc()
raise ValueError(e)[документация]
class BuiltinExtraFileNames(BuiltinFormatterFunction):
name = 'extra_file_names'
arg_count = -1 category = DB_FUNCS
__doc__ = doc = _( r'''
``extra_file_names(sep [, pattern])`` -- returns a ``sep``-separated list of extra files in the book's ``data/`` folder.[/] If the optional parameter
``pattern``, a regular expression, is supplied then the list is filtered to files that match ``pattern``. The pattern match is case insensitive. See also the functions :ref:`has_extra_files`, :ref:`extra_file_modtime` and
:ref:`extra_file_size`. This function can be used only in the GUI and the content server.
''')

def evaluate(self, formatter, kwargs, mi, locals, sep, *args):
if len(args) > 1:
raise ValueError(_('Incorrect number of arguments for function {0}').format('has_extra_files'))
pattern = args[0] if len(args) == 1 else None db = self.get_database(mi, formatter=formatter).new_api try:
files = tuple(f.relpath.partition('/')[-1] for f in db.list_extra_files(mi.id, use_cache=True, pattern=DATA_FILE_PATTERN))
if pattern:
r = re.compile(pattern, re.IGNORECASE)
files = tuple(filter(r.search, files))
return sep.join(files)
except Exception as e:
traceback.print_exc()
raise ValueError(e)

[документация]
class BuiltinExtraFileSize(BuiltinFormatterFunction):
name = 'extra_file_size'
arg_count = 1 category = DB_FUNCS
__doc__ = doc = _( r'''
``extra_file_size(file_name)`` -- returns the size in bytes of the extra file
``file_name`` in the book's ``data/`` folder if it exists, otherwise ``-1``.[/] See also the functions :ref:`has_extra_files`, :ref:`extra_file_names` and
:ref:`extra_file_modtime`. This function can be used only in the GUI and the content server.
''')

def evaluate(self, formatter, kwargs, mi, locals, file_name):
db = self.get_database(mi, formatter=formatter).new_api try:
q = posixpath.join(DATA_DIR_NAME, file_name)
for f in db.list_extra_files(mi.id, use_cache=True, pattern=DATA_FILE_PATTERN):
if f.relpath == q:
return str(f.stat_result.st_size)
return str(-1)
except Exception as e:
traceback.print_exc()
raise ValueError(e)

[документация]
class BuiltinExtraFileModtime(BuiltinFormatterFunction):
name = 'extra_file_modtime'
arg_count = 2 category = DB_FUNCS
__doc__ = doc = _( r'''
``extra_file_modtime(file_name, format_string)`` -- returns the modification time of the extra file ``file_name`` in the book's ``data/`` folder[/] if it exists, otherwise ``-1``. The modtime is formatted according to
``format_string`` (see :ref:`format_date` for details). If ``format_string`` is the empty string, returns the modtime as the floating point number of seconds since the epoch. See also the functions :ref:`has_extra_files`,
:ref:`extra_file_names` and :ref:`extra_file_size`. The epoch is OS dependent.
This function can be used only in the GUI and the content server.
''')

def evaluate(self, formatter, kwargs, mi, locals, file_name, format_string):
db = self.get_database(mi, formatter=formatter).new_api try:
q = posixpath.join(DATA_DIR_NAME, file_name)
for f in db.list_extra_files(mi.id, use_cache=True, pattern=DATA_FILE_PATTERN):
if f.relpath == q:
val = f.stat_result.st_mtime if format_string:
return format_date(datetime.fromtimestamp(val), format_string)
return str(val)
return str(1.0)
except Exception as e:
traceback.print_exc()
raise ValueError(e)

[документация]
class BuiltinGetNote(BuiltinFormatterFunction):
name = 'get_note'
arg_count = 3 category = DB_FUNCS
__doc__ = doc = _( r'''
``get_note(field_name, field_value, plain_text)`` -- fetch the note for field
``field_name`` with value ``field_value``.[/] If ``plain_text`` is empty, return the note's HTML including images. If ``plain_text`` is ``1`` (or ``'1'``), return the note's plain text. If the note doesn't exist, return the empty string in both cases. Example:
[LIST]
[*]Return the HTML of the note attached to the tag `Fiction`:
[CODE]
program:
get_note('tags', 'Fiction', '')
[/CODE]
[*]Return the plain text of the note attached to the author `Isaac Asimov`:
[CODE]
program:
get_note('authors', 'Isaac Asimov', 1)
[/CODE]
[/LIST]
This function works only in the GUI and the content server.
''')

def evaluate(self, formatter, kwargs, mi, locals, field_name, field_value, plain_text):
db = self.get_database(mi, formatter=formatter).new_api try:
note = None item_id = db.get_item_id(field_name, field_value, case_sensitive=True)
if item_id is not None:
note = db.notes_data_for(field_name, item_id)
if note is not None:
if plain_text == '1':
note = note['searchable_text'].partition('\n')[2]
else:
from lxml import html from calibre.db.notes.exim import expand_note_resources, parse_html
# Return the full HTML of the note, including all images
# as data: URLs. Reason: non-exported note html contains
# "calres://" URLs for images. These images won't render
# outside the context of the library where the note
# "lives". For example, they don't work in book jackets
# and book details from a different library. They also
# don't work in tooltips.

# This code depends on the note being wrapped in <body>
# tags by parse_html. The body is changed to a <div>.
# That means we often end up with <div><div> or some
# such, but that is OK root = parse_html(note['doc'])
# There should be only one <body> root = root.xpath('//body')[0]
# Change the body to a div root.tag = 'div'
# Expand all the resources in the note root = expand_note_resources(root, db.get_notes_resource)
note = html.tostring(root, encoding='unicode')
return '' if note is None else note except Exception as e:
traceback.print_exc()
raise ValueError(e)[документация]
class BuiltinHasNote(BuiltinFormatterFunction):
name = 'has_note'
arg_count = 2 category = DB_FUNCS
__doc__ = doc = _( r'''
``has_note(field_name, field_value)``. Check if a field has a note.[/]
This function has two variants:
[LIST]
[*]if ``field_value`` is not ``''`` (the empty string) return ``'1'`` if the value ``field_value`` in the field ``field_name`` has a note, otherwise ``''``.

Example: ``has_note('tags', 'Fiction')`` returns ``'1'`` if the tag ``fiction`` has an attached note, otherwise ``''``.

[*]If ``field_value`` is ``''`` then return a list of values in ``field_name`` that have a note. If no item in the field has a note, return ``''``. This variant is useful for showing column icons if any value in the field has a note, rather than a specific value.

Example: ``has_note('authors', '')`` returns a list of authors that have notes, or
``''`` if no author has a note.
[/LIST]

You can test if all the values in ``field_name`` have a note by comparing the list length of this function's return value against the list length of the values in ``field_name``. Example:
[CODE]
list_count(has_note('authors', ''), '&') ==# list_count_field('authors')
[/CODE]
This function works only in the GUI and the content server.
''')

def evaluate(self, formatter, kwargs, mi, locals, field_name, field_value):
db = self.get_database(mi, formatter=formatter).new_api if field_value:
note = None try:
item_id = db.get_item_id(field_name, field_value, case_sensitive=True)
if item_id is not None:
note = db.notes_data_for(field_name, item_id)
except Exception as e:
traceback.print_exc()
raise ValueError(str(e))
return '1' if note is not None else ''
try:
notes_for_book = db.items_with_notes_in_book(mi.id)
values = list(notes_for_book.get(field_name, {}).values())
return db.field_metadata[field_name]['is_multiple'].get('list_to_ui', ', ').join(values)
except Exception as e:
traceback.print_exc()
raise ValueError(str(e))

[документация]
class BuiltinIsDarkMode(BuiltinFormatterFunction):
name = 'is_dark_mode'
arg_count = 0 category = OTHER
__doc__ = doc = _( r'''
``is_dark_mode()`` -- returns ``'1'`` if calibre is running in dark mode, ``''``
(the empty string) otherwise.[/] This function can be used in advanced color and icon rules to choose different colors/icons according to the mode. Example:
[CODE]
if is_dark_mode() then 'dark.png' else 'light.png' fi
[/CODE]
''')

def evaluate(self, formatter, kwargs, mi, locals):
try:
# Import this here so that Qt isn't referenced unless this function is used.
from calibre.gui2 import is_dark_theme return '1' if is_dark_theme() else ''
except Exception:
only_in_gui_error('is_dark_mode')

[документация]
class BuiltinFieldListCount(BuiltinFormatterFunction):
name = 'list_count_field'
arg_count = 0 category = LIST_MANIPULATION
__doc__ = doc = _( r'''
``list_count_field(lookup_name)``-- returns the count of items in the field with the lookup name ``lookup_name``.[/] The field must be multi-valued such as
``authors`` or ``tags``, otherwise the function raises an error. This function is much faster than ``list_count()`` because it operates directly on calibre data without converting it to a string first. Example: ``list_count_field('tags')``.
''')

def evaluate(self, formatter, kwargs, mi, locals, *args):
# The globals function is implemented in-line in the formatter raise NotImplementedError()

[документация]
class BuiltinMakeUrl(BuiltinFormatterFunction):
name = 'make_url'
arg_count = -1 category = URL_FUNCTIONS
__doc__ = doc = _( r'''
``make_url(path, [query_name, query_value]+)`` -- this function is the easiest way to construct a query URL. It uses a ``path``, the web site and page you want to query, and ``query_name``, ``query_value`` pairs from which the query is built.
In general, the ``query_value`` must be URL-encoded. With this function it is always encoded and spaces are always replaced with ``'+'`` signs.[/]

At least one ``query_name, query_value`` pair must be provided.

Example: constructing a Wikipedia search URL for the author `{0}`:
[CODE]
make_url('https://en.wikipedia.org/w/index.php', 'search', '{0}')
[/CODE]
returns
[CODE]
https://en.wikipedia.org/w/index.php?search=Niccol%C3%B2+Machiavelli
[/CODE]

If you are writing a custom column book details URL template then use ``$item_name`` or
``field('item_name')`` to obtain the value of the field that was clicked on.
Example: if `{0}` was clicked then you can construct the URL using:
[CODE]
make_url('https://en.wikipedia.org/w/index.php', 'search', $item_name)
[/CODE]

See also the functions :ref:`make_url_extended`, :ref:`query_string` and :ref:`encode_for_url`.
''').format('Niccolò Machiavelli') # not translated ans gettext wants pure ascii msgid def evaluate(self, formatter, kwargs, mi, locals, path, *args):
if (len(args) % 2) != 0:
raise ValueError(_('{} requires an odd number of arguments').format('make_url'))
if len(args) < 2:
raise ValueError(_('{} requires at least 3 arguments').format('make_url'))
query_args = []
for i in range(0, len(args), 2):
query_args.append(f'{args[i]}={qquote(args[i+1].strip())}')
return f'{path}?{"&".join(query_args)}'[документация]
class BuiltinMakeUrlExtended(BuiltinFormatterFunction):
name = 'make_url_extended'
arg_count = -1 category = URL_FUNCTIONS
__doc__ = doc = _( r'''
``make_url_extended(...)`` -- this function is similar to :ref:`make_url` but gives you more control over the URL components. The components of a URL are

[B]scheme[/B]:://[B]authority[/B]/[B]path[/B]?[B]query string[/B].

See [URL href="https://en.wikipedia.org/wiki/URL"]Uniform Resource Locator[/URL] on Wikipedia for more detail.

The function has two variants:
[CODE]
make_url_extended(scheme, authority, path, [query_name, query_value]+)
[/CODE]
and
[CODE]
make_url_extended(scheme, authority, path, query_string)
[/CODE]
[/]
This function returns a URL constructed from the ``scheme``, ``authority``, ``path``, and either the ``query_string`` or a query string constructed from the query argument pairs.
The ``authority`` can be empty, which is the case for ``calibre`` scheme URLs.
You must supply either a ``query_string`` or at least one ``query_name, query_value`` pair.
If you supply ``query_string`` and it is empty then the resulting URL will not have a query string section.

Example 1: constructing a Wikipedia search URL for the author `{0}`:
[CODE]
make_url_extended('https', 'en.wikipedia.org', '/w/index.php', 'search', '{0}')
[/CODE]
returns
[CODE]
https://en.wikipedia.org/w/index.php?search=Niccol%C3%B2+Machiavelli
[/CODE]

See the :ref:`query_string` function for an example using ``make_url_extended()`` with a ``query_string``.

If you are writing a custom column book details URL template then use ``$item_name`` or
``field('item_name')`` to obtain the value of the field that was clicked on.
Example: if `{0}` was clicked on then you can construct the URL using :
[CODE]
make_url_extended('https', 'en.wikipedia.org', '/w/index.php', 'search', $item_name')
[/CODE]

See also the functions :ref:`make_url`, :ref:`query_string` and :ref:`encode_for_url`.
''').format('Niccolò Machiavelli') # not translated as gettext wants pure ASCII msgid def evaluate(self, formatter, kwargs, mi, locals, scheme, authority, path, *args):
if len(args) != 1:
if (len(args) % 2) != 0:
raise ValueError(_('{} requires an odd number of arguments').format('make_url_extended'))
if len(args) < 2:
raise ValueError(_('{} requires at least 5 arguments').format('make_url_extended'))
query_args = []
for i in range(0, len(args), 2):
query_args.append(f'{args[i]}={qquote(args[i+1].strip())}')
qs = '&'.join(query_args)
else:
qs = args[0]
if qs:
qs = '?' + qs return (f"{scheme}://{authority}{'/' if authority else ''}"
f"{path[1:] if path.startswith('/') else path}{qs}")[документация]
class BuiltinQueryString(BuiltinFormatterFunction):
name = 'query_string'
arg_count = -1 category = URL_FUNCTIONS
__doc__ = doc = _( r'''
``query_string([query_name, query_value, how_to_encode]+)``-- returns a URL query string constructed from the ``query_name, query_value, how_to_encode`` triads.
A query string is a series of items where each item looks like ``query_name=query_value`` where ``query_value`` is URL-encoded as instructed. The query items are separated by
``'&'`` (ampersand) characters.[/]

If ``how_to_encode`` is ``0`` then ``query_value`` is encoded and spaces are replaced with ``'+'`` (plus) signs. If ``how_to_encode`` is ``1`` then ``query_value`` is encoded with spaces replaced by ``%20``. If ``how_to_encode`` is ``2`` then ``query_value`` is returned unchanged; no encoding is done and spaces are not replaced. If you want
``query_value`` not to be encoded but spaces to be replaced then use the :ref:`re` function, as in ``re($series, ' ', '%20')``

You use this function if you need specific control over how the parts of the query string are constructed. You could then use the resultingquery string in
:ref:`make_url_extended`, as in
[CODE]
make_url_extended(
'https', 'your_host', 'your_path', query_string('encoded', '{0}', 0, 'unencoded', '{0}', 2))
[/CODE]
giving you
[CODE]
https://your_host/your_path?encoded=Hendrik+B%C3%A4%C3%9Fler&unencoded={0}
[/CODE]

You must have at least one ``query_name, query_value, how_to_encode`` triad, but can have as many as you wish.

The returned value is a URL query string with all the specified items, for example:
``name1=val1[&nameN=valN]*``. Note that the ``'?'`` `path` / `query string` separator is not included in the returned result.

If you are writing a custom column book details URL template then use ``$item_name`` or
``field('item_name')`` to obtain the unencoded value of the field that was clicked.
You also have ``item_value_quoted`` where the value is already encoded with plus signs replacing spaces, and ``item_value_no_plus`` where the value is already encoded with ``%20`` replacing spaces.

See also the functions :ref:`make_url`, :ref:`make_url_extended` and :ref:`encode_for_url`.
''').format('Hendrik Bäßler')

def evaluate(self, formatter, kwargs, mi, locals, *args):
if (len(args) % 3) != 0 or len(args) < 3:
raise ValueError(_('{} requires at least one group of 3 arguments').format('query_string'))
funcs = [ partial(qquote, use_plus=True), partial(qquote, use_plus=False), lambda x:x,
]
query_args = []
for i in range(0, len(args), 3):
if (f := args[i+2]) not in ('0', '1', '2'):
raise ValueError(
_('In {} the third argument of a group must be 0, 1, or 2, not {}').format('query_string', f))
query_args.append(f'{args[i]}={funcs[int(f)](args[i+1].strip())}')
return '&'.join(query_args)

[документация]
class BuiltinEncodeForURL(BuiltinFormatterFunction):
name = 'encode_for_url'
arg_count = 2 category = URL_FUNCTIONS
__doc__ = doc = _( r'''
``encode_for_url(value, use_plus)`` -- returns the ``value`` encoded for use in a URL as specified by ``use_plus``. The value is first URL-encoded. Next, if ``use_plus`` is ``0`` then spaces are replaced by ``'+'`` (plus) signs. If it is ``1`` then spaces are replaced by ``%20``.[/]

If you do not want the value to be encoding but to have spaces replaced then use the
:ref:`re` function, as in ``re($series, ' ', '%20')``

See also the functions :ref:`make_url`, :ref:`make_url_extended` and :ref:`query_string`.
''')

def evaluate(self, formatter, kwargs, mi, locals, value, use_plus):
if use_plus not in ('0', '1'):
raise ValueError(
_('In {} the second argument must be 0, or 1, not {}').format('quote_for_url', use_plus))
return qquote(value, use_plus=use_plus=='0')

[документация]
class BuiltinFormatDuration(BuiltinFormatterFunction):
name = 'format_duration'
arg_count = -1 category = FORMATTING_VALUES
__doc__ = doc = _( r'''
``format_duration(value, template, [largest_unit])`` -- format the value, a number of seconds, into a string showing weeks, days, hours, minutes, and seconds. If the value is a float then it is rounded to the nearest integer.[/] You choose how to format the value using a template consisting of value selectors surrounded by ``[`` and ``]`` characters. The selectors are:
[LIST]
[*]``[w]``: weeks
[*]``[d]``: days
[*]``[h]``: hours
[*]``[m]``: minutes
[*]``[s]``: seconds
[/LIST]
You can put arbitrary text between selectors.

The following examples use a duration of 2 days (172,800 seconds) 1 hour (3,600 seconds)
and 20 seconds, which totals to 176,420 seconds.
[LIST]
[*]``format_duration(176420, '[d][h][m][s]')`` will return the value ``2d 1h 0m 20s``.
[*]``format_duration(176420, '[h][m][s]')`` will return the value ``49h 0m 20s``.
[*]``format_duration(176420, 'Your reading time is [d][h][m][s]')`` returns the value
``Your reading time is 49h 0m 20s``.
[*]``format_duration(176420, '[w][d][h][m][s]')`` will return the value ``2d 1h 0m 20s``.
Note that the zero weeks value is not returned.
[/LIST]
If you want to see zero values for items such as weeks in the above example, use an uppercase selector. For example, the following uses ``'W'`` to show zero weeks:

``format_duration(176420, '[W][d][h][m][s]')`` returns ``0w 2d 1h 0m 20s``.

By default the text following a value is the selector followed by a space.
You can change that to whatever text you want. The format for a selector with your text is the selector followed by a colon followed by text segments separated by ``'|'`` characters. You must include any space characters you want in the output.

You can provide from one to three text segments.
[LIST]
[*]If you provide one segment, as in ``[w: weeks ]`` then that segment is used for all values.
[*]If you provide two segments, as in ``[w: weeks | week ]`` then the first segment is used for 0 and more than 1. The second segment is used for 1.
[*]If you provide three segments, as in ``[w: weeks | week | weeks ]`` then the first segment is used for 0, the second segment is used for 1, and the third segment is used for more than 1.
[/LIST]
The second form is equivalent to the third form in many languages.

For example, the selector:
[LIST]
[*]``[w: weeks | week | weeks ]`` produces ``'0 weeks '``, ``'1 week '``, or ``'2 weeks '``.
[*]``[w: weeks | week ]`` produces ``'0 weeks '``, ``'1 week '``, or ``'2 weeks '``.
[*]``[w: weeks ]`` produces ``0 weeks '``, ``1 weeks '``, or ``2 weeks '``.
[/LIST]

The optional ``largest_unit`` parameter specifies the largest of weeks, days, hours, minutes, and seconds that will be produced by the template. It must be one of the value selectors.
This can be useful to truncate a value.

``format_duration(176420, '[h][m][s]', 'd')`` will return the value ``1h 0m 20s`` instead of ``49h 0m 20s``.
''')

def evaluate(self, formatter, kwargs, mi, locals, value, template, largest_unit=''):
if largest_unit not in 'wdhms':
raise ValueError(_('the {0} parameter must be one of {1}').format('largest_unit', 'wdhms'))

pat = re.compile(r'\[(.)(:(.*?))?\]')

if not largest_unit:
highest_index = 0 for m in pat.finditer(template):
try:
# We know that m.group(1) is a single character so the only
# exception possible is that the character is not in the string dex = 'smhdw'.index(m.group(1).lower())
highest_index = dex if dex > highest_index else highest_index except Exception:
raise ValueError(_('The {} format specifier is not valid').format(m.group()))
largest_unit = 'smhdw'[highest_index]

int_val = remainder = round(float(value)) if value else 0 weeks,remainder = divmod(remainder, 60*60*24*7) if largest_unit == 'w' else (-1,remainder)
days,remainder = divmod(remainder, 60*60*24) if largest_unit in 'wd' else (-1,remainder)
hours,remainder = divmod(remainder, 60*60) if largest_unit in 'wdh' else (-1,remainder)
minutes,remainder = divmod(remainder, 60) if largest_unit in 'wdhm' else (-1,remainder)
seconds = remainder def repl(mo):
fmt_char = mo.group(1)
suffixes = mo.group(3)
if suffixes is None:
zero_suffix = one_suffix = more_suffix = fmt_char.lower() + ' '
else:
suffixes = re.split(r'\|', suffixes)
match len(suffixes):
case 1:
zero_suffix = one_suffix = more_suffix = suffixes[0]
case 2:
zero_suffix = more_suffix = suffixes[0]
one_suffix = suffixes[1]
case 3:
zero_suffix = suffixes[0]
one_suffix = suffixes[1]
more_suffix = suffixes[2]
case _:
raise ValueError(_('The group {} has too many suffixes').format(fmt_char))
zero_suffix = one_suffix = more_suffix = '@@too many suffixes@@'

def val_with_suffix(val, test_val):
match val:
case -1:
return ''
case 0 if fmt_char.islower() and int_val < test_val:
return ''
case 0:
return str(val) + zero_suffix case 1:
return str(val) + one_suffix case _:
return str(val) + more_suffix match fmt_char.lower():
case 'w':
return val_with_suffix(weeks, 60*60*24*7)
case 'd':
return val_with_suffix(days, 60*60*24)
case 'h':
return val_with_suffix(hours, 60*60)
case 'm':
return val_with_suffix(minutes, 60)
case 's':
return val_with_suffix(seconds, -1)
case _:
raise ValueError(_('The {} format specifier is not valid').format(fmt_char))

return pat.sub(repl, template)_formatter_builtins = [
BuiltinAdd(), BuiltinAnd(), BuiltinApproximateFormats(), BuiltinArguments(),
BuiltinAssign(),
BuiltinAuthorLinks(), BuiltinAuthorSorts(), BuiltinBookCount(),
BuiltinBookValues(), BuiltinBooksize(),
BuiltinCapitalize(), BuiltinCharacter(), BuiltinCheckYesNo(), BuiltinCeiling(),
BuiltinCmp(), BuiltinConnectedDeviceName(), BuiltinConnectedDeviceUUID(), BuiltinContains(),
BuiltinCount(), BuiltinCurrentLibraryName(), BuiltinCurrentLibraryPath(),
BuiltinCurrentVirtualLibraryName(), BuiltinDateArithmetic(),
BuiltinDaysBetween(), BuiltinDivide(), BuiltinEncodeForURL(), BuiltinEval(),
BuiltinExtraFileNames(), BuiltinExtraFileSize(), BuiltinExtraFileModtime(),
BuiltinFieldListCount(), BuiltinFirstNonEmpty(), BuiltinField(), BuiltinFieldExists(),
BuiltinFinishFormatting(), BuiltinFirstMatchingCmp(), BuiltinFloor(),
BuiltinFormatDate(), BuiltinFormatDateField(), BuiltinFormatDuration(), BuiltinFormatNumber(),
BuiltinFormatsModtimes(),BuiltinFormatsPaths(), BuiltinFormatsPathSegments(),
BuiltinFormatsSizes(), BuiltinFractionalPart(),BuiltinGetLink(),
BuiltinGetNote(), BuiltinGlobals(), BuiltinHasCover(), BuiltinHasExtraFiles(),
BuiltinHasNote(), BuiltinHumanReadable(), BuiltinIdentifierInList(),
BuiltinIfempty(), BuiltinIsDarkMode(), BuiltinLanguageCodes(), BuiltinLanguageStrings(),
BuiltinInList(), BuiltinIsMarked(), BuiltinListCountMatching(),
BuiltinListDifference(), BuiltinListEquals(), BuiltinListIntersection(),
BuiltinListitem(), BuiltinListJoin(), BuiltinListRe(),
BuiltinListReGroup(), BuiltinListRemoveDuplicates(), BuiltinListSort(),
BuiltinListSplit(), BuiltinListUnion(),BuiltinLookup(),
BuiltinLowercase(), BuiltinMakeUrl(), BuiltinMakeUrlExtended(), BuiltinMod(),
BuiltinMultiply(), BuiltinNot(), BuiltinOndevice(),
BuiltinOr(), BuiltinPrint(), BuiltinQueryString(), BuiltinRatingToStars(),
BuiltinRange(), BuiltinRawField(), BuiltinRawList(),
BuiltinRe(), BuiltinReGroup(), BuiltinRound(), BuiltinSelect(), BuiltinSeriesSort(),
BuiltinSetGlobals(), BuiltinShorten(), BuiltinStrcat(), BuiltinStrcatMax(),
BuiltinStrcmp(), BuiltinStrcmpcase(), BuiltinStrInList(), BuiltinStrlen(), BuiltinSubitems(),
BuiltinSublist(),BuiltinSubstr(), BuiltinSubtract(), BuiltinSwapAroundArticles(),
BuiltinSwapAroundComma(), BuiltinSwitch(), BuiltinSwitchIf(),
BuiltinTemplate(), BuiltinTest(), BuiltinTitlecase(), BuiltinToday(),
BuiltinToHex(), BuiltinTransliterate(), BuiltinUppercase(), BuiltinUrlsFromIdentifiers(),
BuiltinUserCategories(), BuiltinVirtualLibraries(), BuiltinAnnotationCount()
]

class FormatterUserFunction(FormatterFunction):

def __init__(self, name, doc, arg_count, program_text, object_type):
self.object_type = object_type self.name = name self.doc = doc self.arg_count = arg_count self.program_text = program_text self.cached_compiled_text = None
# Keep this for external code compatibility. Set it to True if we have a
# python template function, otherwise false. This might break something
# if the code depends on stored templates being in GPM.
self.is_python = True if object_type is StoredObjectType.PythonFunction else False def to_pref(self):
return [self.name, self.doc, self.arg_count, self.program_text]

tabs = re.compile(r'^\t*')

def function_object_type(thing):
# 'thing' can be a preference instance, program text, or an already-compiled function if isinstance(thing, FormatterUserFunction):
return thing.object_type if isinstance(thing, list):
text = thing[3]
else:
text = thing if text.startswith('def'):
return StoredObjectType.PythonFunction if text.startswith('program'):
return StoredObjectType.StoredGPMTemplate if text.startswith('python'):
return StoredObjectType.StoredPythonTemplate raise ValueError('Unknown program type in formatter function pref')

def function_pref_name(pref):
return pref[0]

def compile_user_function(name, doc, arg_count, eval_func):
typ = function_object_type(eval_func)
if typ is not StoredObjectType.PythonFunction:
return FormatterUserFunction(name, doc, arg_count, eval_func, typ)

def replace_func(mo):
return mo.group().replace('\t', ' ')

func = ' ' + '\n '.join([tabs.sub(replace_func, line)
for line in eval_func.splitlines()])
prog = '''
from calibre.utils.formatter_functions import FormatterUserFunction from calibre.utils.formatter_functions import formatter_functions class UserFunction(FormatterUserFunction):
''' + func locals_ = {}
if DEBUG and tweaks.get('enable_template_debug_printing', False):
print(prog)
exec(prog, locals_)
cls = locals_['UserFunction'](name, doc, arg_count, eval_func, typ)
return cls def compile_user_template_functions(funcs):
compiled_funcs = {}
for func in funcs:
try:
# Force a name conflict to test the logic
# if func[0] == 'myFunc2':
# func[0] = 'myFunc3'

# Compile the function so that the tab processing is done on the
# source. This helps ensure that if the function already is defined
# then white space differences don't cause them to compare differently cls = compile_user_function(*func)
cls.object_type = function_object_type(func)
compiled_funcs[cls.name] = cls except Exception:
try:
func_name = func[0]
except Exception:
func_name = 'Unknown'
prints(f'**** Compilation errors in user template function "{func_name}" ****')
traceback.print_exc(limit=10)
prints(f'**** End compilation errors in {func_name} "****"')
return compiled_funcs def load_user_template_functions(library_uuid, funcs, precompiled_user_functions=None):
unload_user_template_functions(library_uuid)
if precompiled_user_functions:
compiled_funcs = precompiled_user_functions else:
compiled_funcs = compile_user_template_functions(funcs)
formatter_functions().register_functions(library_uuid, list(compiled_funcs.values()))

def unload_user_template_functions(library_uuid):
formatter_functions().unregister_functions(library_uuid)

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Навигация

- модули
- Старт »
- Код модуля »
- calibre.web.feeds.news

Исходный код calibre.web.feeds.news

__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''
Defines various abstract base classes that can be subclassed to create powerful news fetching recipes.
'''
__docformat__ = 'restructuredtext en'

import io import os import re import sys import time import traceback from collections import defaultdict from contextlib import closing from urllib.parse import urlparse, urlsplit from calibre import __appname__, as_unicode, browser, force_unicode, iswindows, preferred_encoding, random_user_agent, strftime from calibre.ebooks.BeautifulSoup import BeautifulSoup, CData, NavigableString, Tag from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata.opf2 import OPFCreator from calibre.ebooks.metadata.toc import TOC from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.date import now as nowf from calibre.utils.icu import numeric_sort_key from calibre.utils.img import add_borders_to_image, image_to_data, save_cover_data_to from calibre.utils.localization import _, canonicalize_lang, ngettext from calibre.utils.logging import ThreadSafeWrapper from calibre.utils.threadpool import NoResultsPending, ThreadPool, WorkRequest from calibre.web import Recipe from calibre.web.feeds import Feed, feed_from_xml, feeds_from_index, templates from calibre.web.fetch.simple import AbortArticle, RecursiveFetcher from calibre.web.fetch.simple import option_parser as web2disk_option_parser from calibre.web.fetch.utils import prepare_masthead_image from polyglot.builtins import string_or_bytes def classes(classes):
q = frozenset(classes.split(' '))
return dict(attrs={'class': lambda x: x and frozenset(x.split()).intersection(q)})

def prefixed_classes(classes):
q = frozenset(classes.split(' '))

def matcher(x):
if x:
for candidate in frozenset(x.split()):
for x in q:
if candidate.startswith(x):
return True return False return {'attrs': {'class': matcher}}

class LoginFailed(ValueError):
pass class DownloadDenied(ValueError):
pass

[документация]
class BasicNewsRecipe(Recipe):
'''
Base class that contains logic needed in all recipes. By overriding progressively more of the functionality in this class, you can make progressively more customized/powerful recipes. For a tutorial introduction to creating recipes, see :doc:`news`.
'''

#: The title to use for the e-book title = _('Unknown News Source')

#: A couple of lines that describe the content this recipe downloads.
#: This will be used primarily in a GUI that presents a list of recipes.
description = ''

#: The author of this recipe
__author__ = __appname__

#: Minimum calibre version needed to use this recipe requires_version = (0, 6, 0)

#: The language that the news is in. Must be an ISO-639 code either
#: two or three characters long language = 'und'

#: Maximum number of articles to download from each feed. This is primarily
#: useful for feeds that don't have article dates. For most feeds, you should
#: use :attr:`BasicNewsRecipe.oldest_article` max_articles_per_feed = 100

#: Oldest article to download from this news source. In days.
oldest_article = 7.0

#: Number of levels of links to follow on article webpages recursions = 0

#: The default delay between consecutive downloads in seconds. The argument may be a
#: floating point number to indicate a more precise time. See :meth:`get_url_specific_delay`
#: to implement per URL delays.
delay = 0

#: Publication type
#: Set to newspaper, magazine or blog. If set to None, no publication type
#: metadata will be written to the opf file.
publication_type = 'unknown'

#: Number of simultaneous downloads. Set to 1 if the server is picky.
#: Automatically reduced to 1 if :attr:`BasicNewsRecipe.delay` > 0 simultaneous_downloads = 5

#: Timeout for fetching files from server in seconds timeout = 120.0

#: The format string for the date shown on the first page.
#: By default: Day_Name, Day_Number Month_Name Year timefmt = ' [%a, %d %b %Y]'

#: List of feeds to download.
#: Can be either ``[url1, url2, ...]`` or ``[('title1', url1), ('title2', url2),...]`` feeds = None

#: Max number of characters in the short description summary_length = 500

#: Convenient flag to disable loading of stylesheets for websites
#: that have overly complex stylesheets unsuitable for conversion
#: to e-book formats.
#: If True stylesheets are not downloaded and processed no_stylesheets = False

#: Convenient flag to strip all JavaScript tags from the downloaded HTML remove_javascript = True

#: If True the GUI will ask the user for a username and password
#: to use while downloading.
#: If set to "optional" the use of a username and password becomes optional needs_subscription = False

#: If True the navigation bar is center aligned, otherwise it is left aligned center_navbar = True

#: Specify an override encoding for sites that have an incorrect
#: charset specification. The most common being specifying ``latin1`` and
#: using ``cp1252``. If None, try to detect the encoding. If it is a
#: callable, the callable is called with two arguments: The recipe object
#: and the source to be decoded. It must return the decoded source.
encoding = None

#: Normally we try to guess if a feed has full articles embedded in it
#: based on the length of the embedded content. If `None`, then the
#: default guessing is used. If `True` then the we always assume the feeds has
#: embedded content and if `False` we always assume the feed does not have
#: embedded content.
use_embedded_content = None

#: Set to True and implement :meth:`get_obfuscated_article` to handle
#: websites that try to make it difficult to scrape content.
articles_are_obfuscated = False

#: Reverse the order of articles in each feed reverse_article_order = False

#: Automatically extract all the text from downloaded article pages. Uses
#: the algorithms from the readability project. Setting this to True, means
#: that you do not have to worry about cleaning up the downloaded HTML
#: manually (though manual cleanup will always be superior).
auto_cleanup = False

#: Specify elements that the auto cleanup algorithm should never remove.
#: The syntax is a XPath expression. For example::
#:
#: auto_cleanup_keep = '//div[@id="article-image"]' will keep all divs with
#: id="article-image"
#: auto_cleanup_keep = '//*[@class="important"]' will keep all elements
#: with class="important"
#: auto_cleanup_keep = '//div[@id="article-image"]|//span[@class="important"]'
#: will keep all divs with id="article-image" and spans
#: with class="important"
#:
auto_cleanup_keep = None

#: Specify any extra :term:`CSS` that should be added to downloaded :term:`HTML` files.
#: It will be inserted into `<style>` tags, just before the closing
#: `</head>` tag thereby overriding all :term:`CSS` except that which is
#: declared using the style attribute on individual :term:`HTML` tags.
#: Note that if you want to programmatically generate the extra_css override
#: the :meth:`get_extra_css()` method instead.
#: For example::
#:
#: extra_css = '.heading { font: serif x-large }'
#:
extra_css = None

#: If True empty feeds are removed from the output.
#: This option has no effect if parse_index is overridden in
#: the sub class. It is meant only for recipes that return a list
#: of feeds using `feeds` or :meth:`get_feeds`. It is also used if you use
#: the ignore_duplicate_articles option.
remove_empty_feeds = False

#: List of regular expressions that determines which links to follow.
#: If empty, it is ignored. Used only if is_link_wanted is
#: not implemented. For example::
#:
#: match_regexps = [r'page=[0-9]+']
#:
#: will match all URLs that have `page=some number` in them.
#:
#: Only one of :attr:`BasicNewsRecipe.match_regexps` or
#: :attr:`BasicNewsRecipe.filter_regexps` should be defined.
match_regexps = []

#: List of regular expressions that determines which links to ignore.
#: If empty it is ignored. Used only if is_link_wanted is not
#: implemented. For example::
#:
#: filter_regexps = [r'ads\.doubleclick\.net']
#:
#: will remove all URLs that have `ads.doubleclick.net` in them.
#:
#: Only one of :attr:`BasicNewsRecipe.match_regexps` or
#: :attr:`BasicNewsRecipe.filter_regexps` should be defined.
filter_regexps = []

#: Recipe specific options to control the conversion of the downloaded
#: content into an e-book. These will override any user or plugin specified
#: values, so only use if absolutely necessary. For example::
#:
#: conversion_options = {
#: 'base_font_size' : 16,
#: 'linearize_tables' : True,
#: }
#:
conversion_options = {}

#: List of tags to be removed. Specified tags are removed from downloaded HTML.
#: A tag is specified as a dictionary of the form::
#:
#: {
#: name : 'tag name', #e.g. 'div'
#: attrs : a dictionary, #e.g. {'class': 'advertisement'}
#: }
#:
#: All keys are optional. For a full explanation of the search criteria, see
#: `Beautiful Soup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/#searching-the-tree>`__
#: A common example::
#:
#: remove_tags = [dict(name='div', class_='advert')]
#:
#: This will remove all `<div class="advert">` tags and all
#: their children from the downloaded :term:`HTML`.
remove_tags = []

#: Remove all tags that occur after the specified tag.
#: For the format for specifying a tag see :attr:`BasicNewsRecipe.remove_tags`.
#: For example::
#:
#: remove_tags_after = [dict(id='content')]
#:
#: will remove all
#: tags after the first element with `id="content"`.
remove_tags_after = None

#: Remove all tags that occur before the specified tag.
#: For the format for specifying a tag see :attr:`BasicNewsRecipe.remove_tags`.
#: For example::
#:
#: remove_tags_before = dict(id='content')
#:
#: will remove all
#: tags before the first element with `id="content"`.
remove_tags_before = None

#: List of attributes to remove from all tags.
#: For example::
#:
#: remove_attributes = ['style', 'font']
remove_attributes = []

#: Keep only the specified tags and their children.
#: For the format for specifying a tag see :attr:`BasicNewsRecipe.remove_tags`.
#: If this list is not empty, then the `<body>` tag will be emptied and re-filled with
#: the tags that match the entries in this list. For example::
#:
#: keep_only_tags = [dict(id=['content', 'heading'])]
#:
#: will keep only tags that have an `id` attribute of `"content"` or `"heading"`.
keep_only_tags = []

#: List of :term:`regexp` substitution rules to run on the downloaded :term:`HTML`.
#: Each element of the
#: list should be a two element tuple. The first element of the tuple should
#: be a compiled regular expression and the second a callable that takes
#: a single match object and returns a string to replace the match. For example::
#:
#: preprocess_regexps = [
#: (re.compile(r'<!--Article ends here-->.*</body>', re.DOTALL|re.IGNORECASE),
#: lambda match: '</body>'),
#: ]
#:
#: will remove everything from `<!--Article ends here-->` to `</body>`.
preprocess_regexps = []

#: The CSS that is used to style the templates, i.e., the navigation bars and
#: the Tables of Contents. Rather than overriding this variable, you should
#: use `extra_css` in your recipe to customize look and feel.
template_css = '''
.article_date { color: gray; font-family: monospace;
}

.article_description { text-indent: 0pt;
}

a.article { font-weight: bold; text-align:left;
}

a.feed { font-weight: bold;
}

.calibre_navbar { font-family:monospace;
}
'''

#: By default, calibre will use a default image for the masthead (Kindle only).
#: Override this in your recipe to provide a URL to use as a masthead.
masthead_url = None

#: By default, the cover image returned by get_cover_url() will be used as
#: the cover for the periodical. Overriding this in your recipe instructs
#: calibre to render the downloaded cover into a frame whose width and height
#: are expressed as a percentage of the downloaded cover.
#: cover_margins = (10, 15, '#ffffff') pads the cover with a white margin
#: 10px on the left and right, 15px on the top and bottom.
#: Color names are defined `here <https://www.imagemagick.org/script/color.php>`_.
#: Note that for some reason, white does not always work in Windows. Use
#: #ffffff instead cover_margins = (0, 0, '#ffffff')

#: Set to a non empty string to disable this recipe.
#: The string will be used as the disabled message recipe_disabled = None

#: Ignore duplicates of articles that are present in more than one section.
#: A duplicate article is an article that has the same title and/or URL.
#: To ignore articles with the same title, set this to::
#:
#: ignore_duplicate_articles = {'title'}
#:
#: To use URLs instead, set it to::
#:
#: ignore_duplicate_articles = {'url'}
#:
#: To match on title or URL, set it to::
#:
#: ignore_duplicate_articles = {'title', 'url'}
ignore_duplicate_articles = None

# The following parameters control how the recipe attempts to minimize
# image sizes. Note that if compression is enabled PNG images are converted
# to JPEG.

#: Set this to False to ignore all scaling and compression parameters and
#: pass images through unmodified. If True and the other compression
#: parameters are left at their default values, images will be scaled to fit
#: in the screen dimensions set by the output profile and compressed to size at
#: most (w * h)/16 where w x h are the scaled image dimensions.
compress_news_images = False

#: The factor used when auto compressing JPEG images. If set to None,
#: auto compression is disabled. Otherwise, the images will be reduced in size to
#: (w * h)/compress_news_images_auto_size bytes if possible by reducing
#: the quality level, where w x h are the image dimensions in pixels.
#: The minimum JPEG quality will be 5/100 so it is possible this constraint
#: will not be met. This parameter can be overridden by the parameter
#: compress_news_images_max_size which provides a fixed maximum size for images.
#: Note that if you enable scale_news_images_to_device then the image will
#: first be scaled and then its quality lowered until its size is less than
#: (w * h)/factor where w and h are now the *scaled* image dimensions. In
#: other words, this compression happens after scaling.
compress_news_images_auto_size = 16

#: Set JPEG quality so images do not exceed the size given (in KBytes).
#: If set, this parameter overrides auto compression via compress_news_images_auto_size.
#: The minimum JPEG quality will be 5/100 so it is possible this constraint
#: will not be met.
compress_news_images_max_size = None

#: Rescale images to fit in the device screen dimensions set by the output profile.
#: Ignored if no output profile is set.
scale_news_images_to_device = True

#: Maximum dimensions (w,h) to scale images to. If scale_news_images_to_device is True
#: this is set to the device screen dimensions set by the output profile unless
#: there is no profile set, in which case it is left at whatever value it has been
#: assigned (default None).
scale_news_images = None

#: If set to True then links in downloaded articles that point to other downloaded articles are
#: changed to point to the downloaded copy of the article rather than its original web URL. If you
#: set this to True, you might also need to implement :meth:`canonicalize_internal_url` to work
#: with the URL scheme of your particular website.
resolve_internal_links = False

#: Specify options specific to this recipe. These will be available for the user to customize
#: in the Advanced tab of the Fetch News dialog or at the ebook-convert command line. The options
#: are specified as a dictionary mapping option name to metadata about the option. For example::
#:
#: recipe_specific_options = {
#: 'edition_date': {
#: 'short': 'The issue date to download',
#: 'long': 'Specify a date in the format YYYY-mm-dd to download the issue corresponding to that date',
#: 'default': 'current',
#: }
#: }
#:
#: When the recipe is run, self.recipe_specific_options will be a dict mapping option name to the option value
#: specified by the user. When the option is unspecified by the user, it will have the value specified by 'default'.
#: If no default is specified, the option will not be in the dict at all, when unspecified by the user.
recipe_specific_options = None

#: The simulated browser engine to use when downloading from servers. The default is to use the Python mechanize
#: browser engine, which supports logging in. However, if you don't need logging in, consider changing this
#: to either 'webengine' which uses an actual Chromium browser to do the network requests or 'qt' which
#: uses the Qt Networking backend. Both 'webengine' and 'qt' support HTTP/2, which mechanize does not and
#: are thus harder to fingerprint for bot protection services.
browser_type = 'mechanize'

#: Set to False if you do not want to use gzipped transfers with the mechanize browser.
#: Note that some old servers flake out with gzip.
handle_gzip = True

# See the built-in recipes for examples of these settings.

def short_title(self):
return force_unicode(self.title, preferred_encoding)

[документация]
def is_link_wanted(self, url, tag):
'''
Return True if the link should be followed or False otherwise. By default, raises NotImplementedError which causes the downloader to ignore it.

:param url: The URL to be followed
:param tag: The tag from which the URL was derived
'''
raise NotImplementedError()

[документация]
def get_extra_css(self):
'''
By default returns `self.extra_css`. Override if you want to programmatically generate the extra_css.
'''
return self.extra_css

[документация]
def get_cover_url(self):
'''
Return a :term:`URL` to the cover image for this issue or `None`.
By default it returns the value of the member `self.cover_url` which is normally `None`. If you want your recipe to download a cover for the e-book override this method in your subclass, or set the member variable `self.cover_url` before this method is called.
'''
return getattr(self, 'cover_url', None)

[документация]
def get_masthead_url(self):
'''
Return a :term:`URL` to the masthead image for this issue or `None`.
By default it returns the value of the member `self.masthead_url` which is normally `None`. If you want your recipe to download a masthead for the e-book override this method in your subclass, or set the member variable `self.masthead_url` before this method is called.
Masthead images are used in Kindle MOBI files.
'''
return getattr(self, 'masthead_url', None)

[документация]
def get_feeds(self):
'''
Return a list of :term:`RSS` feeds to fetch for this profile. Each element of the list must be a 2-element tuple of the form (title, url). If title is None or an empty string, the title from the feed is used. This method is useful if your recipe needs to do some processing to figure out the list of feeds to download. If so, override in your subclass.
'''
if not self.feeds:
raise NotImplementedError()
if self.test:
return self.feeds[:self.test[0]]
return self.feeds

[документация]
def get_url_specific_delay(self, url):
'''
Return the delay in seconds before downloading this URL. If you want to programmatically determine the delay for the specified URL, override this method in your subclass, returning self.delay by default for URLs you do not want to affect.

:return: A floating point number, the delay in seconds.
'''
return self.delay

[документация]
@classmethod def print_version(cls, url):
'''
Take a `url` pointing to the webpage with article content and return the
:term:`URL` pointing to the print version of the article. By default does nothing. For example::

def print_version(self, url):
return url + '?&pagewanted=print'

'''
raise NotImplementedError()

[документация]
@classmethod def image_url_processor(cls, baseurl, url):
'''
Perform some processing on image urls (perhaps removing size restrictions for dynamically generated images, etc.) and return the processed URL. Return None or an empty string to skip fetching the image.
'''
return url

[документация]
def preprocess_image(self, img_data, image_url):
'''
Perform some processing on downloaded image data. This is called on the raw data before any resizing is done. Must return the processed raw data. Return
None to skip the image.
'''
return img_data

[документация]
def get_browser(self, *args, **kwargs):
'''
Return a browser instance used to fetch documents from the web. By default it returns a `mechanize <https://mechanize.readthedocs.io/en/latest/>`_ browser instance that supports cookies, ignores robots.txt, handles refreshes and has a random common user agent.

To customize the browser override this method in your sub-class as::

def get_browser(self, *a, **kw):
br = super().get_browser(*a, **kw)
# Add some headers br.addheaders += [
('My-Header', 'one'),
('My-Header2', 'two'),
]
# Set some cookies br.set_cookie('name', 'value')
br.set_cookie('name2', 'value2', domain='.mydomain.com')
# Make a POST request with some data br.open('https://someurl.com', {'username': 'def', 'password': 'pwd'}).read()
# Do a login via a simple web form (only supported with mechanize browsers)
if self.username is not None and self.password is not None:
br.open('https://www.nytimes.com/auth/login')
br.select_form(name='login')
br['USERID'] = self.username br['PASSWORD'] = self.password br.submit()
return br

'''
if 'user_agent' not in kwargs:
# More and more news sites are serving JPEG XR images to IE ua = getattr(self, 'last_used_user_agent', None) or self.calibre_most_common_ua or random_user_agent(allow_ie=False)
kwargs['user_agent'] = self.last_used_user_agent = ua self.log('Using user agent:', kwargs['user_agent'])
if self.browser_type != 'mechanize':
from calibre.scraper.qt import Browser, WebEngineBrowser return {'qt': Browser, 'webengine': WebEngineBrowser}[self.browser_type]( user_agent=kwargs['user_agent'], verify_ssl_certificates=kwargs.get('verify_ssl_certificates', False))
br = browser(*args, **kwargs)
br.addheaders += [('Accept', '*/*')]
if self.handle_gzip:
br.set_handle_gzip(True)
return br

[документация]
def clone_browser(self, br):
'''
Clone the browser br. Cloned browsers are used for multi-threaded downloads, since mechanize is not thread safe. The default cloning routines should capture most browser customization, but if you do something exotic in your recipe, you should override this method in your recipe and clone manually.

Cloned browser instances use the same, thread-safe CookieJar by default, unless you have customized cookie handling.
'''
if callable(getattr(br, 'clone_browser', None)):
return br.clone_browser()

# Uh-oh recipe using something exotic, call get_browser return self.get_browser()

@property def cloned_browser(self):
if hasattr(self.get_browser, 'is_base_class_implementation') and self.browser_type == 'mechanize':
# We are using the default get_browser, which means no need to
# clone br = BasicNewsRecipe.get_browser(self)
else:
br = self.clone_browser(self.browser)
return br

[документация]
def get_article_url(self, article):
'''
Override in a subclass to customize extraction of the :term:`URL` that points to the content for each article. Return the article URL. It is called with `article`, an object representing a parsed article from a feed. See `feedparser <https://pythonhosted.org/feedparser/>`_.
By default it looks for the original link (for feeds syndicated via a service like FeedBurner or Pheedo) and if found, returns that or else returns
`article.link <https://pythonhosted.org/feedparser/reference-entry-link.html>`_.
'''
for key in article.keys():
if key.endswith('_origlink'):
url = article[key]
if url and (url.startswith(('http://', 'https://'))):
return url ans = article.get('link', None)
if not ans and getattr(article, 'links', None):
for item in article.links:
if item.get('rel', 'alternate') == 'alternate':
ans = item['href']
break return ans

[документация]
def skip_ad_pages(self, soup):
'''
This method is called with the source of each downloaded :term:`HTML` file, before any of the cleanup attributes like remove_tags, keep_only_tags are applied. Note that preprocess_regexps will have already been applied.
It is meant to allow the recipe to skip ad pages. If the soup represents an ad page, return the HTML of the real page. Otherwise return
None.

`soup`: A `BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`__ instance containing the downloaded :term:`HTML`.
'''
return

[документация]
def abort_article(self, msg=None):
''' Call this method inside any of the preprocess methods to abort the download for the current article. Useful to skip articles that contain inappropriate content, such as pure video articles. '''
raise AbortArticle(msg or _('Article download aborted'))

[документация]
def preprocess_raw_html(self, raw_html, url):
'''
This method is called with the source of each downloaded :term:`HTML` file, before it is parsed into an object tree. raw_html is a unicode string representing the raw HTML downloaded from the web. url is the URL from which the HTML was downloaded.

Note that this method acts *before* preprocess_regexps.

This method must return the processed raw_html as a unicode object.
'''
return raw_html
def preprocess_raw_html_(self, raw_html, url):
raw_html = self.preprocess_raw_html(raw_html, url)
if self.auto_cleanup:
try:
raw_html = self.extract_readable_article(raw_html, url)
except Exception:
self.log.exception(f'Auto cleanup of URL: {url!r} failed')

return raw_html

[документация]
def preprocess_html(self, soup):
'''
This method is called with the source of each downloaded :term:`HTML` file, before it is parsed for links and images. It is called after the cleanup as specified by remove_tags etc.
It can be used to do arbitrarily powerful pre-processing on the :term:`HTML`.
It should return `soup` after processing it.

`soup`: A `BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`__ instance containing the downloaded :term:`HTML`.
'''
return soup

[документация]
def postprocess_html(self, soup, first_fetch):
'''
This method is called with the source of each downloaded :term:`HTML` file, after it is parsed for links and images.
It can be used to do arbitrarily powerful post-processing on the :term:`HTML`.
It should return `soup` after processing it.

:param soup: A `BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`__ instance containing the downloaded :term:`HTML`.
:param first_fetch: True if this is the first page of an article.

'''
return soup

[документация]
def cleanup(self):
'''
Called after all articles have been download. Use it to do any cleanup like logging out of subscription sites, etc.
'''
pass

[документация]
def canonicalize_internal_url(self, url, is_link=True):
'''
Return a set of canonical representations of ``url``. The default implementation uses just the server hostname and path of the URL, ignoring any query parameters, fragments, etc. The canonical representations must be unique across all URLs for this news source. If they are not, then internal links may be resolved incorrectly.

:param is_link: Is True if the URL is coming from an internal link in an HTML file. False if the URL is the URL used to download an article.
'''
try:
parts = urlparse(url)
except Exception:
self.log.error(f'Failed to parse url: {url!r}, ignoring')
return frozenset()
nl = parts.netloc path = parts.path or ''
if isinstance(nl, bytes):
nl = nl.decode('utf-8', 'replace')
if isinstance(path, bytes):
path = path.decode('utf-8', 'replace')
return frozenset({(nl, path.rstrip('/'))})

[документация]
def index_to_soup(self, url_or_raw, raw=False, as_tree=False, save_raw=None):
'''
Convenience method that takes an URL to the index page and returns a `BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc>`__ of it.

`url_or_raw`: Either a URL or the downloaded index page as a string
'''
if re.match((br'\w+://' if isinstance(url_or_raw, bytes) else r'\w+://'), url_or_raw):
# We may be called in a thread (in the skip_ad_pages method), so
# clone the browser to be safe. We cannot use self.cloned_browser
# as it may or may not actually clone the browser, depending on if
# the recipe implements get_browser() or not br = self.clone_browser(self.browser)
open_func = getattr(br, 'open_novisit', br.open)
with closing(open_func(url_or_raw, timeout=self.timeout)) as f:
_raw = f.read()
if not _raw:
raise RuntimeError(f'Could not fetch index from {url_or_raw}')
else:
_raw = url_or_raw if raw:
return _raw if not isinstance(_raw, str) and self.encoding:
if callable(self.encoding):
_raw = self.encoding(_raw)
else:
_raw = _raw.decode(self.encoding, 'replace')
from calibre.ebooks.chardet import strip_encoding_declarations, xml_to_unicode from calibre.utils.cleantext import clean_xml_chars if isinstance(_raw, str):
_raw = strip_encoding_declarations(_raw)
else:
_raw = xml_to_unicode(_raw, strip_encoding_pats=True, resolve_entities=True)[0]
_raw = clean_xml_chars(_raw)
if save_raw:
with open(save_raw, 'wb') as f:
f.write(_raw.encode('utf-8'))
if as_tree:
from html5_parser import parse return parse(_raw)
return BeautifulSoup(_raw)[документация]
def extract_readable_article(self, html, url):
'''
Extracts main article content from 'html', cleans up and returns as a (article_html, extracted_title) tuple.
Based on the original readability algorithm by Arc90.
'''
from lxml.html import document_fromstring, fragment_fromstring, tostring from calibre.ebooks.readability import readability doc = readability.Document(html, self.log, url=url, keep_elements=self.auto_cleanup_keep)
article_html = doc.summary()
extracted_title = doc.title()

try:
frag = fragment_fromstring(article_html)
except Exception:
doc = document_fromstring(article_html)
frag = doc.xpath('//body')[-1]
if frag.tag == 'html':
root = frag elif frag.tag == 'body':
root = document_fromstring( f'<html><head><title>{extracted_title}</title></head></html>')
root.append(frag)
else:
root = document_fromstring( f'<html><head><title>{extracted_title}</title></head><body/></html>')
root.xpath('//body')[0].append(frag)

body = root.xpath('//body')[0]
has_title = False for x in body.iterdescendants():
if x.text == extracted_title:
has_title = True inline_titles = body.xpath('//h1|//h2')
if not has_title and not inline_titles:
heading = body.makeelement('h2')
heading.text = extracted_title body.insert(0, heading)

raw_html = tostring(root, encoding='unicode')

return raw_html

[документация]
def sort_index_by(self, index, weights):
'''
Convenience method to sort the titles in `index` according to `weights`.
`index` is sorted in place. Returns `index`.

`index`: A list of titles.

`weights`: A dictionary that maps weights to titles. If any titles in index are not in weights, they are assumed to have a weight of 0.
'''
weights = defaultdict(int, weights)
index.sort(key=lambda x: weights[x])
return index

[документация]
def parse_index(self):
'''
This method should be implemented in recipes that parse a website instead of feeds to generate a list of articles. Typical uses are for news sources that have a "Print Edition" webpage that lists all the articles in the current print edition. If this function is implemented, it will be used in preference to :meth:`BasicNewsRecipe.parse_feeds`.

It must return a list. Each element of the list must be a 2-element tuple of the form ``('feed title', list of articles)``.

Each list of articles must contain dictionaries of the form::

{
'title' : article title,
'url' : URL of print version,
'date' : The publication date of the article as a string,
'description' : A summary of the article
'content' : The full article (can be an empty string). Obsolete do not use, instead save the content to a temporary file and pass a file:///path/to/temp/file.html as the URL.
}

For an example, see the recipe for downloading `The Atlantic`.
In addition, you can add 'author' for the author of the article.

If you want to abort processing for some reason and have calibre show the user a simple message instead of an error, call
:meth:`abort_recipe_processing`.
'''
raise NotImplementedError()

[документация]
def abort_recipe_processing(self, msg):
'''
Causes the recipe download system to abort the download of this recipe, displaying a simple feedback message to the user.
'''
from calibre.ebooks.conversion import ConversionUserFeedBack raise ConversionUserFeedBack(_('Failed to download %s')%self.title, msg)

[документация]
def get_obfuscated_article(self, url):
'''
If you set `articles_are_obfuscated` this method is called with every article URL. It should return the path to a file on the filesystem that contains the article HTML. That file is processed by the recursive
HTML fetching engine, so it can contain links to pages/images on the web.
Alternately, you can return a dictionary of the form:
{'data': <HTML data>, 'url': <the resolved URL of the article>}. This avoids needing to create temporary files. The `url` key in the dictionary is useful if the effective URL of the article is different from the URL passed into this method, for example, because of redirects. It can be omitted if the URL is unchanged.

This method is typically useful for sites that try to make it difficult to access article content automatically.
'''
raise NotImplementedError()

[документация]
def add_toc_thumbnail(self, article, src):
'''
Call this from populate_article_metadata with the src attribute of an
<img> tag from the article that is appropriate for use as the thumbnail representing the article in the Table of Contents. Whether the thumbnail is actually used is device dependent (currently only used by the Kindles). Note that the referenced image must be one that was successfully downloaded, otherwise it will be ignored.
'''
if not src or not hasattr(article, 'toc_thumbnail'):
return src = src.replace('\\', '/')
if re.search(r'feed_\d+/article_\d+/images/img', src, flags=re.I) is None:
self.log.warn(f'Ignoring invalid TOC thumbnail image: {src!r}')
return article.toc_thumbnail = re.sub(r'^.*?feed', 'feed', src, flags=re.IGNORECASE)

[документация]
def populate_article_metadata(self, article, soup, first):
'''
Called when each HTML page belonging to article is downloaded.
Intended to be used to get article metadata like author/summary/etc.
from the parsed HTML (soup).

:param article: A object of class :class:`calibre.web.feeds.Article`.
If you change the summary, remember to also change the text_summary
:param soup: Parsed HTML belonging to this article
:param first: True iff the parsed HTML is the first page of the article.
'''
pass

[документация]
def postprocess_book(self, oeb, opts, log):
'''
Run any needed post processing on the parsed downloaded e-book.

:param oeb: An OEBBook object
:param opts: Conversion options
'''
pass
def __init__(self, options, log, progress_reporter):
'''
Initialize the recipe.
:param options: Parsed commandline options
:param log: Logging object
:param progress_reporter: A Callable that takes two arguments: progress (a number between 0 and 1) and a string message. The message should be optional.
'''
self.log = ThreadSafeWrapper(log)
if not isinstance(self.title, str):
self.title = str(self.title, 'utf-8', 'replace')

self.debug = options.verbose > 1 self.output_dir = os.path.abspath(os.getcwd())
self.verbose = options.verbose self.test = options.test if self.test and not isinstance(self.test, tuple):
self.test = (2, 2)
self.username = options.username self.password = options.password self.lrf = options.lrf self.output_profile = options.output_profile self.touchscreen = getattr(self.output_profile, 'touchscreen', False)
if self.touchscreen:
self.template_css += self.output_profile.touchscreen_news_css if self.test:
self.max_articles_per_feed = self.test[1]
self.simultaneous_downloads = min(4, self.simultaneous_downloads)

if self.debug:
self.verbose = True self.report_progress = progress_reporter if self.needs_subscription and ( self.username is None or self.password is None or ( not self.username and not self.password)):
if self.needs_subscription != 'optional':
raise ValueError(_('The "%s" recipe needs a username and password.')%self.title)

self.browser = self.get_browser()
self.image_map, self.image_counter = {}, 1 self.css_map = {}

web2disk_cmdline = ['web2disk',
'--timeout', str(self.timeout),
'--max-recursions', str(self.recursions),
'--delay', str(self.delay),
]

if self.verbose:
web2disk_cmdline.append('--verbose')

if self.no_stylesheets:
web2disk_cmdline.append('--dont-download-stylesheets')

for reg in self.match_regexps:
web2disk_cmdline.extend(['--match-regexp', reg])

for reg in self.filter_regexps:
web2disk_cmdline.extend(['--filter-regexp', reg])

if options.output_profile.short_name in ('default', 'tablet'):
self.scale_news_images_to_device = False elif self.scale_news_images_to_device:
self.scale_news_images = options.output_profile.screen_size self.web2disk_options = web2disk_option_parser().parse_args(web2disk_cmdline)[0]
for extra in ('keep_only_tags', 'remove_tags', 'preprocess_regexps',
'skip_ad_pages', 'preprocess_html', 'remove_tags_after',
'remove_tags_before', 'is_link_wanted',
'compress_news_images', 'compress_news_images_max_size',
'compress_news_images_auto_size', 'scale_news_images'):
setattr(self.web2disk_options, extra, getattr(self, extra))

self.web2disk_options.postprocess_html = self._postprocess_html self.web2disk_options.preprocess_image = self.preprocess_image self.web2disk_options.encoding = self.encoding self.web2disk_options.preprocess_raw_html = self.preprocess_raw_html_ self.web2disk_options.get_delay = self.get_url_specific_delay if self.delay > 0:
self.simultaneous_downloads = 1 self.navbar = templates.TouchscreenNavBarTemplate() if self.touchscreen else \ templates.NavBarTemplate()
self.failed_downloads = []
self.partial_failures = []
self.aborted_articles = []
self.recipe_specific_options_metadata = rso = self.recipe_specific_options or {}
self.recipe_specific_options = {k: rso[k]['default'] for k in rso if 'default' in rso[k]}
for x in (options.recipe_specific_option or ()):
k, sep, v = x.partition(':')
if not sep:
raise ValueError(f'{x} is not a valid recipe specific option')
if k not in rso:
raise KeyError(f'{k} is not an option supported by: {self.title}')
self.recipe_specific_options[k] = v if self.recipe_specific_options:
log('Recipe specific options:')
for k, v in self.recipe_specific_options.items():
log(' ', f'{k} = {v}')

def _postprocess_html(self, soup, first_fetch, job_info):
if self.no_stylesheets:
for link in soup.findAll('link'):
if (link.get('type') or 'text/css').lower() == 'text/css' and 'stylesheet' in (link.get('rel') or ('stylesheet',)):
link.extract()
for style in soup.findAll('style'):
style.extract()
head = soup.find('head')
if not head:
head = soup.find('body')
if not head:
head = soup.find(True)
css = self.template_css + '\n\n' + (self.get_extra_css() or '')
style = soup.new_tag('style', type='text/css', title='override_css')
style.append(css)
head.append(style)
if first_fetch and job_info:
url, f, a, feed_len = job_info body = soup.find('body')
if body is not None:
templ = self.navbar.generate(False, f, a, feed_len, not self.has_single_feed, url, __appname__, center=self.center_navbar, extra_css=self.get_extra_css() or '')
elem = BeautifulSoup(templ.render(doctype='xhtml').decode('utf-8')).find('div')
body.insert(0, elem)
# This is needed because otherwise inserting elements into
# the soup breaks find()
soup = BeautifulSoup(soup.decode_contents())
if self.remove_javascript:
for script in list(soup.findAll('script')):
script.extract()
for o in soup.findAll(onload=True):
del o['onload']

for attr in self.remove_attributes:
for x in soup.findAll(attrs={attr:True}):
del x[attr]
for bad_tag in list(soup.findAll(['base', 'iframe', 'canvas', 'embed', 'button',
'command', 'datalist', 'video', 'audio', 'noscript', 'link', 'meta'])):
# link tags can be used for preloading causing network activity in
# calibre viewer. meta tags can do all sorts of crazy things,
# including http-equiv refresh, viewport shenanigans, etc.
bad_tag.extract()
# srcset causes some viewers, like calibre's to load images from the
# web, and it also possible causes iBooks on iOS to barf, see
# https://bugs.launchpad.net/bugs/1713986 for img in soup.findAll('img', srcset=True):
del img['srcset']

ans = self.postprocess_html(soup, first_fetch)

# Nuke HTML5 tags for x in ans.findAll(['article', 'aside', 'header', 'footer', 'nav',
'figcaption', 'figure', 'section']):
if x.get('class'):
x.get_attribute_list('class').append(f'calibre-nuked-tag-{x.name}')
else:
x['class'] = f'calibre-nuked-tag-{x.name}'
x.name = 'div'

if job_info:
url, f, a, feed_len = job_info try:
article = self.feed_objects[f].articles[a]
except Exception:
self.log.exception('Failed to get article object for postprocessing')
else:
self.populate_article_metadata(article, ans, first_fetch)
return ans

[документация]
def download(self):
'''
Download and pre-process all articles from the feeds in this recipe.
This method should be called only once on a particular Recipe instance.
Calling it more than once will lead to undefined behavior.
:return: Path to index.html
'''
try:
res = self.build_index()
self.report_progress(1, _('Download finished'))
if self.failed_downloads:
self.log.warning(_('Failed to download the following articles:'))
for feed, article, debug in self.failed_downloads:
self.log.warning(article.title, 'from', feed.title)
self.log.debug(article.url)
self.log.debug(debug)
if self.partial_failures:
self.log.warning(_('Failed to download parts of the following articles:'))
for feed, atitle, aurl, debug in self.partial_failures:
self.log.warning(atitle + _(' from ') + feed)
self.log.debug(aurl)
self.log.warning(_('\tFailed links:'))
for l, tb in debug:
self.log.warning(l)
self.log.debug(tb)
return res finally:
self.cleanup()@property def lang_for_html(self):
try:
lang = self.language.replace('_', '-').partition('-')[0].lower()
if lang == 'und':
lang = None except Exception:
lang = None return lang def feeds2index(self, feeds):
templ = (templates.TouchscreenIndexTemplate if self.touchscreen else templates.IndexTemplate)
templ = templ(lang=self.lang_for_html)
css = self.template_css + '\n\n' +(self.get_extra_css() or '')
timefmt = self.timefmt return templ.generate(self.title, 'mastheadImage.jpg', timefmt, feeds, extra_css=css).render(doctype='xhtml')

@classmethod def description_limiter(cls, src):
if not src:
return ''
src = force_unicode(src, 'utf-8')
pos = cls.summary_length fuzz = 50 si = src.find(';', pos)
if si > 0 and si-pos > fuzz:
si = -1 gi = src.find('>', pos)
if gi > 0 and gi-pos > fuzz:
gi = -1 npos = max(si, gi)
if npos < 0:
npos = pos ans = src[:npos+1]
if len(ans) < len(src):
from calibre.utils.cleantext import clean_xml_chars

# Truncating the string could cause a dangling UTF-16 half-surrogate, which will cause lxml to barf, clean it ans = clean_xml_chars(ans) + '…'
return ans def feed2index(self, f, feeds):
feed = feeds[f]
if feed.image_url is not None: # Download feed image imgdir = os.path.join(self.output_dir, 'images')
if not os.path.isdir(imgdir):
os.makedirs(imgdir)

if feed.image_url in self.image_map:
feed.image_url = self.image_map[feed.image_url]
else:
bn = urlsplit(feed.image_url).path if bn:
bn = bn.rpartition('/')[-1]
if bn:
img = os.path.join(imgdir, f'feed_image_{self.image_counter}{os.path.splitext(bn)[-1]}')
try:
with open(img, 'wb') as fi, closing(self.browser.open(feed.image_url, timeout=self.timeout)) as r:
fi.write(r.read())
self.image_counter += 1 feed.image_url = img self.image_map[feed.image_url] = img except Exception:
pass if isinstance(feed.image_url, bytes):
feed.image_url = feed.image_url.decode(sys.getfilesystemencoding(), 'strict')

templ = (templates.TouchscreenFeedTemplate if self.touchscreen else templates.FeedTemplate)
templ = templ(lang=self.lang_for_html)
css = self.template_css + '\n\n' +(self.get_extra_css() or '')

return templ.generate(f, feeds, self.description_limiter, extra_css=css).render(doctype='xhtml')

def _fetch_article(self, url, dir_, f, a, num_of_feeds, preloaded=None):
br = self.browser if hasattr(self.get_browser, 'is_base_class_implementation'):
# We are using the default get_browser, which means no need to
# clone br = BasicNewsRecipe.get_browser(self)
else:
br = self.clone_browser(self.browser)
self.web2disk_options.browser = br fetcher = RecursiveFetcher(self.web2disk_options, self.log, self.image_map, self.css_map,
(url, f, a, num_of_feeds))
fetcher.browser = br fetcher.base_dir = dir_ fetcher.current_dir = dir_ fetcher.show_progress = False fetcher.image_url_processor = self.image_url_processor if preloaded is not None:
fetcher.preloaded_urls[url] = preloaded res, path, failures = fetcher.start_fetch(url), fetcher.downloaded_paths, fetcher.failed_links if not res or not os.path.exists(res):
msg = _('Could not fetch article.') + ' '
if self.debug:
msg += _('The debug traceback is available earlier in this log')
else:
msg += _('Run with -vv to see the reason')
raise Exception(msg)

return res, path, failures def fetch_article(self, url, dir, f, a, num_of_feeds):
return self._fetch_article(url, dir, f, a, num_of_feeds)

def fetch_obfuscated_article(self, url, dir, f, a, num_of_feeds):
x = self.get_obfuscated_article(url)
if isinstance(x, dict):
data = x['data']
if isinstance(data, str):
data = data.encode(self.encoding or 'utf-8')
url = x.get('url', url)
else:
with open(x, 'rb') as of:
data = of.read()
os.remove(x)
return self._fetch_article(url, dir, f, a, num_of_feeds, preloaded=data)

def fetch_embedded_article(self, article, dir, f, a, num_of_feeds):
templ = templates.EmbeddedContent()
raw = templ.generate(article).render('html')
with PersistentTemporaryFile('_feeds2disk.html') as pt:
pt.write(raw)
url = ('file:'+pt.name) if iswindows else ('file://'+pt.name)
return self._fetch_article(url, dir, f, a, num_of_feeds)

def remove_duplicate_articles(self, feeds):
seen_keys = defaultdict(set)
remove = []
for f in feeds:
for article in f:
for key in self.ignore_duplicate_articles:
val = getattr(article, key)
seen = seen_keys[key]
if val:
if val in seen:
remove.append((f, article))
else:
seen.add(val)

for feed, article in remove:
self.log.debug(f'Removing duplicate article: {article.title} from section: {feed.title}')
feed.remove_article(article)

if self.remove_empty_feeds:
feeds = [f for f in feeds if len(f) > 0]
return feeds def build_index(self):
self.report_progress(0, _('Fetching feeds...'))
feeds = None try:
feeds = feeds_from_index(self.parse_index(), oldest_article=self.oldest_article, max_articles_per_feed=self.max_articles_per_feed, log=self.log)
self.report_progress(0, _('Got feeds from index page'))
except NotImplementedError:
pass if feeds is None:
feeds = self.parse_feeds()

if not feeds:
raise ValueError('No articles found, aborting')

if self.ignore_duplicate_articles is not None:
feeds = self.remove_duplicate_articles(feeds)

self.report_progress(0, _('Trying to download cover...'))
self.download_cover()
self.report_progress(0, _('Generating masthead...'))
self.resolve_masthead()

if self.test:
feeds = feeds[:self.test[0]]
self.has_single_feed = len(feeds) == 1 index = os.path.join(self.output_dir, 'index.html')

html = self.feeds2index(feeds)
with open(index, 'wb') as fi:
fi.write(html)

self.jobs = []

if self.reverse_article_order:
for feed in feeds:
if hasattr(feed, 'reverse'):
feed.reverse()

self.feed_objects = feeds for f, feed in enumerate(feeds):
feed_dir = os.path.join(self.output_dir, f'feed_{f}')
if not os.path.isdir(feed_dir):
os.makedirs(feed_dir)

for a, article in enumerate(feed):
if a >= self.max_articles_per_feed:
break art_dir = os.path.join(feed_dir, f'article_{a}')
if not os.path.isdir(art_dir):
os.makedirs(art_dir)
try:
url = self.print_version(article.url)
except NotImplementedError:
url = article.url except Exception:
self.log.exception('Failed to find print version for: '+article.url)
url = None if not url:
continue func, arg = (self.fetch_embedded_article, article) \ if self.use_embedded_content or (self.use_embedded_content is None and feed.has_embedded_content()) \ else \ ((self.fetch_obfuscated_article if self.articles_are_obfuscated else self.fetch_article), url)
req = WorkRequest(func, (arg, art_dir, f, a, len(feed)),
{}, (f, a), self.article_downloaded, self.error_in_article_download)
req.feed = feed req.article = article req.feed_dir = feed_dir self.jobs.append(req)

self.jobs_done = 0 tp = ThreadPool(self.simultaneous_downloads)
for req in self.jobs:
tp.putRequest(req, block=True, timeout=0)

self.report_progress(0, ngettext(
'Starting download in a single thread...',
'Starting download [{} threads]...', self.simultaneous_downloads).format(self.simultaneous_downloads))
while True:
try:
tp.poll()
time.sleep(0.1)
except NoResultsPending:
break for f, feed in enumerate(feeds):
html = self.feed2index(f, feeds)
feed_dir = os.path.join(self.output_dir, f'feed_{f}')
with open(os.path.join(feed_dir, 'index.html'), 'wb') as fi:
fi.write(html)
self.create_opf(feeds)
self.report_progress(1, _('Feeds downloaded to %s')%index)

return index def _download_cover(self):
self.cover_path = None try:
cu = self.get_cover_url()
except Exception as err:
self.log.error(_('Could not download cover: %s')%as_unicode(err))
self.log.debug(traceback.format_exc())
else:
if not cu:
return cdata = None if hasattr(cu, 'read'):
cdata = cu.read()
cu = getattr(cu, 'name', 'cover.jpg')
elif os.access(cu, os.R_OK):
with open(cu, 'rb') as f:
cdata = f.read()
else:
self.report_progress(1, _('Downloading cover from %s')%cu)
with closing(self.browser.open(cu, timeout=self.timeout)) as r:
cdata = r.read()
if not cdata:
return ext = cu.split('/')[-1].rpartition('.')[-1].lower().strip()
if ext == 'pdf':
from calibre.ebooks.metadata.pdf import get_metadata stream = io.BytesIO(cdata)
cdata = None mi = get_metadata(stream)
if mi.cover_data and mi.cover_data[1]:
cdata = mi.cover_data[1]
if not cdata:
return if self.cover_margins[0] or self.cover_margins[1]:
cdata = image_to_data(add_borders_to_image(cdata, left=self.cover_margins[0],right=self.cover_margins[0], top=self.cover_margins[1],bottom=self.cover_margins[1], border_color=self.cover_margins[2]))

cpath = os.path.join(self.output_dir, 'cover.jpg')
save_cover_data_to(cdata, cpath)
self.cover_path = cpath def download_cover(self):
self.cover_path = None try:
self._download_cover()
except Exception:
self.log.exception('Failed to download cover')
self.cover_path = None def _download_masthead(self, mu):
if hasattr(mu, 'rpartition'):
ext = mu.rpartition('.')[-1]
if '?' in ext:
ext = ''
else:
ext = mu.name.rpartition('.')[-1]
ext = ext.lower() if ext else 'jpg'
mpath = os.path.join(self.output_dir, 'masthead_source.'+ext)
outfile = os.path.join(self.output_dir, 'mastheadImage.jpg')
if hasattr(mu, 'read'):
with open(mpath, 'wb') as mfile:
mfile.write(mu.read())
elif os.access(mu, os.R_OK):
with open(mpath, 'wb') as mfile:
mfile.write(open(mu, 'rb').read())
else:
with open(mpath, 'wb') as mfile, closing(self.browser.open(mu, timeout=self.timeout)) as r:
mfile.write(r.read())
self.report_progress(1, _('Masthead image downloaded'))
self.prepare_masthead_image(mpath, outfile)
self.masthead_path = outfile if os.path.exists(mpath):
os.remove(mpath)

def download_masthead(self, url):
try:
self._download_masthead(url)
except Exception:
self.log.exception('Failed to download supplied masthead_url')

def resolve_masthead(self):
self.masthead_path = None try:
murl = self.get_masthead_url()
except Exception:
self.log.exception('Failed to get masthead url')
murl = None if murl is not None:
# Try downloading the user-supplied masthead_url
# Failure sets self.masthead_path to None self.download_masthead(murl)
if self.masthead_path is None:
self.log.info('Synthesizing mastheadImage')
self.masthead_path = os.path.join(self.output_dir, 'mastheadImage.jpg')
try:
self.default_masthead_image(self.masthead_path)
except Exception:
self.log.exception('Failed to generate default masthead image')
self.masthead_path = None

[документация]
def default_cover(self, cover_file):
'''
Create a generic cover for recipes that don't have a cover
'''
try:
from calibre.ebooks.covers import create_cover title = self.title if isinstance(self.title, str) else \ self.title.decode(preferred_encoding, 'replace')
date = strftime(self.timefmt).replace('[', '').replace(']', '')
img_data = create_cover(title, [date])
cover_file.write(img_data)
cover_file.flush()
except Exception:
self.log.exception('Failed to generate default cover')
return False return True

[документация]
def get_masthead_title(self):
'Override in subclass to use something other than the recipe title'
return self.title

MI_WIDTH = 600
MI_HEIGHT = 60 def default_masthead_image(self, out_path):
from calibre.ebooks import generate_masthead generate_masthead(self.get_masthead_title(), output_path=out_path, width=self.MI_WIDTH, height=self.MI_HEIGHT)

def prepare_masthead_image(self, path_to_image, out_path):
prepare_masthead_image(path_to_image, out_path, self.MI_WIDTH, self.MI_HEIGHT)

[документация]
def publication_date(self):
'''
Use this method to set the date when this issue was published.
Defaults to the moment of download. Must return a :class:`datetime.datetime` object.
'''
return nowf()

def create_opf(self, feeds, dir=None):
if dir is None:
dir = self.output_dir title = self.short_title()
pdate = self.publication_date()
if self.output_profile.periodical_date_in_title:
title += strftime(self.timefmt, pdate)
mi = MetaInformation(title, [__appname__])
mi.publisher = __appname__ mi.author_sort = __appname__ if self.publication_type:
mi.publication_type = 'periodical:'+self.publication_type+':'+self.short_title()
mi.timestamp = nowf()
article_titles, aseen = [], set()
for (af, aa) in self.aborted_articles:
aseen.add(aa.title)
for (ff, fa, tb) in self.failed_downloads:
aseen.add(fa.title)
for f in feeds:
for a in f:
if a.title and a.title not in aseen:
aseen.add(a.title)
article_titles.append(force_unicode(a.title, 'utf-8'))

desc = self.description if not isinstance(desc, str):
desc = desc.decode('utf-8', 'replace')
mi.comments = (_('Articles in this issue:'
) + '\n\n' + '\n\n'.join(article_titles)) + '\n\n' + desc language = canonicalize_lang(self.language)
if language is not None:
mi.language = language mi.pubdate = pdate opf_path = os.path.join(dir, 'index.opf')
ncx_path = os.path.join(dir, 'index.ncx')

opf = OPFCreator(dir, mi)
# Add mastheadImage entry to <guide> section mp = getattr(self, 'masthead_path', None)
if mp is not None and os.access(mp, os.R_OK):
from calibre.ebooks.metadata.opf2 import Guide ref = Guide.Reference(os.path.basename(self.masthead_path), os.getcwd())
ref.type = 'masthead'
ref.title = 'Masthead Image'
opf.guide.append(ref)

manifest = [os.path.join(dir, f'feed_{i}') for i in range(len(feeds))]
manifest.append(os.path.join(dir, 'index.html'))
manifest.append(os.path.join(dir, 'index.ncx'))

# Get cover cpath = getattr(self, 'cover_path', None)
if cpath is None:
pf = open(os.path.join(dir, 'cover.jpg'), 'wb')
if self.default_cover(pf):
cpath = pf.name if cpath is not None and os.access(cpath, os.R_OK):
opf.cover = cpath manifest.append(cpath)

# Get masthead mpath = getattr(self, 'masthead_path', None)
if mpath is not None and os.access(mpath, os.R_OK):
manifest.append(mpath)

opf.create_manifest_from_files_in(manifest)
for mani in opf.manifest:
if mani.path.endswith('.ncx'):
mani.id = 'ncx'
if mani.path.endswith('mastheadImage.jpg'):
mani.id = 'masthead-image'

entries = ['index.html']
toc = TOC(base_path=dir)
self.play_order_counter = 0 self.play_order_map = {}

self.article_url_map = aumap = defaultdict(set)

def feed_index(num, parent):
f = feeds[num]
for j, a in enumerate(f):
if getattr(a, 'downloaded', False):
adir = f'feed_{num}/article_{j}/'
auth = a.author if not auth:
auth = None desc = a.text_summary if not desc:
desc = None else:
desc = self.description_limiter(desc)
tt = a.toc_thumbnail if a.toc_thumbnail else None entries.append(f'{adir}index.html')
po = self.play_order_map.get(entries[-1], None)
if po is None:
self.play_order_counter += 1 po = self.play_order_counter arelpath = f'{adir}index.html'
for curl in self.canonicalize_internal_url(a.orig_url, is_link=False):
aumap[curl].add(arelpath)
article_toc_entry = parent.add_item(arelpath, None, a.title if a.title else _('Untitled article'), play_order=po, author=auth, description=desc, toc_thumbnail=tt)
for entry in a.internal_toc_entries:
anchor = entry.get('anchor')
if anchor:
self.play_order_counter += 1 po += 1 article_toc_entry.add_item( arelpath, entry['anchor'], entry['title'] or _('Unknown section'), play_order=po
)
last = os.path.join(self.output_dir, (f'{adir}index.html').replace('/', os.sep))
for sp in a.sub_pages:
prefix = os.path.commonprefix([opf_path, sp])
relp = sp[len(prefix):]
entries.append(relp.replace(os.sep, '/'))
last = sp if os.path.exists(last):
with open(last, 'rb') as fi:
src = fi.read().decode('utf-8')
soup = BeautifulSoup(src)
body = soup.find('body')
if body is not None:
prefix = '/'.join('..'for i in range(2*len(re.findall(r'link\d+', last))))
templ = self.navbar.generate(True, num, j, len(f), not self.has_single_feed, a.orig_url, __appname__, prefix=prefix, center=self.center_navbar)
elem = BeautifulSoup(templ.render(doctype='xhtml').decode('utf-8')).find('div')
body.insert(len(body.contents), elem)
with open(last, 'wb') as fi:
fi.write(str(soup).encode('utf-8'))
if len(feeds) == 0:
raise Exception('All feeds are empty, aborting.')

if len(feeds) > 1:
for i, f in enumerate(feeds):
entries.append(f'feed_{i}/index.html')
po = self.play_order_map.get(entries[-1], None)
if po is None:
self.play_order_counter += 1 po = self.play_order_counter auth = getattr(f, 'author', None)
if not auth:
auth = None desc = getattr(f, 'description', None)
if not desc:
desc = None feed_index(i, toc.add_item(f'feed_{i}/index.html', None, f.title, play_order=po, description=desc, author=auth))

else:
entries.append('feed_0/index.html')
feed_index(0, toc)

for i, p in enumerate(entries):
entries[i] = os.path.join(dir, p.replace('/', os.sep))
opf.create_spine(entries)
opf.set_toc(toc)

with open(opf_path, 'wb') as opf_file, open(ncx_path, 'wb') as ncx_file:
opf.render(opf_file, ncx_file)

def article_downloaded(self, request, result):
index = os.path.join(os.path.dirname(result[0]), 'index.html')
if index != result[0]:
if os.path.exists(index):
os.remove(index)
os.rename(result[0], index)
a = request.requestID[1]

article = request.article self.log.debug('Downloaded article:', article.title, 'from', article.url)
article.orig_url = article.url article.url = f'article_{a}/index.html'
article.downloaded = True article.sub_pages = result[1][1:]
self.jobs_done += 1 self.report_progress(float(self.jobs_done)/len(self.jobs),
_('Article downloaded: %s')%force_unicode(article.title))
if result[2]:
self.partial_failures.append((request.feed.title, article.title, article.url, result[2]))

def error_in_article_download(self, request, traceback):
self.jobs_done += 1 if traceback and re.search(r'^AbortArticle:', traceback, flags=re.M) is not None:
self.log.warn('Aborted download of article:', request.article.title,
'from', request.article.url)
self.report_progress(float(self.jobs_done)/len(self.jobs),
_('Article download aborted: %s')%force_unicode(request.article.title))
self.aborted_articles.append((request.feed, request.article))
else:
self.log.error('Failed to download article:', request.article.title,
'from', request.article.url)
self.log.debug(traceback)
self.log.debug('\n')
self.report_progress(float(self.jobs_done)/len(self.jobs),
_('Article download failed: %s')%force_unicode(request.article.title))
self.failed_downloads.append((request.feed, request.article, traceback))

[документация]
def parse_feeds(self):
'''
Create a list of articles from the list of feeds returned by :meth:`BasicNewsRecipe.get_feeds`.
Return a list of :class:`Feed` objects.
'''
feeds = self.get_feeds()
parsed_feeds = []
br = self.browser for obj in feeds:
if isinstance(obj, string_or_bytes):
title, url = None, obj else:
title, url = obj if isinstance(title, bytes):
title = title.decode('utf-8')
if isinstance(url, bytes):
url = url.decode('utf-8')
if url.startswith('feed://'):
url = 'http'+url[4:]
self.report_progress(0, _('Fetching feed')+f' {title if title else url}...')
try:
purl = urlparse(url, allow_fragments=False)
if purl.username or purl.password:
hostname = purl.hostname if purl.port:
hostname += f':{purl.port}'
url = purl._replace(netloc=hostname).geturl()
if purl.username and purl.password:
br.add_password(url, purl.username, purl.password)
with closing(br.open_novisit(url, timeout=self.timeout)) as f:
raw = f.read()
parsed_feeds.append(feed_from_xml( raw, title=title, log=self.log, oldest_article=self.oldest_article, max_articles_per_feed=self.max_articles_per_feed, get_article_url=self.get_article_url
))
except Exception as err:
feed = Feed()
msg = f'Failed feed: {title if title else url}'
feed.populate_from_preparsed_feed(msg, [])
feed.description = as_unicode(err)
parsed_feeds.append(feed)
self.log.exception(msg)
delay = self.get_url_specific_delay(url)
if delay > 0:
time.sleep(delay)

remove = [fl for fl in parsed_feeds if len(fl) == 0 and self.remove_empty_feeds]
for f in remove:
parsed_feeds.remove(f)

return parsed_feeds[документация]
@classmethod def tag_to_string(self, tag, use_alt=True, normalize_whitespace=True):
'''
Convenience method to take a
`BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`_
:code:`Tag` and extract the text from it recursively, including any CDATA sections and alt tag attributes. Return a possibly empty Unicode string.

`use_alt`: If `True` try to use the alt attribute for tags that don't have any textual content

`tag`: `BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`_
:code:`Tag`
'''
if tag is None:
return ''
if isinstance(tag, string_or_bytes):
return tag if callable(getattr(tag, 'xpath', None)) and not hasattr(tag, 'contents'): # a lxml tag from lxml.etree import tostring ans = tostring(tag, method='text', encoding='unicode', with_tail=False)
else:
strings = []
for item in tag.contents:
if isinstance(item, (NavigableString, CData)):
strings.append(item.string)
elif isinstance(item, Tag):
res = self.tag_to_string(item)
if res:
strings.append(res)
elif use_alt:
try:
strings.append(item['alt'])
except KeyError:
pass ans = ''.join(strings)
if normalize_whitespace:
ans = re.sub(r'\s+', ' ', ans)
return ans@classmethod def soup(cls, raw):
return BeautifulSoup(raw)

[документация]
@classmethod def adeify_images(cls, soup):
'''
If your recipe when converted to EPUB has problems with images when viewed in Adobe Digital Editions, call this method from within
:meth:`postprocess_html`.
'''
for item in soup.findAll('img'):
for attrib in ['height','width','border','align','style']:
try:
del item[attrib]
except KeyError:
pass oldParent = item.parent myIndex = oldParent.contents.index(item)
item.extract()
divtag = soup.new_tag('div')
brtag = soup.new_tag('br')
oldParent.insert(myIndex, divtag)
divtag.append(item)
divtag.append(brtag)
return soup
def internal_postprocess_book(self, oeb, opts, log):
seen = set()
for i, item in enumerate(oeb.spine):
for a in item.data.xpath('//*[local-name()="a" and @href]'):
if (rel := a.get('rel')) == 'calibre-downloaded-from':
continue url = a.get('href')
if not url:
continue if rel in ('articlenextlink', 'articleprevlink'):
abshref = item.abshref(url)
if abshref not in oeb.manifest.hrefs:
if rel == 'articlenextlink':
nextitem = oeb.spine[i + 1] if i + 1 < len(oeb.spine) else None else:
nextitem = oeb.spine[i - 1] if i else None if nextitem is None:
a.text = None a.attrib.pop('href')
else:
a.set('href', item.relhref(nextitem.href))
continue if self.resolve_internal_links and self.article_url_map:
for curl in self.canonicalize_internal_url(url):
articles = self.article_url_map.get(curl)
if articles:
arelpath = sorted(articles, key=numeric_sort_key)[0]
a.set('href', item.relhref(arelpath))
if a.text and len(a) == 0:
a.text = a.text + '·' # mark as local link if url not in seen:
log.debug(f'Resolved internal URL: {url} -> {arelpath}')
seen.add(url)class CustomIndexRecipe(BasicNewsRecipe):

def custom_index(self):
'''
Return the filesystem path to a custom HTML document that will serve as the index for this recipe. The index document will typically contain many `<a href="...">` tags that point to resources on the internet that should be downloaded.
'''
raise NotImplementedError def create_opf(self):
mi = MetaInformation(self.title + strftime(self.timefmt), [__appname__])
mi.publisher = __appname__ mi.author_sort = __appname__ mi = OPFCreator(self.output_dir, mi)
mi.create_manifest_from_files_in([self.output_dir])
mi.create_spine([os.path.join(self.output_dir, 'index.html')])
with open(os.path.join(self.output_dir, 'index.opf'), 'wb') as opf_file:
mi.render(opf_file)

def download(self):
index = os.path.abspath(self.custom_index())
url = 'file:'+index if iswindows else 'file://'+index self.web2disk_options.browser = self.clone_browser(self.browser)
fetcher = RecursiveFetcher(self.web2disk_options, self.log)
fetcher.base_dir = self.output_dir fetcher.current_dir = self.output_dir fetcher.show_progress = False res = fetcher.start_fetch(url)
self.create_opf()
return res class AutomaticNewsRecipe(BasicNewsRecipe):

auto_cleanup = True class CalibrePeriodical(BasicNewsRecipe):

#: Set this to the slug for the calibre periodical calibre_periodicals_slug = None

LOG_IN = 'https://news.calibre-ebook.com/accounts/login'
needs_subscription = True
__author__ = 'calibre Periodicals'

def get_browser(self):
br = BasicNewsRecipe.get_browser(self)
br.open(self.LOG_IN)
br.select_form(name='login')
br['username'] = self.username br['password'] = self.password raw = br.submit().read()
if 'href="/my-account"' not in raw:
raise LoginFailed(
_('Failed to log in, check your username and password for'
' the calibre Periodicals service.'))

return br get_browser.is_base_class_implementation = True def download(self):
self.log('Fetching downloaded recipe')
try:
raw = self.browser.open_novisit( f'https://news.calibre-ebook.com/subscribed_files/{self.calibre_periodicals_slug}/0/temp.downloaded_recipe'
).read()
except Exception as e:
if hasattr(e, 'getcode') and e.getcode() == 403:
raise DownloadDenied(
_('You do not have permission to download this issue.'
' Either your subscription has expired or you have'
' exceeded the maximum allowed downloads for today.'))
raise f = io.BytesIO(raw)
from calibre.utils.zipfile import ZipFile zf = ZipFile(f)
zf.extractall()
zf.close()
from glob import glob from calibre.web.feeds.recipes import compile_recipe try:
recipe = compile_recipe(open(glob('*.recipe')[0],
'rb').read())
self.conversion_options = recipe.conversion_options except Exception:
self.log.exception('Failed to compile downloaded recipe')
return os.path.abspath('index.html')

© Копирайт Kovid Goyal.
Последнее обновление июл. 18, 2025.

Table of Contents

Руководство пользователя calibre

Графический интерфейс пользователя

1. Действия

1. Добавить книги
2. Редактировать метаданные
3. Преобразовать книги
4. Просмотр
5. Отправить на устройство
6. Собрать новости
7. Библиотека
8. Устройство
9. Сохранить на диск
10. Подключить/поделиться
11. Удалить книги

2. Параметры
3. Каталоги
4. Поиск и сортировка
5. Интерфейс поиска
6. Сохранение поисковых строк
7. Поиск по полному тексту всех книг
8. Виртуальные библиотеки
9. Временно пометить книги

10. Предположение метаданных на основе имён файлов

11. Описание книги
12. Браузер тегов
13. Сетка обложек
14. Браузер обложек

15. Добавление примечаний для авторов, серий и т.д.

16. Быстрый просмотр
17. Задания
18. Сочетания клавиш

Добавление любимых новостных веб-сайтов

1. Полностью автоматическая загрузка

1. Блог calibre

2. bbc.co.uk

2. Настройка процесса получения

1. Использование печатной версии bbc.co.uk
2. Замена стилей статей
3. Шинковка ломтиками и кубиками
4. Пример из реальной жизни

3. Советы по созданию новых рецептов

4. Дальнейшее чтение

5. Документация API

1. Документация API рецептов

1. BasicNewsRecipe

Просмотрщик электронных книг

1. Запуск просмотрщика электронных книг

2. Навигация по электронной книге

1. Закладки
2. Оглавление
3. Навигация по местоположению
4. Режим отсылок

3. Выделение текста
4. Чтение вслух
5. Поиск текста
6. Переход по ссылкам только с клавиатуры

7. Настройка внешнего вида по вашим предпочтениям

8. Поиск в словаре
9. Копирование текста и изображений
10. Увеличение масштаба изображений

11. Синхронизация с бумажным изданием текущей книги

12. Горячие клавиши

13. Неперетекающий контент

14. Проектирование вашей книги для хорошего просмотра в просмотрщике электронных книг calibre

Конвертирование книг

1. Предисловие

2. Оформление

1. Шрифты
2. Текст
3. Макет
4. Стилизация
5. Стили трансформации
6. Трансформировать HTML

3. Настройка страницы
4. Эвристическая обработка
5. Поиск и замена

6. Определение структуры

1. Главы и разрывы страниц

2. Разное

7. Содержание

8. Использование изображений в качестве заголовков глав при преобразовании входных документов HTML

9. Использование атрибутов тега для предоставления текста для записей в оглавлении

10. Как параметры устанавливаются/сохраняются для конвертации

11. Формат-специфичные советы

1. Конвертация документов Microsoft Word

1. Старые файлы .doc

2. Конвертировать TXT документы
3. Преобразование PDF документов
4. Коллекции комиксов

5. Демоверсия расширенного форматирования EPUB

6. Конвертировать ODT документы

7. Конвертация в PDF

1. Верхние и нижние колонтитулы

2. Оглавление для печати

3. Пользовательские поля страницы для отдельных файлов HTML

Редактирование книг

Основной рабочий процесс

Браузер файлов

1. Изменение имени файлов
2. Объединить файлы
3. Изменение порядка текстовых файлов
4. Создание обложек
5. Удаление файлов
6. Экспортировать файлы

7. Добавление новых изображений/шрифтов/и т. д. или создание новых пустых файлов

8. Замена файлов

9. Эффективное связывание таблиц стилей с файлами HTML

Поиск и замена

1. Сохранённые поиски
2. Функциональный режим
3. Поиск, игнорирующий теги HTML

Автоматизированные инструменты

1. Редактирование оглавления
2. Проверка книги
3. Добавление обложки
4. Внедрение упоминаемых шрифтов
5. Сокращение внедрённых шрифтов
6. Улучшение пунктуации
7. Преобразование свойств CSS
8. Удаление неиспользуемых CSS правил
9. Исправление HTML
10. Улучшение файлов
11. Вставка встроенного Оглавления
12. Установка семантики
13. Фильтрация информации о стилях
14. Обновление внутренней структуры книги

Контрольные точки

Панель живого предпросмотра

1. Разделение HTML файлов

Панель живого CSS

Разные инструменты

1. Вид Оглавления

2. Проверка правописания слов в книге

1. Добавление словарей

3. Вставка специальных символов
4. Вид инспектор кода
5. Проверка внешних ссылок
6. Скачать внешние ресурсы
7. Упорядочить файлы в папках по типу

8. Импорт файлов других форматов электронных книг в EPUB

9. Инструмент Отчётов

Специальные функции в редакторе кода

1. Подсветка синтаксиса
2. Контекстная помощь
3. Автозавершение
4. Фрагменты

Контент-сервер calibre

1. Доступ к контент-серверу с других устройств

1. Доступ к серверу с устройств в вашей домашней сети

1. Устранение неполадок подключения к домашней сети

2. Доступ к серверу из любой точки интернета

2. Интерфейс сервера

1. Список книг

2. Просмотрщик книг

3. Поддержка браузеров

4. Включение режима автономной работы

5. Управление пользовательскими аккаунтами с помощью командной строки

6. Интеграция контент-сервера calibre с другими серверами

1. Использование полностью виртуального хоста

2. Использование префикса URL-адреса

7. Создание сервиса для сервера calibre в современной системе Linux

Сравнение книг

1. Что и где показано в окне сравнения

2. Запуск инструмента сравнения

1. Сравнение двух файлов электронных книг

2. Сравнение ORIGINAL_FMT и FMT

3. Сравнение контрольной точки с текущим состоянием книги при редактировании

Редактирование метаданных книги

1. Редактирование метаданных одной книги за раз

1. Загрузка метаданных
2. Управление форматами книг
3. Всё об обложках

2. Редактирование метаданных нескольких книг одновременно

1. Поиск и замена

2. Массовая загрузка метаданных

3. Добавить дополнительные файлы данных в книгу

Часто задаваемые вопросы

Конвертация форматов электронных книг

1. Какие форматы конвертации поддерживает calibre?

2. Каковы оптимальные исходные форматы для конвертации?

3. Я преобразовал PDF файл, но в результате имеются разные проблемы?

4. Как конвертировать мой файл, если он включает не английские символы или «умные» кавычки?

5. Что случилось с оглавлением MOBI-файлов?

6. Каким образом можно преобразовывать коллекцию HTML-файлов в определенной последовательности?

7. Если EPUB, созданный в calibre, не работает?

8. Как я могу использовать некоторые из расширенных возможностей инструментов конвертации?

Интеграция устройств

1. Какие устройства поддерживает calibre?

2. Как я мог помочь получению поддержки моего устройства в calibre?

3. Моё устройство не определяется в calibre?

4. Моё устройство нестандартное или необычное. Что я могу сделать, чтобы подключить его?

5. Как я могу использовать calibre с моим iPad/iPhone/iPod touch?

1. Использование сервера контента

6. Как использовать calibre с телефоном/планшетом Android или Kindle Fire?

1. Используя кабель USB

2. По беспроводному соединению

7. Могу ли я получить доступ к своим книгам calibre с помощью веб-браузера на Kindle или другом устройстве для чтения?

8. Я не могу посылать электронную почту при помощи calibre?

9. Мое устройство монтируется в Linux только для чтения, поэтому calibre не может подключиться к нему?

10. Почему calibre не поддерживает коллекции на Kindle или полку на Nook?

11. Я получаю сообщение об ошибке, когда пытаюсь использовать calibre с моим Kobo Touch/Glo и т. д.?

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

13. Обложки для книг, отправленных на мой Kindle ColorSoft и новее, не отображаются в списке книг?

14. Обложки моих MOBI-файлов перестали отображаться в Kindle для PC/Kindle, Android/iPad и т.п.

15. Я перекинул несколько книг в мой Kindle при помощи caibre и их не видно?

Управление библиотекой

1. Где хранятся файлы книг?

2. Как calibre управляет именами авторов и сортировкой?

3. Почему calibre не позволяет мне хранить книги в собственной структуре папок?

4. Почему calibre не имет какого-либо необходимого мне столбца?

5. Могу ли я добавить столбец с форматами или ISBN?

6. Как перенести мою копию calibre с одного компьютера на другой?

7. Список книг в calibre пуст!

8. Я получаю ошибки, когда моя библиотека calibre находится на сетевом диске/NAS?

Разное

1. Amazon прекращает доставку файлов MOBI по электронной почте?

2. Я хочу, чтобы calibre загружал новостис моих любимых сайтов новостей.

3. Почему программа называется calibre?

4. Почему calibre показывает только некоторые из моих шрифтов в macOS?

5. calibre не запускается в Windows?

6. calibre время от времени тормозит/вылетает?

7. Просмотрщик электронных книг calibre и инструменты редактирования книг не работают в Windows?

8. Использование просмотрщика или выполнение любых конвертаций приводит к ошибке нехватки разрешений в Windows

9. calibre не запускается/вылетает на macOS?

10. Я получаю только чёрный или белый экран при запуске просмотрщика электронных книг Calibre?

11. Я загрузил установщик, но он не работает?

12. Мой антивирус утверждает, что calibre - вирус/троян?

13. Как я могу сделать резервную копию calibre?

14. Как я могу использовать купленные EPUB книги с calibre (или что делать с файлами .acsm)?

15. Я получаю ошибку «Доступ запрещён»?

16. Могу ли я показать метаданные комментария на моем ридере?

17. Как я могу указать calibre использовать мой HTTP прокси?

18. Я хочу, чтобы в calibre были добавлены некоторые возможности. Что я могу сделать?

19. Почему у calibre нет автоматического обновления?

20. Как лицензирована calibre?

21. Как я могу запустить calibre с USB-флешки?

22. Как мне запустить такие части calibre, как загрузка новостей и сервер содержимого, на моём собственном сервере Linux?

Руководства

Добавление любимых новостных веб-сайтов

1. Полностью автоматическая загрузка

1. Блог calibre

2. bbc.co.uk

2. Настройка процесса получения

1. Использование печатной версии bbc.co.uk
2. Замена стилей статей
3. Шинковка ломтиками и кубиками
4. Пример из реальной жизни

3. Советы по созданию новых рецептов

4. Дальнейшее чтение

5. Документация API

1. Документация API рецептов

Управление подгруппами книг, например «genre»

1. Установка
2. Поиск
3. Ограничения
4. Полезные шаблонные функции

Учебник по XPath

1. Выбор по имени тега
2. Выбор по атрибутам
3. Выбор по содержимому тега
4. Образец электронной книги
5. Встроенные функции XPath

Язык шаблонов calibre

1. Базовые шаблоны

2. Расширенное форматирование

3. Использование шаблонов для определения пользовательских столбцов

4. Шаблоны и коммутации

5. Использование функций в шаблонах - режим одной функции

6. Общий программный режим

7. Более сложные программы в шаблонных выражениях - Template Program Mode

8. Режим шаблона Python
9. Шаблоны и URL
10. Сохранённые шаблоны

11. Передача шаблонам дополнительной информации

12. Замечания о разнице между режимами

13. Пользовательские шаблонные функции Python

14. Особые указания по использованию шаблонов в различных контекстах

15. Особые примечания для шаблонов сохранения/отправки

16. Советы

17. Справочник по шаблонным функциям

1. Reference for all built-in template language functions

Всё об использования регулярных выражений в calibre

1. Во-первых, слово предупреждения и слово мужества

2. Где в calibre вы можете использовать регулярные выражения?

3. Что на земле является регулярным выражением?

4. Хотите объяснений?

5. Это звучит не так уж плохо. Что дальше?

6. Эй, аккуратно! Это начинает иметь смысл!

7. Ну, эти специальные символы очень аккуратные и все такое, но что, если я хочу сопоставить точку или знак вопроса?

8. Итак, какие наборы наиболее полезны?

9. Но если бы у меня было несколько переменных строк, которые я хотел бы сопоставить, все усложнялось?

10. Вы пропустили…

11. В начале вы сказали, что есть способ сделать регулярное выражение нечувствительным к регистру?

12. Я думаю, что начинаю понимать эти регулярные выражения сейчас … как я могу использовать их в calibre?

1. Конвертации
2. Добавление книг
3. Массовое редактирование метаданных

13. Краткая справка

1. Краткая справка о синтаксисе регулярных выражений

14. Признательность

Создание собственных плагинов для расширения функциональности calibre

1. Анатомия плагина calibre

2. Плагин пользовательского интерфейса

1. __init__.py
2. ui.py
3. main.py
4. Получение ресурсов из плагина ZIP-файла

5. Включение пользовательской конфигурации вашего плагина

3. Редактировать книжные плагины

1. main.py

4. Добавление переводов в ваш плагин
5. API плагина
6. Отладка плагинов
7. Больше примеров плагинов
8. Поделиться своими плагинами с другими

Типографика математических выражений в электронных книгах

1. Простой HTML-файл с формулами

2. Дополнительная информация

Создание AZW3 • EPUB • MOBI каталогов

1. Выбор книг для каталогизации
2. Включенные разделы
3. Префиксы
4. Исключены книги
5. Исключенные жанры
6. Другие настройки
7. Настраиваемый каталог обложек
8. Дополнительные справочные ресурсы

Виртуальные библиотеки

1. Создание виртуальных библиотек

1. Примеры полезных виртуальных библиотек

2. Работа с виртуальными библиотеками

3. Использование виртуальных библиотек в поиске

4. Использование дополнительных ограничений

Схема URL-адресов calibre://

1. Переключение на определённую библиотеку

2. Отображение определённой книги в calibre

3. Открытие определённой книги в читалке электронных книг в указанном месте

4. Поиск книг

5. Открыть окно сведений о книге в какой-либо библиотеке

6. Открыть примечания, связанные с автором/серией/и т.д.

7. Шестнадцатеричное кодирование параметров URL-адреса

Настройка calibre

1. Переменные окружения
2. Твики
3. Переопределение значков, шаблонов и т. д.

4. Создание собственной темы значков для calibre

5. Настройка calibre с помощью плагинов

Командная строка
Документированные команды
calibre

1. [опции]

calibre-customize

1. [опции]

calibre-debug

1. [опции]

calibre-server

1. [опции]

calibre-smtp

1. [опции]

calibredb

1. ГЛОБАЛЬНЫЕ НАСТРОЙКИ
2. list
3. add
4. remove
5. add_format
6. remove_format
7. show_metadata
8. set_metadata
9. export
10. catalog
11. saved_searches
12. add_custom_column
13. custom_columns
14. remove_custom_column
15. set_custom
16. restore_database
17. check_library
18. list_categories
19. backup_metadata
20. clone
21. embed_metadata
22. search
23. fts_index
24. fts_search

ebook-convert

1. ВХОДНЫЕ НАСТРОЙКИ
2. ВЫХОДНЫЕ НАСТРОЙКИ
3. ВИД И ФУНКЦИИ
4. ЭВРИСТИЧЕСКАЯ ОБРАБОТКА
5. НАЙТИ И ЗАМЕНИТЬ
6. ОПРЕДЕЛЕНИЕ СТРУКТУРЫ
7. ОГЛАВЛЕНИЕ
8. МЕТАДАННЫЕ
9. ОТЛАДКА

ebook-edit

1. [опции]

ebook-meta

1. [опции]

ebook-polish

1. [опции]

ebook-viewer

1. [опции]

fetch-ebook-metadata

1. [опции]

lrf2lrs

1. [опции]

lrfviewer

1. [опции]

lrs2lrf

1. [опции]

web2disk

1. [опции]

Недокументированные команды

Настройка среды разработки calibre

Философия разработки

1. Макет кода

Получение кода

1. Отправка ваших изменений для включения

Среда разработки под Windows
Среда разработки под macOS
Среда разработки под Linux

Раздельная установка «обычной» и «разрабатываемой» версий calibre на одном компьютере

Советы по отладке

1. Использование print statements

2. Использование интерактивного интерпретатора Python

3. Использовать отладчик Python как удаленный отладчик

4. Использование отладчика в вашей любимой Python IDE

5. Выполнение произвольных скриптов в окружении Python calibre

Использование calibre в собственных проектах

1. Установка исполняемого файла calibre

2. Установка из исходного кода в Linux

Документация API различных частей calibre

1. Документация API рецептов

1. BasicNewsRecipe

2. Документация по API для плагинов

1. Плагин
2. FileTypePlugin
3. Плагины метаданных
4. Плагины каталогизации
5. Плагины загрузки метаданных
6. Плагины преобразования
7. Драйверы устройств
8. Действия в пользовательском интерфейсе
9. Плагины параметров

3. Документация API интерфейса базы данных

1. Cache

4. Документация API инструмента редактирования электронных книг

1. Объект контейнера

2. Управление файлами компонентов в контейнере

3. Хорошая печать и автоматическое исправление ошибок синтаксического анализа

4. Управление обложками книг
5. Разделение и слияние файлов
6. Управление обложками
7. Работа с CSS
8. Работа с оглавлением
9. Средство редактирования книги

10. Управление пользовательским интерфейсом редактора

Технические средства защиты авторских прав (DRM — digital rights management)

1. Что несёт DRM лично для меня?

2. Что DRM даёт авторам?

3. DRM и свобода

4. Почему calibre не поддерживает DRM?

5. Каково мнение calibre о поставщиках информации?

6. Как я могу помочь бороться с DRM?

Глоссарий